refactor(cli)!: split the contributor tool out of the ludic CLI

`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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 23:15:12 +03:00
parent f369fbd227
commit e175619543
46 changed files with 630 additions and 558 deletions

View file

@ -8,13 +8,13 @@ Closes #
## Checklist ## Checklist
- [ ] `bin/ludic dev test` passes. - [ ] `bin/ludic-dev test` passes.
- [ ] For compiler/runtime changes: `bin/ludic dev reseed && bin/ludic dev bootstrap-cfree` - [ ] 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. 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). - [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and - [ ] New/changed stdlib symbols are documented under `docs/language/**` and
registered in `tools/docgen/inventory.json` 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). - [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated - [ ] No new C / Python / JS in tooling (Ludic only), and no generated
artifacts committed outside `build/` / `bin/`. artifacts committed outside `build/` / `bin/`.

View file

@ -43,11 +43,11 @@ jobs:
set -eu set -eu
mkdir -p bin mkdir -p bin
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc 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 - 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 # 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 # the checked-in seed. It returns non-zero if they differ — i.e. if the
# seed is stale relative to the compiler source. # seed is stale relative to the compiler source.
run: bin/ludic dev bootstrap-cfree run: bin/ludic-dev bootstrap-cfree

View file

@ -2,7 +2,7 @@ name: ci
# Build the language toolchain from its IR seed and run the regression suites on # 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 # 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 # compiler even building from the seed. See also bootstrap.yml, which proves the
# C-free self-rebuild reproduces the seed byte-for-byte. # C-free self-rebuild reproduces the seed byte-for-byte.
on: on:
@ -45,7 +45,7 @@ jobs:
# The toolchain is macOS-first; on this Linux runner it links against a # 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 # tiny C-free IR shim that supplies the Darwin standard-stream globals
# (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through # (__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 # and each compiled test program — picks it up. Absolute path so it
# still resolves if a step changes directory. # still resolves if a step changes directory.
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" 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. # pre-built binaries: the language builds itself from source + seed.
mkdir -p bin mkdir -p bin
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc 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
bin/ludic dev build bin/ludic-dev build
- name: Regression suite (ludic dev test) - name: Regression suite (ludic-dev test)
run: bin/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 # Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's
# formatting contract — hand alignment is deliberately preserved, so the # 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 # 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 # assets. Cross-file LSP behaviour and the golden renders are macOS-ABI
# bound and skip here — visibly — until the runtime's directory walk and # bound and skip here — visibly — until the runtime's directory walk and
# windowing are portable. # windowing are portable.
run: bin/ludic dev test-tools run: bin/ludic-dev test-tools
- name: Docs cover the implementation - name: Docs cover the implementation
run: | run: |
@ -81,9 +81,9 @@ jobs:
# The whole docs toolchain is written in Ludic and runs through x — # The whole docs toolchain is written in Ludic and runs through x —
# no Python anywhere. check-impl / check-vocabulary / check-docs guard # no Python anywhere. check-impl / check-vocabulary / check-docs guard
# the sources; docs-gen builds the site and docs-check is its coverage # the sources; docs-gen builds the site and docs-check is its coverage
# + integrity guard. (check-vocabulary also runs in `ludic dev test-tools`.) # + integrity guard. (check-vocabulary also runs in `ludic-dev test-tools`.)
bin/ludic dev check-impl bin/ludic-dev check-impl
bin/ludic dev check-vocabulary bin/ludic-dev check-vocabulary
bin/ludic dev check-docs bin/ludic-dev check-docs
bin/ludic dev docs-gen --out build/pages bin/ludic-dev docs-gen --out build/pages
bin/ludic dev docs-check build/pages bin/ludic-dev docs-check build/pages

View file

@ -38,7 +38,7 @@ jobs:
runs-on: docker runs-on: docker
# The generator is now Ludic, so this builds the toolchain from its IR seed # 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 # (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. # for the clone + publish; clang-16 is the only extra the bootstrap needs.
container: node:20-bookworm container: node:20-bookworm
steps: steps:
@ -67,9 +67,9 @@ jobs:
export LUDIC_HOME="$(pwd)" export LUDIC_HOME="$(pwd)"
mkdir -p bin mkdir -p bin
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc 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
bin/ludic dev docs-gen --out ../public bin/ludic-dev docs-gen --out ../public
bin/ludic dev docs-check ../public bin/ludic-dev docs-check ../public
cd .. cd ..
echo "--- generated files ---" echo "--- generated files ---"
ls -la public ls -la public

View file

@ -1,6 +1,6 @@
name: release 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 # 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 # 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 # the time, with no checksums and nothing proving the tagged tree even passed its
@ -71,15 +71,15 @@ jobs:
set -eu set -eu
mkdir -p bin mkdir -p bin
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc 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
bin/ludic dev build bin/ludic-dev build
- name: The tagged tree must pass its own suites - name: The tagged tree must pass its own suites
run: | run: |
set -eu set -eu
bin/ludic dev test bin/ludic-dev test
bin/ludic dev test-tools bin/ludic-dev test-tools
bin/ludic dev bootstrap-cfree bin/ludic-dev bootstrap-cfree
- name: Publish the release - name: Publish the release
env: env:
@ -91,9 +91,9 @@ jobs:
echo "::error::No FORGEJO_TOKEN secret; cannot create the release." echo "::error::No FORGEJO_TOKEN secret; cannot create the release."
exit 1 exit 1
fi 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, # toolchain, SHA256SUMS), takes the notes from the CHANGELOG section,
# and creates the release. Re-running it only adds missing assets, so # and creates the release. Re-running it only adds missing assets, so
# a maintainer can afterwards attach the macOS toolchain from a Mac # a maintainer can afterwards attach the macOS toolchain from a Mac
# with the same command. # with the same command.
bin/ludic dev publish "$TAG" bin/ludic-dev publish "$TAG"

8
.gitignore vendored
View file

@ -1,5 +1,5 @@
# Generated build tree: LLVM IR, objects, compiled apps, the headless render # 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 # `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. # accidentally ignored. Nothing is written to the repo root any more.
/build/ /build/
@ -9,8 +9,8 @@
# packaged plugin .zip are local-only build inputs/outputs. # packaged plugin .zip are local-only build inputs/outputs.
*.zip *.zip
# the toolchain binaries (ludicc, ludic, ludic-fmt, ludic-lsp) — all built # 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 # 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). # only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
/bin/ /bin/
@ -39,7 +39,7 @@ tools/editors/jetbrains/build/
__pycache__/ __pycache__/
*.pyc *.pyc
# Release artifacts produced by `ludic dev release` # Release artifacts produced by `ludic-dev release`
/dist/ /dist/
# Build/release tarballs anywhere in the tree. `git -C <repo> archive -o foo.tgz` # Build/release tarballs anywhere in the tree. `git -C <repo> archive -o foo.tgz`

View file

@ -25,8 +25,8 @@
> ```bash > ```bash
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic > # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic
> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc > 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
> bin/ludic dev build # the whole toolchain into bin/ > bin/ludic-dev build # the whole toolchain into bin/
> # (ludicc, ludic, ludic-fmt, ludic-lsp) > # (ludicc, ludic, ludic-fmt, ludic-lsp)
> bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly > bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly
> bin/ludic build examples/games/snake.ludic # or through the CLI > 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 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 asserts exactly that, which is a much stronger check on the backend than
"it started". "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 graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
the retained UI. 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 survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
build, run and render from IR alone. build, run and render from IR alone.
## Every flag ## 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:
``` ```
<file.ludic> the program to compile (first non-flag argument) <file.ludic> the program to compile (first non-flag argument)

View file

@ -19,23 +19,24 @@ From a clean checkout, one line lifts the toolchain off the seed:
```bash ```bash
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc 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 That gives you `bin/ludic-dev`, the contributor tool: it replaces every
carries the toolchain's own tasks under `ludic dev` and replaces every build/test build/test shell script in the repo and builds everything, including itself and
shell script in the repo. From then on it builds everything — including itself: `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 ```bash
bin/ludic dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp) 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-dev help # every contributor task
bin/ludic help # what a user of the language sees bin/ludic help # what a user of the language sees
``` ```
Always run `ludic dev` from the repository root, so `assets/` and `selfhost/` Always run `ludic-dev` from the repository root, so `assets/` and `selfhost/`
resolve. (A checkout is an install root: `bin/` beside `runtime/` and resolve. (A checkout is also an install root: `bin/` beside `runtime/` and
`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`. That is `packages/`, exactly the shape `install.sh` lays down under `~/.ludic`, which is
why the same binary serves both.) why `bin/ludic` behaves there exactly as an installed one does.)
## The development loop ## The development loop
@ -43,17 +44,17 @@ When you change the compiler or runtime, prove the self-hosting fixpoint still
holds before you push: holds before you push:
```bash ```bash
bin/ludic dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change 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 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 test # the full regression suite
``` ```
Other useful targets: Other useful targets:
```bash ```bash
bin/ludic build <file.ludic> [--headless] # compile a program to a native app in build/ bin/ludic build <file.ludic> [--headless] # compile a program to a native app in build/
bin/ludic dev selfhost-test # correctness + bootstrap fixpoints bin/ludic-dev selfhost-test # correctness + bootstrap fixpoints
bin/ludic dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp) bin/ludic-dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
bin/ludic clean # remove build/, out.ppm and stray artifacts 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 and register its id in `tools/docgen/inventory.json`. Each documented
namespace gets exactly **one** directory (the docs check enforces this). 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. 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. 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 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. [`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: (step 5 above). Read the next release before cutting it:
```bash ```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: Then cut it:
```bash ```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 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 — grouped by change type, with each changeset's markdown kept intact — bumps
`VERSION`, commits `chore(release): vX.Y.Z`, and tags it. `VERSION`, commits `chore(release): vX.Y.Z`, and tags it.
**Pushing the tag is what publishes.** The `release` workflow builds the **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 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 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 `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 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 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 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 ```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`, 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. move can be rehearsed before it is committed.
**Hosts and URLs.** The install one-liner is served from the documentation site, **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: copies it in; `docs-check` fails without it). Change the host in:
| Where | What | | 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 | | `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/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` | | `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 | | 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`, `is_ludic_file`), every editor asset (`tools/editors/shared/*.json`,
`vscode/package.json`, the JetBrains `LudicFileType`), and every source file `vscode/package.json`, the JetBrains `LudicFileType`), and every source file
in the tree. in the tree.
- **The binaries** `ludic`, `ludicc`, `ludic-fmt`, `ludic-lsp` — `build.ludic`'s - **The binaries** `ludic`, `ludicc`, `ludic-dev`, `ludic-fmt`, `ludic-lsp` —
`cmd_dev_build`, the release staging in `release.ludic`, `install.sh`, the `cmd_dev_build` in `toolchain.ludic`, the release staging in `release.ludic`,
editors' executable-name lists. `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/`, - **The install root** `~/.ludic` and the source directories `tools/ludic-cli/`,
`tools/ludic-tools/`, `packages/ludic.*`. `tools/ludic-tools/`, `packages/ludic.*`.
- **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`, - **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 - **Identifiers that are contracts with other software**: the TextMate scope
`source.ludic`, the VS Code language id `ludic`, the JetBrains plugin id `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 `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 - **The prose**: `README.md`, `LANGUAGE.md`, `COMPILING.md`, `docs/**`, and
`docs/site/site.json`'s `brand`/`meta`. `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 toolchain, stages an install, and runs `new` → `build` → `test` through it, so a
half-finished rename fails there rather than in someone's terminal. 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 ## Pull requests
- Base your branch on `main`. - 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. pass, and that `ludic-fmt` leaves your files unchanged.
- Fill in the PR template checklist. Reference the issue you close with - Fill in the PR template checklist. Reference the issue you close with
`Closes #NN` in the description or a commit message. `Closes #NN` in the description or a commit message.

View file

@ -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 `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`, authoritative CLI reference** — the full flag set (`-o`, `--windowed`,
`--headless`, `--emit-llvm`, `--save-temps`, `--run`), the `LUDIC_HOME` / `--headless`, `--emit-llvm`, `--save-temps`, `--run`), the `LUDIC_HOME` /
`LUDIC_CC` environment variables, and the IR-to-stdout bootstrap contract (no `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 is auto: a file with `handler`s links windowed, otherwise headless; an explicit
flag always wins. flag always wins.
@ -1053,7 +1053,7 @@ duplicate types, unknown fields, arity) are future work.
### Editors ### Editors
```bash ```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 -w src/ # format in place (keeps comments)
bin/ludic-fmt --check . # CI: exit 1 if anything is unformatted bin/ludic-fmt --check . # CI: exit 1 if anything is unformatted
bin/ludic-lsp --stdio # the language server, for any editor 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 > **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks
> compile; [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) runs and is checked > 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 > implicit active-scene register, states numbered by declaration order, and
> `become` as two direct calls plus a store. Richer scene features (the overlay > `become` as two direct calls plus a store. Richer scene features (the overlay
> stack, scene-owned entities, scene-local state, transition parameters) are > stack, scene-owned entities, scene-local state, transition parameters) are

View file

@ -77,16 +77,18 @@ of them with `ludic build examples/games/snake.ludic`.
### Building from a checkout ### Building from a checkout
Contributors work from the repository, where the same CLI carries the toolchain's Contributors also get `ludic-dev`, a second binary carrying the toolchain's own
own tasks under `ludic dev`. Bootstrapping is the only step Ludic cannot do for tasks — building the compiler, the suites, the docs site, releases. It is built
itself, since compiling Ludic needs a compiler — clang assembles the checked-in from a checkout and is not part of an install, so nothing a user runs is mixed
IR seed, and that compiler builds the rest: 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 ```bash
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc 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
bin/ludic dev build # -> bin/{ludicc,ludic,ludic-fmt,ludic-lsp} bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp}
bin/ludic dev test # the regression suite bin/ludic-dev test # the regression suite
``` ```
## The language ## 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 self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are
in the same position. See [COMPILING.md](COMPILING.md). 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). and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md).
## Contributing ## Contributing

View file

@ -1,7 +1,7 @@
# Changesets # Changesets
A **changeset** is one small Markdown file describing a single user-facing change, 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. `CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files.
## Format ## Format
@ -14,7 +14,7 @@ the changelog. Markdown is fine.
``` ```
- `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped - `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped
by the **highest** level among the pending changesets (unless `ludic dev release <level>` by the **highest** level among the pending changesets (unless `ludic-dev release <level>`
overrides it). overrides it).
- `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It - `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It
decides which group the change lands in: `feat` → **Features**, `fix` → 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: Preview how the next release will read before cutting it — this writes nothing:
```bash ```bash
ludic dev release --dry-run ludic-dev release --dry-run
``` ```

16
changes/split-dev-cli.md Normal file
View file

@ -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 <task>` becomes `ludic-dev <task>`** 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.

View file

@ -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/` Worked, self-checking examples for every controller live in `examples/games/`
(`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`) (`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`)
and `examples/library/gameplay_foundation.ludic`; each is a deterministic regression 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 ## What we deliberately do NOT do

View file

@ -19,10 +19,10 @@ Assertions (each records a failure and prints <code>file:line: … failed</code>
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. 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.
<strong>Line coverage.</strong> Compile with the <code>--coverage</code> 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 <code>$LUDIC_COVERAGE</code> (default <code>ludic.cov</code>) as a <code>FILE &lt;name&gt;</code> header followed by one <code>&lt;line&gt; &lt;hits&gt;</code> 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. <code>bin/ludic dev test --coverage</code> 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: <strong>Line coverage.</strong> Compile with the <code>--coverage</code> 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 <code>$LUDIC_COVERAGE</code> (default <code>ludic.cov</code>) as a <code>FILE &lt;name&gt;</code> header followed by one <code>&lt;line&gt; &lt;hits&gt;</code> 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. <code>bin/ludic-dev test --coverage</code> 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/coverage.ludic 16/17 lines 94% uncovered: 33
examples/library/testing.ludic 17/17 lines 100% examples/library/testing.ludic 17/17 lines 100%
---- ----

View file

@ -1,7 +1,7 @@
# Examples # Examples
A curated tour of Ludic, grouped by intent. Every example here is exercised by 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. directory silently rots.
Run any program straight from the repository root (so `assets/` resolves): Run any program straight from the repository root (so `assets/` resolves):

View file

@ -11,7 +11,7 @@
# 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20 # 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20
# 0 the Shield is gone — nothing matches # 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 </dev/null # /tmp/detach </dev/null
program Detach { program Detach {
property Tag { v: int = 0 } property Tag { v: int = 0 }

View file

@ -9,7 +9,7 @@
# 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500 # 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500
# 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000 # 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000
# #
# bin/ludic dev game-build bin/ludicc examples/lang/reason.ludic /tmp/reason # bin/ludic-dev game-build bin/ludicc examples/lang/reason.ludic /tmp/reason
# /tmp/reason </dev/null # /tmp/reason </dev/null
program Reasons { program Reasons {
property Health { hp: int = 0 } property Health { hp: int = 0 }

View file

@ -12,7 +12,7 @@
# 201 900 frame 3: Play.World.Step, then Hud.Draw # 201 900 frame 3: Play.World.Step, then Hud.Draw
# 202 900 frame 4: Step reaches 2 -> quit(); Hud.Draw paints the last frame # 202 900 frame 4: Step reaches 2 -> 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 # printf 'aaaa' | /tmp/scenes
program SceneDemo { program SceneDemo {
var counter: int = 0 var counter: int = 0

View file

@ -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 # #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. # into a per-file line-coverage report.
# #
# The tests below exercise `sign` fully but only the taken branches of `grade`, # The tests below exercise `sign` fully but only the taken branches of `grade`,

View file

@ -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 --branch "v${VERSION}" "${REPO_URL}.git" "$_src" 2>/dev/null \
|| git clone --quiet --depth 1 "${REPO_URL}.git" "$_src" \ || git clone --quiet --depth 1 "${REPO_URL}.git" "$_src" \
|| die "could not clone ${REPO_URL}" || die "could not clone ${REPO_URL}"
# A release older than the CLI cannot produce one, and the build would fail # Which entry point builds the toolchain depends on the age of the tag: the
# deep inside clang with nothing pointing at the cause. # contributor tool was split out of the CLI after 0.5.2, so an older release
[ -f "${_src}/tools/ludic-cli/main.ludic" ] || die "release v${VERSION} predates the ludic CLI; install a newer one with --version X.Y.Z" # 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 CC=clang; have clang || CC=cc
( cd "$_src" \ ( cd "$_src" \
&& mkdir -p bin \ && mkdir -p bin \
&& "$CC" selfhost/ludicc.seed.ll -o bin/ludicc 2>/dev/null \ && "$CC" selfhost/ludicc.seed.ll -o bin/ludicc 2>/dev/null \
&& bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic \ && eval "$_boot" >/dev/null ) || die "the source build failed"
&& bin/ludic dev build >/dev/null ) || die "the source build failed"
_stage="${TMP}/stage" _stage="${TMP}/stage"
mkdir -p "${_stage}/bin" mkdir -p "${_stage}/bin"

View file

@ -5,7 +5,7 @@
# no allocation, identical codegen to writing the hex by hand. Unknown names are # no allocation, identical codegen to writing the hex by hand. Unknown names are
# a compile error (color_lookup returns -1, which emit_expr reports). # 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. # in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.
# ============================================================================ # ============================================================================

View file

@ -12,7 +12,7 @@
# #
# This is the compiler, not the command line a user of the language meets: that # 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 # 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. # bootstrap` rely on, so the bootstrap is untouched.
# basename: the part of a path after the last '/'. # 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 # 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 # 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) let has_target = run or (out != null)
if want == 1 { g_windowed = true } if want == 1 { g_windowed = true }
else if want == 2 { g_windowed = false } else if want == 2 { g_windowed = false }

View file

@ -10,7 +10,7 @@ docs/
language/<category>/<id>.md one file per symbol — keyword, type, phase, language/<category>/<id>.md one file per symbol — keyword, type, phase,
builtin, namespace method, operator, annotation builtin, namespace method, operator, annotation
language/<category>/_section.md section title + blurb + order language/<category>/_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/site.json landing-page messaging (hero, features, …)
site/snippets/*.ludic the code shown on the landing page (real programs) site/snippets/*.ludic the code shown on the landing page (real programs)
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks 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. `inventory.json` are the only inputs the tools here still read directly.
```bash ```bash
bin/ludic dev docs-gen --out build/pages # generate the whole site 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 docs-check build/pages # coverage + duplicate-token + link guard
bin/ludic dev check-docs # parse every ```ludic doc fence 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 `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 ## Publish
`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs `.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 `tools/docgen/**` or `tools/ludic-cli/**`, and publishes the result to the `pages` branch
root. `index.html` + `.nojekyll` always stay at the root. root. `index.html` + `.nojekyll` always stay at the root.
## Colors ## Colors
`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated `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 `tools/ludic-cli/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
generated files. generated files.

View file

@ -31,11 +31,11 @@ it needs no path configuration.
From a checkout, build them with: From a checkout, build them with:
```bash ```bash
bin/ludic dev tools bin/ludic-dev tools
``` ```
Produces `bin/ludic-fmt` and `bin/ludic-lsp`. Add `--install` to symlink both 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 ## 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, 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 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` ## 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; (`const R_DIR: int = 0 # 0 up`) survive a save;
- `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight. - `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 the tree and re-running the *compiler's* canonical dump produces byte-identical
output. The formatter cannot change what a program means. 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 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 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 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 failure, so `bin/ludic-dev check-vocabulary` (written in Ludic) compares all five, and
`bin/ludic dev test-tools` runs it. `bin/ludic-dev test-tools` runs it.
When you add a keyword or builtin: put it in `ludic_syntax.h`, then run 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.

View file

@ -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, so expect it to take a while; later builds take seconds. Once the cache is warm,
```bash ```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. 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 **Settings -> Languages & Frameworks -> Ludic** — the path to `ludic-lsp` and to
`ludicc`. Both default to `bin/` under the project root, which is where `ludicc`. Both default to `bin/` under the project root, which is where
`bin/ludic dev tools` puts them. `bin/ludic-dev tools` puts them.

View file

@ -17,7 +17,7 @@ import java.io.File
/** /**
* Where to find the toolchain. Both paths may be left blank, in which case the * 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. * which is the case for anyone working in the Ludic repository itself.
*/ */
@Service(Service.Level.PROJECT) @Service(Service.Level.PROJECT)
@ -121,7 +121,7 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne
append(" on PATH, and in ~/.ludic/bin.\n") append(" on PATH, and in ~/.ludic/bin.\n")
} }
append("\nInstall the toolchain (curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh), ") 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("or set the path in Settings | Languages & Frameworks | Ludic. ")
append("Syntax highlighting works without it; completion and diagnostics do not.") append("Syntax highlighting works without it; completion and diagnostics do not.")
} }

