@@NAME@@
-@@SIG@@
- @@DESC@@
diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md
index 7202e9f3..b39680fc 100644
--- a/.forgejo/pull_request_template.md
+++ b/.forgejo/pull_request_template.md
@@ -14,7 +14,7 @@ Closes #
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
registered in `tools/docgen/inventory.json`
- (`python3 tools/docgen/check.py` passes).
+ (`bin/x docs-gen && bin/x docs-check build/pages` passes).
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
artifacts committed outside `build/` / `bin/`.
diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
index 7f242aeb..b09b91ba 100644
--- a/.forgejo/workflows/ci.yml
+++ b/.forgejo/workflows/ci.yml
@@ -17,20 +17,20 @@ jobs:
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
runs-on: docker
# Reuse the runner's own base image (Debian bookworm with git + node already
- # present) and add just the two things the toolchain needs: a modern clang
- # (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required) and
- # python3 for the docs/vocabulary checks. A prebuilt image with these baked
- # in is the obvious future speed-up (see issue #33's packaging work).
+ # present) and add just the one thing the toolchain needs: a modern clang
+ # (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required). The docs
+ # generator and its guards are now Ludic, so the job carries no Python. A
+ # prebuilt image with clang baked in is the obvious future speed-up (see
+ # issue #33's packaging work).
container: node:20-bookworm
steps:
- - name: Install clang-16 and python3
+ - name: Install clang-16
run: |
set -eu
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
- apt-get install -y -qq --no-install-recommends clang-16 python3 git ca-certificates
+ apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
clang-16 --version | head -1
- python3 --version
- name: Check out the triggering commit
run: |
@@ -75,11 +75,12 @@ jobs:
- name: Docs cover the implementation
run: |
set -eu
- # check-impl / check-vocabulary are written in Ludic and run through
- # x — no Python. (check-vocabulary also runs in `x test-tools`.) The
- # site generator (gen.py) is still Python; its port is tracked.
+ # The whole docs toolchain is written in Ludic and runs through x —
+ # no Python anywhere. check-impl / check-vocabulary / check-docs guard
+ # the sources; docs-gen builds the site and docs-check is its coverage
+ # + integrity guard. (check-vocabulary also runs in `x test-tools`.)
bin/x check-impl
bin/x check-vocabulary
bin/x check-docs
- python3 tools/docgen/gen.py --out build/pages
- python3 tools/docgen/check.py build/pages
+ bin/x docs-gen --out build/pages
+ bin/x docs-check build/pages
diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml
index f1edaec7..2a5fb4ad 100644
--- a/.forgejo/workflows/docs.yml
+++ b/.forgejo/workflows/docs.yml
@@ -10,6 +10,7 @@ on:
paths:
- 'docs/**'
- 'tools/docgen/**'
+ - 'tools/x/**'
- '.forgejo/workflows/docs.yml'
workflow_dispatch: {}
@@ -23,11 +24,20 @@ jobs:
# GitHub-ism this runner does not register, so a job requesting it sits in
# "Waiting" forever with "no online runner found matching this label".
runs-on: docker
- # Run in a Python image: the generator is pure-Python stdlib, and this image
- # already has git for the clone + publish. No node actions are used, so the
- # job never depends on the runner's base image having python installed.
- container: python:3.12
+ # The generator is now Ludic, so this builds the toolchain from its IR seed
+ # (clang assembles the seed into bin/ludicc, which compiles bin/x) exactly
+ # like the ci workflow, then runs `x docs-gen`. node:20-bookworm carries git
+ # for the clone + publish; clang-16 is the only extra the bootstrap needs.
+ container: node:20-bookworm
steps:
+ - name: Install clang-16
+ run: |
+ set -eu
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update -qq
+ apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
+ clang-16 --version | head -1
+
- name: Generate the documentation site
env:
SOURCE_REF: ${{ github.ref_name }}
@@ -36,9 +46,19 @@ jobs:
git config --global --add safe.directory '*'
git clone --depth 1 --branch "${SOURCE_REF:-main}" \
https://git.workshopsoft.io/workshopsoft/ludic.git src
- python3 --version
- python3 src/tools/docgen/gen.py --out public
- python3 src/tools/docgen/check.py public
+ cd src
+ # The toolchain is macOS-first; on this Linux runner it links against a
+ # tiny C-free IR shim supplying the Darwin stdout/stderr globals over
+ # glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no
+ # windowing), so the C-free bootstrap is all it needs.
+ export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll"
+ export LUDIC_HOME="$(pwd)"
+ mkdir -p bin
+ clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
+ bin/ludicc tools/x/main.ludic -o bin/x
+ bin/x docs-gen --out ../public
+ bin/x docs-check ../public
+ cd ..
echo "--- generated files ---"
ls -la public
diff --git a/selfhost/backend/stdlib/emit_color.ludic b/selfhost/backend/stdlib/emit_color.ludic
index 8c3f2b0f..4ceaa973 100644
--- a/selfhost/backend/stdlib/emit_color.ludic
+++ b/selfhost/backend/stdlib/emit_color.ludic
@@ -5,8 +5,8 @@
# no allocation, identical codegen to writing the hex by hand. Unknown names are
# a compile error (color_lookup returns -1, which emit_expr reports).
#
-# GENERATED by scratchpad/palette.py from the single source-of-truth palette.
-# Edit the palette there and regenerate; do not hand-edit this file.
+# GENERATED by `x docs-palette` from the single source-of-truth palette table
+# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.
# ============================================================================
function color_lookup(name: pointer) -> int {
diff --git a/tools/docgen/README.md b/tools/docgen/README.md
index 11243ddd..2378b137 100644
--- a/tools/docgen/README.md
+++ b/tools/docgen/README.md
@@ -10,7 +10,7 @@ docs/
language/\1", s)
-
-def split_body(body):
- examples = FENCE.findall(body)
- body = FENCE.sub("", body)
- # pull out a "Parameters:" block
- params, keep = [], []
- lines = body.split("\n")
- i, n = 0, len(lines)
- while i < n:
- if lines[i].strip().lower().startswith("parameters:"):
- i += 1
- while i < n and lines[i].strip():
- m = PARAM.match(lines[i].strip())
- if m:
- params.append({"name": m.group(1), "desc": codeify(m.group(2).strip())})
- i += 1
- else:
- keep.append(lines[i]); i += 1
- desc = "\n".join(keep).strip()
- paras = [codeify(p.strip()) for p in re.split(r"\n\s*\n", desc) if p.strip()]
- return "
".join(paras), examples, params - -def load_sections(): - sections = [] - for cat in sorted(os.listdir(LANG)): - cdir = os.path.join(LANG, cat) - if not os.path.isdir(cdir): - continue - smeta, sblurb = {}, "" - sp = os.path.join(cdir, "_section.md") - if os.path.exists(sp): - smeta, sbody = parse_doc(sp) - sblurb = codeify(sbody.strip()) - entries = [] - for fn in sorted(os.listdir(cdir)): - if not fn.endswith(".md") or fn == "_section.md": - continue - meta, body = parse_doc(os.path.join(cdir, fn)) - desc, examples, params = split_body(body) - meta["desc_html"] = desc - meta["examples"] = examples - meta["params"] = params - meta["tokens_list"] = meta.get("tokens", "").split() - meta["related_list"] = meta.get("related", "").split() - entries.append(meta) - entries.sort(key=lambda e: (int(e.get("order", 999)), e.get("name", ""))) - # A namespace section is discovered, not hardcoded: it is any section - # whose symbols are `namespace-method`s, named by their `ns:` field. So - # adding a namespace is just adding its docs dir — gen.py needs no edit. - ns_name = next((e["ns"] for e in entries - if e.get("kind") == "namespace-method" and e.get("ns")), None) - sections.append({"id": smeta.get("id", cat), "title": smeta.get("title", cat.title()), - "order": int(smeta.get("order", 999)), "blurb": sblurb, - "cat": cat, "entries": entries, "ns_name": ns_name}) - sections.sort(key=lambda s: (s["order"], s["title"])) - return sections - -# --------------------------------------------------------------------------- -# symbol model for the highlighter + search + cards -# --------------------------------------------------------------------------- -def ns_page(nsname): return "ns-" + nsname.lower() - -def build_symbols(sections, palette): - sym = {"keywords": {}, "types": {}, "phases": {}, "builtins": {}, - "nsmethods": {}, "annotations": {}, "namespaces": {}, "tips": {}, - "cards": {}, "colors_page": "ns-color"} - items = {} # id -> full record for search - for s in sections: - for e in s["entries"]: - eid = e["id"]; kind = e.get("kind", ""); tip = e.get("tip", "") - page = eid + ".html" - items[eid] = {"id": eid, "name": e.get("name", ""), "sig": e.get("sig", ""), - "kind": kind, "category": s["id"], "section": s["title"], - "tip": tip, "page": page} - sym["cards"][eid] = {"name": e.get("name",""), "sig": e.get("sig",""), - "tip": tip, "kind": kind, "page": page, "section": s["title"]} - for tok in e["tokens_list"]: - if kind == "keyword": sym["keywords"][tok] = eid - elif kind == "type": sym["types"][tok] = eid - elif kind == "phase": sym["phases"][tok] = eid - elif kind == "builtin": - sym["builtins"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]} - elif kind == "namespace-method": - sym["nsmethods"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]} - elif kind == "annotation": - sym["annotations"][tok] = eid - if tip: sym["tips"][tok] = tip - # namespaces are discovered from the ns methods present, plus Color; each - # namespace's tip falls back to its section blurb so no hardcoded list of - # namespaces is needed here when a new one is added. - ns_blurb = {s["ns_name"]: plain_text(s["blurb"]) for s in sections if s.get("ns_name")} - nsnames = set() - for tok in sym["nsmethods"]: - if "." in tok: nsnames.add(tok.split(".", 1)[0]) - nsnames.add("Color") - for nn in sorted(nsnames): - sym["namespaces"][nn] = ns_page(nn) - sym["cards"][ns_page(nn)] = {"name": nn, "sig": nn + ".*", "kind": "namespace", - "tip": ns_tip(nn, ns_blurb), "page": ns_page(nn) + ".html", - "section": "Namespaces"} - return sym, items - -def plain_text(s, limit=160): - """The first sentence of a blurb, stripped of tags — for use as a one-line tip.""" - t = re.sub(r"<[^>]+>", "", s or "").strip() - m = re.match(r"(.+?[.!?])(\s|$)", t) - if m: t = m.group(1) - return t if len(t) <= limit else t[:limit].rsplit(" ", 1)[0] + "…" - -def ns_tip(nsname, ns_blurb): - """A namespace's tip: an optional curated override, else its section blurb.""" - return NS_TIP.get(nsname) or ns_blurb.get(nsname, "") - -# Optional short overrides for a namespace's one-line tip. A namespace NOT -# listed here falls back to the first sentence of its section blurb (see -# ns_tip), so a newly added namespace needs no edit here. Color has no docs -# section of its own, so its override is required. -NS_TIP = {"Color": "The named color palette."} - -# --------------------------------------------------------------------------- -# shared chrome -# --------------------------------------------------------------------------- -def nav_html(cfg, links=None, cta_href="api.html"): - out = [] - for n in (links if links is not None else cfg["nav_links"]): - cls = 'class="nav-cta" ' if n.get("href") == cta_href else "" - out.append('%s' % (cls, n["href"], esc(n["label"]))) - return "".join(out) - -def head_html(title, desc, css_file): - css = open(os.path.join(ASSETS, css_file)).read() - return fill(""" - -
- - -%s'
- '%s%s' % esc(code) for code in e["examples"]) - return '
@@SIG@@
- @@DESC@@