From e1756195431b26c93ff3ccbd8eabd212f61ea6f5 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sat, 5 Sep 2026 23:15:12 +0300 Subject: [PATCH] refactor(cli)!: split the contributor tool out of the ludic CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ludic help` ended with a section titled "contributing to the toolchain itself", listing bootstrap, reseed, docs-gen and release tasks. None of that is available to someone who installed the language — those tasks need the repository — so the shipped tool was advertising work its user cannot do, in a namespace they have to read past to find `new` and `run`. The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a checkout and excluded from every release artifact. `ludic` keeps the project and package commands and nothing else; `ludic dev …` now explains where the tasks went instead of failing as an unknown command. What this shook out: the two programs share prelude/build/project/pkg, so the helpers each had accreted in whichever file first needed them — cc(), ensure_ludicc, the string functions, title_case, cmd_version — moved to where both can see them. The argument-shift indirection added for the `dev` namespace is gone with the namespace, so commands read argv directly again. `ludic-dev test` asserts the split rather than trusting it: the staged install must build a project, and `ludic dev build` there must fail while naming ludic-dev. install.sh keeps building older tags, whose bootstrap goes through main.ludic. Co-Authored-By: Claude Opus 5 --- .forgejo/pull_request_template.md | 6 +- .forgejo/workflows/bootstrap.yml | 6 +- .forgejo/workflows/ci.yml | 28 ++-- .forgejo/workflows/docs.yml | 8 +- .forgejo/workflows/release.yml | 16 +- .gitignore | 8 +- COMPILING.md | 10 +- CONTRIBUTING.md | 62 +++---- LANGUAGE.md | 8 +- README.md | 18 +- changes/README.md | 6 +- changes/split-dev-cli.md | 16 ++ docs/CONTROLLERS.md | 2 +- docs/language/testing/kw-test.md | 4 +- examples/README.md | 2 +- examples/lang/detach.ludic | 2 +- examples/lang/reason.ludic | 2 +- examples/lang/scenes.ludic | 2 +- examples/library/coverage.ludic | 4 +- install.sh | 17 +- selfhost/backend/stdlib/emit_color.ludic | 2 +- selfhost/main.ludic | 4 +- tools/docgen/README.md | 12 +- tools/editors/README.md | 14 +- tools/editors/jetbrains/README.md | 4 +- .../main/kotlin/io/ludic/ide/LudicServer.kt | 4 +- .../src/main/resources/META-INF/plugin.xml | 2 +- tools/editors/vscode/README.md | 2 +- tools/editors/vscode/src/extension.js | 2 +- tools/ludic-cli/build.ludic | 64 +++---- tools/ludic-cli/checks.ludic | 81 +-------- tools/ludic-cli/dev.ludic | 137 +++++++++++++++ tools/ludic-cli/docgen.ludic | 52 +----- tools/ludic-cli/docgen_check.ludic | 4 +- tools/ludic-cli/docgen_gen.ludic | 10 +- tools/ludic-cli/lsp_test.ludic | 2 +- tools/ludic-cli/main.ludic | 151 +++-------------- tools/ludic-cli/pkg.ludic | 4 +- tools/ludic-cli/prelude.ludic | 157 +++++++++++++++--- tools/ludic-cli/project.ludic | 37 ++++- tools/ludic-cli/release.ludic | 60 +++---- tools/ludic-cli/selfhost.ludic | 26 +-- tools/ludic-cli/selfhost_test.ludic | 12 +- tools/ludic-cli/test.ludic | 50 +++--- tools/ludic-cli/toolchain.ludic | 44 +++++ tools/ludic-cli/tools.ludic | 24 +-- 46 files changed, 630 insertions(+), 558 deletions(-) create mode 100644 changes/split-dev-cli.md create mode 100644 tools/ludic-cli/dev.ludic create mode 100644 tools/ludic-cli/toolchain.ludic diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index 20968e75..0a04e381 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -8,13 +8,13 @@ Closes # ## Checklist -- [ ] `bin/ludic dev test` passes. -- [ ] For compiler/runtime changes: `bin/ludic dev reseed && bin/ludic dev bootstrap-cfree` +- [ ] `bin/ludic-dev test` passes. +- [ ] For compiler/runtime changes: `bin/ludic-dev reseed && bin/ludic-dev bootstrap-cfree` still reaches the self-hosting fixpoint with no C compiler in the loop. - [ ] `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` - (`bin/ludic dev docs-gen && bin/ludic dev docs-check build/pages` passes). + (`bin/ludic-dev docs-gen && bin/ludic-dev 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/bootstrap.yml b/.forgejo/workflows/bootstrap.yml index 6de1bdcd..926e4922 100644 --- a/.forgejo/workflows/bootstrap.yml +++ b/.forgejo/workflows/bootstrap.yml @@ -43,11 +43,11 @@ jobs: set -eu mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic + bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev - name: Rebuild the compiler from the seed and assert byte-identity - # `ludic dev bootstrap-cfree` assembles the seed with clang, has that seed + # `ludic-dev bootstrap-cfree` assembles the seed with clang, has that seed # compiler recompile selfhost.ludic to out.ll, and `cmp`s out.ll against # the checked-in seed. It returns non-zero if they differ — i.e. if the # seed is stale relative to the compiler source. - run: bin/ludic dev bootstrap-cfree + run: bin/ludic-dev bootstrap-cfree diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 877b81ef..afd5c567 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -2,7 +2,7 @@ name: ci # Build the language toolchain from its IR seed and run the regression suites on # every push to main and every pull request. Until this landed the only workflow -# was docs.yml, so nothing gated a change on `ludic dev test` / `ludic dev test-tools` or on the +# was docs.yml, so nothing gated a change on `ludic-dev test` / `ludic-dev test-tools` or on the # compiler even building from the seed. See also bootstrap.yml, which proves the # C-free self-rebuild reproduces the seed byte-for-byte. on: @@ -45,7 +45,7 @@ jobs: # The toolchain is macOS-first; on this Linux runner it links against a # tiny C-free IR shim that supplies the Darwin standard-stream globals # (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through - # LUDIC_CC so every clang invocation — the seed bootstrap, `ludic dev build`, + # LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`, # and each compiled test program — picks it up. Absolute path so it # still resolves if a step changes directory. echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" @@ -60,20 +60,20 @@ jobs: # pre-built binaries: the language builds itself from source + seed. mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic - bin/ludic dev build + bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev + bin/ludic-dev build - - name: Regression suite (ludic dev test) - run: bin/ludic dev test + - name: Regression suite (ludic-dev test) + run: bin/ludic-dev test - - name: Editor-toolchain suite (ludic dev test-tools) + - name: Editor-toolchain suite (ludic-dev test-tools) # Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's # formatting contract — hand alignment is deliberately preserved, so the # gate is fmt(fmt(x)) == fmt(x), not fmt(x) == x), and the JSON/XML editor # assets. Cross-file LSP behaviour and the golden renders are macOS-ABI # bound and skip here — visibly — until the runtime's directory walk and # windowing are portable. - run: bin/ludic dev test-tools + run: bin/ludic-dev test-tools - name: Docs cover the implementation run: | @@ -81,9 +81,9 @@ jobs: # 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 `ludic dev test-tools`.) - bin/ludic dev check-impl - bin/ludic dev check-vocabulary - bin/ludic dev check-docs - bin/ludic dev docs-gen --out build/pages - bin/ludic dev docs-check build/pages + # + integrity guard. (check-vocabulary also runs in `ludic-dev test-tools`.) + bin/ludic-dev check-impl + bin/ludic-dev check-vocabulary + bin/ludic-dev check-docs + bin/ludic-dev docs-gen --out build/pages + bin/ludic-dev docs-check build/pages diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml index 79913cd8..fd8f5f20 100644 --- a/.forgejo/workflows/docs.yml +++ b/.forgejo/workflows/docs.yml @@ -38,7 +38,7 @@ jobs: runs-on: docker # The generator is now Ludic, so this builds the toolchain from its IR seed # (clang assembles the seed into bin/ludicc, which compiles bin/ludic) exactly - # like the ci workflow, then runs `ludic dev docs-gen`. node:20-bookworm carries git + # like the ci workflow, then runs `ludic-dev 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: @@ -67,9 +67,9 @@ jobs: 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/ludic-cli/main.ludic -o bin/ludic - bin/ludic dev docs-gen --out ../public - bin/ludic dev docs-check ../public + bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev + bin/ludic-dev docs-gen --out ../public + bin/ludic-dev docs-check ../public cd .. echo "--- generated files ---" ls -la public diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a55715fb..45e928e8 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -1,6 +1,6 @@ name: release -# Cutting a release is `ludic dev release` + `git push --tags`; everything after that +# Cutting a release is `ludic-dev release` + `git push --tags`; everything after that # happens here. Before this workflow existed the artifacts were built on whatever # machine the maintainer happened to be sitting at, from whatever was in bin/ at # the time, with no checksums and nothing proving the tagged tree even passed its @@ -71,15 +71,15 @@ jobs: set -eu mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic - bin/ludic dev build + bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev + bin/ludic-dev build - name: The tagged tree must pass its own suites run: | set -eu - bin/ludic dev test - bin/ludic dev test-tools - bin/ludic dev bootstrap-cfree + bin/ludic-dev test + bin/ludic-dev test-tools + bin/ludic-dev bootstrap-cfree - name: Publish the release env: @@ -91,9 +91,9 @@ jobs: echo "::error::No FORGEJO_TOKEN secret; cannot create the release." exit 1 fi - # ludic dev publish builds dist/ (source tarball from the tag, this host's + # ludic-dev publish builds dist/ (source tarball from the tag, this host's # toolchain, SHA256SUMS), takes the notes from the CHANGELOG section, # and creates the release. Re-running it only adds missing assets, so # a maintainer can afterwards attach the macOS toolchain from a Mac # with the same command. - bin/ludic dev publish "$TAG" + bin/ludic-dev publish "$TAG" diff --git a/.gitignore b/.gitignore index 5ba1fb6d..ed4671a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Generated build tree: LLVM IR, objects, compiled apps, the headless render -# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic dev build` / +# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic-dev build` / # `bin/ludic clean`). Root-anchored so a source dir named "build" elsewhere is never # accidentally ignored. Nothing is written to the repo root any more. /build/ @@ -9,8 +9,8 @@ # packaged plugin .zip are local-only build inputs/outputs. *.zip -# the toolchain binaries (ludicc, ludic, ludic-fmt, ludic-lsp) — all built -# into bin/ by the one-line bootstrap + `bin/ludic dev build`; never checked in. The +# the toolchain binaries (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp) — all built +# into bin/ by the one-line bootstrap + `bin/ludic-dev build`; never checked in. The # only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll). /bin/ @@ -39,7 +39,7 @@ tools/editors/jetbrains/build/ __pycache__/ *.pyc -# Release artifacts produced by `ludic dev release` +# Release artifacts produced by `ludic-dev release` /dist/ # Build/release tarballs anywhere in the tree. `git -C archive -o foo.tgz` diff --git a/COMPILING.md b/COMPILING.md index f300416e..2fd42133 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -25,8 +25,8 @@ > ```bash > # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic > mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -> bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic -> bin/ludic dev build # the whole toolchain into bin/ +> bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +> bin/ludic-dev build # the whole toolchain into bin/ > # (ludicc, ludic, ludic-fmt, ludic-lsp) > bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly > bin/ludic build examples/games/snake.ludic # or through the CLI @@ -320,7 +320,7 @@ node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss ``` Because Ludic is fixed-point and its RNG is seeded, the native headless binary -and the wasm one must render byte-identical frames from the same input. `bin/ludic dev test` +and the wasm one must render byte-identical frames from the same input. `bin/ludic-dev test` asserts exactly that, which is a much stronger check on the backend than "it started". @@ -334,13 +334,13 @@ entity allocator, save/load snapshots, the frame loop, the window, and the whole graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and the retained UI. -None of it goes through C. `bin/ludic dev test` asserts that directly: no C source +None of it goes through C. `bin/ludic-dev test` asserts that directly: no C source survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all build, run and render from IR alone. ## Every flag -The self-hosted `ludicc`/`ludic` (built with `bin/ludic dev build-cli`) accept: +The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept: ``` the program to compile (first non-flag argument) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a63b40e..d1c8128a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,23 +19,24 @@ From a clean checkout, one line lifts the toolchain off the seed: ```bash mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic +bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev ``` -That gives you `bin/ludic`, the CLI — the same binary users install, which also -carries the toolchain's own tasks under `ludic dev` and replaces every build/test -shell script in the repo. From then on it builds everything — including itself: +That gives you `bin/ludic-dev`, the contributor tool: it replaces every +build/test shell script in the repo and builds everything, including itself and +`bin/ludic`. It is deliberately a separate binary from the `ludic` users install +— that one carries none of these tasks and is never asked to. ```bash -bin/ludic dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp) -bin/ludic dev help # every contributor task +bin/ludic-dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp) +bin/ludic-dev help # every contributor task bin/ludic help # what a user of the language sees ``` -Always run `ludic dev` from the repository root, so `assets/` and `selfhost/` -resolve. (A checkout is an install root: `bin/` beside `runtime/` and -`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`. That is -why the same binary serves both.) +Always run `ludic-dev` from the repository root, so `assets/` and `selfhost/` +resolve. (A checkout is also an install root: `bin/` beside `runtime/` and +`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`, which is +why `bin/ludic` behaves there exactly as an installed one does.) ## The development loop @@ -43,17 +44,17 @@ When you change the compiler or runtime, prove the self-hosting fixpoint still holds before you push: ```bash -bin/ludic dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change -bin/ludic dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop -bin/ludic dev test # the full regression suite +bin/ludic-dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change +bin/ludic-dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop +bin/ludic-dev test # the full regression suite ``` Other useful targets: ```bash bin/ludic build [--headless] # compile a program to a native app in build/ -bin/ludic dev selfhost-test # correctness + bootstrap fixpoints -bin/ludic dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp) +bin/ludic-dev selfhost-test # correctness + bootstrap fixpoints +bin/ludic-dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp) bin/ludic clean # remove build/, out.ppm and stray artifacts ``` @@ -67,7 +68,7 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces and register its id in `tools/docgen/inventory.json`. Each documented namespace gets exactly **one** directory (the docs check enforces this). 3. Add or extend an example under `examples/` and a case in the test suite. -4. Run `bin/ludic dev docs-gen --out build/pages && bin/ludic dev docs-check build/pages` — the +4. Run `bin/ludic-dev docs-gen --out build/pages && bin/ludic-dev docs-check build/pages` — the check fails if any inventory symbol lacks a page or is still seed text. 5. Add a **changeset** for the user-facing change: a small file under [`changes/`](changes/README.md) with a `bump:` level and a one-line summary. @@ -82,22 +83,22 @@ Releases are changeset-driven. Every user-facing change ships with a changeset (step 5 above). Read the next release before cutting it: ```bash -ludic dev release --dry-run # render the CHANGELOG section, write nothing +ludic-dev release --dry-run # render the CHANGELOG section, write nothing ``` Then cut it: ```bash -ludic dev release [major|minor|patch] # omit the level to derive it from the changesets +ludic-dev release [major|minor|patch] # omit the level to derive it from the changesets git push origin main --follow-tags ``` -`ludic dev release` aggregates the pending changesets into a new `CHANGELOG.md` section +`ludic-dev release` aggregates the pending changesets into a new `CHANGELOG.md` section — grouped by change type, with each changeset's markdown kept intact — bumps `VERSION`, commits `chore(release): vX.Y.Z`, and tags it. **Pushing the tag is what publishes.** The `release` workflow builds the -toolchain from the IR seed, runs `ludic dev test`, `ludic dev test-tools` and `ludic dev bootstrap-cfree` +toolchain from the IR seed, runs `ludic-dev test`, `ludic-dev test-tools` and `ludic-dev bootstrap-cfree` against the tagged tree, and only then creates the Forgejo release — with the source tarball, a Linux toolchain build, a `.sha256` beside each, and that version's `CHANGELOG.md` section as the notes. It refuses to publish if the tag and @@ -113,10 +114,10 @@ macOS artifacts cannot be produced on the Linux runner — a `darwin-arm64` buil needs a macOS host, and there is no cross-compile path (it would need the Xcode SDK and a Mach-O linker). Attaching one therefore means either registering a macOS runner and giving it a job, or running the same command CI runs from a -Mac. Either way it is `ludic dev publish`, which only adds assets the release is missing: +Mac. Either way it is `ludic-dev publish`, which only adds assets the release is missing: ```bash -FORGEJO_TOKEN=… ludic dev publish v0.4.0 +FORGEJO_TOKEN=… ludic-dev publish v0.4.0 ``` Checksums are one `.sha256` file per artifact rather than a single `SHA256SUMS`, @@ -138,14 +139,14 @@ broken link at a time. Everything host-shaped has an environment override, so a move can be rehearsed before it is committed. **Hosts and URLs.** The install one-liner is served from the documentation site, -which publishes `install.sh` beside the pages that quote it (`ludic dev docs-gen` +which publishes `install.sh` beside the pages that quote it (`ludic-dev docs-gen` copies it in; `docs-check` fails without it). Change the host in: | Where | What | |---|---| | `install.sh` | `REPO_API`, `REPO_URL`, `INSTALL_URL` — each `${LUDIC_…:-default}`, so `LUDIC_REPO_URL=… sh install.sh` tests a move without editing anything | | `tools/ludic-cli/project.ludic` | `install_url()` (`$LUDIC_INSTALL_URL`), used by `ludic upgrade` and `ludic doctor` | -| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic dev publish` | +| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic-dev publish` | | `docs/site/site.json` | `repo_url`, the `start.terminal` one-liner, and the doc links in `nav_links` | | Prose | `README.md`, `COMPILING.md`, `tools/editors/README.md`, and the two editor plugins' "server not found" messages | @@ -155,9 +156,10 @@ copies it in; `docs-check` fails without it). Change the host in: `is_ludic_file`), every editor asset (`tools/editors/shared/*.json`, `vscode/package.json`, the JetBrains `LudicFileType`), and every source file in the tree. -- **The binaries** `ludic`, `ludicc`, `ludic-fmt`, `ludic-lsp` — `build.ludic`'s - `cmd_dev_build`, the release staging in `release.ludic`, `install.sh`, the - editors' executable-name lists. +- **The binaries** `ludic`, `ludicc`, `ludic-dev`, `ludic-fmt`, `ludic-lsp` — + `cmd_dev_build` in `toolchain.ludic`, the release staging in `release.ludic`, + `install.sh`, the editors' executable-name lists. Only the first, third and + fourth of those ship: `ludic-dev` is built from a checkout and stays there. - **The install root** `~/.ludic` and the source directories `tools/ludic-cli/`, `tools/ludic-tools/`, `packages/ludic.*`. - **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`, @@ -167,11 +169,11 @@ copies it in; `docs-check` fails without it). Change the host in: - **Identifiers that are contracts with other software**: the TextMate scope `source.ludic`, the VS Code language id `ludic`, the JetBrains plugin id `io.ludic.ide`, and the `ludic` code-fence tag understood by the Markdown - injection and by `ludic dev check-docs`. + injection and by `ludic-dev check-docs`. - **The prose**: `README.md`, `LANGUAGE.md`, `COMPILING.md`, `docs/**`, and `docs/site/site.json`'s `brand`/`meta`. -`ludic dev test` is the safety net for the mechanical part — it builds the +`ludic-dev test` is the safety net for the mechanical part — it builds the toolchain, stages an install, and runs `new` → `build` → `test` through it, so a half-finished rename fails there rather than in someone's terminal. @@ -239,7 +241,7 @@ non-destructive version of "tidy the history" without touching a single commit. ## Pull requests - Base your branch on `main`. -- Ensure `bin/ludic dev test` (and `bin/ludic dev bootstrap-cfree` for compiler/runtime changes) +- Ensure `bin/ludic-dev test` (and `bin/ludic-dev bootstrap-cfree` for compiler/runtime changes) pass, and that `ludic-fmt` leaves your files unchanged. - Fill in the PR template checklist. Reference the issue you close with `Closes #NN` in the description or a commit message. diff --git a/LANGUAGE.md b/LANGUAGE.md index 658db1ad..5933f8b9 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -1031,11 +1031,11 @@ ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR ``` `ludic` is the CLI (`ludic help`); `ludicc` is the compiler it drives, built from -the IR seed by `bin/ludic dev build-cli`. **[COMPILING.md](COMPILING.md) is the +the IR seed by `bin/ludic-dev build-cli`. **[COMPILING.md](COMPILING.md) is the authoritative CLI reference** — the full flag set (`-o`, `--windowed`, `--headless`, `--emit-llvm`, `--save-temps`, `--run`), the `LUDIC_HOME` / `LUDIC_CC` environment variables, and the IR-to-stdout bootstrap contract (no -`-o`) that `bin/ludic build` / `bin/ludic dev reseed` rely on. The default mode +`-o`) that `bin/ludic build` / `bin/ludic-dev reseed` rely on. The default mode is auto: a file with `handler`s links windowed, otherwise headless; an explicit flag always wins. @@ -1053,7 +1053,7 @@ duplicate types, unknown fields, arity) are future work. ### Editors ```bash -bin/ludic dev tools # -> bin/ludic-fmt, bin/ludic-lsp +bin/ludic-dev tools # -> bin/ludic-fmt, bin/ludic-lsp bin/ludic-fmt -w src/ # format in place (keeps comments) bin/ludic-fmt --check . # CI: exit 1 if anything is unformatted bin/ludic-lsp --stdio # the language server, for any editor @@ -1107,7 +1107,7 @@ self-hosting; their lowerings are in > **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks > compile; [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) runs and is checked -> by `bin/ludic dev test`. A scene lowers to a `machine` the compiler writes for you: one +> by `bin/ludic-dev test`. A scene lowers to a `machine` the compiler writes for you: one > implicit active-scene register, states numbered by declaration order, and > `become` as two direct calls plus a store. Richer scene features (the overlay > stack, scene-owned entities, scene-local state, transition parameters) are diff --git a/README.md b/README.md index 16990ed3..43c5abb7 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,18 @@ of them with `ludic build examples/games/snake.ludic`. ### Building from a checkout -Contributors work from the repository, where the same CLI carries the toolchain's -own tasks under `ludic dev`. Bootstrapping is the only step Ludic cannot do for -itself, since compiling Ludic needs a compiler — clang assembles the checked-in -IR seed, and that compiler builds the rest: +Contributors also get `ludic-dev`, a second binary carrying the toolchain's own +tasks — building the compiler, the suites, the docs site, releases. It is built +from a checkout and is not part of an install, so nothing a user runs is mixed +up with it. Bootstrapping is the only step Ludic cannot do for itself, since +compiling Ludic needs a compiler — clang assembles the checked-in IR seed, and +that compiler builds the rest: ```bash mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic -bin/ludic dev build # -> bin/{ludicc,ludic,ludic-fmt,ludic-lsp} -bin/ludic dev test # the regression suite +bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp} +bin/ludic-dev test # the regression suite ``` ## The language @@ -154,7 +156,7 @@ capability of the retired C compiler and has not been re-wired on the self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are in the same position. See [COMPILING.md](COMPILING.md). -Releases follow SemVer and are cut from changesets by `ludic dev release`, then built +Releases follow SemVer and are cut from changesets by `ludic-dev release`, then built and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md). ## Contributing diff --git a/changes/README.md b/changes/README.md index edcdb02c..1affea04 100644 --- a/changes/README.md +++ b/changes/README.md @@ -1,7 +1,7 @@ # Changesets A **changeset** is one small Markdown file describing a single user-facing change, -dropped in this directory. `ludic dev release` consumes every changeset here into a new +dropped in this directory. `ludic-dev release` consumes every changeset here into a new `CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files. ## Format @@ -14,7 +14,7 @@ the changelog. Markdown is fine. ``` - `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped - by the **highest** level among the pending changesets (unless `ludic dev release ` + by the **highest** level among the pending changesets (unless `ludic-dev release ` overrides it). - `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It decides which group the change lands in: `feat` → **Features**, `fix` → @@ -47,5 +47,5 @@ filename works except this `README.md`, which the release step always skips. Preview how the next release will read before cutting it — this writes nothing: ```bash -ludic dev release --dry-run +ludic-dev release --dry-run ``` diff --git a/changes/split-dev-cli.md b/changes/split-dev-cli.md new file mode 100644 index 00000000..ca889407 --- /dev/null +++ b/changes/split-dev-cli.md @@ -0,0 +1,16 @@ +bump: minor +type: refactor +**`ludic` is only the language's command line now.** The toolchain's own tasks — +building the compiler from its IR seed, the regression suites, the docs site, +releases — moved out of it into a separate `ludic-dev` binary that is built from +a checkout and is not part of an install. + +- **`ludic help` is what a user can actually do**: `new`, `run`, `build`, `test`, + `add`, `fmt`, `lsp`, `doctor`, `upgrade`. No section about a repository they do + not have. Typing `ludic dev …` says where those tasks went rather than failing + as an unknown command. +- **`ludic dev ` becomes `ludic-dev `** for contributors; every task + is otherwise unchanged. The bootstrap is now + `bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev build`. +- The shipped binary drops from ~880 KB to ~190 KB, since none of the release, + docs-generation or bootstrap machinery is linked into it any more. diff --git a/docs/CONTROLLERS.md b/docs/CONTROLLERS.md index 8ebb23af..8e95e086 100644 --- a/docs/CONTROLLERS.md +++ b/docs/CONTROLLERS.md @@ -74,7 +74,7 @@ LUDIC_MODULES=packages ludicc examples/games/platformer_demo.ludic -o platformer Worked, self-checking examples for every controller live in `examples/games/` (`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`) and `examples/library/gameplay_foundation.ludic`; each is a deterministic regression -case in `ludic dev test`. +case in `ludic-dev test`. ## What we deliberately do NOT do diff --git a/docs/language/testing/kw-test.md b/docs/language/testing/kw-test.md index b0f88df8..e424afdc 100644 --- a/docs/language/testing/kw-test.md +++ b/docs/language/testing/kw-test.md @@ -19,10 +19,10 @@ Assertions (each records a failure and prints file:line: … failed Run a spec file directly with the compiler-runner — `ludic mymath_test.ludic` compiles it to a native binary, runs it, and forwards the pass/fail exit code — so it drops straight into `bin/ludic` and CI. -Line coverage. Compile with the --coverage flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by $LUDIC_COVERAGE (default ludic.cov) as a FILE <name> header followed by one <line> <hits> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. bin/ludic dev test --coverage compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached: +Line coverage. Compile with the --coverage flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by $LUDIC_COVERAGE (default ludic.cov) as a FILE <name> header followed by one <line> <hits> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. bin/ludic-dev test --coverage compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached: ``` -== line coverage (bin/ludic dev test --coverage) == +== line coverage (bin/ludic-dev test --coverage) == examples/library/coverage.ludic 16/17 lines 94% uncovered: 33 examples/library/testing.ludic 17/17 lines 100% ---- diff --git a/examples/README.md b/examples/README.md index 71546d33..c2ed8deb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,7 @@ # Examples A curated tour of Ludic, grouped by intent. Every example here is exercised by -the test suite (`bin/ludic dev test` / `bin/ludic dev selfhost-test`), so nothing in this +the test suite (`bin/ludic-dev test` / `bin/ludic-dev selfhost-test`), so nothing in this directory silently rots. Run any program straight from the repository root (so `assets/` resolves): diff --git a/examples/lang/detach.ludic b/examples/lang/detach.ludic index 5322e9e4..d2f9ee99 100644 --- a/examples/lang/detach.ludic +++ b/examples/lang/detach.ludic @@ -11,7 +11,7 @@ # 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20 # 0 the Shield is gone — nothing matches # -# bin/ludic dev game-build bin/ludicc examples/lang/detach.ludic /tmp/detach +# bin/ludic-dev game-build bin/ludicc examples/lang/detach.ludic /tmp/detach # /tmp/detach quit(); Hud.Draw paints the last frame # -# bin/ludic dev game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes +# bin/ludic-dev game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes # printf 'aaaa' | /tmp/scenes program SceneDemo { var counter: int = 0 diff --git a/examples/library/coverage.ludic b/examples/library/coverage.ludic index bbd903a9..59895081 100644 --- a/examples/library/coverage.ludic +++ b/examples/library/coverage.ludic @@ -1,6 +1,6 @@ -# coverage.ludic — a spec built to be measured by `bin/ludic dev test --coverage` (issue +# coverage.ludic — a spec built to be measured by `bin/ludic-dev test --coverage` (issue # #45). Compiled with `--coverage`, every statement bumps a per-source-line hit -# counter; at exit the counts are dumped and `bin/ludic dev test --coverage` turns them +# counter; at exit the counts are dumped and `bin/ludic-dev test --coverage` turns them # into a per-file line-coverage report. # # The tests below exercise `sign` fully but only the taken branches of `grade`, diff --git a/install.sh b/install.sh index f4bc70c5..dadf09e2 100755 --- a/install.sh +++ b/install.sh @@ -199,15 +199,22 @@ install_from_source() { git clone --quiet --depth 1 --branch "v${VERSION}" "${REPO_URL}.git" "$_src" 2>/dev/null \ || git clone --quiet --depth 1 "${REPO_URL}.git" "$_src" \ || die "could not clone ${REPO_URL}" - # A release older than the CLI cannot produce one, and the build would fail - # deep inside clang with nothing pointing at the cause. - [ -f "${_src}/tools/ludic-cli/main.ludic" ] || die "release v${VERSION} predates the ludic CLI; install a newer one with --version X.Y.Z" + # Which entry point builds the toolchain depends on the age of the tag: the + # contributor tool was split out of the CLI after 0.5.2, so an older release + # still bootstraps through main.ludic. A release older than either predates the + # CLI entirely, and saying so beats failing deep inside clang. + if [ -f "${_src}/tools/ludic-cli/dev.ludic" ]; then + _boot='bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev build' + elif [ -f "${_src}/tools/ludic-cli/main.ludic" ]; then + _boot='bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic && bin/ludic dev build' + else + die "release v${VERSION} predates the ludic CLI; install a newer one with --version X.Y.Z" + fi CC=clang; have clang || CC=cc ( cd "$_src" \ && mkdir -p bin \ && "$CC" selfhost/ludicc.seed.ll -o bin/ludicc 2>/dev/null \ - && bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic \ - && bin/ludic dev build >/dev/null ) || die "the source build failed" + && eval "$_boot" >/dev/null ) || die "the source build failed" _stage="${TMP}/stage" mkdir -p "${_stage}/bin" diff --git a/selfhost/backend/stdlib/emit_color.ludic b/selfhost/backend/stdlib/emit_color.ludic index eccc99b1..e13bcd09 100644 --- a/selfhost/backend/stdlib/emit_color.ludic +++ b/selfhost/backend/stdlib/emit_color.ludic @@ -5,7 +5,7 @@ # 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 `ludic dev docs-palette` from the single source-of-truth palette table +# GENERATED by `ludic-dev docs-palette` from the single source-of-truth palette table # in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit. # ============================================================================ diff --git a/selfhost/main.ludic b/selfhost/main.ludic index f55d664d..18eaac57 100644 --- a/selfhost/main.ludic +++ b/selfhost/main.ludic @@ -12,7 +12,7 @@ # # This is the compiler, not the command line a user of the language meets: that # is `ludic` (tools/ludic-cli), which drives this. With no -o and no --run, IR -# still goes to stdout — the contract `ludic dev reseed` and `ludic dev +# still goes to stdout — the contract `ludic-dev reseed` and `ludic dev # bootstrap` rely on, so the bootstrap is untouched. # basename: the part of a path after the last '/'. @@ -124,7 +124,7 @@ entry { # a game gets a window by default; a plain program stays headless. An explicit # flag always wins. The stdout-IR path (no target) also stays headless, which - # is what `ludic dev reseed` compiles the compiler itself with. + # is what `ludic-dev reseed` compiles the compiler itself with. let has_target = run or (out != null) if want == 1 { g_windowed = true } else if want == 2 { g_windowed = false } diff --git a/tools/docgen/README.md b/tools/docgen/README.md index afe0e5cb..b35e9551 100644 --- a/tools/docgen/README.md +++ b/tools/docgen/README.md @@ -10,7 +10,7 @@ docs/ language//.md one file per symbol — keyword, type, phase, builtin, namespace method, operator, annotation language//_section.md section title + blurb + order - language/colors/palette.json the 221 named colors (generated by `ludic dev docs-palette`) + language/colors/palette.json the 221 named colors (generated by `ludic-dev docs-palette`) site/site.json landing-page messaging (hero, features, …) site/snippets/*.ludic the code shown on the landing page (real programs) tools/docgen/inventory.json the authoritative symbol set the coverage guard checks @@ -66,9 +66,9 @@ no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and `inventory.json` are the only inputs the tools here still read directly. ```bash -bin/ludic dev docs-gen --out build/pages # generate the whole site -bin/ludic dev docs-check build/pages # coverage + duplicate-token + link guard -bin/ludic dev check-docs # parse every ```ludic doc fence +bin/ludic-dev docs-gen --out build/pages # generate the whole site +bin/ludic-dev docs-check build/pages # coverage + duplicate-token + link guard +bin/ludic-dev check-docs # parse every ```ludic doc fence ``` `docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token @@ -79,13 +79,13 @@ built `bin/ludic` (bootstrapped from the IR seed with clang alone). ## Publish `.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs -`ludic dev docs-gen` + `ludic dev docs-check` on every push to `main` that touches `docs/**`, +`ludic-dev docs-gen` + `ludic-dev docs-check` on every push to `main` that touches `docs/**`, `tools/docgen/**` or `tools/ludic-cli/**`, and publishes the result to the `pages` branch root. `index.html` + `.nojekyll` always stay at the root. ## Colors `palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated -by `bin/ludic dev docs-palette` from a single palette table — the `pal_add(...)` rows in +by `bin/ludic-dev docs-palette` from a single palette table — the `pal_add(...)` rows in `tools/ludic-cli/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the generated files. diff --git a/tools/editors/README.md b/tools/editors/README.md index 82016f63..9f59b52c 100644 --- a/tools/editors/README.md +++ b/tools/editors/README.md @@ -31,11 +31,11 @@ it needs no path configuration. From a checkout, build them with: ```bash -bin/ludic dev tools +bin/ludic-dev tools ``` Produces `bin/ludic-fmt` and `bin/ludic-lsp`. Add `--install` to symlink both -into `~/.local/bin`, `--test` to run `bin/ludic dev test-tools` afterwards. +into `~/.local/bin`, `--test` to run `bin/ludic-dev test-tools` afterwards. ## What you get, in any editor @@ -118,7 +118,7 @@ ln -sf ../../tools/git-hooks/pre-commit .git/hooks/pre-commit The hook runs `ludic-fmt --check` over the staged `.ludic` and `.md` files only, and does nothing at all when `bin/ludic-fmt` has not been built — so it never -blocks a commit on a machine that has not run `bin/ludic dev tools`. +blocks a commit on a machine that has not run `bin/ludic-dev tools`. ## Why the formatter is not `ludicc --fmt` @@ -140,7 +140,7 @@ both spellings are what the language documents and uses: (`const R_DIR: int = 0 # 0 up`) survive a save; - `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight. -`bin/ludic dev test-tools` checks the property that matters: formatting every file in +`bin/ludic-dev test-tools` checks the property that matters: formatting every file in the tree and re-running the *compiler's* canonical dump produces byte-identical output. The formatter cannot change what a program means. @@ -149,8 +149,8 @@ output. The formatter cannot change what a program means. The vocabulary is written down in five places that cannot include each other — the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent -failure, so `bin/ludic dev check-vocabulary` (written in Ludic) compares all five, and -`bin/ludic dev test-tools` runs it. +failure, so `bin/ludic-dev check-vocabulary` (written in Ludic) compares all five, and +`bin/ludic-dev test-tools` runs it. When you add a keyword or builtin: put it in `ludic_syntax.h`, then run -`bin/ludic dev test-tools` and let it tell you which copies still need it. +`bin/ludic-dev test-tools` and let it tell you which copies still need it. diff --git a/tools/editors/jetbrains/README.md b/tools/editors/jetbrains/README.md index 13049d1b..a563c56f 100644 --- a/tools/editors/jetbrains/README.md +++ b/tools/editors/jetbrains/README.md @@ -43,7 +43,7 @@ The first build downloads an IntelliJ IDEA Community distribution (over 1 GB), so expect it to take a while; later builds take seconds. Once the cache is warm, ```bash -LUDIC_TEST_JETBRAINS=1 bin/ludic dev test-tools +LUDIC_TEST_JETBRAINS=1 bin/ludic-dev test-tools ``` includes the plugin build in the toolchain's own test run. @@ -55,4 +55,4 @@ LSP4IJ is a required dependency; the IDE offers to install it for you. **Settings -> Languages & Frameworks -> Ludic** — the path to `ludic-lsp` and to `ludicc`. Both default to `bin/` under the project root, which is where -`bin/ludic dev tools` puts them. +`bin/ludic-dev tools` puts them. diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt index cc1c4f4c..8cb39e28 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt @@ -17,7 +17,7 @@ import java.io.File /** * Where to find the toolchain. Both paths may be left blank, in which case the - * plugin looks under the project root for what `bin/ludic dev tools` produces — + * plugin looks under the project root for what `bin/ludic-dev tools` produces — * which is the case for anyone working in the Ludic repository itself. */ @Service(Service.Level.PROJECT) @@ -121,7 +121,7 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne append(" on PATH, and in ~/.ludic/bin.\n") } append("\nInstall the toolchain (curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh), ") - append("build it with `bin/ludic dev tools` from a checkout, ") + append("build it with `bin/ludic-dev tools` from a checkout, ") append("or set the path in Settings | Languages & Frameworks | Ludic. ") append("Syntax highlighting works without it; completion and diagnostics do not.") } diff --git a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml index f2b11170..51c515d9 100644 --- a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml +++ b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml @@ -67,7 +67,7 @@ name="Ludic" factoryClass="io.ludic.ide.LudicLanguageServerFactory"> bin/ludic dev tools. + The Ludic language server. Built by bin/ludic-dev tools. ]]> diff --git a/tools/editors/vscode/README.md b/tools/editors/vscode/README.md index 6bbfde27..c7cd3bc4 100644 --- a/tools/editors/vscode/README.md +++ b/tools/editors/vscode/README.md @@ -23,7 +23,7 @@ for the language reference. Build the toolchain once, from the repository root: ```bash -bin/ludic dev tools +bin/ludic-dev tools ``` That produces `bin/ludic-lsp` and `bin/ludic-fmt`, which the extension finds diff --git a/tools/editors/vscode/src/extension.js b/tools/editors/vscode/src/extension.js index b576913c..a6aa6db9 100644 --- a/tools/editors/vscode/src/extension.js +++ b/tools/editors/vscode/src/extension.js @@ -98,7 +98,7 @@ async function start(context) { // Not an error worth a modal: the grammar still highlights, and plenty of // people open a .ludic file without having built the toolchain. output.appendLine( - 'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic dev tools`, or set "ludic.server.path". ' + + 'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic-dev tools`, or set "ludic.server.path". ' + 'Syntax highlighting works without it; completion and diagnostics do not.' ); return; diff --git a/tools/ludic-cli/build.ludic b/tools/ludic-cli/build.ludic index 640b27ce..146afd98 100644 --- a/tools/ludic-cli/build.ludic +++ b/tools/ludic-cli/build.ludic @@ -1,56 +1,30 @@ -# build.ludic — compiling: a user's program (ludic build / ludic run) and the -# toolchain's own binaries (ludic dev build). +# build.ludic — turning a .ludic file into an executable. # -# compile_app is the one place a .ludic file becomes an executable, so a project -# build and the repo's own example builds cannot drift apart. Everything the -# toolchain owns — the compiler, the engine runtime it splices — is addressed -# through ludic_home(), so the same code path works from a checkout and from an -# installed ~/.ludic. +# compile_app is the one place that happens, shared by `ludic build` / `ludic +# run` and by the contributor tool, so a user's build and the repo's own example +# builds cannot drift apart. Everything the toolchain owns — the compiler, the +# engine runtime it splices — is addressed through ludic_home(), so the same code +# path works from a checkout and from an installed ~/.ludic. -# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). -# Returns true on success. Used for x itself and the editor tools. -function build_tool(name: pointer, src: pointer) -> bool { - let ll = `build/{name}.ll` - if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } - # write to a temp then move, so a running bin/ludic can rebuild itself in place - if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false } - run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`) - return true -} - -# ---- build-cli: the self-hosted front-end binaries --------------------------- -# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed -# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just -# compiles. -function cmd_dev_build_cli() -> int { +# ensure bin/ludicc exists and is current with the seed. Built from the checked-in +# IR seed with clang alone — no C compiler is ever involved. +function ensure_ludicc() -> void { + if not in_toolchain_repo() { return } run("mkdir -p bin build") - print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") - if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 } - run("chmod +x bin/ludicc") - print("done. built bin/ludicc") - return 0 -} - -# ---- build: the whole toolchain (ludicc, ludic, ludic-fmt, ludic-lsp) ------- -function cmd_dev_build() -> int { - if cmd_dev_build_cli() != 0 { return 1 } - print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI rebuilds itself)") - if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 } - print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt") - if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 } - print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp") - if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 } - sync_vscode_grammar() - print("done. toolchain in bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)") - return 0 + if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") { + print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") + if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { + err("ludic: could not assemble the seed\n"); exit(1) + } + } } # ---- compiling one program to a native binary ------------------------------- # # mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is # rendered to build/out.ppm from piped input — deterministic output for tests). -# Returns true on success. `ludic build`, `ludic run` and `ludic dev app` all go -# through here. +# Returns true on success. `ludic build`, `ludic run` and the contributor tool all +# go through here. function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool { ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale let home = ludic_home() @@ -91,7 +65,7 @@ function dir_of_path(p: pointer) -> pointer { # build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A # legacy out.ppm in the root (from an older toolchain) is swept too. Binaries in # bin/ are kept so the running CLI survives; in the toolchain repo, rebuild them -# any time with `ludic dev build`. +# any time with `ludic-dev build`. function cmd_clean() -> int { run("rm -rf build") run("rm -f out.ppm bin/*.tmp") diff --git a/tools/ludic-cli/checks.ludic b/tools/ludic-cli/checks.ludic index 804375cf..6d692fb3 100644 --- a/tools/ludic-cli/checks.ludic +++ b/tools/ludic-cli/checks.ludic @@ -9,75 +9,6 @@ # plain indexing; `==` on pointers is a byte-string compare (the whole toolchain # leans on this). -# ---- small string helpers --------------------------------------------------- - -# length of a NUL-terminated buffer -function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n } - -# a fresh NUL-terminated copy of s[start .. end) (end exclusive) -function sslice(s: pointer, start: int, end: int) -> pointer { - if end < start { return "" } - let n = end - start - let b = bytes(n + 1) - var i = 0 - while i < n { b[i] = s[start + i]; i += 1 } - b[n] = 0 - return b -} - -# index of the first byte of `needle` in `hay` at or after `from`, else -1 -function s_index(hay: pointer, needle: pointer, from: int) -> int { - let hn = slen(hay) - let nn = slen(needle) - if nn == 0 { return from } - var i = from - while i + nn <= hn { - var j = 0 - while j < nn and hay[i + j] == needle[j] { j += 1 } - if j == nn { return i } - i += 1 - } - return -1 -} - -function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 } - -# does `hay` contain `needle` exactly at position `at`? -function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool { - let nn = slen(needle) - var i = 0 - while i < nn { if hay[at + i] != needle[i] { return false }; i += 1 } - return true -} - -# does `s` (a whole line) begin with `pre`? -function s_starts(s: pointer, pre: pointer) -> bool { - let pn = slen(pre) - var i = 0 - while i < pn { if s[i] != pre[i] { return false }; i += 1 } - return true -} - -# is byte c an ASCII space/tab? -function is_ws(c: int) -> bool { return c == ' ' or c == '\t' } - -# the substring from `start` up to the next '\n' (or end) -function line_at(s: pointer, start: int) -> pointer { - var e = start - while s[e] != 0 and s[e] != '\n' { e += 1 } - return sslice(s, start, e) -} - -# trim leading/trailing ASCII whitespace (space, tab, cr, nl) -function s_trim(s: pointer) -> pointer { - let n = slen(s) - var a = 0 - while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 } - var b = n - while b > a and (is_ws(s[b - 1]) or s[b - 1] == '\n' or s[b - 1] == '\r') { b -= 1 } - return sslice(s, a, b) -} - # ============================================================================ # check-docs — every ```ludic fence in the docs must parse (or be marked) # ============================================================================ @@ -177,7 +108,7 @@ function count_nl(s: pointer, upto: int) -> int { return n } -# usage: ludic dev check-docs (scans the user-facing docs + docs/language/**) +# usage: ludic-dev check-docs (scans the user-facing docs + docs/language/**) function cmd_check_docs() -> int { ensure_ludicc() DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = "" @@ -361,7 +292,7 @@ var CI_PROB: pointer = "" var CI_NPROB: int = 0 function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 } -# usage: ludic dev check-impl +# usage: ludic-dev check-impl function cmd_check_impl() -> int { CI_PROB = ""; CI_NPROB = 0 let allsrc = read_all_selfhost() @@ -568,7 +499,7 @@ function set_union(a: []pointer, b: []pointer) -> []pointer { return out } -# usage: ludic dev check-vocabulary +# usage: ludic-dev check-vocabulary function cmd_check_vocab() -> int { CV_PROB = ""; CV_N = 0 let h = read_file("tools/ludic-tools/ludic_syntax.h") @@ -702,10 +633,10 @@ function xml_valid(text: pointer) -> bool { return sp == 0 } -# usage: ludic dev lint-asset (validates one .json or .xml editor asset) +# usage: ludic-dev lint-asset (validates one .json or .xml editor asset) function cmd_lint_asset() -> int { - if arg_total() < 3 { err("usage: ludic dev lint-asset \n"); return 2 } - let path = arg_n(2) + if arg_count() < 3 { err("usage: ludic-dev lint-asset \n"); return 2 } + let path = arg(2) let t = read_file(path) if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 } if s_index(path, ".json", 0) >= 0 { diff --git a/tools/ludic-cli/dev.ludic b/tools/ludic-cli/dev.ludic new file mode 100644 index 00000000..dd28829d --- /dev/null +++ b/tools/ludic-cli/dev.ludic @@ -0,0 +1,137 @@ +# ludic-dev — the contributor tool for the Ludic toolchain itself: building the +# compiler from its IR seed, the regression suites, the docs site, releases. +# +# It is a separate binary from `ludic` on purpose. Those two have different +# audiences and nothing in common but the compile step: a person who installed +# the language cannot bootstrap a compiler they do not have the source of, and +# should not have to read past a screen of tasks that only mean something inside +# this repository. So `ludic` ships and `ludic-dev` does not — it is built from a +# checkout, lives in bin/, and is left out of every release artifact. +# +# From a clean checkout (the one step Ludic cannot do for itself, since compiling +# Ludic needs a compiler): +# +# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc +# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +# bin/ludic-dev build +# +# Thereafter `ludic-dev build` rebuilds the whole toolchain — bin/ludic, this +# tool, the compiler, the editor tools. Run it from the repository root. +program LudicDev { + import "prelude.ludic" + import "build.ludic" + import "toolchain.ludic" + import "selfhost.ludic" + import "tools.ludic" + import "selfhost_test.ludic" + import "test.ludic" + import "json.ludic" + import "lsp_test.ludic" + import "forgejo.ludic" + import "checks.ludic" + import "docgen.ludic" + import "docgen_gen.ludic" + import "docgen_check.ludic" + import "release.ludic" + import "pkg.ludic" + import "pkg_test.ludic" + + function usage() -> void { + print("ludic-dev — tasks for a checkout of the Ludic toolchain (run from the repository root)") + print("") + print("This is the contributor tool. The language's own command line is `ludic`,") + print("which ships to users and carries none of the below.") + print("") + print("build:") + print(" build build the whole toolchain into bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)") + print(" build-cli build just bin/ludicc from the IR seed") + print(" tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)") + print(" clean remove build/") + print("") + print("test:") + print(" test the full regression suite") + print(" test --coverage per-file line coverage over the test specs") + print(" test-pkg the package-manager suite (hermetic git repos, offline)") + print(" selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)") + print(" test-tools the editor-toolchain suite") + print(" test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)") + print(" golden regenerate selfhost/golden/renders.sha256 (review with git diff)") + print("") + print("doc / lint checks (Ludic, no Python):") + print(" check-docs every ```ludic doc fence parses (or is marked skip/expect-error)") + print(" check-impl every implemented feature has a docs/language page") + print(" check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") + print(" lint-asset validate one editor .json / .xml asset") + print(" docs-gen [--out DIR] generate the documentation site (default build/pages)") + print(" docs-check [DIR] coverage/integrity guard over a generated docs site") + print(" docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table") + print("") + print("release:") + print(" release [major|minor|patch] [--dry-run] [--publish]") + print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)") + print(" publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push") + print(" changelog-section print that release's CHANGELOG.md section") + print(" changelog-render ") + print(" render a CHANGELOG section from a directory of changesets") + print("") + print("self-host internals:") + print(" selfhost-build [ludicc] [out] assemble + compile the self-host compiler") + print(" bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)") + print(" bootstrap-cfree rebuild the compiler from the seed with no C compiler") + print(" reseed regenerate selfhost/ludicc.seed.ll after a compiler change") + print(" game-build compile a Ludic game (headless)") + print(" sh-compile compile + link one .ludic file") + } + + function dispatch(cmd: pointer) -> int { + if (cmd == "build") { return cmd_dev_build() } + if (cmd == "build-cli") { return cmd_dev_build_cli() } + if (cmd == "tools") { return cmd_tools() } + if (cmd == "clean") { return cmd_clean() } + if (cmd == "test") { + if (argn(2, "") == "--coverage") { return cmd_test_coverage() } + return cmd_dev_test() + } + if (cmd == "selfhost-test") { return cmd_selfhost_test() } + if (cmd == "test-tools") { return cmd_test_tools() } + if (cmd == "test-lsp") { return cmd_test_lsp() } + if (cmd == "test-pkg") { return cmd_test_pkg() } + if (cmd == "check-docs") { return cmd_check_docs() } + if (cmd == "check-impl") { return cmd_check_impl() } + if (cmd == "check-vocabulary") { return cmd_check_vocab() } + if (cmd == "lint-asset") { return cmd_lint_asset() } + if (cmd == "docs-palette") { return cmd_docs_palette() } + if (cmd == "docs-gen") { return cmd_docs_gen() } + if (cmd == "docs-check") { return cmd_docs_check() } + if (cmd == "golden") { return cmd_golden() } + if (cmd == "bootstrap") { return cmd_bootstrap() } + if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() } + if (cmd == "reseed") { return cmd_reseed() } + if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost")) } + if (cmd == "sh-compile") { + if (arg_count() < 5) { err("usage: ludic-dev sh-compile \n"); exit(1) } + return cmd_sh_compile(arg(2), arg(3), arg(4)) + } + if (cmd == "game-build") { + if (arg_count() < 5) { err("usage: ludic-dev game-build \n"); exit(1) } + return cmd_game_build(arg(2), arg(3), arg(4)) + } + if (cmd == "release") { return cmd_release() } + if (cmd == "changelog-render") { return cmd_changelog_render() } + if (cmd == "changelog-section") { return cmd_changelog_section() } + if (cmd == "publish") { return cmd_publish() } + if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 } + return -1 + } + + entry { + if (arg_count() < 2) { usage(); exit(1) } + let cmd = arg(1) + let code = dispatch(cmd) + tmp_cleanup() + if code >= 0 { exit(code) } + err(`ludic-dev: unknown task '{cmd}'\n`) + usage() + exit(1) + } +} diff --git a/tools/ludic-cli/docgen.ludic b/tools/ludic-cli/docgen.ludic index fac191d2..bba7c340 100644 --- a/tools/ludic-cli/docgen.ludic +++ b/tools/ludic-cli/docgen.ludic @@ -1,17 +1,17 @@ # docgen.ludic — the Ludic documentation-site generator and its guards, ported # off Python (gen.py / check.py / palette.py). Three x subcommands: # -# ludic dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic + +# ludic-dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic + # docs/language/colors/palette.json from the palette # table below (the named-colour source of truth). -# ludic dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset +# ludic-dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset # templates, emit every page + symbols.json + the # highlighter + .nojekyll into DIR (default build/pages). -# ludic dev docs-check [DIR] the coverage/integrity guard over a generated site. +# ludic-dev docs-check [DIR] the coverage/integrity guard over a generated site. # # String work is over NUL-terminated byte buffers; helpers here mirror gen.py's # html.escape / json.dumps / front-matter parsing closely enough to reproduce the -# Python generator's output byte-for-byte (verified against the oracle in ludic dev test). +# Python generator's output byte-for-byte (verified against the oracle in ludic-dev test). # ---- a growable byte buffer (avoids O(n^2) string building) ------------------ property Sb { data: pointer = null, len: int = 0, cap: int = 0 } @@ -87,40 +87,6 @@ function lstrip_nl(s: pointer) -> pointer { while s[i] == '\n' { i += 1 } return sslice(s, i, slen(s)) } -# lowercase ASCII A-Z -function lower_ascii(s: pointer) -> pointer { - let n = slen(s) - let b = bytes(n + 1) - var i = 0 - while i < n { - var c = s[i] - if c >= 'A' and c <= 'Z' { c += 32 } - b[i] = c - i += 1 - } - b[n] = 0 - return b -} -# Python str.title(): capitalise the first letter of each alpha run, lower the rest -function title_case(s: pointer) -> pointer { - let n = slen(s) - let b = bytes(n + 1) - var i = 0 - var prev_alpha = false - while i < n { - var c = s[i] - let al = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') - if al { - if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } } - else { if c >= 'a' and c <= 'z' { c -= 32 } } - } - b[i] = c - prev_alpha = al - i += 1 - } - b[n] = 0 - return b -} # lexicographic byte compare: <0, 0, >0 function str_cmp(a: pointer, b: pointer) -> int { var i = 0 @@ -563,7 +529,7 @@ function palette_emit_ludic(path: pointer) -> bool { sb_puts(b, "# no allocation, identical codegen to writing the hex by hand. Unknown names are\n") sb_puts(b, "# a compile error (color_lookup returns -1, which emit_expr reports).\n") sb_puts(b, "#\n") - sb_puts(b, "# GENERATED by `ludic dev docs-palette` from the single source-of-truth palette table\n") + sb_puts(b, "# GENERATED by `ludic-dev docs-palette` from the single source-of-truth palette table\n") sb_puts(b, "# in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n") sb_puts(b, "# ============================================================================\n") sb_puts(b, "\n") @@ -620,11 +586,11 @@ function palette_emit_json(path: pointer) -> bool { return write_file(path, sb_str(b)) } -# `ludic dev docs-palette` rewrites the two tracked outputs; `ludic dev docs-palette --check` +# `ludic-dev docs-palette` rewrites the two tracked outputs; `ludic-dev docs-palette --check` # regenerates into scratch files and compares them to the tracked ones instead, # so the drift guard judges the working tree, not git HEAD. function cmd_docs_palette() -> int { - let check = arg_total() > 2 and arg_n(2) == "--check" + let check = arg_count() > 2 and arg(2) == "--check" palette_init() # guard: duplicate names let seen = new []pointer @@ -640,8 +606,8 @@ function cmd_docs_palette() -> int { if not palette_emit_ludic(ludic_out) { err("cannot write emit_color.ludic\n"); return 1 } if not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 } if check { - if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 } - if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 } + if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic-dev docs-palette)\n"); return 1 } + if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic-dev docs-palette)\n"); return 1 } } print(`OK {string(len(pal_names))} colors`) return 0 diff --git a/tools/ludic-cli/docgen_check.ludic b/tools/ludic-cli/docgen_check.ludic index 1c3af2bb..79e9e6ce 100644 --- a/tools/ludic-cli/docgen_check.ludic +++ b/tools/ludic-cli/docgen_check.ludic @@ -1,4 +1,4 @@ -# docgen_check.ludic — `ludic dev docs-check [DIR]`: the coverage / integrity guard over +# docgen_check.ludic — `ludic-dev docs-check [DIR]`: the coverage / integrity guard over # a generated docs site, ported from tools/docgen/check.py. Fails (exit 1) if the # pages contract is broken, an inventory symbol lacks a source file or page, a # token is documented on two pages, a namespace/section is split across dirs, or @@ -94,7 +94,7 @@ function drop_md(fn: pointer) -> pointer { function cmd_docs_check() -> int { var site = "build/pages" - if arg_total() >= 3 { site = arg_n(2) } + if arg_count() >= 3 { site = arg(2) } let problems = new []pointer let warnings = new []pointer diff --git a/tools/ludic-cli/docgen_gen.ludic b/tools/ludic-cli/docgen_gen.ludic index ba62d8b7..bad731a0 100644 --- a/tools/ludic-cli/docgen_gen.ludic +++ b/tools/ludic-cli/docgen_gen.ludic @@ -1,9 +1,9 @@ -# docgen_gen.ludic — `ludic dev docs-gen`: the documentation-site generator, ported from +# docgen_gen.ludic — `ludic-dev docs-gen`: the documentation-site generator, ported from # tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and # docs/site/*, reads the asset templates in tools/docgen/assets, and emits the # whole pages-branch payload (per-symbol pages, namespace/color/api pages, the # landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output -# is byte-for-byte identical to the Python generator (gated in ludic dev test). +# is byte-for-byte identical to the Python generator (gated in ludic-dev test). # # Shared helpers (Sb, hesc/hescattr, the JSON writer, codeify, string utils) # live in docgen.ludic; the JSON reader (json_parse/j_get) in json.ludic. @@ -951,11 +951,11 @@ function render_highlighter(sym: JVal) -> pointer { function cmd_docs_gen() -> int { var out = "build/pages" var i = 2 - while i < arg_total() { - let a = arg_n(i) + while i < arg_count() { + let a = arg(i) if a == "--out" { i += 1 - if i < arg_total() { out = arg_n(i) } + if i < arg_count() { out = arg(i) } } i += 1 } diff --git a/tools/ludic-cli/lsp_test.ludic b/tools/ludic-cli/lsp_test.ludic index fa30de84..f625ca87 100644 --- a/tools/ludic-cli/lsp_test.ludic +++ b/tools/ludic-cli/lsp_test.ludic @@ -1,5 +1,5 @@ # lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real -# source tree (`ludic dev test-lsp`; replaces tools/test-lsp.py). Every assertion is +# source tree (`ludic-dev test-lsp`; replaces tools/test-lsp.py). Every assertion is # about a feature an editor actually calls: if this passes, VS Code, JetBrains, # Neovim and the rest are exercising code paths that have been checked, because # they all speak to this one binary. diff --git a/tools/ludic-cli/main.ludic b/tools/ludic-cli/main.ludic index 7453b247..ef5ef63b 100644 --- a/tools/ludic-cli/main.ludic +++ b/tools/ludic-cli/main.ludic @@ -1,13 +1,13 @@ -# ludic — the command-line interface to the Ludic toolchain. One native binary +# ludic — the command-line interface to the Ludic language. One native binary # (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and # the compiler it drives are invoked through the `run` intrinsic. # -# Two audiences, one binary. A user of the language sees a small surface — -# new, build, run, test, add, fmt — that works from any directory against an -# installed toolchain. A contributor to the toolchain itself gets everything -# else under `ludic dev`: bootstrapping the compiler from its IR seed, the -# regression suites, the docs site, releases. Splitting them by namespace keeps -# `ludic help` about the language rather than about the repository. +# This is everything a user of the language does: create a project, build it, run +# it, test it, add a package, format, upgrade. Nothing here is about the +# toolchain's own repository — building the compiler, reseeding it, generating +# the docs site, cutting a release live in a separate tool (dev.ludic -> +# bin/ludic-dev) that only exists in a checkout and is never installed. Someone +# who installs Ludic should not have to read past commands they cannot use. # # Installing (macOS, Linux): # @@ -17,29 +17,13 @@ # compiling Ludic needs a compiler: # # mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc -# bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic -# bin/ludic dev build -# -# Thereafter `ludic dev build` rebuilds the whole toolchain — including the CLI -# itself. The `dev` tasks run from the repository root. +# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev +# bin/ludic-dev build program Ludic { import "prelude.ludic" - import "selfhost.ludic" import "build.ludic" import "project.ludic" - import "tools.ludic" - import "selfhost_test.ludic" - import "test.ludic" - import "json.ludic" - import "lsp_test.ludic" - import "forgejo.ludic" - import "checks.ludic" - import "docgen.ludic" - import "docgen_gen.ludic" - import "docgen_check.ludic" - import "release.ludic" import "pkg.ludic" - import "pkg_test.ludic" function usage() -> void { print("ludic — the toolchain for the Ludic language") @@ -71,61 +55,9 @@ program Ludic { print(" lsp run the language server on stdio (what editors spawn)") print(" help this message") print("") - print("contributing to the toolchain itself:") - print(" dev build, test, bootstrap, docs, release (ludic dev help)") - print("") print("A file argument runs it: ludic mygame.ludic") } - function dev_usage() -> void { - print("ludic dev — tasks for a checkout of the Ludic toolchain (run from the repository root)") - print("") - print("build:") - print(" dev build build the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)") - print(" dev build-cli build just bin/ludicc from the IR seed") - print(" dev tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)") - print("") - print("test:") - print(" dev test the full regression suite") - print(" dev test --coverage per-file line coverage over the test specs") - print(" dev test-pkg the package-manager suite (hermetic git repos, offline)") - print(" dev selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)") - print(" dev test-tools the editor-toolchain suite") - print(" dev test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)") - print(" dev golden regenerate selfhost/golden/renders.sha256 (review with git diff)") - print("") - print("doc / lint checks (Ludic, no Python):") - print(" dev check-docs every ```ludic doc fence parses (or is marked skip/expect-error)") - print(" dev check-impl every implemented feature has a docs/language page") - print(" dev check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") - print(" dev lint-asset validate one editor .json / .xml asset") - print(" dev docs-gen [--out DIR] generate the documentation site (default build/pages)") - print(" dev docs-check [DIR] coverage/integrity guard over a generated docs site") - print(" dev docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table") - print("") - print("release:") - print(" dev release [major|minor|patch] [--dry-run] [--publish]") - print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)") - print(" dev publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push") - print(" dev changelog-section print that release's CHANGELOG.md section") - print(" dev changelog-render ") - print(" render a CHANGELOG section from a directory of changesets") - print("") - print("self-host internals:") - print(" dev selfhost-build [ludicc] [out] assemble + compile the self-host compiler") - print(" dev bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)") - print(" dev bootstrap-cfree rebuild the compiler from the seed with no C compiler") - print(" dev reseed regenerate selfhost/ludicc.seed.ll after a compiler change") - print(" dev game-build compile a Ludic game (headless)") - print(" dev sh-compile compile + link one .ludic file") - } - - # a positional argument, or a default when absent - function argn(i: int, dflt: pointer) -> pointer { - if (i < arg_count()) { return arg(i) } - return dflt - } - # does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape # the compiler-runner had before the CLI grew subcommands. function is_ludic_file(s: pointer) -> bool { @@ -134,55 +66,6 @@ program Ludic { return sslice(s, n - 6, n) == ".ludic" } - # ---- ludic dev — the toolchain repo's own tasks --------------------- - # Every task here reads its arguments one position further along than a - # top-level command, so the shift is set once, here. - function dispatch_dev(cmd: pointer) -> int { - g_shift = 1 - if (cmd == "build") { return cmd_dev_build() } - if (cmd == "build-cli") { return cmd_dev_build_cli() } - if (cmd == "tools") { return cmd_tools() } - if (cmd == "clean") { return cmd_clean() } - if (cmd == "test") { - if (argn(3, "") == "--coverage") { return cmd_test_coverage() } - return cmd_dev_test() - } - if (cmd == "selfhost-test") { return cmd_selfhost_test() } - if (cmd == "test-tools") { return cmd_test_tools() } - if (cmd == "test-lsp") { return cmd_test_lsp() } - if (cmd == "test-pkg") { return cmd_test_pkg() } - if (cmd == "check-docs") { return cmd_check_docs() } - if (cmd == "check-impl") { return cmd_check_impl() } - if (cmd == "check-vocabulary") { return cmd_check_vocab() } - if (cmd == "lint-asset") { return cmd_lint_asset() } - if (cmd == "docs-palette") { return cmd_docs_palette() } - if (cmd == "docs-gen") { return cmd_docs_gen() } - if (cmd == "docs-check") { return cmd_docs_check() } - if (cmd == "golden") { return cmd_golden() } - if (cmd == "bootstrap") { return cmd_bootstrap() } - if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() } - if (cmd == "reseed") { return cmd_reseed() } - if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(3, "bin/ludicc"), argn(4, "build/selfhost")) } - if (cmd == "sh-compile") { - if (arg_count() < 6) { err("usage: ludic dev sh-compile \n"); exit(1) } - return cmd_sh_compile(arg(3), arg(4), arg(5)) - } - if (cmd == "game-build") { - if (arg_count() < 6) { err("usage: ludic dev game-build \n"); exit(1) } - return cmd_game_build(arg(3), arg(4), arg(5)) - } - if (cmd == "release") { return cmd_release() } - if (cmd == "changelog-render") { return cmd_changelog_render() } - if (cmd == "changelog-section") { return cmd_changelog_section() } - if (cmd == "publish") { return cmd_publish() } - if (cmd == "help") or (cmd == "--help") or (cmd == "-h") or (cmd == "") { dev_usage(); return 0 } - # reported here rather than by entry, so the message names the dev task the - # user actually typed and the usage shown is the dev one - err(`ludic dev: unknown task '{cmd}'\n`) - dev_usage() - return 1 - } - # route one subcommand to its implementation and return its exit code; -1 when # the name is unknown. Returning (rather than exiting in place) lets entry tear # down the per-process scratch directory before the process ends. @@ -206,13 +89,23 @@ program Ludic { if (cmd == "link-flags") { return cmd_pkg_link_flags() } if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() } - if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 } - if (cmd == "dev") { return dispatch_dev(argn(2, "")) } + if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 } + + # The toolchain's own tasks used to live here under `ludic dev`. Say where + # they went rather than printing "unknown command" at someone who read an + # older README or a commit message. + if (cmd == "dev") { + err("ludic: the toolchain's own tasks live in `ludic-dev`, which is built from a\n") + err(" checkout of the Ludic repository and is not part of an install:\n") + err(" clang selfhost/ludicc.seed.ll -o bin/ludicc\n") + err(" bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev help\n") + return 1 + } return -1 } entry { - if (arg_count() < 2) { usage(); exit(1) } + if (arg_count() < 2) { usage(); exit(1) } let cmd = arg(1) # `ludic mygame.ludic` — build and run that file if is_ludic_file(cmd) { diff --git a/tools/ludic-cli/pkg.ludic b/tools/ludic-cli/pkg.ludic index 1a89327f..74017c41 100644 --- a/tools/ludic-cli/pkg.ludic +++ b/tools/ludic-cli/pkg.ludic @@ -539,8 +539,8 @@ function set_require(module: pointer, ver: pointer) -> void { # ludic add [@version] — add/update a dependency then install function cmd_pkg_add() -> int { - if arg_total() < 3 { err("usage: ludic add [@version]\n"); return 1 } - let spec = split_spec(arg_n(2)) + if arg_count() < 3 { err("usage: ludic add [@version]\n"); return 1 } + let spec = split_spec(arg(2)) let module = spec[0] var ver = spec[1] if slen(ver) == 0 { diff --git a/tools/ludic-cli/prelude.ludic b/tools/ludic-cli/prelude.ludic index a8d78538..0e151d79 100644 --- a/tools/ludic-cli/prelude.ludic +++ b/tools/ludic-cli/prelude.ludic @@ -1,16 +1,14 @@ -# prelude.ludic — the shared runtime for `ludic`, the command-line interface. +# prelude.ludic — the shared runtime for both command-line programs: `ludic`, +# which a user of the language runs, and `ludic-dev`, the contributor tool that +# builds the toolchain itself. Both are native binaries written in Ludic and +# compiled by Ludic, driving clang, the compiler and the unix tools through +# `run`. This fragment is the tiny standard library their commands lean on: +# process control, file IO, strings and a colored PASS/FAIL test harness. It +# carries no ECS, so both link as plain CLI programs. # -# `ludic` is the one tool a user of the language ever runs: it creates projects, -# compiles and runs them, resolves packages, formats, tests, and (under -# `ludic dev`) drives every build/bootstrap/release task of the toolchain repo -# itself. It is a single native binary written in Ludic and compiled by Ludic, -# driving clang, the compiler and the unix tools through `run`. This fragment is -# the tiny standard library the commands lean on: process control, file IO, -# string trimming and a colored PASS/FAIL test harness. It carries no ECS, so it -# links as a plain CLI program. -# -# The user-facing commands work from any directory. The `ludic dev` tasks run -# relative to the current directory and expect the toolchain repo root. +# `ludic`'s commands work from any directory, against whatever toolchain is +# installed. `ludic-dev`'s tasks run relative to the current directory and expect +# the toolchain repo root. # ---- file IO ---------------------------------------------------------------- @@ -53,7 +51,7 @@ function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 } # ---- scratch files ------------------------------------------------------------ # Every scratch file the runner writes lives under one per-process directory -# (`$TMPDIR/x_`), so `ludic dev test` and an `x check-*` can run side by side without +# (`$TMPDIR/x_`), so `ludic-dev test` and an `x check-*` can run side by side without # clobbering each other's captures. main removes it on the way out. var x_tmp: pointer = null @@ -175,17 +173,11 @@ function report() -> int { return 1 } -# ---- command arguments ------------------------------------------------------- -# -# A command reads its own arguments as arg_n(1), arg_n(2)… whether it was -# reached as `ludic get` or as `ludic dev docs-check DIR`. main sets the shift -# once per dispatch, so no command has to know how deep its namespace is — -# getting that wrong is how `docs-check DIR` silently checked the wrong -# directory. -var g_shift: int = 0 - -function arg_n(i: int) -> pointer { return arg(i + g_shift) } -function arg_total() -> int { return arg_count() - g_shift } +# a positional argument, or a default when it is absent +function argn(i: int, dflt: pointer) -> pointer { + if (i < arg_count()) { return arg(i) } + return dflt +} # ---- the toolchain install --------------------------------------------------- # @@ -249,8 +241,121 @@ function tool(name: pointer) -> pointer { function ludicc() -> pointer { return tool("ludicc") } -# true in a checkout of the toolchain repo itself, where the `ludic dev` tasks -# have something to work on. +# true in a checkout of the toolchain repo itself, where a seed is present to +# assemble the compiler from. function in_toolchain_repo() -> bool { return file_exists("selfhost/ludicc.seed.ll") and file_exists("tools/ludic-cli/main.ludic") } + +# ---- strings ---------------------------------------------------------------- +# Shared by every command, so they live here rather than in whichever file +# happened to need them first. + +# length of a NUL-terminated buffer +function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n } + +# a fresh NUL-terminated copy of s[start .. end) (end exclusive) +function sslice(s: pointer, start: int, end: int) -> pointer { + if end < start { return "" } + let n = end - start + let b = bytes(n + 1) + var i = 0 + while i < n { b[i] = s[start + i]; i += 1 } + b[n] = 0 + return b +} + +# index of the first byte of `needle` in `hay` at or after `from`, else -1 +function s_index(hay: pointer, needle: pointer, from: int) -> int { + let hn = slen(hay) + let nn = slen(needle) + if nn == 0 { return from } + var i = from + while i + nn <= hn { + var j = 0 + while j < nn and hay[i + j] == needle[j] { j += 1 } + if j == nn { return i } + i += 1 + } + return -1 +} + +function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 } + +# does `hay` contain `needle` exactly at position `at`? +function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool { + let nn = slen(needle) + var i = 0 + while i < nn { if hay[at + i] != needle[i] { return false }; i += 1 } + return true +} + +# does `s` (a whole line) begin with `pre`? +function s_starts(s: pointer, pre: pointer) -> bool { + let pn = slen(pre) + var i = 0 + while i < pn { if s[i] != pre[i] { return false }; i += 1 } + return true +} + +# is byte c an ASCII space/tab? +function is_ws(c: int) -> bool { return c == ' ' or c == '\t' } + +# the substring from `start` up to the next '\n' (or end) +function line_at(s: pointer, start: int) -> pointer { + var e = start + while s[e] != 0 and s[e] != '\n' { e += 1 } + return sslice(s, start, e) +} + +# trim leading/trailing ASCII whitespace (space, tab, cr, nl) +function s_trim(s: pointer) -> pointer { + let n = slen(s) + var a = 0 + while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 } + var b = n + while b > a and (is_ws(s[b - 1]) or s[b - 1] == '\n' or s[b - 1] == '\r') { b -= 1 } + return sslice(s, a, b) +} + +# lowercase ASCII A-Z +function lower_ascii(s: pointer) -> pointer { + let n = slen(s) + let b = bytes(n + 1) + var i = 0 + while i < n { + var c = s[i] + if c >= 'A' and c <= 'Z' { c += 32 } + b[i] = c + i += 1 + } + b[n] = 0 + return b +} + +# Python str.title(): capitalise the first letter of each alpha run, lower the rest +function title_case(s: pointer) -> pointer { + let n = slen(s) + let b = bytes(n + 1) + var i = 0 + var prev_alpha = false + while i < n { + var c = s[i] + let al = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') + if al { + if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } } + else { if c >= 'a' and c <= 'z' { c -= 32 } } + } + b[i] = c + prev_alpha = al + i += 1 + } + b[n] = 0 + return b +} + +# ---- the C toolchain -------------------------------------------------------- +# clang assembles the emitted IR and drives the linker. -Wno-override-module for +# the same reason the compiler itself passes it (see selfhost/main.ludic): the IR +# names no target triple, so clang substitutes the host's and warns every time. +function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") + " -Wno-override-module" } diff --git a/tools/ludic-cli/project.ludic b/tools/ludic-cli/project.ludic index a4c5e823..c30742f1 100644 --- a/tools/ludic-cli/project.ludic +++ b/tools/ludic-cli/project.ludic @@ -131,11 +131,11 @@ function template_readme(name: pointer) -> pointer { # ludic new — scaffold a project that builds and runs as it stands. function cmd_new() -> int { - if arg_total() < 3 { + if arg_count() < 3 { err("usage: ludic new \n") return 1 } - let name = arg_n(2) + let name = arg(2) if file_exists(name) { err(`ludic new: {name} already exists\n`) return 1 @@ -173,12 +173,12 @@ function parse_build_args(start: int) -> pointer { g_out = "" g_save = false var ai = start - while ai < arg_total() { - let a = arg_n(ai) + while ai < arg_count() { + let a = arg(ai) if a == "--headless" { g_mode = 2 } else if a == "--windowed" { g_mode = 1 } else if a == "--save-temps" { g_save = true } - else if a == "-o" { ai += 1; if ai < arg_total() { g_out = arg_n(ai) } } + else if a == "-o" { ai += 1; if ai < arg_count() { g_out = arg(ai) } } else if a[0] != '-' { src = a } ai += 1 } @@ -245,7 +245,7 @@ function test_files() -> []pointer { function cmd_test() -> int { var files = new []pointer var ai = 2 - while ai < arg_total() { if arg_n(ai)[0] != '-' { push(files, arg_n(ai)) }; ai += 1 } + while ai < arg_count() { if arg(ai)[0] != '-' { push(files, arg(ai)) }; ai += 1 } if len(files) == 0 { files = test_files() } if len(files) == 0 { err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n") @@ -298,7 +298,7 @@ function strip_ext(p: pointer) -> pointer { function cmd_fmt() -> int { var args = "" var ai = 2 - while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 } + while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 } if args == "" { let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort") if found == "" { @@ -315,7 +315,7 @@ function cmd_fmt() -> int { function cmd_lsp() -> int { var args = "" var ai = 2 - while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 } + while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 } return sh(`exec {tool("ludic-lsp")}{args}`) } @@ -377,6 +377,25 @@ function cmd_doctor() -> int { return 1 } +# ludic version — report the version of the toolchain this CLI belongs to. +# +# It asks the compiler (proving that path works), addressed through ludic_home() +# rather than as a relative bin/ludicc: `ludic version` is run from a user's +# project, not from the toolchain's own directory, and looking beside the current +# directory found nothing there and reported "(version unknown)" for a perfectly +# good install. +function cmd_version() -> int { + let cc = ludicc() + if is_exec(cc) { + run(`{cc} --version`) + return 0 + } + let v = read_file(`{ludic_home()}VERSION`) + if v != null { print(`ludic {s_trim(v)}`); return 0 } + print("ludic (version unknown)") + return 0 +} + # ---- ludic upgrade ---------------------------------------------------------- function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") } @@ -386,7 +405,7 @@ function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https # drift from a fresh install. function cmd_upgrade() -> int { var ver = "" - if arg_total() >= 3 { ver = arg_n(2) } + if arg_count() >= 3 { ver = arg(2) } if not shq("command -v curl >/dev/null 2>&1") { err("ludic upgrade: needs curl\n") return 1 diff --git a/tools/ludic-cli/release.ludic b/tools/ludic-cli/release.ludic index 091b6de5..bfbcc028 100644 --- a/tools/ludic-cli/release.ludic +++ b/tools/ludic-cli/release.ludic @@ -1,13 +1,13 @@ # release.ludic — versioning + release cutting for the toolchain. # # ludic version print the toolchain version (from the VERSION file) -# ludic dev release [level] cut a release: aggregate changes/ into CHANGELOG.md, +# ludic-dev release [level] cut a release: aggregate changes/ into CHANGELOG.md, # bump VERSION, commit, and tag vX.Y.Z. `level` is # major|minor|patch; omitted, it is derived from the # highest `bump:` among the pending changesets. -# ludic dev release [level] --dry-run render the changelog section to stdout and +# ludic-dev release [level] --dry-run render the changelog section to stdout and # stop: nothing is written, committed, tagged or pushed. -# ludic dev release [level] --publish ...then push main + the tag and create a +# ludic-dev release [level] --publish ...then push main + the tag and create a # Forgejo release with source + toolchain tarballs. # Needs FORGEJO_TOKEN in the environment. # @@ -49,24 +49,6 @@ function compute_next(cur: pointer, level: pointer) -> pointer { return `{maj}.{min}.{p2}` } -# ludic version — report the version of the toolchain this CLI belongs to. -# -# It asks the compiler (proving that path works), addressed through ludic_home() -# rather than as a relative bin/ludicc: `ludic version` is run from a user's -# project, not from the toolchain's own directory, and looking beside the current -# directory found nothing there and reported "(version unknown)" for a perfectly -# good install. -function cmd_version() -> int { - let cc = ludicc() - if is_exec(cc) { - run(`{cc} --version`) - return 0 - } - let v = read_file(`{ludic_home()}VERSION`) - if v != null { print(`ludic {s_trim(v)}`); return 0 } - print(`ludic {read_version_or("(version unknown)")}`) - return 0 -} # ---- changelog rendering ---------------------------------------------------- # @@ -241,16 +223,16 @@ function build_section(ver: pointer) -> void { write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes")) } -# ludic dev changelog-render — print the CHANGELOG section that +# ludic-dev changelog-render — print the CHANGELOG section that # `dir`'s changesets would produce. Used to re-render the sections of releases # cut before the renderer preserved markdown structure: check the changesets out # of the tag's parent commit, point this at them, and splice the result back in. function cmd_changelog_render() -> int { - if arg_total() < 5 { - err("usage: ludic dev changelog-render \n") + if arg_count() < 5 { + err("usage: ludic-dev changelog-render \n") return 1 } - out(render_section(arg_n(2), arg_n(3), arg_n(4))) + out(render_section(arg(2), arg(3), arg(4))) return 0 } @@ -258,7 +240,7 @@ function cmd_changelog_render() -> int { # release section (or at the end of the header if this is the first release). function prepend_changelog() -> void { if not file_exists("CHANGELOG.md") { - run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md") + run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic-dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md") } let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1") if (ln == "") { @@ -297,11 +279,11 @@ function changelog_section(ver: pointer) -> pointer { return sslice(text, start, n) } -# ludic dev changelog-section — print that release's CHANGELOG section. +# ludic-dev changelog-section — print that release's CHANGELOG section. function cmd_changelog_section() -> int { - if arg_total() < 3 { err("usage: ludic dev changelog-section \n"); return 1 } - let sec = changelog_section(arg_n(2)) - if slen(sec) == 0 { err(`changelog-section: no section for v{arg_n(2)} in CHANGELOG.md\n`); return 1 } + if arg_count() < 3 { err("usage: ludic-dev changelog-section \n"); return 1 } + let sec = changelog_section(arg(2)) + if slen(sec) == 0 { err(`changelog-section: no section for v{arg(2)} in CHANGELOG.md\n`); return 1 } out(sec) return 0 } @@ -343,7 +325,7 @@ function build_artifacts(ver: pointer) -> bool { let stage = `{tmp_dir()}/{name}` run(`rm -rf {stage} && mkdir -p {stage}/bin`) if not shq(`cp bin/ludic bin/ludicc bin/ludic-fmt bin/ludic-lsp {stage}/bin/`) { - err("release: the toolchain is not built (run: ludic dev build)\n") + err("release: the toolchain is not built (run: ludic-dev build)\n") return false } run(`cp -R runtime {stage}/runtime`) @@ -366,10 +348,10 @@ function build_artifacts(ver: pointer) -> bool { return true } -# ludic dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts, +# ludic-dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts, # take the notes from that version's CHANGELOG section, and create the Forgejo # release. Defaults to the version in VERSION. This is what CI runs on a tag -# push, and what `ludic dev release --publish` calls once it has tagged. +# push, and what `ludic-dev release --publish` calls once it has tagged. function publish_tag(ver: pointer) -> int { if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 } let body = tmp_path("rel_notes.md") @@ -386,8 +368,8 @@ function publish_tag(ver: pointer) -> int { function cmd_publish() -> int { var ver = read_version_or("") - if arg_total() >= 3 { - var a = arg_n(2) + if arg_count() >= 3 { + var a = arg(2) if s_starts(a, "v") { a = sslice(a, 1, slen(a)) } ver = a } @@ -409,8 +391,8 @@ function cmd_release() -> int { var publish = false var dry = false var ai = 2 - while ai < arg_total() { - let a = arg_n(ai) + while ai < arg_count() { + let a = arg(ai) if (a == "--publish") { publish = true } else { if (a == "--dry-run") { dry = true } else { if (a == "major") or (a == "minor") or (a == "patch") { level = a } @@ -437,7 +419,7 @@ function cmd_release() -> int { let sec = read_file(tmp_path("rel_section.md")) if sec != null { out(sec) } print("") - print(` dry run — nothing written. cut it with: ludic dev release {level}`) + print(` dry run — nothing written. cut it with: ludic-dev release {level}`) return 0 } prepend_changelog() @@ -452,7 +434,7 @@ function cmd_release() -> int { print(` committed + tagged v{ver}`) if publish { return publish_release(ver) } - print(` local release ready. publish with: FORGEJO_TOKEN=… ludic dev release {level} --publish`) + print(` local release ready. publish with: FORGEJO_TOKEN=… ludic-dev release {level} --publish`) print(` (or push: git push origin HEAD && git push origin v{ver})`) return 0 } diff --git a/tools/ludic-cli/selfhost.ludic b/tools/ludic-cli/selfhost.ludic index be911287..d7f0d509 100644 --- a/tools/ludic-cli/selfhost.ludic +++ b/tools/ludic-cli/selfhost.ludic @@ -6,10 +6,6 @@ # now lives here once, in selfhost_frags(). # the C toolchain driver (clang) that assembles and links the emitted IR -# The C toolchain used to assemble and link IR. -Wno-override-module for the -# same reason the compiler itself passes it (see selfhost/main.ludic): the IR -# names no target triple, so clang substitutes the host's and warns every time. -function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") + " -Wno-override-module" } # the self-host compiler's source fragments, in link order. This is THE list; # the old scripts each carried their own copy. @@ -95,21 +91,9 @@ function line_count(path: pointer) -> pointer { return capture_line(`wc -l < {path}`) } -# ensure bin/ludicc exists and is current with the seed. Built from the checked-in -# IR seed with clang alone — no C compiler is ever involved. -function ensure_ludicc() -> void { - if not in_toolchain_repo() { return } - run("mkdir -p bin build") - if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") { - print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") - if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { - err("ludic: could not assemble the seed\n"); exit(1) - } - } -} # ---- selfhost-build: assemble + compile the self-host compiler --------------- -# usage: ludic dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost) +# usage: ludic-dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost) function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int { run("mkdir -p build") let src = "build/selfhost.ludic" @@ -123,7 +107,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int { } # ---- sh-compile: compile one .ludic with a given selfhost binary and link ---- -# usage: ludic dev sh-compile +# usage: ludic-dev sh-compile function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int { if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 } if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 } @@ -144,7 +128,7 @@ function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool { return true } -# usage: ludic dev game-build +# usage: ludic-dev game-build function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int { if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 } print("build failed:") @@ -194,7 +178,7 @@ function cmd_bootstrap_cfree() -> int { print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler") return 0 } - print(" out.ll != seed.ll (seed is stale — regenerate with: ludic dev reseed)") + print(" out.ll != seed.ll (seed is stale — regenerate with: ludic-dev reseed)") out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head")) return 1 } @@ -203,7 +187,7 @@ function cmd_bootstrap_cfree() -> int { # Uses the CURRENT seed to build the new one, compiling twice so the seed is a # fixed point of the NEW compiler, not a one-step image of the old. Falls back to # bin/ludicc only if the seed itself no longer assembles. Verify with -# `ludic dev bootstrap-cfree` afterwards. +# `ludic-dev bootstrap-cfree` afterwards. function cmd_reseed() -> int { run("mkdir -p build/cfree") if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } diff --git a/tools/ludic-cli/selfhost_test.ludic b/tools/ludic-cli/selfhost_test.ludic index aa7223c6..adac72d6 100644 --- a/tools/ludic-cli/selfhost_test.ludic +++ b/tools/ludic-cli/selfhost_test.ludic @@ -19,7 +19,7 @@ function sh_case(name: pointer, exp: pointer) -> void { # framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256. # A hash is tiny and text-diffable — a regression shows as a changed hex line in a # review, not an opaque "binary files differ". Regenerate the baseline with -# `bin/ludic dev golden` (see cmd_golden below). +# `bin/ludic-dev golden` (see cmd_golden below). function game_case(path: pointer, keys: pointer) -> void { let nm = flat(path) if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return } @@ -32,13 +32,13 @@ function game_case(path: pointer, keys: pointer) -> void { if not is_darwin() { skip(`{path} render (golden hash blessed on macOS)`); return } let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1") let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`) - if (want == "") { bad2(path, "no golden hash (run: bin/ludic dev golden)"); return } + if (want == "") { bad2(path, "no golden hash (run: bin/ludic-dev golden)"); return } if (got == want) { ok(`{path} matches the golden render`) } else { bad2(path, `render hash {got} != golden {want}`) } } # render one golden game and return its " \n" manifest line (empty -# string on build failure). Shared by `bin/ludic dev golden` and nothing else. +# string on build failure). Shared by `bin/ludic-dev golden` and nothing else. function bless_render(path: pointer, keys: pointer) -> pointer { let nm = flat(path) if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) { @@ -117,12 +117,12 @@ function cmd_selfhost_test() -> int { game_case("rendering/draw_prims", "") print("== the bootstrap fixpoint (seeded from bin/ludicc) ==") - if shq(`bin/ludic dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") } + if shq(`bin/ludic-dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") } else { bad("fixpoint not reached"); out(capture(`cat {tmp_dir()}/boot.out`)) } print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==") - if shq(`bin/ludic dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") } - else { bad("C-free bootstrap failed (seed stale? run: ludic dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) } + if shq(`bin/ludic-dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") } + else { bad("C-free bootstrap failed (seed stale? run: ludic-dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) } return report() } diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 39fd24d4..7c957347 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -91,6 +91,16 @@ function install_layout_case() -> void { if ver != `ludic {s_trim(read_file("VERSION"))}` { bad2("an installed `ludic version` reports the install's VERSION", `got [{ver}]`); return } + # and the shipped CLI carries none of the contributor tasks: `ludic dev …` is + # not a command there, it is a pointer at the tool that has them. + let devlog = `{work}/dev.out` + let devrc = sh(`cd {work}/demo && {envp} ludic dev build > {devlog} 2>&1`) + var devout = read_file(devlog) + if devout == null { devout = "" } + if devrc == 0 or not s_contains(devout, "ludic-dev") { + bad2("an installed `ludic` must not carry the toolchain's own tasks", `rc {string(devrc)}, said [{s_trim(devout)}]`) + return + } ok(lbl) } @@ -161,7 +171,7 @@ function panic_case() -> void { else { bad2("panic", `rc={string(rc)} err=[{msg}]`) } } -# issue #45: `bin/ludic dev test --coverage`. Compile each test-spec with `--coverage`, +# issue #45: `bin/ludic-dev test --coverage`. Compile each test-spec with `--coverage`, # run it with LUDIC_COVERAGE pointed at a per-file dump, then aggregate the dumps # into a clean per-file line-coverage report. The instrumentation is flag-gated, # so this reuses the same seed-built bin/ludicc the rest of the suite does. @@ -211,7 +221,7 @@ function cmd_test_coverage() -> int { } COV_COVERED = 0 COV_TOTAL = 0 - print("== line coverage (bin/ludic dev test --coverage) ==") + print("== line coverage (bin/ludic-dev test --coverage) ==") cov_one("library/coverage", `{tmp_dir()}/cov_out`) cov_one("library/testing", `{tmp_dir()}/cov_out`) var pct = 100 @@ -244,11 +254,11 @@ function cmd_dev_test() -> int { game_case("games/chronorift", "ddddwwwwaassK") print("== self-hosting and the bootstrap fixpoints ==") - run(`bin/ludic dev selfhost-test > {tmp_dir()}/sh.out 2>&1`) + run(`bin/ludic-dev selfhost-test > {tmp_dir()}/sh.out 2>&1`) if shq(`grep -q 'gen2.ll == gen3.ll' {tmp_dir()}/sh.out`) { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") } if shq(`grep -q 'no C compiler' {tmp_dir()}/sh.out`) { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") } let corr = capture_line(`grep -c PASS {tmp_dir()}/sh.out`) - ok(`self-host suite: {corr} checks passed (see: ludic dev selfhost-test)`) + ok(`self-host suite: {corr} checks passed (see: ludic-dev selfhost-test)`) print("== documented syntax stays compilable (guards against spec/compiler drift) ==") qsmoke("lang/qdecl") @@ -405,9 +415,9 @@ function cmd_dev_test() -> int { print("== the compiler and the CLI (ludicc / ludic) ==") # ludicc comes out of the IR seed with clang alone; the CLI is then compiled # by it, from Ludic. - if shq(`bin/ludic dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") { - ok("ludic dev build-cli builds bin/ludicc from the seed (clang-only)") - } else { bad2("ludic dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) } + if shq(`bin/ludic-dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") { + ok("ludic-dev build-cli builds bin/ludicc from the seed (clang-only)") + } else { bad2("ludic-dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) } # ludicc -o drives clang to a native binary. A game links windowed by default # (LUDIC_HOME points the linker at runtime/native/cocoa.ll) — that path is @@ -456,27 +466,27 @@ function cmd_dev_test() -> int { # store + ludic_modules/ links + build, run as its own hermetic suite (it # stands up throwaway git repos, so like selfhost-test it runs as a sub-process # and this stage checks its footer). - print("== package manager (ludic dev test-pkg) ==") - run(`bin/ludic dev test-pkg > {tmp_dir()}/pkg.out 2>&1`) + print("== package manager (ludic-dev test-pkg) ==") + run(`bin/ludic-dev test-pkg > {tmp_dir()}/pkg.out 2>&1`) if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) { let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`) - ok(`package-manager suite: {pc} checks passed (see: ludic dev test-pkg)`) - } else { bad2("ludic dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) } + ok(`package-manager suite: {pc} checks passed (see: ludic-dev test-pkg)`) + } else { bad2("ludic-dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) } # docs site generator + guard, in Ludic (no Python). docs-gen emits the whole # pages payload; docs-check is its coverage/integrity guard; docs-palette is # the named-colour source of truth (its --check mode regenerates the tracked # emit_color.ludic + palette.json into scratch files and compares). print("== docs site (Ludic generator, no Python) ==") - if shq(`bin/ludic dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) { - ok("ludic dev docs-gen -> the pages-branch payload") - } else { bad2("ludic dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) } - if shq(`bin/ludic dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) { - ok("ludic dev docs-check passes on the generated site") - } else { bad2("ludic dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) } - if shq(`bin/ludic dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) { - ok("ludic dev docs-palette regenerates emit_color.ludic + palette.json byte-identically") - } else { bad2("ludic dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) } + if shq(`bin/ludic-dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) { + ok("ludic-dev docs-gen -> the pages-branch payload") + } else { bad2("ludic-dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) } + if shq(`bin/ludic-dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) { + ok("ludic-dev docs-check passes on the generated site") + } else { bad2("ludic-dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) } + if shq(`bin/ludic-dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) { + ok("ludic-dev docs-palette regenerates emit_color.ludic + palette.json byte-identically") + } else { bad2("ludic-dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) } return report() } diff --git a/tools/ludic-cli/toolchain.ludic b/tools/ludic-cli/toolchain.ludic new file mode 100644 index 00000000..61c1a3dd --- /dev/null +++ b/tools/ludic-cli/toolchain.ludic @@ -0,0 +1,44 @@ +# toolchain.ludic — building the toolchain's own binaries. Contributor-only: +# this is what `ludic-dev build` does, and none of it ships to a user. +# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). +# Returns true on success. Used for the CLI, the contributor tool and the editor +# tools — every binary the toolchain builds of itself. +function build_tool(name: pointer, src: pointer) -> bool { + let ll = `build/{name}.ll` + if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } + # write to a temp then move, so a running bin/ludic can rebuild itself in place + if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false } + run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`) + return true +} + +# ---- build-cli: the self-hosted front-end binaries --------------------------- +# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed +# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just +# compiles. +function cmd_dev_build_cli() -> int { + run("mkdir -p bin build") + print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") + if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 } + run("chmod +x bin/ludicc") + print("done. built bin/ludicc") + return 0 +} + +# ---- build: the whole toolchain -------------------------------------------- +# bin/ludic is what a user installs; bin/ludic-dev is this tool, which exists +# only in a checkout and is never part of a release. +function cmd_dev_build() -> int { + if cmd_dev_build_cli() != 0 { return 1 } + print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI users install)") + if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 } + print("ludicc: tools/ludic-cli/dev.ludic -> bin/ludic-dev (this tool, rebuilding itself)") + if not build_tool("ludic-dev", "tools/ludic-cli/dev.ludic") { return 1 } + print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt") + if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 } + print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp") + if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 } + sync_vscode_grammar() + print("done. toolchain in bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)") + return 0 +} diff --git a/tools/ludic-cli/tools.ludic b/tools/ludic-cli/tools.ludic index 42c00961..695dadab 100644 --- a/tools/ludic-cli/tools.ludic +++ b/tools/ludic-cli/tools.ludic @@ -27,12 +27,12 @@ function sync_vscode_grammar() -> void { # is `flag` present anywhere in argv? function has_flag(flag: pointer) -> bool { var i = 2 - while i < arg_total() { if (arg_n(i) == flag) { return true }; i += 1 } + while i < arg_count() { if (arg(i) == flag) { return true }; i += 1 } return false } # ---- tools: build the editor toolchain -------------------------------------- -# usage: ludic dev tools [--install] [--test] +# usage: ludic-dev tools [--install] [--test] function cmd_tools() -> int { ensure_ludicc() print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)") @@ -57,8 +57,8 @@ function cmd_test_tools() -> int { PASS = 0 FAIL = 0 let fmt = "bin/ludic-fmt" - if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic dev tools\n"); return 2 } - if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic dev tools\n"); return 2 } + if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic-dev tools\n"); return 2 } + if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic-dev tools\n"); return 2 } print("formatter") @@ -129,7 +129,7 @@ function cmd_test_tools() -> int { # features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the # end-to-end protocol test on Darwin until the directory walk is portable. if is_darwin() { - let lrc = sh("bin/ludic dev test-lsp") + let lrc = sh("bin/ludic-dev test-lsp") if lrc == 0 { ok("language server protocol") } else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") } else { bad("language server protocol") } @@ -160,10 +160,10 @@ function cmd_test_tools() -> int { # VS Code carries its own copy of the shared grammar; catch an edit to the copy if shq("cmp -s tools/editors/shared/ludic.tmLanguage.json tools/editors/vscode/syntaxes/ludic.tmLanguage.json") and shq("cmp -s tools/editors/shared/ludic.markdown-injection.json tools/editors/vscode/syntaxes/ludic.markdown-injection.json") and shq("cmp -s tools/editors/shared/language-configuration.json tools/editors/vscode/language-configuration.json") { ok("vscode grammar copies match tools/editors/shared") - } else { bad2("vscode grammar copies have drifted", "run: ludic dev tools to resync") } + } else { bad2("vscode grammar copies have drifted", "run: ludic-dev tools to resync") } # the JetBrains plugin is off by default (its first build pulls a gigabyte of - # IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic dev test-tools + # IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic-dev test-tools if (getenv_or("LUDIC_TEST_JETBRAINS", "0") == "1") { if shq("test -x tools/editors/jetbrains/gradlew") { if shq("cd tools/editors/jetbrains && ./gradlew buildPlugin verifyPluginStructure --console=plain -q >/dev/null 2>&1 && test -f build/distributions/Ludic-1.0.0.zip") { ok("jetbrains plugin builds and verifies") } else { bad("jetbrains plugin build") } @@ -177,17 +177,17 @@ function cmd_test_tools() -> int { # TextMate grammar, the Kotlin lexer) is the failure mode this layout prevents. # The check is written in Ludic (tools/ludic-cli/checks.ludic) and runs through x — no # Python in the loop. - if shq("bin/ludic dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") } + if shq("bin/ludic-dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") } # every feature the compiler actually implements — namespace methods, keywords, # types, phases — must have a docs/language page. This reads the implementation # (emit_ns_call + ludic_syntax.h), so shipping a feature without docs fails here. # Ported to Ludic; runs through x. - if shq("bin/ludic dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") } + if shq("bin/ludic-dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") } # every ```ludic fence in the docs must parse (or be marked skip/expect-error), # so a documented snippet can't drift from what the compiler accepts. - if shq("bin/ludic dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic dev check-docs") } + if shq("bin/ludic-dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic-dev check-docs") } return report() } @@ -195,9 +195,9 @@ function cmd_test_tools() -> int { # JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python). function test_json(path: pointer) -> void { let bn = capture_line(`basename {path}`) - if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) } + if shq(`bin/ludic-dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) } } function test_xml(path: pointer) -> void { let bn = capture_line(`basename {path}`) - if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) } + if shq(`bin/ludic-dev lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) } }