View file

@ -67,7 +67,7 @@
name="Ludic" name="Ludic"
factoryClass="io.ludic.ide.LudicLanguageServerFactory"> factoryClass="io.ludic.ide.LudicLanguageServerFactory">
<description><![CDATA[ <description><![CDATA[
The Ludic language server. Built by <code>bin/ludic dev tools</code>. The Ludic language server. Built by <code>bin/ludic-dev tools</code>.
]]></description> ]]></description>
</server> </server>
<languageMapping language="Ludic" serverId="ludic"/> <languageMapping language="Ludic" serverId="ludic"/>

View file

@ -23,7 +23,7 @@ for the language reference.
Build the toolchain once, from the repository root: Build the toolchain once, from the repository root:
```bash ```bash
bin/ludic dev tools bin/ludic-dev tools
``` ```
That produces `bin/ludic-lsp` and `bin/ludic-fmt`, which the extension finds That produces `bin/ludic-lsp` and `bin/ludic-fmt`, which the extension finds

View file

@ -98,7 +98,7 @@ async function start(context) {
// Not an error worth a modal: the grammar still highlights, and plenty of // Not an error worth a modal: the grammar still highlights, and plenty of
// people open a .ludic file without having built the toolchain. // people open a .ludic file without having built the toolchain.
output.appendLine( 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.' 'Syntax highlighting works without it; completion and diagnostics do not.'
); );
return; return;

View file

@ -1,56 +1,30 @@
# build.ludic — compiling: a user's program (ludic build / ludic run) and the # build.ludic — turning a .ludic file into an executable.
# toolchain's own binaries (ludic dev build).
# #
# compile_app is the one place a .ludic file becomes an executable, so a project # compile_app is the one place that happens, shared by `ludic build` / `ludic
# build and the repo's own example builds cannot drift apart. Everything the # run` and by the contributor tool, so a user's build and the repo's own example
# toolchain owns — the compiler, the engine runtime it splices — is addressed # builds cannot drift apart. Everything the toolchain owns — the compiler, the
# through ludic_home(), so the same code path works from a checkout and from an # engine runtime it splices — is addressed through ludic_home(), so the same code
# installed ~/.ludic. # path works from a checkout and from an installed ~/.ludic.
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). # ensure bin/ludicc exists and is current with the seed. Built from the checked-in
# Returns true on success. Used for x itself and the editor tools. # IR seed with clang alone — no C compiler is ever involved.
function build_tool(name: pointer, src: pointer) -> bool { function ensure_ludicc() -> void {
let ll = `build/{name}.ll` if not in_toolchain_repo() { return }
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") run("mkdir -p bin build")
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 } print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
run("chmod +x bin/ludicc") if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) {
print("done. built bin/ludicc") err("ludic: could not assemble the seed\n"); exit(1)
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
} }
# ---- compiling one program to a native binary ------------------------------- # ---- compiling one program to a native binary -------------------------------
# #
# mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is # 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). # 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 # Returns true on success. `ludic build`, `ludic run` and the contributor tool all
# through here. # go through here.
function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool { 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 ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale
let home = ludic_home() 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 # 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 # 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 # 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 { function cmd_clean() -> int {
run("rm -rf build") run("rm -rf build")
run("rm -f out.ppm bin/*.tmp") run("rm -f out.ppm bin/*.tmp")

View file

@ -9,75 +9,6 @@
# plain indexing; `==` on pointers is a byte-string compare (the whole toolchain # plain indexing; `==` on pointers is a byte-string compare (the whole toolchain
# leans on this). # 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) # 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 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 { function cmd_check_docs() -> int {
ensure_ludicc() ensure_ludicc()
DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = "" DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = ""
@ -361,7 +292,7 @@ var CI_PROB: pointer = ""
var CI_NPROB: int = 0 var CI_NPROB: int = 0
function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 } 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 { function cmd_check_impl() -> int {
CI_PROB = ""; CI_NPROB = 0 CI_PROB = ""; CI_NPROB = 0
let allsrc = read_all_selfhost() let allsrc = read_all_selfhost()
@ -568,7 +499,7 @@ function set_union(a: []pointer, b: []pointer) -> []pointer {
return out return out
} }
# usage: ludic dev check-vocabulary # usage: ludic-dev check-vocabulary
function cmd_check_vocab() -> int { function cmd_check_vocab() -> int {
CV_PROB = ""; CV_N = 0 CV_PROB = ""; CV_N = 0
let h = read_file("tools/ludic-tools/ludic_syntax.h") let h = read_file("tools/ludic-tools/ludic_syntax.h")
@ -702,10 +633,10 @@ function xml_valid(text: pointer) -> bool {
return sp == 0 return sp == 0
} }
# usage: ludic dev lint-asset <file> (validates one .json or .xml editor asset) # usage: ludic-dev lint-asset <file> (validates one .json or .xml editor asset)
function cmd_lint_asset() -> int { function cmd_lint_asset() -> int {
if arg_total() < 3 { err("usage: ludic dev lint-asset <file.json|file.xml>\n"); return 2 } if arg_count() < 3 { err("usage: ludic-dev lint-asset <file.json|file.xml>\n"); return 2 }
let path = arg_n(2) let path = arg(2)
let t = read_file(path) let t = read_file(path)
if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 } if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 }
if s_index(path, ".json", 0) >= 0 { if s_index(path, ".json", 0) >= 0 {

137
tools/ludic-cli/dev.ludic Normal file
View file

@ -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 <file> 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 <ver> print that release's CHANGELOG.md section")
print(" changelog-render <ver> <date> <dir>")
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 <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
print(" sh-compile <ludicc> <in.ludic> <out> 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 <ludicc> <in.ludic> <out>\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 <ludicc> <game.ludic> <out>\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)
}
}

View file

@ -1,17 +1,17 @@
# docgen.ludic — the Ludic documentation-site generator and its guards, ported # docgen.ludic — the Ludic documentation-site generator and its guards, ported
# off Python (gen.py / check.py / palette.py). Three x subcommands: # 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 # docs/language/colors/palette.json from the palette
# table below (the named-colour source of truth). # 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 # templates, emit every page + symbols.json + the
# highlighter + .nojekyll into DIR (default build/pages). # 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 # 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 # 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) ------------------ # ---- a growable byte buffer (avoids O(n^2) string building) ------------------
property Sb { data: pointer = null, len: int = 0, cap: int = 0 } 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 } while s[i] == '\n' { i += 1 }
return sslice(s, i, slen(s)) 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 # lexicographic byte compare: <0, 0, >0
function str_cmp(a: pointer, b: pointer) -> int { function str_cmp(a: pointer, b: pointer) -> int {
var i = 0 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, "# 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, "# a compile error (color_lookup returns -1, which emit_expr reports).\n")
sb_puts(b, "#\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, "# 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")
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)) 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, # regenerates into scratch files and compares them to the tracked ones instead,
# so the drift guard judges the working tree, not git HEAD. # so the drift guard judges the working tree, not git HEAD.
function cmd_docs_palette() -> int { 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() palette_init()
# guard: duplicate names # guard: duplicate names
let seen = new []pointer 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_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 not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 }
if check { 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 {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 {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`) print(`OK {string(len(pal_names))} colors`)
return 0 return 0

View file

@ -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 # 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 # 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 # 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 { function cmd_docs_check() -> int {
var site = "build/pages" var site = "build/pages"
if arg_total() >= 3 { site = arg_n(2) } if arg_count() >= 3 { site = arg(2) }
let problems = new []pointer let problems = new []pointer
let warnings = new []pointer let warnings = new []pointer

View file

@ -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 # tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and
# docs/site/*, reads the asset templates in tools/docgen/assets, and emits the # 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 # whole pages-branch payload (per-symbol pages, namespace/color/api pages, the
# landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output # 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) # 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. # 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 { function cmd_docs_gen() -> int {
var out = "build/pages" var out = "build/pages"
var i = 2 var i = 2
while i < arg_total() { while i < arg_count() {
let a = arg_n(i) let a = arg(i)
if a == "--out" { if a == "--out" {
i += 1 i += 1
if i < arg_total() { out = arg_n(i) } if i < arg_count() { out = arg(i) }
} }
i += 1 i += 1
} }

View file

@ -1,5 +1,5 @@
# lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real # 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, # 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 # Neovim and the rest are exercising code paths that have been checked, because
# they all speak to this one binary. # they all speak to this one binary.

View file

@ -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 # (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and
# the compiler it drives are invoked through the `run` intrinsic. # the compiler it drives are invoked through the `run` intrinsic.
# #
# Two audiences, one binary. A user of the language sees a small surface — # This is everything a user of the language does: create a project, build it, run
# new, build, run, test, add, fmt — that works from any directory against an # it, test it, add a package, format, upgrade. Nothing here is about the
# installed toolchain. A contributor to the toolchain itself gets everything # toolchain's own repository — building the compiler, reseeding it, generating
# else under `ludic dev`: bootstrapping the compiler from its IR seed, the # the docs site, cutting a release live in a separate tool (dev.ludic ->
# regression suites, the docs site, releases. Splitting them by namespace keeps # bin/ludic-dev) that only exists in a checkout and is never installed. Someone
# `ludic help` about the language rather than about the repository. # who installs Ludic should not have to read past commands they cannot use.
# #
# Installing (macOS, Linux): # Installing (macOS, Linux):
# #
@ -17,29 +17,13 @@
# compiling Ludic needs a compiler: # compiling Ludic needs a compiler:
# #
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc # 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
# bin/ludic dev build # bin/ludic-dev build
#
# Thereafter `ludic dev build` rebuilds the whole toolchain — including the CLI
# itself. The `dev` tasks run from the repository root.
program Ludic { program Ludic {
import "prelude.ludic" import "prelude.ludic"
import "selfhost.ludic"
import "build.ludic" import "build.ludic"
import "project.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.ludic"
import "pkg_test.ludic"
function usage() -> void { function usage() -> void {
print("ludic — the toolchain for the Ludic language") 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(" lsp run the language server on stdio (what editors spawn)")
print(" help this message") print(" help this message")
print("") print("")
print("contributing to the toolchain itself:")
print(" dev <task> build, test, bootstrap, docs, release (ludic dev help)")
print("")
print("A file argument runs it: ludic mygame.ludic") 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 <file> 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 <ver> print that release's CHANGELOG.md section")
print(" dev changelog-render <ver> <date> <dir>")
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 <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
print(" dev sh-compile <ludicc> <in.ludic> <out> 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 # does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape
# the compiler-runner had before the CLI grew subcommands. # the compiler-runner had before the CLI grew subcommands.
function is_ludic_file(s: pointer) -> bool { function is_ludic_file(s: pointer) -> bool {
@ -134,55 +66,6 @@ program Ludic {
return sslice(s, n - 6, n) == ".ludic" return sslice(s, n - 6, n) == ".ludic"
} }
# ---- ludic dev <task> — 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 <ludicc> <in.ludic> <out>\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 <ludicc> <game.ludic> <out>\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 # 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 # the name is unknown. Returning (rather than exiting in place) lets entry tear
# down the per-process scratch directory before the process ends. # 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 == "link-flags") { return cmd_pkg_link_flags() }
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() } 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 == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
if (cmd == "dev") { return dispatch_dev(argn(2, "")) }
# 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 return -1
} }
entry { entry {
if (arg_count() < 2) { usage(); exit(1) } if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1) let cmd = arg(1)
# `ludic mygame.ludic` — build and run that file # `ludic mygame.ludic` — build and run that file
if is_ludic_file(cmd) { if is_ludic_file(cmd) {

View file

@ -539,8 +539,8 @@ function set_require(module: pointer, ver: pointer) -> void {
# ludic add <module>[@version] — add/update a dependency then install # ludic add <module>[@version] — add/update a dependency then install
function cmd_pkg_add() -> int { function cmd_pkg_add() -> int {
if arg_total() < 3 { err("usage: ludic add <module>[@version]\n"); return 1 } if arg_count() < 3 { err("usage: ludic add <module>[@version]\n"); return 1 }
let spec = split_spec(arg_n(2)) let spec = split_spec(arg(2))
let module = spec[0] let module = spec[0]
var ver = spec[1] var ver = spec[1]
if slen(ver) == 0 { if slen(ver) == 0 {

View file

@ -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, # `ludic`'s commands work from any directory, against whatever toolchain is
# compiles and runs them, resolves packages, formats, tests, and (under # installed. `ludic-dev`'s tasks run relative to the current directory and expect
# `ludic dev`) drives every build/bootstrap/release task of the toolchain repo # the toolchain repo root.
# 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.
# ---- file IO ---------------------------------------------------------------- # ---- file IO ----------------------------------------------------------------
@ -53,7 +51,7 @@ function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 }
# ---- scratch files ------------------------------------------------------------ # ---- scratch files ------------------------------------------------------------
# Every scratch file the runner writes lives under one per-process directory # Every scratch file the runner writes lives under one per-process directory
# (`$TMPDIR/x_<pid>`), so `ludic dev test` and an `x check-*` can run side by side without # (`$TMPDIR/x_<pid>`), 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. # clobbering each other's captures. main removes it on the way out.
var x_tmp: pointer = null var x_tmp: pointer = null
@ -175,17 +173,11 @@ function report() -> int {
return 1 return 1
} }
# ---- command arguments ------------------------------------------------------- # a positional argument, or a default when it is absent
# function argn(i: int, dflt: pointer) -> pointer {
# A command reads its own arguments as arg_n(1), arg_n(2)… whether it was if (i < arg_count()) { return arg(i) }
# reached as `ludic get` or as `ludic dev docs-check DIR`. main sets the shift return dflt
# 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 }
# ---- the toolchain install --------------------------------------------------- # ---- the toolchain install ---------------------------------------------------
# #
@ -249,8 +241,121 @@ function tool(name: pointer) -> pointer {
function ludicc() -> pointer { return tool("ludicc") } function ludicc() -> pointer { return tool("ludicc") }
# true in a checkout of the toolchain repo itself, where the `ludic dev` tasks # true in a checkout of the toolchain repo itself, where a seed is present to
# have something to work on. # assemble the compiler from.
function in_toolchain_repo() -> bool { function in_toolchain_repo() -> bool {
return file_exists("selfhost/ludicc.seed.ll") and file_exists("tools/ludic-cli/main.ludic") 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" }

View file

@ -131,11 +131,11 @@ function template_readme(name: pointer) -> pointer {
# ludic new <name> — scaffold a project that builds and runs as it stands. # ludic new <name> — scaffold a project that builds and runs as it stands.
function cmd_new() -> int { function cmd_new() -> int {
if arg_total() < 3 { if arg_count() < 3 {
err("usage: ludic new <name>\n") err("usage: ludic new <name>\n")
return 1 return 1
} }
let name = arg_n(2) let name = arg(2)
if file_exists(name) { if file_exists(name) {
err(`ludic new: {name} already exists\n`) err(`ludic new: {name} already exists\n`)
return 1 return 1
@ -173,12 +173,12 @@ function parse_build_args(start: int) -> pointer {
g_out = "" g_out = ""
g_save = false g_save = false
var ai = start var ai = start
while ai < arg_total() { while ai < arg_count() {
let a = arg_n(ai) let a = arg(ai)
if a == "--headless" { g_mode = 2 } if a == "--headless" { g_mode = 2 }
else if a == "--windowed" { g_mode = 1 } else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true } 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 } else if a[0] != '-' { src = a }
ai += 1 ai += 1
} }
@ -245,7 +245,7 @@ function test_files() -> []pointer {
function cmd_test() -> int { function cmd_test() -> int {
var files = new []pointer var files = new []pointer
var ai = 2 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 { files = test_files() }
if len(files) == 0 { if len(files) == 0 {
err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n") 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 { function cmd_fmt() -> int {
var args = "" var args = ""
var ai = 2 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 == "" { if args == "" {
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort") let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
if found == "" { if found == "" {
@ -315,7 +315,7 @@ function cmd_fmt() -> int {
function cmd_lsp() -> int { function cmd_lsp() -> int {
var args = "" var args = ""
var ai = 2 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}`) return sh(`exec {tool("ludic-lsp")}{args}`)
} }
@ -377,6 +377,25 @@ function cmd_doctor() -> int {
return 1 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 ---------------------------------------------------------- # ---- ludic upgrade ----------------------------------------------------------
function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") } 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. # drift from a fresh install.
function cmd_upgrade() -> int { function cmd_upgrade() -> int {
var ver = "" 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") { if not shq("command -v curl >/dev/null 2>&1") {
err("ludic upgrade: needs curl\n") err("ludic upgrade: needs curl\n")
return 1 return 1

View file

@ -1,13 +1,13 @@
# release.ludic — versioning + release cutting for the toolchain. # release.ludic — versioning + release cutting for the toolchain.
# #
# ludic version print the toolchain version (from the VERSION file) # 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 # bump VERSION, commit, and tag vX.Y.Z. `level` is
# major|minor|patch; omitted, it is derived from the # major|minor|patch; omitted, it is derived from the
# highest `bump:` among the pending changesets. # 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. # 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. # Forgejo release with source + toolchain tarballs.
# Needs FORGEJO_TOKEN in the environment. # Needs FORGEJO_TOKEN in the environment.
# #
@ -49,24 +49,6 @@ function compute_next(cur: pointer, level: pointer) -> pointer {
return `{maj}.{min}.{p2}` 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 ---------------------------------------------------- # ---- changelog rendering ----------------------------------------------------
# #
@ -241,16 +223,16 @@ function build_section(ver: pointer) -> void {
write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes")) write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes"))
} }
# ludic dev changelog-render <version> <date> <dir> — print the CHANGELOG section that # ludic-dev changelog-render <version> <date> <dir> — print the CHANGELOG section that
# `dir`'s changesets would produce. Used to re-render the sections of releases # `dir`'s changesets would produce. Used to re-render the sections of releases
# cut before the renderer preserved markdown structure: check the changesets out # 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. # of the tag's parent commit, point this at them, and splice the result back in.
function cmd_changelog_render() -> int { function cmd_changelog_render() -> int {
if arg_total() < 5 { if arg_count() < 5 {
err("usage: ludic dev changelog-render <version> <date> <changesets-dir>\n") err("usage: ludic-dev changelog-render <version> <date> <changesets-dir>\n")
return 1 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 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). # release section (or at the end of the header if this is the first release).
function prepend_changelog() -> void { function prepend_changelog() -> void {
if not file_exists("CHANGELOG.md") { 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") let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1")
if (ln == "") { if (ln == "") {
@ -297,11 +279,11 @@ function changelog_section(ver: pointer) -> pointer {
return sslice(text, start, n) return sslice(text, start, n)
} }
# ludic dev changelog-section <version> — print that release's CHANGELOG section. # ludic-dev changelog-section <version> — print that release's CHANGELOG section.
function cmd_changelog_section() -> int { function cmd_changelog_section() -> int {
if arg_total() < 3 { err("usage: ludic dev changelog-section <version>\n"); return 1 } if arg_count() < 3 { err("usage: ludic-dev changelog-section <version>\n"); return 1 }
let sec = changelog_section(arg_n(2)) let sec = changelog_section(arg(2))
if slen(sec) == 0 { err(`changelog-section: no section for v{arg_n(2)} in CHANGELOG.md\n`); return 1 } if slen(sec) == 0 { err(`changelog-section: no section for v{arg(2)} in CHANGELOG.md\n`); return 1 }
out(sec) out(sec)
return 0 return 0
} }
@ -343,7 +325,7 @@ function build_artifacts(ver: pointer) -> bool {
let stage = `{tmp_dir()}/{name}` let stage = `{tmp_dir()}/{name}`
run(`rm -rf {stage} && mkdir -p {stage}/bin`) run(`rm -rf {stage} && mkdir -p {stage}/bin`)
if not shq(`cp bin/ludic bin/ludicc bin/ludic-fmt bin/ludic-lsp {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 return false
} }
run(`cp -R runtime {stage}/runtime`) run(`cp -R runtime {stage}/runtime`)
@ -366,10 +348,10 @@ function build_artifacts(ver: pointer) -> bool {
return true 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 # 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 # 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 { function publish_tag(ver: pointer) -> int {
if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 } if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
let body = tmp_path("rel_notes.md") let body = tmp_path("rel_notes.md")
@ -386,8 +368,8 @@ function publish_tag(ver: pointer) -> int {
function cmd_publish() -> int { function cmd_publish() -> int {
var ver = read_version_or("") var ver = read_version_or("")
if arg_total() >= 3 { if arg_count() >= 3 {
var a = arg_n(2) var a = arg(2)
if s_starts(a, "v") { a = sslice(a, 1, slen(a)) } if s_starts(a, "v") { a = sslice(a, 1, slen(a)) }
ver = a ver = a
} }
@ -409,8 +391,8 @@ function cmd_release() -> int {
var publish = false var publish = false
var dry = false var dry = false
var ai = 2 var ai = 2
while ai < arg_total() { while ai < arg_count() {
let a = arg_n(ai) let a = arg(ai)
if (a == "--publish") { publish = true } if (a == "--publish") { publish = true }
else { if (a == "--dry-run") { dry = true } else { if (a == "--dry-run") { dry = true }
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a } 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")) let sec = read_file(tmp_path("rel_section.md"))
if sec != null { out(sec) } if sec != null { out(sec) }
print("") 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 return 0
} }
prepend_changelog() prepend_changelog()
@ -452,7 +434,7 @@ function cmd_release() -> int {
print(` committed + tagged v{ver}`) print(` committed + tagged v{ver}`)
if publish { return publish_release(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})`) print(` (or push: git push origin HEAD && git push origin v{ver})`)
return 0 return 0
} }

View file

@ -6,10 +6,6 @@
# now lives here once, in selfhost_frags(). # now lives here once, in selfhost_frags().
# the C toolchain driver (clang) that assembles and links the emitted IR # 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 self-host compiler's source fragments, in link order. This is THE list;
# the old scripts each carried their own copy. # the old scripts each carried their own copy.
@ -95,21 +91,9 @@ function line_count(path: pointer) -> pointer {
return capture_line(`wc -l < {path}`) 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 --------------- # ---- 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 { function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
run("mkdir -p build") run("mkdir -p build")
let src = "build/selfhost.ludic" 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 ---- # ---- sh-compile: compile one .ludic with a given selfhost binary and link ----
# usage: ludic dev sh-compile <selfhost-binary> <input.ludic> <output-binary> # usage: ludic-dev sh-compile <selfhost-binary> <input.ludic> <output-binary>
function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int { function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 } if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 }
if not shq(`{cc()} {outbin}.ll -o {outbin}`) { 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 return true
} }
# usage: ludic dev game-build <selfhost-binary> <game.ludic> <out-binary> # usage: ludic-dev game-build <selfhost-binary> <game.ludic> <out-binary>
function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int { function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int {
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 } if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
print("build failed:") 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") print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
return 0 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")) out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head"))
return 1 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 # 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 # 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 # 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 { function cmd_reseed() -> int {
run("mkdir -p build/cfree") run("mkdir -p build/cfree")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }

View file

@ -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. # 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 # 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 # 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 { function game_case(path: pointer, keys: pointer) -> void {
let nm = flat(path) let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return } 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 } 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 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`) 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`) } if (got == want) { ok(`{path} matches the golden render`) }
else { bad2(path, `render hash {got} != golden {want}`) } else { bad2(path, `render hash {got} != golden {want}`) }
} }
# render one golden game and return its "<path> <sha256>\n" manifest line (empty # render one golden game and return its "<path> <sha256>\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 { function bless_render(path: pointer, keys: pointer) -> pointer {
let nm = flat(path) let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) { 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", "") game_case("rendering/draw_prims", "")
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==") 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`)) } 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) ==") 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") } 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`)) } else { bad("C-free bootstrap failed (seed stale? run: ludic-dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
return report() return report()
} }

View file

@ -91,6 +91,16 @@ function install_layout_case() -> void {
if ver != `ludic {s_trim(read_file("VERSION"))}` { if ver != `ludic {s_trim(read_file("VERSION"))}` {
bad2("an installed `ludic version` reports the install's VERSION", `got [{ver}]`); return 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) ok(lbl)
} }
@ -161,7 +171,7 @@ function panic_case() -> void {
else { bad2("panic", `rc={string(rc)} err=[{msg}]`) } 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 # 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, # 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. # 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_COVERED = 0
COV_TOTAL = 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/coverage", `{tmp_dir()}/cov_out`)
cov_one("library/testing", `{tmp_dir()}/cov_out`) cov_one("library/testing", `{tmp_dir()}/cov_out`)
var pct = 100 var pct = 100
@ -244,11 +254,11 @@ function cmd_dev_test() -> int {
game_case("games/chronorift", "ddddwwwwaassK") game_case("games/chronorift", "ddddwwwwaassK")
print("== self-hosting and the bootstrap fixpoints ==") 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 '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") } 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`) 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) ==") print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
qsmoke("lang/qdecl") qsmoke("lang/qdecl")
@ -405,9 +415,9 @@ function cmd_dev_test() -> int {
print("== the compiler and the CLI (ludicc / ludic) ==") print("== the compiler and the CLI (ludicc / ludic) ==")
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled # ludicc comes out of the IR seed with clang alone; the CLI is then compiled
# by it, from Ludic. # by it, from Ludic.
if shq(`bin/ludic dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") { 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)") 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`)) } } 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 # 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 # (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 # 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 # stands up throwaway git repos, so like selfhost-test it runs as a sub-process
# and this stage checks its footer). # and this stage checks its footer).
print("== package manager (ludic dev test-pkg) ==") print("== package manager (ludic-dev test-pkg) ==")
run(`bin/ludic dev test-pkg > {tmp_dir()}/pkg.out 2>&1`) run(`bin/ludic-dev test-pkg > {tmp_dir()}/pkg.out 2>&1`)
if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) { if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) {
let pc = capture_line(`grep -c PASS {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)`) 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`)) } } 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 # 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 # pages payload; docs-check is its coverage/integrity guard; docs-palette is
# the named-colour source of truth (its --check mode regenerates the tracked # the named-colour source of truth (its --check mode regenerates the tracked
# emit_color.ludic + palette.json into scratch files and compares). # emit_color.ludic + palette.json into scratch files and compares).
print("== docs site (Ludic generator, no Python) ==") print("== docs site (Ludic generator, no Python) ==")
if shq(`bin/ludic dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) { 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") ok("ludic-dev docs-gen -> the pages-branch payload")
} else { bad2("ludic dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) } } 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`) { 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") 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`)) } } 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`) { 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") 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`)) } } else { bad2("ludic-dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
return report() return report()
} }

View file

@ -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
}

View file

@ -27,12 +27,12 @@ function sync_vscode_grammar() -> void {
# is `flag` present anywhere in argv? # is `flag` present anywhere in argv?
function has_flag(flag: pointer) -> bool { function has_flag(flag: pointer) -> bool {
var i = 2 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 return false
} }
# ---- tools: build the editor toolchain -------------------------------------- # ---- tools: build the editor toolchain --------------------------------------
# usage: ludic dev tools [--install] [--test] # usage: ludic-dev tools [--install] [--test]
function cmd_tools() -> int { function cmd_tools() -> int {
ensure_ludicc() ensure_ludicc()
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)") 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 PASS = 0
FAIL = 0 FAIL = 0
let fmt = "bin/ludic-fmt" 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(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("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic-dev tools\n"); return 2 }
print("formatter") print("formatter")
@ -129,7 +129,7 @@ function cmd_test_tools() -> int {
# features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the # 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. # end-to-end protocol test on Darwin until the directory walk is portable.
if is_darwin() { 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") } if lrc == 0 { ok("language server protocol") }
else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") } else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") }
else { bad("language server protocol") } 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 # 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") { 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") 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 # 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 (getenv_or("LUDIC_TEST_JETBRAINS", "0") == "1") {
if shq("test -x tools/editors/jetbrains/gradlew") { 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") } 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. # 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 # The check is written in Ludic (tools/ludic-cli/checks.ludic) and runs through x — no
# Python in the loop. # 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, # every feature the compiler actually implements — namespace methods, keywords,
# types, phases — must have a docs/language page. This reads the implementation # 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. # (emit_ns_call + ludic_syntax.h), so shipping a feature without docs fails here.
# Ported to Ludic; runs through x. # 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), # 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. # 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() return report()
} }
@ -195,9 +195,9 @@ function cmd_test_tools() -> int {
# JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python). # JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python).
function test_json(path: pointer) -> void { function test_json(path: pointer) -> void {
let bn = capture_line(`basename {path}`) 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 { function test_xml(path: pointer) -> void {
let bn = capture_line(`basename {path}`) 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}`) }
} }