From aca263642db49d2bdab38e4b68bf3439170ded9b Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sat, 5 Sep 2026 22:01:52 +0300 Subject: [PATCH] feat(cli): install in one command, and call the CLI `ludic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Getting started meant cloning the repository, bootstrapping a compiler and learning a task runner called `x`. That is a contributor's workflow handed to everyone who wants to try the language. Installing is now one command: curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh install.sh puts a complete toolchain — compiler, CLI, engine runtime, bundled ludic.* packages, formatter, language server — in ~/.ludic and adds it to PATH. Prebuilt artifacts are checksum-verified; where a platform has none, or the release predates this layout, it bootstraps from the compiler's own IR seed with clang. The docs site publishes the script beside the pages that quote it, so the page and the script can never come from different releases. `x` becomes `ludic`, and the surface splits by audience. A user of the language sees `new`, `run`, `build`, `test`, `add`, `fmt`, `lsp`, `doctor`, `upgrade`; `ludic new` scaffolds a project that builds and plays as it stands. Everything the toolchain repo needs moved under `ludic dev` — build, test, reseed, bootstrap-cfree, docs-gen, release — unchanged apart from the namespace. Those tasks read arguments one position further along, so dispatch_dev sets a shift and commands use arg_n()/arg_total() rather than each knowing its own depth. Release artifacts become complete install roots (bin/ beside runtime/, packages/ and VERSION) rather than bare binaries, which is what the installer unpacks. `ludic dev test` asserts the whole shape: it stages an install, puts it on PATH with no LUDIC_HOME, and runs new -> build -> test through it. Co-Authored-By: Claude Opus 5 --- .forgejo/issue_template/bug.md | 2 +- .forgejo/pull_request_template.md | 6 +- .forgejo/workflows/bootstrap.yml | 6 +- .forgejo/workflows/ci.yml | 28 +- .forgejo/workflows/docs.yml | 12 +- .forgejo/workflows/release.yml | 16 +- .gitignore | 14 +- COMPILING.md | 89 ++-- CONTRIBUTING.md | 103 ++++- LANGUAGE.md | 34 +- README.md | 82 ++-- changes/README.md | 6 +- changes/install-and-ludic-cli.md | 34 ++ changes/per-artifact-checksums.md | 2 +- docs/CONTROLLERS.md | 4 +- docs/PACKAGES.md | 36 +- docs/language/testing/_section.md | 2 +- docs/language/testing/kw-test.md | 6 +- docs/site/site.json | 48 ++- examples/README.md | 6 +- examples/games/snake.ludic | 2 +- examples/lang/detach.ludic | 2 +- examples/lang/reason.ludic | 2 +- examples/lang/scenes.ludic | 2 +- examples/library/coverage.ludic | 4 +- examples/networking/net_demo.ludic | 2 +- install.sh | 335 +++++++++++++++ tools/docgen/README.md | 18 +- tools/editors/README.md | 30 +- tools/editors/jetbrains/README.md | 4 +- .../main/kotlin/io/ludic/ide/LudicServer.kt | 27 +- .../src/main/resources/META-INF/plugin.xml | 2 +- tools/editors/vscode/README.md | 2 +- tools/editors/vscode/src/extension.js | 16 +- tools/ludic-cli/build.ludic | 100 +++++ tools/{x => ludic-cli}/checks.ludic | 12 +- tools/{x => ludic-cli}/docgen.ludic | 20 +- tools/{x => ludic-cli}/docgen_check.ludic | 7 +- tools/{x => ludic-cli}/docgen_gen.ludic | 16 +- tools/{x => ludic-cli}/forgejo.ludic | 0 tools/{x => ludic-cli}/json.ludic | 0 tools/{x => ludic-cli}/lsp_test.ludic | 2 +- tools/ludic-cli/main.ludic | 230 ++++++++++ tools/{x => ludic-cli}/pkg.ludic | 40 +- tools/{x => ludic-cli}/pkg_test.ludic | 74 ++-- tools/{x => ludic-cli}/prelude.ludic | 108 ++++- tools/ludic-cli/project.ludic | 397 ++++++++++++++++++ tools/{x => ludic-cli}/release.ludic | 70 +-- tools/{x => ludic-cli}/selfhost.ludic | 13 +- tools/{x => ludic-cli}/selfhost_test.ludic | 12 +- tools/{x => ludic-cli}/test.ludic | 103 +++-- tools/{x => ludic-cli}/tools.ludic | 26 +- tools/x/build.ludic | 101 ----- tools/x/main.ludic | 157 ------- 54 files changed, 1802 insertions(+), 670 deletions(-) create mode 100644 changes/install-and-ludic-cli.md create mode 100755 install.sh create mode 100644 tools/ludic-cli/build.ludic rename tools/{x => ludic-cli}/checks.ludic (98%) rename tools/{x => ludic-cli}/docgen.ludic (96%) rename tools/{x => ludic-cli}/docgen_check.ludic (96%) rename tools/{x => ludic-cli}/docgen_gen.ludic (98%) rename tools/{x => ludic-cli}/forgejo.ludic (100%) rename tools/{x => ludic-cli}/json.ludic (100%) rename tools/{x => ludic-cli}/lsp_test.ludic (99%) create mode 100644 tools/ludic-cli/main.ludic rename tools/{x => ludic-cli}/pkg.ludic (94%) rename tools/{x => ludic-cli}/pkg_test.ludic (80%) rename tools/{x => ludic-cli}/prelude.ludic (57%) create mode 100644 tools/ludic-cli/project.ludic rename tools/{x => ludic-cli}/release.ludic (85%) rename tools/{x => ludic-cli}/selfhost.ludic (96%) rename tools/{x => ludic-cli}/selfhost_test.ludic (92%) rename tools/{x => ludic-cli}/test.ludic (86%) rename tools/{x => ludic-cli}/tools.ludic (90%) delete mode 100644 tools/x/build.ludic delete mode 100644 tools/x/main.ludic diff --git a/.forgejo/issue_template/bug.md b/.forgejo/issue_template/bug.md index e5100d79..3995f0a1 100644 --- a/.forgejo/issue_template/bug.md +++ b/.forgejo/issue_template/bug.md @@ -24,7 +24,7 @@ labels: ## Environment -- Command used (e.g. `bin/x app foo.ludic --headless`): +- Command used (e.g. `bin/ludic build foo.ludic --headless`): - Target (native macOS / headless / web-wasm): - Commit (`git rev-parse --short HEAD`): - OS / arch: diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index b39680fc..20968e75 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -8,13 +8,13 @@ Closes # ## Checklist -- [ ] `bin/x test` passes. -- [ ] For compiler/runtime changes: `bin/x reseed && bin/x bootstrap-cfree` +- [ ] `bin/ludic dev test` passes. +- [ ] For compiler/runtime changes: `bin/ludic dev reseed && bin/ludic dev bootstrap-cfree` still reaches the self-hosting fixpoint with no C compiler in the loop. - [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8). - [ ] New/changed stdlib symbols are documented under `docs/language/**` and registered in `tools/docgen/inventory.json` - (`bin/x docs-gen && bin/x docs-check build/pages` passes). + (`bin/ludic dev docs-gen && bin/ludic dev docs-check build/pages` passes). - [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org). - [ ] No new C / Python / JS in tooling (Ludic only), and no generated artifacts committed outside `build/` / `bin/`. diff --git a/.forgejo/workflows/bootstrap.yml b/.forgejo/workflows/bootstrap.yml index cedd7096..6de1bdcd 100644 --- a/.forgejo/workflows/bootstrap.yml +++ b/.forgejo/workflows/bootstrap.yml @@ -43,11 +43,11 @@ jobs: set -eu mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/x/main.ludic -o bin/x + bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic - name: Rebuild the compiler from the seed and assert byte-identity - # `x bootstrap-cfree` assembles the seed with clang, has that seed + # `ludic dev bootstrap-cfree` assembles the seed with clang, has that seed # compiler recompile selfhost.ludic to out.ll, and `cmp`s out.ll against # the checked-in seed. It returns non-zero if they differ — i.e. if the # seed is stale relative to the compiler source. - run: bin/x bootstrap-cfree + run: bin/ludic dev bootstrap-cfree diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 4eb2b108..877b81ef 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -2,7 +2,7 @@ name: ci # Build the language toolchain from its IR seed and run the regression suites on # every push to main and every pull request. Until this landed the only workflow -# was docs.yml, so nothing gated a change on `x test` / `x test-tools` or on the +# was docs.yml, so nothing gated a change on `ludic dev test` / `ludic dev test-tools` or on the # compiler even building from the seed. See also bootstrap.yml, which proves the # C-free self-rebuild reproduces the seed byte-for-byte. on: @@ -45,7 +45,7 @@ jobs: # The toolchain is macOS-first; on this Linux runner it links against a # tiny C-free IR shim that supplies the Darwin standard-stream globals # (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through - # LUDIC_CC so every clang invocation — the seed bootstrap, `x build`, + # LUDIC_CC so every clang invocation — the seed bootstrap, `ludic dev build`, # and each compiled test program — picks it up. Absolute path so it # still resolves if a step changes directory. echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV" @@ -60,20 +60,20 @@ jobs: # pre-built binaries: the language builds itself from source + seed. mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/x/main.ludic -o bin/x - bin/x build + bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic + bin/ludic dev build - - name: Regression suite (x test) - run: bin/x test + - name: Regression suite (ludic dev test) + run: bin/ludic dev test - - name: Editor-toolchain suite (x test-tools) + - name: Editor-toolchain suite (ludic dev test-tools) # Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's # formatting contract — hand alignment is deliberately preserved, so the # gate is fmt(fmt(x)) == fmt(x), not fmt(x) == x), and the JSON/XML editor # assets. Cross-file LSP behaviour and the golden renders are macOS-ABI # bound and skip here — visibly — until the runtime's directory walk and # windowing are portable. - run: bin/x test-tools + run: bin/ludic dev test-tools - name: Docs cover the implementation run: | @@ -81,9 +81,9 @@ jobs: # The whole docs toolchain is written in Ludic and runs through x — # no Python anywhere. check-impl / check-vocabulary / check-docs guard # the sources; docs-gen builds the site and docs-check is its coverage - # + integrity guard. (check-vocabulary also runs in `x test-tools`.) - bin/x check-impl - bin/x check-vocabulary - bin/x check-docs - bin/x docs-gen --out build/pages - bin/x docs-check build/pages + # + integrity guard. (check-vocabulary also runs in `ludic dev test-tools`.) + bin/ludic dev check-impl + bin/ludic dev check-vocabulary + bin/ludic dev check-docs + bin/ludic dev docs-gen --out build/pages + bin/ludic dev docs-check build/pages diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml index 5097153c..3e9acf18 100644 --- a/.forgejo/workflows/docs.yml +++ b/.forgejo/workflows/docs.yml @@ -10,7 +10,7 @@ on: paths: - 'docs/**' - 'tools/docgen/**' - - 'tools/x/**' + - 'tools/ludic-cli/**' - '.forgejo/workflows/docs.yml' workflow_dispatch: {} @@ -33,8 +33,8 @@ jobs: # "Waiting" forever with "no online runner found matching this label". runs-on: docker # The generator is now Ludic, so this builds the toolchain from its IR seed - # (clang assembles the seed into bin/ludicc, which compiles bin/x) exactly - # like the ci workflow, then runs `x docs-gen`. node:20-bookworm carries git + # (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 # for the clone + publish; clang-16 is the only extra the bootstrap needs. container: node:20-bookworm steps: @@ -63,9 +63,9 @@ jobs: export LUDIC_HOME="$(pwd)" mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/x/main.ludic -o bin/x - bin/x docs-gen --out ../public - bin/x docs-check ../public + bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic + bin/ludic dev docs-gen --out ../public + bin/ludic dev docs-check ../public cd .. echo "--- generated files ---" ls -la public diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 6c42c2f8..a55715fb 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -1,6 +1,6 @@ name: release -# Cutting a release is `x release` + `git push --tags`; everything after that +# Cutting a release is `ludic dev release` + `git push --tags`; everything after that # happens here. Before this workflow existed the artifacts were built on whatever # machine the maintainer happened to be sitting at, from whatever was in bin/ at # the time, with no checksums and nothing proving the tagged tree even passed its @@ -71,15 +71,15 @@ jobs: set -eu mkdir -p bin clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc - bin/ludicc tools/x/main.ludic -o bin/x - bin/x build + bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic + bin/ludic dev build - name: The tagged tree must pass its own suites run: | set -eu - bin/x test - bin/x test-tools - bin/x bootstrap-cfree + bin/ludic dev test + bin/ludic dev test-tools + bin/ludic dev bootstrap-cfree - name: Publish the release env: @@ -91,9 +91,9 @@ jobs: echo "::error::No FORGEJO_TOKEN secret; cannot create the release." exit 1 fi - # x publish builds dist/ (source tarball from the tag, this host's + # ludic dev publish builds dist/ (source tarball from the tag, this host's # toolchain, SHA256SUMS), takes the notes from the CHANGELOG section, # and creates the release. Re-running it only adds missing assets, so # a maintainer can afterwards attach the macOS toolchain from a Mac # with the same command. - bin/x publish "$TAG" + bin/ludic dev publish "$TAG" diff --git a/.gitignore b/.gitignore index 591a37c6..5ba1fb6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Generated build tree: LLVM IR, objects, compiled apps, the headless render -# (build/out.ppm) and the docs site all land under build/ (see `bin/x build` / -# `bin/x clean`). Root-anchored so a source dir named "build" elsewhere is never +# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic dev build` / +# `bin/ludic clean`). Root-anchored so a source dir named "build" elsewhere is never # accidentally ignored. Nothing is written to the repo root any more. /build/ @@ -9,14 +9,14 @@ # packaged plugin .zip are local-only build inputs/outputs. *.zip -# the toolchain binaries (ludicc, ludic, x, ludic-fmt, ludic-lsp) — all built -# into bin/ by the one-line bootstrap + `bin/x build`; never checked in. The +# the toolchain binaries (ludicc, ludic, ludic-fmt, ludic-lsp) — all built +# into bin/ by the one-line bootstrap + `bin/ludic dev build`; never checked in. The # only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll). /bin/ # package manager (issue #63): the per-project linked view into the global -# content-addressed store, and the optional hermetic copy from `x vendor`. Both -# are regenerated by `x get` / `x vendor` — package.ludic + package.lock.ludic +# content-addressed store, and the optional hermetic copy from `ludic vendor`. Both +# are regenerated by `ludic get` / `ludic vendor` — package.ludic + package.lock.ludic # are the tracked source of truth, so these stay out of the tree. ludic_modules/ vendor/ @@ -39,7 +39,7 @@ tools/editors/jetbrains/build/ __pycache__/ *.pyc -# Release artifacts produced by `x release` +# Release artifacts produced by `ludic dev release` /dist/ # Build/release tarballs anywhere in the tree. `git -C archive -o foo.tgz` diff --git a/COMPILING.md b/COMPILING.md index e74861ad..f300416e 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,37 +1,46 @@ # Compiling Ludic -> **Note (2026-08-27):** `ludicc` is now **written in Ludic** (`selfhost/*.ludic`) -> and built from a checked-in IR seed — the C compiler this document describes has -> been deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is -> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so -> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling -> command `ludic app.ludic` compiles to a temporary binary and runs it in one -> step. The whole toolchain is built by `bin/x build`; `bin/x app` remains as a -> convenience wrapper over the compiler. `--fmt` is reimplemented as a lex+parse -> gate (the doc-check hook). The `--target`/cross-compile and `--shared` paths are -> still features of the old C driver not yet re-implemented on the self-hosted -> toolchain. See the [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap) §5.7 on the wiki. +> **Note:** `ludicc` is **written in Ludic** (`selfhost/*.ludic`) and built from a +> checked-in IR seed — the C compiler this document once described has been +> deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is +> unchanged. `ludicc` drives clang itself (via an `os_system` intrinsic), so +> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly. `--fmt` is +> reimplemented as a lex+parse gate (the doc-check hook). The `--target`/ +> cross-compile and `--shared` paths are still features of the old C driver not +> yet re-implemented on the self-hosted toolchain. See the +> [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap) §5.7 on the wiki. > -> From a clean checkout, build the compiler and the task-runner in one line, then -> let `bin/x` do the rest (run it from the repository root): +> Most people never invoke `ludicc` directly: the `ludic` CLI drives it. > > ```bash -> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/x -> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x -> bin/x build # rebuild the whole toolchain into bin/ -> # (ludicc, ludic, x, ludic-fmt, ludic-lsp) -> bin/ludicc examples/games/snake.ludic -o bin/snake # compile -> bin/ludic examples/games/snake.ludic # compile + run -> bin/x help # list every command +> curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh # the toolchain, into ~/.ludic +> ludic new mygame && cd mygame +> ludic run # compile + run +> ludic build --headless # compile, deterministic render > ``` > -> The binaries are multi-call (one native binary under two names): invoked as -> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with -> handlers is a game and links windowed by default; `--headless` and `--windowed` -> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via -> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in -> `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the -> assembler/linker (default `clang`). +> From a clean checkout, the compiler and the CLI come up in two lines and the +> CLI does the rest (run it from the repository root): +> +> ```bash +> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic +> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc +> bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic +> bin/ludic dev build # the whole toolchain into bin/ +> # (ludicc, ludic, ludic-fmt, ludic-lsp) +> bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly +> bin/ludic build examples/games/snake.ludic # or through the CLI +> bin/ludic help # every command +> ``` +> +> A `.ludic` file with handlers is a game and links windowed by default; +> `--headless` and `--windowed` force the mode. The engine runtime +> (`runtime/native/cocoa.ll`, the spliced `runtime/native/*.ludic`) and the +> bundled `ludic.*` packages are found under the **install root**: `$LUDIC_HOME` +> if set, otherwise derived from the binary's own location — the parent of its +> `bin/` directory, which is both `~/.ludic` for an install and the repository +> root for a checkout. `$LUDIC_CC` overrides the assembler/linker (default +> `clang`). `ludicc` is a compiler, not a translator. It lexes, parses, checks and lowers @@ -73,17 +82,17 @@ the old C driver and are **not yet re-implemented** on the self-hosted toolchain (see the note at the top). The rows above the line work today via the self-hosted `ludicc`. -`bin/x app` wraps the common cases: +`bin/ludic build` wraps the common cases: ```bash -bin/x app examples/games/snake.ludic # -> build/snake (native) -bin/x app examples/library/combat.ludic --lib # -> build/libcombat.* (library) -bin/x app examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm) -bin/x app examples/games/snake.ludic --web # -> build/web/ (browser) +bin/ludic build examples/games/snake.ludic # -> build/snake (native) +bin/ludic build examples/library/combat.ludic --lib # -> build/libcombat.* (library) +bin/ludic build examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm) +bin/ludic build examples/games/snake.ludic --web # -> build/web/ (browser) ``` The `--lib` and `--web` targets were part of the old C driver and are **not yet -re-implemented** on the self-hosted toolchain — `bin/x app` supports the native +re-implemented** on the self-hosted toolchain — `bin/ludic build` supports the native windowed and `--headless` builds today. ## Programs and libraries @@ -223,7 +232,7 @@ only the triple changes. ``` ```bash -bin/x app examples/games/chronorift.ludic --web +bin/ludic build examples/games/chronorift.ludic --web python3 -m http.server -d build/web 8000 # then open http://localhost:8000/ ``` @@ -311,7 +320,7 @@ node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss ``` Because Ludic is fixed-point and its RNG is seeded, the native headless binary -and the wasm one must render byte-identical frames from the same input. `bin/x test` +and the wasm one must render byte-identical frames from the same input. `bin/ludic dev test` asserts exactly that, which is a much stronger check on the backend than "it started". @@ -325,13 +334,13 @@ entity allocator, save/load snapshots, the frame loop, the window, and the whole graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and the retained UI. -None of it goes through C. `bin/x test` asserts that directly: no C source +None of it goes through C. `bin/ludic dev test` asserts that directly: no C source survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all build, run and render from IR alone. ## Every flag -The self-hosted `ludicc`/`ludic` (built with `bin/x build-cli`) accept: +The self-hosted `ludicc`/`ludic` (built with `bin/ludic dev build-cli`) accept: ``` the program to compile (first non-flag argument) @@ -344,12 +353,14 @@ The self-hosted `ludicc`/`ludic` (built with `bin/x build-cli`) accept: --fmt lex + parse only; exit 0 if it parses, 1 on a parse error (the check-docs gate; canonical formatting not yet restored) --save-temps keep the intermediate .ll - --run compile then run (implicit when invoked as `ludic`) + --run compile then run (what `ludic run` uses) (unknown -flags are ignored with a warning, never taken as the input file) environment: LUDIC_CC the LLVM that assembles IR and drives the linker (clang) - LUDIC_HOME where runtime/native/ lives (default: the binary's dir) + LUDIC_HOME the install root — runtime/, packages/, VERSION + (default: the parent of the binary's bin/ directory) + LUDIC_MODULES the project's fetched packages (default: ./ludic_modules) ``` Mode is automatic when neither `--windowed` nor `--headless` is given: a program diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb4d23a7..9a63b40e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,18 +18,24 @@ runtime, and the tooling are all written in Ludic and built by Ludic. From a clean checkout, one line lifts the toolchain off the seed: ```bash -mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x +mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc +bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic ``` -That gives you `bin/x`, the Ludic task runner that replaces every build/test +That gives you `bin/ludic`, the CLI — the same binary users install, which also +carries the toolchain's own tasks under `ludic dev` and replaces every build/test shell script in the repo. From then on it builds everything — including itself: ```bash -bin/x build # rebuild the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp) -bin/x help # list every command +bin/ludic dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp) +bin/ludic dev help # every contributor task +bin/ludic help # what a user of the language sees ``` -Always run `x` from the repository root, so `assets/` and `selfhost/` resolve. +Always run `ludic dev` from the repository root, so `assets/` and `selfhost/` +resolve. (A checkout is an install root: `bin/` beside `runtime/` and +`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`. That is +why the same binary serves both.) ## The development loop @@ -37,18 +43,18 @@ When you change the compiler or runtime, prove the self-hosting fixpoint still holds before you push: ```bash -bin/x reseed # regenerate selfhost/ludicc.seed.ll after a compiler change -bin/x bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop -bin/x test # the full regression suite +bin/ludic dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change +bin/ludic dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop +bin/ludic dev test # the full regression suite ``` Other useful targets: ```bash -bin/x app [--headless] # compile a program to a native app in build/ -bin/x selfhost-test # correctness + bootstrap fixpoints -bin/x test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp) -bin/x clean # remove build/, out.ppm and stray artifacts +bin/ludic build [--headless] # compile a program to a native app in build/ +bin/ludic dev selfhost-test # correctness + bootstrap fixpoints +bin/ludic dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp) +bin/ludic clean # remove build/, out.ppm and stray artifacts ``` ## Adding to the standard library @@ -61,7 +67,7 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces and register its id in `tools/docgen/inventory.json`. Each documented namespace gets exactly **one** directory (the docs check enforces this). 3. Add or extend an example under `examples/` and a case in the test suite. -4. Run `bin/x docs-gen --out build/pages && bin/x docs-check build/pages` — the +4. Run `bin/ludic dev docs-gen --out build/pages && bin/ludic dev docs-check build/pages` — the check fails if any inventory symbol lacks a page or is still seed text. 5. Add a **changeset** for the user-facing change: a small file under [`changes/`](changes/README.md) with a `bump:` level and a one-line summary. @@ -70,41 +76,47 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces ## Versioning & releases The toolchain is versioned with [SemVer](https://semver.org); `VERSION` is the -single source of truth and `ludicc --version` (or `x version`) reports it. +single source of truth and `ludicc --version` (or `ludic version`) reports it. Releases are changeset-driven. Every user-facing change ships with a changeset (step 5 above). Read the next release before cutting it: ```bash -x release --dry-run # render the CHANGELOG section, write nothing +ludic dev release --dry-run # render the CHANGELOG section, write nothing ``` Then cut it: ```bash -x 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 ``` -`x release` aggregates the pending changesets into a new `CHANGELOG.md` section +`ludic dev release` aggregates the pending changesets into a new `CHANGELOG.md` section — grouped by change type, with each changeset's markdown kept intact — bumps `VERSION`, commits `chore(release): vX.Y.Z`, and tags it. **Pushing the tag is what publishes.** The `release` workflow builds the -toolchain from the IR seed, runs `x test`, `x test-tools` and `x bootstrap-cfree` +toolchain from the IR seed, runs `ludic dev test`, `ludic dev test-tools` and `ludic dev bootstrap-cfree` against the tagged tree, and only then creates the Forgejo release — with the source tarball, a Linux toolchain build, a `.sha256` beside each, and that version's `CHANGELOG.md` section as the notes. It refuses to publish if the tag and `VERSION` disagree or the changelog has no section for it. +Each toolchain artifact is a complete install root — `bin/` beside `runtime/`, +`packages/` and `VERSION` — which is exactly what `install.sh` unpacks into +`~/.ludic`. A release with no artifact for a platform is not a broken install +there: the installer falls back to bootstrapping from the source tarball's IR +seed. But the macOS artifacts are the ones most people get, so attach them. + macOS artifacts cannot be produced on the Linux runner — a `darwin-arm64` build needs a macOS host, and there is no cross-compile path (it would need the Xcode SDK and a Mach-O linker). Attaching one therefore means either registering a macOS runner and giving it a job, or running the same command CI runs from a -Mac. Either way it is `x publish`, which only adds assets the release is missing: +Mac. Either way it is `ludic dev publish`, which only adds assets the release is missing: ```bash -FORGEJO_TOKEN=… x publish v0.4.0 +FORGEJO_TOKEN=… ludic dev publish v0.4.0 ``` Checksums are one `.sha256` file per artifact rather than a single `SHA256SUMS`, @@ -118,6 +130,51 @@ shasum -a 256 -c ludic-0.4.0-src.tar.gz.sha256 The tag doubles as the reproducible bootstrap point: the source archive plus its checked-in seed rebuild that exact toolchain. +## Where the name and the URLs live + +The language may yet be renamed and the project may yet move hosts, so the +things that carry a name are kept few and listed here rather than discovered one +broken link at a time. Everything host-shaped has an environment override, so a +move can be rehearsed before it is committed. + +**Hosts and URLs.** The install one-liner is served from the documentation site, +which publishes `install.sh` beside the pages that quote it (`ludic dev docs-gen` +copies it in; `docs-check` fails without it). Change the host in: + +| Where | What | +|---|---| +| `install.sh` | `REPO_API`, `REPO_URL`, `INSTALL_URL` — each `${LUDIC_…:-default}`, so `LUDIC_REPO_URL=… sh install.sh` tests a move without editing anything | +| `tools/ludic-cli/project.ludic` | `install_url()` (`$LUDIC_INSTALL_URL`), used by `ludic upgrade` and `ludic doctor` | +| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic dev publish` | +| `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 | + +**The name itself.** A rename touches, in rough order of blast radius: + +- **The file extension** `.ludic` — the compiler (`strip_ludic`, `do_import`, + `is_ludic_file`), every editor asset (`tools/editors/shared/*.json`, + `vscode/package.json`, the JetBrains `LudicFileType`), and every source file + in the tree. +- **The binaries** `ludic`, `ludicc`, `ludic-fmt`, `ludic-lsp` — `build.ludic`'s + `cmd_dev_build`, the release staging in `release.ludic`, `install.sh`, the + editors' executable-name lists. +- **The install root** `~/.ludic` and the source directories `tools/ludic-cli/`, + `tools/ludic-tools/`, `packages/ludic.*`. +- **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`, + `LUDIC_STORE`, `LUDIC_PKG_PROXY`, `LUDIC_INSTALL_URL`, `LUDIC_KEEP_TMP`, + `LUDIC_COVERAGE` — keep the old names working for a release if anyone has them + in a script. +- **Identifiers that are contracts with other software**: the TextMate scope + `source.ludic`, the VS Code language id `ludic`, the JetBrains plugin id + `io.ludic.ide`, and the `ludic` code-fence tag understood by the Markdown + injection and by `ludic dev check-docs`. +- **The prose**: `README.md`, `LANGUAGE.md`, `COMPILING.md`, `docs/**`, and + `docs/site/site.json`'s `brand`/`meta`. + +`ludic dev test` is the safety net for the mechanical part — it builds the +toolchain, stages an install, and runs `new` → `build` → `test` through it, so a +half-finished rename fails there rather than in someone's terminal. + ## Conventions - **Commits:** [Conventional Commits](https://www.conventionalcommits.org) — @@ -132,7 +189,7 @@ checked-in seed rebuild that exact toolchain. | `perf` | a performance improvement | | `docs` | documentation only (`docs/`, README, comments) | | `test` | tests only | - | `build` | the build/bootstrap machinery (seed, `bin/x`, linking) | + | `build` | the build/bootstrap machinery (seed, `bin/ludic`, linking) | | `ci` | CI workflows under `.forgejo/` | | `style` | formatting/whitespace, no behaviour change | | `chore` | routine housekeeping with no other bucket | @@ -154,7 +211,7 @@ checked-in seed rebuild that exact toolchain. so a green local commit is a green CI run. - **Formatting:** `ludic-fmt` is the source of truth (2-space indent, LF, UTF-8); - the repo `.editorconfig` mirrors it. Run `bin/ludic-fmt -w` on files you touch. + the repo `.editorconfig` mirrors it. Run `bin/ludic fmt` on files you touch. The contract CI enforces is *idempotence* — `ludic-fmt` re-run on its own output is a no-op — which leaves deliberate hand alignment in place; it is not a blanket `fmt(x) == x`. @@ -182,7 +239,7 @@ non-destructive version of "tidy the history" without touching a single commit. ## Pull requests - Base your branch on `main`. -- Ensure `bin/x test` (and `bin/x bootstrap-cfree` for compiler/runtime changes) +- Ensure `bin/ludic dev test` (and `bin/ludic dev bootstrap-cfree` for compiler/runtime changes) pass, and that `ludic-fmt` leaves your files unchanged. - Fill in the PR template checklist. Reference the issue you close with `Closes #NN` in the description or a commit message. diff --git a/LANGUAGE.md b/LANGUAGE.md index ae43c8d1..658db1ad 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -1021,24 +1021,28 @@ literal; test any pointer/record/slice with `x == null` / `x != null` (an unset ## Tooling ```bash -ludicc app.ludic -o build/app # native binary (windowed for a game) -ludicc app.ludic --headless -o app # headless build (renders out.ppm; reads stdin) +ludic new mygame # a project that builds and plays as it stands +ludic run # compile src/main.ludic and run it +ludic build --headless # headless build (renders out.ppm; reads stdin) +ludic test # compile and run the project's `test` blocks + +ludicc app.ludic -o build/app # the compiler directly: a native binary ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR -ludic app.ludic # compile AND run (forwards the exit code) ``` -`ludicc` (compile) and `ludic` (compile-and-run) are one multi-call binary built -by `bin/x build-cli`. **[COMPILING.md](COMPILING.md) is the authoritative CLI -reference** — the full flag set (`-o`, `--windowed`, `--headless`, `--emit-llvm`, -`--save-temps`, `--run`), the `LUDIC_HOME` / `LUDIC_CC` environment variables, -and the IR-to-stdout bootstrap contract (no `-o`, invoked as `ludicc`) that -`bin/x app` / `bin/x reseed` rely on. The default mode is auto: a file with `handler`s -links windowed, otherwise headless; an explicit flag always wins. +`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 +authoritative CLI reference** — the full flag set (`-o`, `--windowed`, +`--headless`, `--emit-llvm`, `--save-temps`, `--run`), the `LUDIC_HOME` / +`LUDIC_CC` environment variables, and the IR-to-stdout bootstrap contract (no +`-o`) that `bin/ludic build` / `bin/ludic dev reseed` rely on. The default mode +is auto: a file with `handler`s links windowed, otherwise headless; an explicit +flag always wins. The retired C driver's `--shared`, `--fmt`, `-c`, cross-compile (`--target`) and wasm modes are **not** on the self-hosted toolchain (see "Not yet implemented"). -Source formatting now lives in the standalone `bin/ludic-fmt` (below), not a -compiler flag. +Source formatting now lives in the standalone formatter — `ludic fmt` (below) — +not a compiler flag. The self-hosted compiler is intentionally permissive: it has no separate validation pass yet, so unknown types lower to `ptr` and call arity is not @@ -1049,7 +1053,7 @@ duplicate types, unknown fields, arity) are future work. ### Editors ```bash -bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp +bin/ludic dev tools # -> bin/ludic-fmt, bin/ludic-lsp bin/ludic-fmt -w src/ # format in place (keeps comments) bin/ludic-fmt --check . # CI: exit 1 if anything is unformatted bin/ludic-lsp --stdio # the language server, for any editor @@ -1076,7 +1080,7 @@ Emacs, Sublime and Zed, are in `tools/editors/` — see - `examples/games/snake.ludic` — Snake, no assets — same compiler, proving generality. ```bash -bin/x app examples/games/snake.ludic && ./build/snake +bin/ludic build examples/games/snake.ludic && ./build/snake ``` ## Not yet implemented @@ -1103,7 +1107,7 @@ self-hosting; their lowerings are in > **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks > compile; [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) runs and is checked -> by `bin/x test`. A scene lowers to a `machine` the compiler writes for you: one +> by `bin/ludic dev test`. A scene lowers to a `machine` the compiler writes for you: one > implicit active-scene register, states numbered by declaration order, and > `become` as two direct calls plus a store. Richer scene features (the overlay > stack, scene-owned entities, scene-local state, transition parameters) are diff --git a/README.md b/README.md index c4e8c503..cf674c6d 100644 --- a/README.md +++ b/README.md @@ -35,34 +35,57 @@ program Hello { ## Getting started -`bin/x` is the project's task runner: one native binary, written in Ludic and -compiled by Ludic, that replaces every build and test script. Bootstrapping it -is the only step Ludic cannot do for itself, since compiling Ludic needs a -compiler — clang assembles the checked-in IR seed: +Install the toolchain — the compiler, the `ludic` CLI, the engine runtime, the +formatter and the language server — with one command: ```bash -mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x +curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh ``` -Then build the toolchain and run a game: +It installs into `~/.ludic` and puts `~/.ludic/bin` on your `PATH`; nothing else +on the machine is touched, and uninstalling is `rm -rf ~/.ludic`. Where a +prebuilt toolchain exists for your platform it is downloaded and verified against +a published checksum; where it does not, the installer bootstraps from the +compiler's own IR seed with clang. Either way you need clang (or Xcode's Command +Line Tools) to link, since Ludic emits LLVM IR and links it natively. + +Then make a game: ```bash -bin/x build # -> bin/{ludicc,ludic,x,ludic-fmt,ludic-lsp} -bin/x app examples/games/snake.ludic # compile and open a native window -./build/snake +ludic new mygame +cd mygame +ludic run # compiles src/main.ludic and opens a native window ``` -Rendering is deterministic, so a frame can be produced without a window — this -is what CI diffs: +`ludic new` writes a manifest, a program that already moves something on screen, +and a test. `ludic build` stops at the binary — one self-contained executable +with nothing to ship beside it. Rendering is deterministic, so a frame can be +produced without a window, which is what CI diffs: ```bash -bin/x app examples/games/snake.ludic --headless -printf 'ddddwww' | ./build/snake_headless # writes build/out.ppm +ludic test +ludic build --headless +printf 'ddddwww' | ./build/mygame_headless # writes build/out.ppm ``` -`bin/x help` lists every command. [`examples/`](examples/README.md) is a tour -grouped by intent: games, rendering, ECS, events, networking, language features -and the standard library. +`ludic help` lists every command, and `ludic doctor` checks the install. +[`examples/`](examples/README.md) is a tour grouped by intent: games, rendering, +ECS, events, networking, language features and the standard library — compile any +of them with `ludic build examples/games/snake.ludic`. + +### Building from a checkout + +Contributors work from the repository, where the same CLI carries the toolchain's +own tasks under `ludic dev`. Bootstrapping is the only step Ludic cannot do for +itself, since compiling Ludic needs a compiler — clang assembles the checked-in +IR seed, and that compiler builds the rest: + +```bash +mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc +bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic +bin/ludic dev build # -> bin/{ludicc,ludic,ludic-fmt,ludic-lsp} +bin/ludic dev test # the regression suite +``` ## The language @@ -93,24 +116,25 @@ Dependencies are identified by URL, resolved with minimal version selection, and cached in a content-addressed store: ```bash -bin/x add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/ -bin/x get # install from package.ludic, write the lock -bin/x verify # check locked packages against the store +ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/ +ludic get # install from package.ludic, write the lock +ludic verify # check locked packages against the store ``` +The `ludic.*` packages — canonical ECS components, the gameplay, platformer, +RPG, shooter and NPC-AI modules — ship with the toolchain, so importing one needs +no fetch step at all. + See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest and lockfile model. ## Editor support -```bash -bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp -``` - -`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor: -completion, diagnostics from the compiler itself, go-to-definition and rename -across imports, and comment-preserving formatting. `ludic-fmt` is the same -formatter as a CLI, for pre-commit hooks. Both understand ```` ```ludic ```` -fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim, +Editors spawn `ludic lsp`; the server ships with the toolchain, so there is +nothing extra to install. It speaks LSP 3.17 over stdio, so one binary serves +every editor: completion, diagnostics from the compiler itself, go-to-definition +and rename across imports, and comment-preserving formatting. `ludic fmt` runs +the same formatter as a CLI, for pre-commit hooks. Both understand +```` ```ludic ```` fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim, Helix, Emacs, Sublime and Zed are in [`tools/editors/`](tools/editors/README.md). ## Status @@ -128,7 +152,7 @@ capability of the retired C compiler and has not been re-wired on the self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are in the same position. See [COMPILING.md](COMPILING.md). -Releases follow SemVer and are cut from changesets by `x release`, then built +Releases follow SemVer and are cut from changesets by `ludic dev release`, then built and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md). ## Contributing diff --git a/changes/README.md b/changes/README.md index 10b708ec..edcdb02c 100644 --- a/changes/README.md +++ b/changes/README.md @@ -1,7 +1,7 @@ # Changesets A **changeset** is one small Markdown file describing a single user-facing change, -dropped in this directory. `x release` consumes every changeset here into a new +dropped in this directory. `ludic dev release` consumes every changeset here into a new `CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files. ## Format @@ -14,7 +14,7 @@ the changelog. Markdown is fine. ``` - `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped - by the **highest** level among the pending changesets (unless `x release ` + by the **highest** level among the pending changesets (unless `ludic dev release ` overrides it). - `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It decides which group the change lands in: `feat` → **Features**, `fix` → @@ -47,5 +47,5 @@ filename works except this `README.md`, which the release step always skips. Preview how the next release will read before cutting it — this writes nothing: ```bash -x release --dry-run +ludic dev release --dry-run ``` diff --git a/changes/install-and-ludic-cli.md b/changes/install-and-ludic-cli.md new file mode 100644 index 00000000..3fb281f4 --- /dev/null +++ b/changes/install-and-ludic-cli.md @@ -0,0 +1,34 @@ +bump: minor +type: feat +**One command installs Ludic, and `ludic` is the command you use.** Getting +started no longer means cloning the repository and learning a task runner called +`x`. + +- **`curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh`** installs a complete + toolchain — compiler, CLI, engine runtime, bundled `ludic.*` packages, + formatter and language server — into `~/.ludic` and puts it on your `PATH`. + Prebuilt artifacts are verified against a published checksum; where none + exists for the platform, the installer bootstraps from the compiler's own IR + seed with clang. Uninstalling is `rm -rf ~/.ludic`, and `ludic upgrade` + re-runs the same script. +- **`ludic` replaces `x`** and is the only command a user of the language meets: + `ludic new` scaffolds a project that builds and plays as it stands, `ludic + run` / `ludic build` compile it (`--headless` for a deterministic render), + `ludic test` runs every `test` block in the project, and `ludic add` / `get` / + `update` / `verify` / `vendor` drive packages. `ludic fmt` and `ludic lsp` are + the formatter and language server, so an editor needs no path configuration. + `ludic doctor` reports whether the install is complete and usable. +- **The toolchain's own tasks moved under `ludic dev`** — `dev build`, `dev + test`, `dev reseed`, `dev bootstrap-cfree`, `dev docs-gen`, `dev release` and + the rest are unchanged apart from the namespace. `bin/x` is gone; the + bootstrap is now `clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc + tools/ludic-cli/main.ludic -o bin/ludic`. +- **An install root is a first-class layout.** The compiler derives it from its + own location — the parent of its `bin/` directory — so `~/.ludic` and a repo + checkout are the same shape, and `$LUDIC_HOME` is no longer needed to build a + windowed game outside the repo. The bundled `ludic.*` packages resolve from + `$LUDIC_HOME/packages`, so `import "ludic.core/components.ludic"` works with no + `ludic_modules/` to set up. Release artifacts are complete install roots + (`bin/` beside `runtime/`, `packages/` and `VERSION`) rather than bare + binaries, and the installer ships with the documentation site it is served + from. diff --git a/changes/per-artifact-checksums.md b/changes/per-artifact-checksums.md index a94f83e5..d69db5d4 100644 --- a/changes/per-artifact-checksums.md +++ b/changes/per-artifact-checksums.md @@ -2,7 +2,7 @@ bump: patch type: fix Release checksums are one `.sha256` file per artifact instead of a single `SHA256SUMS`. A release is assembled from more than one host — a Linux runner -cannot build the macOS toolchain — and `x publish` never overwrites an asset that +cannot build the macOS toolchain — and `ludic dev publish` never overwrites an asset that is already attached, so a shared `SHA256SUMS` was written by whichever host published first and then never covered anything added afterwards. Per-artifact names compose across hosts. Verify one with diff --git a/docs/CONTROLLERS.md b/docs/CONTROLLERS.md index e12dd868..8ebb23af 100644 --- a/docs/CONTROLLERS.md +++ b/docs/CONTROLLERS.md @@ -64,7 +64,7 @@ controller. The genre packages build on the engine's `Body`/`Collider` + `esys_move` (swept-AABB tile collision, in `runtime/native/systems_move.ludic`) and on `ludic.gameplay`. In a -real project you `x add` them; in this repo the packages live under `packages/` and a +real project you `ludic add` them; in this repo the packages live under `packages/` and a game compiles against them with `LUDIC_MODULES`: ``` @@ -74,7 +74,7 @@ LUDIC_MODULES=packages ludicc examples/games/platformer_demo.ludic -o platformer Worked, self-checking examples for every controller live in `examples/games/` (`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`) and `examples/library/gameplay_foundation.ludic`; each is a deterministic regression -case in `x test`. +case in `ludic dev test`. ## What we deliberately do NOT do diff --git a/docs/PACKAGES.md b/docs/PACKAGES.md index 5351e67a..6676d8ec 100644 --- a/docs/PACKAGES.md +++ b/docs/PACKAGES.md @@ -1,6 +1,6 @@ # Ludic packages -Ludic has a package manager built into the task runner (`bin/x`). It fetches, +Ludic has a package manager built into the task runner (`bin/ludic`). It fetches, resolves, stores and links third-party packages with no new infrastructure to run — it drives plain `git` and rides on the Forgejo host and its release tags. @@ -18,14 +18,14 @@ This is the v1 implementation of the direction decided in issue #63. ## Commands ``` -x add [@version] add a dependency to package.ludic, then resolve + fetch + link -x get resolve every dependency in package.ludic, link them, write the lock -x update [module] bump a dependency (or all) to its latest published version, then relock -x verify check every locked package against the store by content hash -x vendor copy the resolved packages into ./vendor for hermetic/offline builds +ludic add [@version] add a dependency to package.ludic, then resolve + fetch + link +ludic get resolve every dependency in package.ludic, link them, write the lock +ludic update [module] bump a dependency (or all) to its latest published version, then relock +ludic verify check every locked package against the store by content hash +ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds ``` -`x add` with no `@version` picks the latest published tag and records it as the +`ludic add` with no `@version` picks the latest published tag and records it as the minimum. All the install commands print the resolved build list and write `package.lock.ludic`. @@ -52,17 +52,17 @@ optional for a leaf application). ## The lockfile — `package.lock.ludic` -Generated by `x get`; do not edit by hand. One line per resolved module, pinning +Generated by `ludic get`; do not edit by hand. One line per resolved module, pinning its selected version, content hash, kind and provided namespaces: ``` -# package.lock.ludic — generated by `x get`. Do not edit by hand. +# package.lock.ludic — generated by `ludic get`. Do not edit by hand. lock 1 module "git.workshopsoft.io/orkun/greeter" version "1.2.0" hash "sha256:…" kind "source" provides "Greet" module "git.workshopsoft.io/orkun/util" version "1.0.0" hash "sha256:…" kind "source" provides "Util" ``` -`x verify` rehashes each store entry and confirms the project links to it, so a +`ludic verify` rehashes each store entry and confirms the project links to it, so a tampered or missing dependency is caught before it reaches a build. ## The store and the project view @@ -176,31 +176,31 @@ always emitted (unused parts dead-strip). **Publishing.** In the package repo: ``` -x build-lib module.ludic # -> lib//lib.dylib +ludic build-lib module.ludic # -> lib//lib.dylib # add `kind prebuilt` and `targets ""` to package.ludic, commit lib/, git tag ``` **Consuming.** In the game project: ``` -x add git.host/user/module # kind prebuilt is resolved + the dylib linked into the view -x get # links the artifact for the build target (hard error if the target is missing) +ludic add git.host/user/module # kind prebuilt is resolved + the dylib linked into the view +ludic get # links the artifact for the build target (hard error if the target is missing) ``` -then link the module dylibs into the game build. `x link-flags` prints the exact +then link the module dylibs into the game build. `ludic link-flags` prints the exact clang flags (the dylib, an rpath to the store, `-export_dynamic`) for any build system to splice into its link step: ``` -clang -O2 game.ll $(x link-flags) -o game +clang -O2 game.ll $(ludic link-flags) -o game ``` -(`x app` links them automatically when building in-repo.) If the package does -not ship the build target, `x get` fails — build from source instead where the +(`ludic build` links them automatically when building in-repo.) If the package does +not ship the build target, `ludic get` fails — build from source instead where the package offers it. ## Offline / hermetic builds -`x vendor` copies the resolved packages out of the store into `./vendor`. Build +`ludic vendor` copies the resolved packages out of the store into `./vendor`. Build against the copy with `LUDIC_MODULES=vendor`, so the build needs neither the network nor the global store. diff --git a/docs/language/testing/_section.md b/docs/language/testing/_section.md index 1aa8fae8..24552c99 100644 --- a/docs/language/testing/_section.md +++ b/docs/language/testing/_section.md @@ -4,4 +4,4 @@ title: Testing order: 34 --- -A built-in testing framework in the spirit of Go's go test: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A test "name" { … } block is discovered automatically and run by a synthetic entry point that prints ok - name or FAIL - name for each, a == N passed, M failed == summary, and exits non-zero if anything failed (so CI and the bin/x runner catch it). Inside a test, the expect, expect_eq and expect_near assertions check a condition and, on failure, print file:line: … failed (got …, want …) and mark the test failed — without aborting, so one run reports every failure. expect_near takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: function, print. +A built-in testing framework in the spirit of Go's go test: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A test "name" { … } block is discovered automatically and run by a synthetic entry point that prints ok - name or FAIL - name for each, a == N passed, M failed == summary, and exits non-zero if anything failed (so CI and the bin/ludic runner catch it). Inside a test, the expect, expect_eq and expect_near assertions check a condition and, on failure, print file:line: … failed (got …, want …) and mark the test failed — without aborting, so one run reports every failure. expect_near takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: function, print. diff --git a/docs/language/testing/kw-test.md b/docs/language/testing/kw-test.md index 868eca27..b0f88df8 100644 --- a/docs/language/testing/kw-test.md +++ b/docs/language/testing/kw-test.md @@ -17,12 +17,12 @@ Assertions (each records a failure and prints file:line: … failed - `expect_eq(a, b)` — `a` must equal `b`; on failure prints `(got a, want b)`. - `expect_near(a, b, tol)` — `a` must be within `tol` of `b` (absolute). Use it for `fixed`-point results and accumulated integer math, where an exact match is too brittle. -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/x` 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. -Line coverage. Compile with the --coverage flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by $LUDIC_COVERAGE (default ludic.cov) as a FILE <name> header followed by one <line> <hits> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. bin/x test --coverage compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached: +Line coverage. Compile with the --coverage flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by $LUDIC_COVERAGE (default ludic.cov) as a FILE <name> header followed by one <line> <hits> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. bin/ludic dev test --coverage compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached: ``` -== line coverage (bin/x test --coverage) == +== line coverage (bin/ludic dev test --coverage) == examples/library/coverage.ludic 16/17 lines 94% uncovered: 33 examples/library/testing.ludic 17/17 lines 100% ---- diff --git a/docs/site/site.json b/docs/site/site.json index 290012a2..fdcda08b 100644 --- a/docs/site/site.json +++ b/docs/site/site.json @@ -152,63 +152,69 @@ }, "start": { "kicker": "Get started", - "title": "From clone to a native window.", - "intro": "Bootstrap the task runner once from the checked-in IR seed, then build the toolchain and your game. Everything runs from the repo root.", + "title": "One command to install. One to play.", + "intro": "Install the toolchain with a single command — it brings the compiler, the ludic CLI, the engine runtime and the editor tooling, and needs nothing else on your machine but a C toolchain to link with. Then create a project and run it.", "steps": [ { - "title": "Bootstrap from the seed", - "html": "bin/ is not checked in, so it is created first; then clang assembles the compiler's own checked-in LLVM IR seed, and that compiler builds bin/x, the task runner. This is the only step Ludic cannot do for itself." + "title": "Install", + "html": "The installer downloads a verified toolchain for your platform into ~/.ludic and puts it on your PATH. Nothing else is touched; uninstalling is rm -rf ~/.ludic. On a platform with no prebuilt toolchain it bootstraps from the compiler's own IR seed instead — same result, a few seconds longer." }, { - "title": "Build the toolchain", - "html": "bin/x build produces ludicc, ludic, ludic-fmt and ludic-lsp — all compiled by Ludic, from Ludic." + "title": "Create a project", + "html": "ludic new mygame writes a manifest, a program that already moves something on screen, a test, and an assets/ directory. There is no scaffolding to choose and no build file to write." }, { - "title": "Compile & run an example", - "html": "bin/x app examples/games/snake.ludic turns a .ludic file into a native binary. Run it to open a real window." + "title": "Run it", + "html": "ludic run compiles src/main.ludic to a native binary and launches it. ludic build stops at the binary — one self-contained executable, with nothing to ship beside it." }, { - "title": "Go headless for tests", - "html": "--headless renders frames to a .ppm from piped input — deterministic output you can diff in CI." + "title": "Test it, headlessly", + "html": "ludic test compiles and runs every test block in the project. --headless renders frames to a .ppm from piped input, so a game is deterministic enough to diff in CI." } ], "terminal_name": "zsh — ludic", "terminal": [ { - "comment": "bootstrap the task runner (clang + the IR seed, once)" + "comment": "install the toolchain (macOS, Linux)" }, { - "cmd": "mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc" + "cmd": "curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh" }, { - "cmd": "bin/ludicc tools/x/main.ludic -o bin/x" + "out": "→ installed ludic 0.4.0 → ~/.ludic" }, { "blank": true }, { - "comment": "build the toolchain, then an example (opens a window)" + "comment": "a project that builds and plays as it stands" }, { - "cmd": "bin/x build" + "cmd": "ludic new mygame" }, { - "cmd": "bin/x app examples/games/snake.ludic" + "cmd": "cd mygame" }, { - "cmd": "./build/snake" + "cmd": "ludic run" + }, + { + "out": "→ a native window, running your game" }, { "blank": true }, { - "comment": "deterministic headless render for tests" + "comment": "tests, and a deterministic headless render for CI" }, { - "cmd": "bin/x app examples/games/snake.ludic --headless" + "cmd": "ludic test" }, { - "cmd": "printf 'ddddwww' | ./build/snake_headless" + "cmd": "ludic build --headless" + }, + { + "cmd": "printf 'ddddwww' | ./build/mygame_headless" }, { "out": "→ writes build/out.ppm" @@ -228,6 +234,6 @@ "Sublime Text", "Zed" ], - "note": "bin/x tools builds ludic-fmt and ludic-lsp — the same formatter runs as a CLI for pre-commit hooks and CI." + "note": "Editors spawn ludic lsp — it ships with the toolchain, so there is nothing extra to build or install. The same formatter runs as ludic fmt for pre-commit hooks and CI." } } diff --git a/examples/README.md b/examples/README.md index 9e236cd8..71546d33 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,14 +1,14 @@ # Examples A curated tour of Ludic, grouped by intent. Every example here is exercised by -the test suite (`bin/x test` / `bin/x selfhost-test`), so nothing in this +the test suite (`bin/ludic dev test` / `bin/ludic dev selfhost-test`), so nothing in this directory silently rots. Run any program straight from the repository root (so `assets/` resolves): ```sh -bin/x app examples/games/snake.ludic # compile + open a real window -bin/x app examples/games/snake.ludic --headless # render one frame to build/out.ppm +bin/ludic build examples/games/snake.ludic # compile + open a real window +bin/ludic build examples/games/snake.ludic --headless # render one frame to build/out.ppm bin/ludic examples/lang/offline_rewards.ludic # compile + run a plain program ``` diff --git a/examples/games/snake.ludic b/examples/games/snake.ludic index 0032bc72..2cc24601 100644 --- a/examples/games/snake.ludic +++ b/examples/games/snake.ludic @@ -1,6 +1,6 @@ # ============================================================================ # snake.ludic — a small, complete game in Ludic. No sprites or assets: every -# pixel is drawn from primitives. Build: bin/x app examples/games/snake.ludic +# pixel is drawn from primitives. Build: bin/ludic build examples/games/snake.ludic # # It shows the everyday shape of a Ludic game: an ECS for the moving parts (one # entity per snake segment), named program state for the rest, and a Render diff --git a/examples/lang/detach.ludic b/examples/lang/detach.ludic index fd3ad8c5..5322e9e4 100644 --- a/examples/lang/detach.ludic +++ b/examples/lang/detach.ludic @@ -11,7 +11,7 @@ # 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20 # 0 the Shield is gone — nothing matches # -# bin/x game-build bin/ludicc examples/lang/detach.ludic /tmp/detach +# bin/ludic dev game-build bin/ludicc examples/lang/detach.ludic /tmp/detach # /tmp/detach quit(); Hud.Draw paints the last frame # -# bin/x game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes +# bin/ludic dev game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes # printf 'aaaa' | /tmp/scenes program SceneDemo { var counter: int = 0 diff --git a/examples/library/coverage.ludic b/examples/library/coverage.ludic index da9a933b..bbd903a9 100644 --- a/examples/library/coverage.ludic +++ b/examples/library/coverage.ludic @@ -1,6 +1,6 @@ -# coverage.ludic — a spec built to be measured by `bin/x test --coverage` (issue +# coverage.ludic — a spec built to be measured by `bin/ludic dev test --coverage` (issue # #45). Compiled with `--coverage`, every statement bumps a per-source-line hit -# counter; at exit the counts are dumped and `bin/x test --coverage` turns them +# counter; at exit the counts are dumped and `bin/ludic dev test --coverage` turns them # into a per-file line-coverage report. # # The tests below exercise `sign` fully but only the taken branches of `grade`, diff --git a/examples/networking/net_demo.ludic b/examples/networking/net_demo.ludic index f84974f2..dc156d16 100644 --- a/examples/networking/net_demo.ludic +++ b/examples/networking/net_demo.ludic @@ -14,7 +14,7 @@ # 5. rt_receive() — the client reconciles to the authoritative x=5 # # Prints 5 / 999 / 5. Build & run with the Ludic toolchain only: -# bin/x app examples/networking/net_demo.ludic --headless && ./build/net_demo_headless +# bin/ludic build examples/networking/net_demo.ludic --headless && ./build/net_demo_headless import "net_rt.ludic" program NetDemo { diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..71248777 --- /dev/null +++ b/install.sh @@ -0,0 +1,335 @@ +#!/bin/sh +# install.sh — install the Ludic toolchain. +# +# curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh +# +# That URL is the documentation site, which publishes this file alongside the +# pages it is quoted on (see cmd_docs_gen). Everything host-shaped here is a +# variable with an environment override, so moving the project — a new domain, +# a new forge, a new name for the language — is an edit to the block below and +# to the prose that quotes it (CONTRIBUTING.md has the checklist). +# +# It puts a complete toolchain — the compiler, the `ludic` CLI, the formatter, +# the language server, the engine runtime and the bundled ludic.* packages — +# under ~/.ludic, and puts ~/.ludic/bin on your PATH. Nothing else on the system +# is touched, and uninstalling is `rm -rf ~/.ludic`. +# +# Where a prebuilt toolchain exists for the platform it is downloaded and +# checksum-verified. Where it does not, the source is fetched and bootstrapped +# from the compiler's own checked-in LLVM IR seed, which needs only clang and +# takes a few seconds — the same path a contributor uses. +# +# Options (also usable through `ludic upgrade`): +# --version X.Y.Z install that release instead of the latest +# --dir PATH install root (default: $HOME/.ludic) +# --no-modify-path do not touch any shell profile +# --from-source skip the prebuilt artifacts and bootstrap from source +# --help +# +# POSIX sh on purpose: this is the first thing a new user runs, and it has to +# work on a stock macOS /bin/sh and on a minimal Linux image alike. + +set -eu + +REPO_API="${LUDIC_FORGEJO_API:-https://git.workshopsoft.io/api/v1/repos/workshopsoft/ludic}" +REPO_URL="${LUDIC_REPO_URL:-https://git.workshopsoft.io/workshopsoft/ludic}" +INSTALL_URL="${LUDIC_INSTALL_URL:-https://workshopsoft.pages.workshopsoft.io/ludic/install.sh}" +INSTALL_DIR="${LUDIC_INSTALL_DIR:-${HOME}/.ludic}" +VERSION="" +MODIFY_PATH=1 +FROM_SOURCE=0 + +# ---- output ----------------------------------------------------------------- + +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); R=$(printf '\033[0m') + GREEN=$(printf '\033[32m'); RED=$(printf '\033[31m') +else + B=''; DIM=''; R=''; GREEN=''; RED='' +fi + +say() { printf '%s\n' "$*"; } +step() { printf '%s==>%s %s\n' "$B" "$R" "$*"; } +warn() { printf '%swarning:%s %s\n' "$RED" "$R" "$*" >&2; } +die() { printf '%serror:%s %s\n' "$RED" "$R" "$*" >&2; exit 1; } +have() { command -v "$1" >/dev/null 2>&1; } + +usage() { + cat </dev/null | first_tag) + [ -n "$_v" ] || _v=$(curl -fsSL "${REPO_API}/releases?limit=1" 2>/dev/null | first_tag) + printf '%s\n' "$_v" +} + +first_tag() { + sed -n 's/.*"tag_name" *: *"v\{0,1\}\([^"]*\)".*/\1/p' | head -1 +} + +sha256_of() { + if have sha256sum; then sha256sum "$1" | cut -d' ' -f1 + elif have shasum; then shasum -a 256 "$1" | cut -d' ' -f1 + else return 1 + fi +} + +# download the toolchain tarball for this platform into $TMP, verify it against +# the published checksum, and echo its path. Silent failure (returns 1) means +# "no prebuilt for this platform" — the caller falls back to source. +download_prebuilt() { + _name="ludic-${VERSION}-${OS}-${ARCH}.tar.gz" + _url="${REPO_URL}/releases/download/v${VERSION}/${_name}" + _out="${TMP}/${_name}" + fetch "$_url" "$_out" 2>/dev/null || return 1 + if fetch "${_url}.sha256" "${_out}.sha256" 2>/dev/null; then + _want=$(cut -d' ' -f1 < "${_out}.sha256") + _got=$(sha256_of "$_out" || true) + if [ -n "$_got" ] && [ "$_want" != "$_got" ]; then + die "checksum mismatch for ${_name} (expected ${_want}, got ${_got})" + fi + [ -n "$_got" ] || warn "no sha256 tool found; the download was not verified" + else + warn "no published checksum for ${_name}; the download was not verified" + fi + printf '%s\n' "$_out" +} + +# ---- installing ------------------------------------------------------------- + +# Replace the install root with the staged tree. The swap is done at the end so +# an interrupted download never leaves a half-installed toolchain behind, and a +# working install is only removed once the new one is ready to take its place. +# Is this directory a usable install root? A toolchain is not just binaries: a +# game links against the engine runtime and resolves the bundled packages from +# here, so an artifact missing them installs something that cannot build +# anything. Older releases predate this layout, which is exactly the case that +# must degrade to a source build rather than to a broken ~/.ludic. +valid_root() { + [ -x "${1}/bin/ludic" ] && [ -x "${1}/bin/ludicc" ] \ + && [ -d "${1}/runtime/native" ] && [ -f "${1}/VERSION" ] +} + +install_staged() { + _staged="$1" + valid_root "$_staged" || die "the staged toolchain is incomplete" + mkdir -p "$(dirname "$INSTALL_DIR")" + if [ -d "$INSTALL_DIR" ]; then + rm -rf "${INSTALL_DIR}.old" + mv "$INSTALL_DIR" "${INSTALL_DIR}.old" + fi + mv "$_staged" "$INSTALL_DIR" + rm -rf "${INSTALL_DIR}.old" +} + +# Build the toolchain from the source of a tag: clang assembles the compiler's +# own IR seed, that compiler builds the CLI, and the CLI builds the rest. This +# is the only step Ludic cannot do for itself, and it is also the whole story of +# how the language bootstraps. +install_from_source() { + have git || die "installing from source needs git" + have clang || have cc || die "installing from source needs clang. ${CC_HINT}" + step "building from source (no prebuilt toolchain for ${OS}-${ARCH})" + _src="${TMP}/src" + git clone --quiet --depth 1 --branch "v${VERSION}" "${REPO_URL}.git" "$_src" 2>/dev/null \ + || git clone --quiet --depth 1 "${REPO_URL}.git" "$_src" \ + || die "could not clone ${REPO_URL}" + # A release older than the CLI cannot produce one, and the build would fail + # deep inside clang with nothing pointing at the cause. + [ -f "${_src}/tools/ludic-cli/main.ludic" ] || die "release v${VERSION} predates the ludic CLI; install a newer one with --version X.Y.Z" + CC=clang; have clang || CC=cc + ( cd "$_src" \ + && mkdir -p bin \ + && "$CC" selfhost/ludicc.seed.ll -o bin/ludicc 2>/dev/null \ + && bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic \ + && bin/ludic dev build >/dev/null ) || die "the source build failed" + + _stage="${TMP}/stage" + mkdir -p "${_stage}/bin" + cp "${_src}"/bin/ludic "${_src}"/bin/ludicc "${_src}"/bin/ludic-fmt "${_src}"/bin/ludic-lsp "${_stage}/bin/" + cp -R "${_src}/runtime" "${_stage}/runtime" + cp -R "${_src}/packages" "${_stage}/packages" + cp "${_src}/VERSION" "${_src}/LICENSE" "${_src}/README.md" "${_stage}/" + install_staged "$_stage" +} + +# ---- PATH ------------------------------------------------------------------- + +# The shell profiles worth touching: the login shell's, plus ~/.profile as the +# catch-all. Appending a guarded line is the least surprising thing an installer +# can do to someone's dotfiles, and it is easy to find and delete. +profiles() { + case "${SHELL:-}" in + */zsh) printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc" ;; + */bash) printf '%s\n' "$HOME/.bashrc" ;; + */fish) printf '%s\n' "$HOME/.config/fish/config.fish" ;; + esac + [ -f "$HOME/.profile" ] && printf '%s\n' "$HOME/.profile" + return 0 +} + +add_to_path() { + _bin="${INSTALL_DIR}/bin" + case ":${PATH}:" in *":${_bin}:"*) return 0 ;; esac + [ "$MODIFY_PATH" -eq 1 ] || return 0 + _added='' + for _p in $(profiles); do + [ -e "$_p" ] || continue + if grep -q 'ludic/bin\|LUDIC_INSTALL' "$_p" 2>/dev/null; then _added="$_p"; continue; fi + case "$_p" in + */config.fish) printf '\n# Ludic\nset -gx PATH %s $PATH\n' "$_bin" >> "$_p" ;; + *) printf '\n# Ludic\nexport PATH="%s:$PATH"\n' "$_bin" >> "$_p" ;; + esac + _added="$_p" + done + PATH_NOTE="$_added" +} + +# ---- go --------------------------------------------------------------------- + +TMP=$(mktemp -d "${TMPDIR:-/tmp}/ludic-install.XXXXXX") +trap 'rm -rf "$TMP"' EXIT INT TERM + +if [ -z "$VERSION" ]; then + step "looking up the latest release" + VERSION=$(latest_version || true) + [ -n "$VERSION" ] || die "could not determine the latest version (set one with --version X.Y.Z)" +fi +say " ludic ${B}${VERSION}${R} ${DIM}${OS}-${ARCH} -> ${INSTALL_DIR}${R}" + +if [ "$FROM_SOURCE" -eq 1 ]; then + install_from_source +else + step "downloading the toolchain" + if TARBALL=$(download_prebuilt); then + STAGE="${TMP}/stage" + mkdir -p "$STAGE" + tar -xzf "$TARBALL" -C "$STAGE" + # A current artifact unpacks to one top-level directory that is the install + # root; an older one unpacks its contents directly. Try both, and if neither + # is a usable root the release predates this layout — build from source + # instead of installing something that cannot compile a game. + ROOTDIR="" + if valid_root "$STAGE"; then + ROOTDIR="$STAGE" + else + for _d in "$STAGE"/*; do + if [ -d "$_d" ] && valid_root "$_d"; then ROOTDIR="$_d"; break; fi + done + fi + if [ -n "$ROOTDIR" ]; then + install_staged "$ROOTDIR" + else + warn "the ${VERSION} artifact for ${OS}-${ARCH} is not a complete toolchain; building from source" + install_from_source + fi + else + install_from_source + fi +fi + +PATH_NOTE='' +add_to_path + +# ---- report ----------------------------------------------------------------- + +INSTALLED=$("${INSTALL_DIR}/bin/ludic" version 2>/dev/null || echo "ludic ${VERSION}") +say "" +say "${GREEN}installed${R} ${INSTALLED} -> ${INSTALL_DIR}" + +if [ -n "$CC_HINT" ]; then + say "" + warn "no clang found. Ludic emits LLVM IR and needs clang to assemble and link it." + say " ${CC_HINT}" +fi +if [ "$OS" != darwin ]; then + say "" + say " ${DIM}note: windowing is macOS-only today. On ${OS}, programs build and run" + say " headless (ludic build --headless); a window will not open yet.${R}" +fi + +say "" +if [ -n "$PATH_NOTE" ]; then + say " ${DIM}added ${INSTALL_DIR}/bin to your PATH in ${PATH_NOTE} — open a new shell, or:${R}" + say " export PATH=\"${INSTALL_DIR}/bin:\$PATH\"" + say "" +else + case ":${PATH}:" in + *":${INSTALL_DIR}/bin:"*) ;; + *) + say " ${DIM}add this to your shell profile:${R}" + say " export PATH=\"${INSTALL_DIR}/bin:\$PATH\"" + say "" + ;; + esac +fi + +say "next:" +say " ${B}ludic new mygame${R} create a project" +say " ${B}cd mygame && ludic run${R} build it and play" +say " ${B}ludic help${R} everything else" diff --git a/tools/docgen/README.md b/tools/docgen/README.md index 2378b137..afe0e5cb 100644 --- a/tools/docgen/README.md +++ b/tools/docgen/README.md @@ -10,7 +10,7 @@ docs/ language//.md one file per symbol — keyword, type, phase, builtin, namespace method, operator, annotation language//_section.md section title + blurb + order - language/colors/palette.json the 221 named colors (generated by `x docs-palette`) + language/colors/palette.json the 221 named colors (generated by `ludic dev docs-palette`) site/site.json landing-page messaging (hero, features, …) site/snippets/*.ludic the code shown on the landing page (real programs) tools/docgen/inventory.json the authoritative symbol set the coverage guard checks @@ -66,26 +66,26 @@ no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and `inventory.json` are the only inputs the tools here still read directly. ```bash -bin/x docs-gen --out build/pages # generate the whole site -bin/x docs-check build/pages # coverage + duplicate-token + link guard -bin/x check-docs # parse every ```ludic doc fence +bin/ludic dev docs-gen --out build/pages # generate the whole site +bin/ludic dev docs-check build/pages # coverage + duplicate-token + link guard +bin/ludic dev check-docs # parse every ```ludic doc fence ``` `docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token is documented on two pages, or if a highlighter link points at a missing page — so "every symbol is documented, autogenerated each time" is enforced. It needs a -built `bin/x` (bootstrapped from the IR seed with clang alone). +built `bin/ludic` (bootstrapped from the IR seed with clang alone). ## Publish `.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs -`x docs-gen` + `x docs-check` on every push to `main` that touches `docs/**`, -`tools/docgen/**` or `tools/x/**`, and publishes the result to the `pages` branch +`ludic dev docs-gen` + `ludic dev docs-check` on every push to `main` that touches `docs/**`, +`tools/docgen/**` or `tools/ludic-cli/**`, and publishes the result to the `pages` branch root. `index.html` + `.nojekyll` always stay at the root. ## Colors `palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated -by `bin/x docs-palette` from a single palette table — the `pal_add(...)` rows in -`tools/x/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the +by `bin/ludic dev docs-palette` from a single palette table — the `pal_add(...)` rows in +`tools/ludic-cli/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the generated files. diff --git a/tools/editors/README.md b/tools/editors/README.md index 234f507e..82016f63 100644 --- a/tools/editors/README.md +++ b/tools/editors/README.md @@ -21,14 +21,21 @@ tools/editors/ neovim/ helix/ emacs/ sublime/ zed/ configuration, no plugin needed ``` -## Build it +## Getting the server + +An installed toolchain already has it: `curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh` +puts `ludic-lsp` and `ludic-fmt` in `~/.ludic/bin`, on your `PATH`, and +`ludic lsp` runs the server on stdio — that is what an editor should spawn, since +it needs no path configuration. + +From a checkout, build them with: ```bash -bin/x tools +bin/ludic dev tools ``` Produces `bin/ludic-fmt` and `bin/ludic-lsp`. Add `--install` to symlink both -into `~/.local/bin`, `--test` to run `bin/x test-tools` afterwards. +into `~/.local/bin`, `--test` to run `bin/ludic dev test-tools` afterwards. ## What you get, in any editor @@ -63,11 +70,14 @@ and otherwise keywords, builtins, and everything in scope. - **Anything else** that speaks LSP: ``` - command: bin/ludic-lsp --stdio + command: ludic lsp languages: ludic, markdown - initializationOptions: { "compilerPath": "bin/ludicc", "compilerDiagnostics": true, "indentSize": 2 } + initializationOptions: { "compilerDiagnostics": true, "indentSize": 2 } ``` + (`ludic lsp` is on `PATH` after an install; from a checkout it is + `bin/ludic lsp`, and the server finds `ludicc` next to itself.) + ## Markdown Markdown fenced-code highlighting is not one standard — it is three mechanisms, @@ -108,7 +118,7 @@ ln -sf ../../tools/git-hooks/pre-commit .git/hooks/pre-commit The hook runs `ludic-fmt --check` over the staged `.ludic` and `.md` files only, and does nothing at all when `bin/ludic-fmt` has not been built — so it never -blocks a commit on a machine that has not run `bin/x tools`. +blocks a commit on a machine that has not run `bin/ludic dev tools`. ## Why the formatter is not `ludicc --fmt` @@ -130,7 +140,7 @@ both spellings are what the language documents and uses: (`const R_DIR: int = 0 # 0 up`) survive a save; - `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight. -`bin/x test-tools` checks the property that matters: formatting every file in +`bin/ludic dev test-tools` checks the property that matters: formatting every file in the tree and re-running the *compiler's* canonical dump produces byte-identical output. The formatter cannot change what a program means. @@ -139,8 +149,8 @@ output. The formatter cannot change what a program means. The vocabulary is written down in five places that cannot include each other — the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent -failure, so `bin/x check-vocabulary` (written in Ludic) compares all five, and -`bin/x test-tools` runs it. +failure, so `bin/ludic dev check-vocabulary` (written in Ludic) compares all five, and +`bin/ludic dev test-tools` runs it. When you add a keyword or builtin: put it in `ludic_syntax.h`, then run -`bin/x test-tools` and let it tell you which copies still need it. +`bin/ludic dev test-tools` and let it tell you which copies still need it. diff --git a/tools/editors/jetbrains/README.md b/tools/editors/jetbrains/README.md index 28c703bb..13049d1b 100644 --- a/tools/editors/jetbrains/README.md +++ b/tools/editors/jetbrains/README.md @@ -43,7 +43,7 @@ The first build downloads an IntelliJ IDEA Community distribution (over 1 GB), so expect it to take a while; later builds take seconds. Once the cache is warm, ```bash -LUDIC_TEST_JETBRAINS=1 bin/x test-tools +LUDIC_TEST_JETBRAINS=1 bin/ludic dev test-tools ``` includes the plugin build in the toolchain's own test run. @@ -55,4 +55,4 @@ LSP4IJ is a required dependency; the IDE offers to install it for you. **Settings -> Languages & Frameworks -> Ludic** — the path to `ludic-lsp` and to `ludicc`. Both default to `bin/` under the project root, which is where -`bin/x tools` puts them. +`bin/ludic dev tools` puts them. diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt index 642908df..cc1c4f4c 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt @@ -17,7 +17,7 @@ import java.io.File /** * Where to find the toolchain. Both paths may be left blank, in which case the - * plugin looks under the project root for what `bin/x tools` produces — + * plugin looks under the project root for what `bin/ludic dev tools` produces — * which is the case for anyone working in the Ludic repository itself. */ @Service(Service.Level.PROJECT) @@ -60,13 +60,23 @@ internal fun findExecutable(root: String?, configured: String, names: List): String? { - val path = System.getenv("PATH") ?: return null - for (dir in path.split(File.pathSeparator)) { - for (name in names) { - val f = File(dir, name) - if (f.isFile && f.canExecute()) return f.absolutePath + val path = System.getenv("PATH") + if (path != null) { + for (dir in path.split(File.pathSeparator)) { + for (name in names) { + val f = File(dir, name) + if (f.isFile && f.canExecute()) return f.absolutePath + } } } + // The install root, last: an IDE launched from the desktop does not inherit + // the PATH the installer wrote to the shell profile, so a working ~/.ludic + // install would otherwise look missing. + val home = System.getenv("LUDIC_HOME") ?: (System.getProperty("user.home") + "/.ludic") + for (name in names) { + val f = File(File(home, "bin"), name) + if (f.isFile && f.canExecute()) return f.absolutePath + } return null } @@ -108,9 +118,10 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne append(if (root != null) File(File(root, d), "ludic-lsp").path else "/$d/ludic-lsp") append('\n') } - append(" and on PATH.\n") + append(" on PATH, and in ~/.ludic/bin.\n") } - append("\nBuild it with `bin/x tools` from the repository root, ") + 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("or set the path in Settings | Languages & Frameworks | Ludic. ") append("Syntax highlighting works without it; completion and diagnostics do not.") } diff --git a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml index a1377298..f2b11170 100644 --- a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml +++ b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml @@ -67,7 +67,7 @@ name="Ludic" factoryClass="io.ludic.ide.LudicLanguageServerFactory"> bin/x tools. + The Ludic language server. Built by bin/ludic dev tools. ]]> diff --git a/tools/editors/vscode/README.md b/tools/editors/vscode/README.md index 31a650e9..6bbfde27 100644 --- a/tools/editors/vscode/README.md +++ b/tools/editors/vscode/README.md @@ -23,7 +23,7 @@ for the language reference. Build the toolchain once, from the repository root: ```bash -bin/x tools +bin/ludic dev tools ``` That produces `bin/ludic-lsp` and `bin/ludic-fmt`, which the extension finds diff --git a/tools/editors/vscode/src/extension.js b/tools/editors/vscode/src/extension.js index 47f5c11b..b576913c 100644 --- a/tools/editors/vscode/src/extension.js +++ b/tools/editors/vscode/src/extension.js @@ -38,9 +38,21 @@ function resolveServer(folders) { if (isExecutable(p)) return p; } } + // The install root, checked last: an editor launched from Finder or a desktop + // launcher does not inherit the PATH the installer wrote to the shell profile, + // so a perfectly good ~/.ludic install would otherwise look missing. + for (const name of names) { + const p = path.join(installRoot(), 'bin', name); + if (isExecutable(p)) return p; + } return null; } +/** The toolchain install root: $LUDIC_HOME, else ~/.ludic (what install.sh uses). */ +function installRoot() { + return process.env.LUDIC_HOME || path.join(os.homedir(), '.ludic'); +} + function resolveCompiler(folders) { const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath'); if (configured) return expand(configured, folders); @@ -54,6 +66,8 @@ function resolveCompiler(folders) { const p = path.join(dir, 'ludicc'); if (isExecutable(p)) return p; } + const installed = path.join(installRoot(), 'bin', 'ludicc'); + if (isExecutable(installed)) return installed; return ''; } @@ -84,7 +98,7 @@ async function start(context) { // Not an error worth a modal: the grammar still highlights, and plenty of // people open a .ludic file without having built the toolchain. output.appendLine( - 'ludic-lsp not found. Build it with `bin/x tools`, or set "ludic.server.path". ' + + 'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic dev tools`, or set "ludic.server.path". ' + 'Syntax highlighting works without it; completion and diagnostics do not.' ); return; diff --git a/tools/ludic-cli/build.ludic b/tools/ludic-cli/build.ludic new file mode 100644 index 00000000..640b27ce --- /dev/null +++ b/tools/ludic-cli/build.ludic @@ -0,0 +1,100 @@ +# build.ludic — compiling: a user's program (ludic build / ludic run) and the +# toolchain's own binaries (ludic dev build). +# +# compile_app is the one place a .ludic file becomes an executable, so a project +# build and the repo's own example builds cannot drift apart. Everything the +# toolchain owns — the compiler, the engine runtime it splices — is addressed +# through ludic_home(), so the same code path works from a checkout and from an +# installed ~/.ludic. + +# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). +# Returns true on success. Used for x itself and the editor tools. +function build_tool(name: pointer, src: pointer) -> bool { + let ll = `build/{name}.ll` + if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } + # write to a temp then move, so a running bin/ludic can rebuild itself in place + if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false } + run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`) + return true +} + +# ---- build-cli: the self-hosted front-end binaries --------------------------- +# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed +# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just +# compiles. +function cmd_dev_build_cli() -> int { + run("mkdir -p bin build") + print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)") + if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 } + run("chmod +x bin/ludicc") + print("done. built bin/ludicc") + return 0 +} + +# ---- build: the whole toolchain (ludicc, ludic, ludic-fmt, ludic-lsp) ------- +function cmd_dev_build() -> int { + if cmd_dev_build_cli() != 0 { return 1 } + print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI rebuilds itself)") + if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 } + print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt") + if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 } + print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp") + if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 } + sync_vscode_grammar() + print("done. toolchain in bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)") + return 0 +} + +# ---- compiling one program to a native binary ------------------------------- +# +# mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is +# rendered to build/out.ppm from piped input — deterministic output for tests). +# Returns true on success. `ludic build`, `ludic run` and `ludic dev app` all go +# through here. +function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool { + ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale + let home = ludic_home() + let pbf = prebuilt_link_flags() # #64: link resolved prebuilt module dylibs, if any + let odir = dir_of_path(out) + if len(odir) > 0 { run(`mkdir -p {odir}`) } + run("mkdir -p build") + let ll = `{out}.ll` + + if mode == 2 { + if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false } + if not shq(`{cc()} -O2 {ll}{pbf} -o {out}`) { return false } + if not save { run(`rm -f {ll}`) } + return true + } + + if not shq(`{ludicc()} --windowed {src} --emit-llvm -o {ll}`) { return false } + # audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the + # canonical `ludicc -o` path links it only when Audio.* is used. + let cocoa = `{home}runtime/native/cocoa.ll` + let audio = `{home}runtime/native/audio.ll` + if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{pbf} -o {out}`) { return false } + if not save { run(`rm -f {ll}`) } + return true +} + +# the directory part of a path, without the trailing '/' ("" when there is none) +function dir_of_path(p: pointer) -> pointer { + var last = -1 + var i = 0 + while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 } + if last < 0 { return "" } + return p[0..last] +} + +# Remove every generated artifact and leave the tracked source untouched: the +# whole build/ tree (IR, objects, compiled apps, the headless render at +# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A +# legacy out.ppm in the root (from an older toolchain) is swept too. Binaries in +# bin/ are kept so the running CLI survives; in the toolchain repo, rebuild them +# any time with `ludic dev build`. +function cmd_clean() -> int { + run("rm -rf build") + run("rm -f out.ppm bin/*.tmp") + print("cleaned: build/ (incl. build/out.ppm) and stray artifacts (bin/ is left alone)") + return 0 +} diff --git a/tools/x/checks.ludic b/tools/ludic-cli/checks.ludic similarity index 98% rename from tools/x/checks.ludic rename to tools/ludic-cli/checks.ludic index 28f1e7ce..804375cf 100644 --- a/tools/x/checks.ludic +++ b/tools/ludic-cli/checks.ludic @@ -177,7 +177,7 @@ function count_nl(s: pointer, upto: int) -> int { return n } -# usage: x check-docs (scans the user-facing docs + docs/language/**) +# usage: ludic dev check-docs (scans the user-facing docs + docs/language/**) function cmd_check_docs() -> int { ensure_ludicc() DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = "" @@ -361,7 +361,7 @@ var CI_PROB: pointer = "" var CI_NPROB: int = 0 function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 } -# usage: x check-impl +# usage: ludic dev check-impl function cmd_check_impl() -> int { CI_PROB = ""; CI_NPROB = 0 let allsrc = read_all_selfhost() @@ -568,7 +568,7 @@ function set_union(a: []pointer, b: []pointer) -> []pointer { return out } -# usage: x check-vocabulary +# usage: ludic dev check-vocabulary function cmd_check_vocab() -> int { CV_PROB = ""; CV_N = 0 let h = read_file("tools/ludic-tools/ludic_syntax.h") @@ -702,10 +702,10 @@ function xml_valid(text: pointer) -> bool { return sp == 0 } -# usage: x lint-asset (validates one .json or .xml editor asset) +# usage: ludic dev lint-asset (validates one .json or .xml editor asset) function cmd_lint_asset() -> int { - if arg_count() < 3 { err("usage: x lint-asset \n"); return 2 } - let path = arg(2) + if arg_total() < 3 { err("usage: ludic dev lint-asset \n"); return 2 } + let path = arg_n(2) let t = read_file(path) if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 } if s_index(path, ".json", 0) >= 0 { diff --git a/tools/x/docgen.ludic b/tools/ludic-cli/docgen.ludic similarity index 96% rename from tools/x/docgen.ludic rename to tools/ludic-cli/docgen.ludic index 55bcc166..fac191d2 100644 --- a/tools/x/docgen.ludic +++ b/tools/ludic-cli/docgen.ludic @@ -1,17 +1,17 @@ # docgen.ludic — the Ludic documentation-site generator and its guards, ported # off Python (gen.py / check.py / palette.py). Three x subcommands: # -# x docs-palette emit selfhost/backend/stdlib/emit_color.ludic + +# ludic dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic + # docs/language/colors/palette.json from the palette # table below (the named-colour source of truth). -# x docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset +# ludic dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset # templates, emit every page + symbols.json + the # highlighter + .nojekyll into DIR (default build/pages). -# x docs-check [DIR] the coverage/integrity guard over a generated site. +# ludic dev docs-check [DIR] the coverage/integrity guard over a generated site. # # String work is over NUL-terminated byte buffers; helpers here mirror gen.py's # html.escape / json.dumps / front-matter parsing closely enough to reproduce the -# Python generator's output byte-for-byte (verified against the oracle in x test). +# Python generator's output byte-for-byte (verified against the oracle in ludic dev test). # ---- a growable byte buffer (avoids O(n^2) string building) ------------------ property Sb { data: pointer = null, len: int = 0, cap: int = 0 } @@ -563,8 +563,8 @@ function palette_emit_ludic(path: pointer) -> bool { sb_puts(b, "# no allocation, identical codegen to writing the hex by hand. Unknown names are\n") sb_puts(b, "# a compile error (color_lookup returns -1, which emit_expr reports).\n") sb_puts(b, "#\n") - sb_puts(b, "# GENERATED by `x docs-palette` from the single source-of-truth palette table\n") - sb_puts(b, "# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n") + sb_puts(b, "# GENERATED by `ludic dev docs-palette` from the single source-of-truth palette table\n") + sb_puts(b, "# in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n") sb_puts(b, "# ============================================================================\n") sb_puts(b, "\n") sb_puts(b, "function color_lookup(name: pointer) -> int {\n") @@ -620,11 +620,11 @@ function palette_emit_json(path: pointer) -> bool { return write_file(path, sb_str(b)) } -# `x docs-palette` rewrites the two tracked outputs; `x docs-palette --check` +# `ludic dev docs-palette` rewrites the two tracked outputs; `ludic dev docs-palette --check` # regenerates into scratch files and compares them to the tracked ones instead, # so the drift guard judges the working tree, not git HEAD. function cmd_docs_palette() -> int { - let check = arg_count() > 2 and arg(2) == "--check" + let check = arg_total() > 2 and arg_n(2) == "--check" palette_init() # guard: duplicate names let seen = new []pointer @@ -640,8 +640,8 @@ function cmd_docs_palette() -> int { if not palette_emit_ludic(ludic_out) { err("cannot write emit_color.ludic\n"); return 1 } if not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 } if check { - if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: x 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: x docs-palette)\n"); return 1 } + if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 } + if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 } } print(`OK {string(len(pal_names))} colors`) return 0 diff --git a/tools/x/docgen_check.ludic b/tools/ludic-cli/docgen_check.ludic similarity index 96% rename from tools/x/docgen_check.ludic rename to tools/ludic-cli/docgen_check.ludic index 913a613f..1c3af2bb 100644 --- a/tools/x/docgen_check.ludic +++ b/tools/ludic-cli/docgen_check.ludic @@ -1,4 +1,4 @@ -# docgen_check.ludic — `x docs-check [DIR]`: the coverage / integrity guard over +# docgen_check.ludic — `ludic dev docs-check [DIR]`: the coverage / integrity guard over # a generated docs site, ported from tools/docgen/check.py. Fails (exit 1) if the # pages contract is broken, an inventory symbol lacks a source file or page, a # token is documented on two pages, a namespace/section is split across dirs, or @@ -94,7 +94,7 @@ function drop_md(fn: pointer) -> pointer { function cmd_docs_check() -> int { var site = "build/pages" - if arg_count() >= 3 { site = arg(2) } + if arg_total() >= 3 { site = arg_n(2) } let problems = new []pointer let warnings = new []pointer @@ -102,6 +102,9 @@ function cmd_docs_check() -> int { if not file_exists(`{site}/index.html`) { push(problems, "missing required file: index.html") } if not file_exists(`{site}/api.html`) { push(problems, "missing required file: api.html") } if not file_exists(`{site}/.nojekyll`) { push(problems, "missing required file: .nojekyll") } + # the landing page tells people to curl this; a site without it publishes a + # broken install command + if not file_exists(`{site}/install.sh`) { push(problems, "missing required file: install.sh") } # 2) coverage + thin detection over docs/language/** let id2src = meta_new() diff --git a/tools/x/docgen_gen.ludic b/tools/ludic-cli/docgen_gen.ludic similarity index 98% rename from tools/x/docgen_gen.ludic rename to tools/ludic-cli/docgen_gen.ludic index 32194fb4..ba62d8b7 100644 --- a/tools/x/docgen_gen.ludic +++ b/tools/ludic-cli/docgen_gen.ludic @@ -1,9 +1,9 @@ -# docgen_gen.ludic — `x docs-gen`: the documentation-site generator, ported from +# docgen_gen.ludic — `ludic dev docs-gen`: the documentation-site generator, ported from # tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and # docs/site/*, reads the asset templates in tools/docgen/assets, and emits the # whole pages-branch payload (per-symbol pages, namespace/color/api pages, the # landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output -# is byte-for-byte identical to the Python generator (gated in x test). +# is byte-for-byte identical to the Python generator (gated in ludic dev test). # # Shared helpers (Sb, hesc/hescattr, the JSON writer, codeify, string utils) # live in docgen.ludic; the JSON reader (json_parse/j_get) in json.ludic. @@ -951,11 +951,11 @@ function render_highlighter(sym: JVal) -> pointer { function cmd_docs_gen() -> int { var out = "build/pages" var i = 2 - while i < arg_count() { - let a = arg(i) + while i < arg_total() { + let a = arg_n(i) if a == "--out" { i += 1 - if i < arg_count() { out = arg(i) } + if i < arg_total() { out = arg_n(i) } } i += 1 } @@ -1013,6 +1013,12 @@ function cmd_docs_gen() -> int { jvput(root, "highlight", syms.sym) write_file(`{out}/symbols.json`, json_dumps(root, true, false)) write_file(`{out}/.nojekyll`, "") + # The install one-liner on the landing page points at this site, so the + # installer is published with it: the page and the script that page tells you + # to pipe into sh can then never be from different releases. + let inst = read_file("install.sh") + if inst == null { err("docs-gen: install.sh is missing (the landing page's one-liner serves it)\n"); return 1 } + write_file(`{out}/install.sh`, inst) print(`docs generated -> {out}`) print(` pages: {string(npages)}`) return 0 diff --git a/tools/x/forgejo.ludic b/tools/ludic-cli/forgejo.ludic similarity index 100% rename from tools/x/forgejo.ludic rename to tools/ludic-cli/forgejo.ludic diff --git a/tools/x/json.ludic b/tools/ludic-cli/json.ludic similarity index 100% rename from tools/x/json.ludic rename to tools/ludic-cli/json.ludic diff --git a/tools/x/lsp_test.ludic b/tools/ludic-cli/lsp_test.ludic similarity index 99% rename from tools/x/lsp_test.ludic rename to tools/ludic-cli/lsp_test.ludic index 6a00894e..fa30de84 100644 --- a/tools/x/lsp_test.ludic +++ b/tools/ludic-cli/lsp_test.ludic @@ -1,5 +1,5 @@ # lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real -# source tree (`x test-lsp`; replaces tools/test-lsp.py). Every assertion is +# source tree (`ludic dev test-lsp`; replaces tools/test-lsp.py). Every assertion is # about a feature an editor actually calls: if this passes, VS Code, JetBrains, # Neovim and the rest are exercising code paths that have been checked, because # they all speak to this one binary. diff --git a/tools/ludic-cli/main.ludic b/tools/ludic-cli/main.ludic new file mode 100644 index 00000000..7453b247 --- /dev/null +++ b/tools/ludic-cli/main.ludic @@ -0,0 +1,230 @@ +# ludic — the command-line interface to the Ludic toolchain. One native binary +# (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and +# the compiler it drives are invoked through the `run` intrinsic. +# +# Two audiences, one binary. A user of the language sees a small surface — +# new, build, run, test, add, fmt — that works from any directory against an +# installed toolchain. A contributor to the toolchain itself gets everything +# else under `ludic dev`: bootstrapping the compiler from its IR seed, the +# regression suites, the docs site, releases. Splitting them by namespace keeps +# `ludic help` about the language rather than about the repository. +# +# Installing (macOS, Linux): +# +# curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh +# +# Building from a 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/main.ludic -o bin/ludic +# bin/ludic dev build +# +# Thereafter `ludic dev build` rebuilds the whole toolchain — including the CLI +# itself. The `dev` tasks run from the repository root. +program Ludic { + import "prelude.ludic" + import "selfhost.ludic" + import "build.ludic" + import "project.ludic" + import "tools.ludic" + import "selfhost_test.ludic" + import "test.ludic" + import "json.ludic" + import "lsp_test.ludic" + import "forgejo.ludic" + import "checks.ludic" + import "docgen.ludic" + import "docgen_gen.ludic" + import "docgen_check.ludic" + import "release.ludic" + import "pkg.ludic" + import "pkg_test.ludic" + + function usage() -> void { + print("ludic — the toolchain for the Ludic language") + print("") + print("usage: ludic [arguments]") + print("") + print("start a game:") + print(" new create a project that builds and runs as it stands") + print(" run [file] [--headless] build the project and run it") + print(" build [file] [--headless] [-o out]") + print(" compile to build/ (a native binary, nothing to ship beside it)") + print(" test [file...] compile and run the project's tests") + print(" clean remove build/") + print("") + print("packages:") + print(" add [@version] add a dependency, then resolve + fetch + link") + print(" get resolve every package.ludic dependency and write the lock") + print(" update [module] bump a dependency (or all) to its latest published version") + print(" verify check every locked package against the store by content hash") + print(" vendor copy the resolved packages into ./vendor for offline builds") + print(" build-lib compile a package's module to a prebuilt dylib in lib//") + print(" link-flags print the clang flags to link this project's prebuilt module dylibs") + print("") + print("the toolchain:") + print(" version print the toolchain version") + print(" upgrade [version] reinstall from the docs site (the same script that installed it)") + print(" doctor check that the install is complete and usable") + print(" fmt [paths...] format Ludic source in place") + print(" lsp run the language server on stdio (what editors spawn)") + print(" help this message") + print("") + print("contributing to the toolchain itself:") + print(" dev build, test, bootstrap, docs, release (ludic dev help)") + print("") + print("A file argument runs it: ludic mygame.ludic") + } + + function dev_usage() -> void { + print("ludic dev — tasks for a checkout of the Ludic toolchain (run from the repository root)") + print("") + print("build:") + print(" dev build build the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)") + print(" dev build-cli build just bin/ludicc from the IR seed") + print(" dev tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)") + print("") + print("test:") + print(" dev test the full regression suite") + print(" dev test --coverage per-file line coverage over the test specs") + print(" dev test-pkg the package-manager suite (hermetic git repos, offline)") + print(" dev selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)") + print(" dev test-tools the editor-toolchain suite") + print(" dev test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)") + print(" dev golden regenerate selfhost/golden/renders.sha256 (review with git diff)") + print("") + print("doc / lint checks (Ludic, no Python):") + print(" dev check-docs every ```ludic doc fence parses (or is marked skip/expect-error)") + print(" dev check-impl every implemented feature has a docs/language page") + print(" dev check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") + print(" dev lint-asset validate one editor .json / .xml asset") + print(" dev docs-gen [--out DIR] generate the documentation site (default build/pages)") + print(" dev docs-check [DIR] coverage/integrity guard over a generated docs site") + print(" dev docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table") + print("") + print("release:") + print(" dev release [major|minor|patch] [--dry-run] [--publish]") + print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)") + print(" dev publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push") + print(" dev changelog-section print that release's CHANGELOG.md section") + print(" dev changelog-render ") + print(" render a CHANGELOG section from a directory of changesets") + print("") + print("self-host internals:") + print(" dev selfhost-build [ludicc] [out] assemble + compile the self-host compiler") + print(" dev bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)") + print(" dev bootstrap-cfree rebuild the compiler from the seed with no C compiler") + print(" dev reseed regenerate selfhost/ludicc.seed.ll after a compiler change") + print(" dev game-build compile a Ludic game (headless)") + print(" dev sh-compile compile + link one .ludic file") + } + + # a positional argument, or a default when absent + function argn(i: int, dflt: pointer) -> pointer { + if (i < arg_count()) { return arg(i) } + return dflt + } + + # does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape + # the compiler-runner had before the CLI grew subcommands. + function is_ludic_file(s: pointer) -> bool { + let n = slen(s) + if n <= 6 { return false } + return sslice(s, n - 6, n) == ".ludic" + } + + # ---- ludic dev — the toolchain repo's own tasks --------------------- + # Every task here reads its arguments one position further along than a + # top-level command, so the shift is set once, here. + function dispatch_dev(cmd: pointer) -> int { + g_shift = 1 + if (cmd == "build") { return cmd_dev_build() } + if (cmd == "build-cli") { return cmd_dev_build_cli() } + if (cmd == "tools") { return cmd_tools() } + if (cmd == "clean") { return cmd_clean() } + if (cmd == "test") { + if (argn(3, "") == "--coverage") { return cmd_test_coverage() } + return cmd_dev_test() + } + if (cmd == "selfhost-test") { return cmd_selfhost_test() } + if (cmd == "test-tools") { return cmd_test_tools() } + if (cmd == "test-lsp") { return cmd_test_lsp() } + if (cmd == "test-pkg") { return cmd_test_pkg() } + if (cmd == "check-docs") { return cmd_check_docs() } + if (cmd == "check-impl") { return cmd_check_impl() } + if (cmd == "check-vocabulary") { return cmd_check_vocab() } + if (cmd == "lint-asset") { return cmd_lint_asset() } + if (cmd == "docs-palette") { return cmd_docs_palette() } + if (cmd == "docs-gen") { return cmd_docs_gen() } + if (cmd == "docs-check") { return cmd_docs_check() } + if (cmd == "golden") { return cmd_golden() } + if (cmd == "bootstrap") { return cmd_bootstrap() } + if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() } + if (cmd == "reseed") { return cmd_reseed() } + if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(3, "bin/ludicc"), argn(4, "build/selfhost")) } + if (cmd == "sh-compile") { + if (arg_count() < 6) { err("usage: ludic dev sh-compile \n"); exit(1) } + return cmd_sh_compile(arg(3), arg(4), arg(5)) + } + if (cmd == "game-build") { + if (arg_count() < 6) { err("usage: ludic dev game-build \n"); exit(1) } + return cmd_game_build(arg(3), arg(4), arg(5)) + } + if (cmd == "release") { return cmd_release() } + if (cmd == "changelog-render") { return cmd_changelog_render() } + if (cmd == "changelog-section") { return cmd_changelog_section() } + if (cmd == "publish") { return cmd_publish() } + if (cmd == "help") or (cmd == "--help") or (cmd == "-h") or (cmd == "") { dev_usage(); return 0 } + # reported here rather than by entry, so the message names the dev task the + # user actually typed and the usage shown is the dev one + err(`ludic dev: unknown task '{cmd}'\n`) + dev_usage() + return 1 + } + + # route one subcommand to its implementation and return its exit code; -1 when + # the name is unknown. Returning (rather than exiting in place) lets entry tear + # down the per-process scratch directory before the process ends. + function dispatch(cmd: pointer) -> int { + if (cmd == "new") { return cmd_new() } + if (cmd == "build") { return cmd_build() } + if (cmd == "run") { return cmd_run() } + if (cmd == "test") { return cmd_test() } + if (cmd == "clean") { return cmd_clean() } + if (cmd == "fmt") { return cmd_fmt() } + if (cmd == "lsp") { return cmd_lsp() } + if (cmd == "doctor") { return cmd_doctor() } + if (cmd == "upgrade") { return cmd_upgrade() } + + if (cmd == "add") { return cmd_pkg_add() } + if (cmd == "get") { return cmd_pkg_get() } + if (cmd == "update") { return cmd_pkg_update() } + if (cmd == "verify") { return cmd_pkg_verify() } + if (cmd == "vendor") { return cmd_pkg_vendor() } + if (cmd == "build-lib") { return cmd_pkg_build_lib() } + if (cmd == "link-flags") { return cmd_pkg_link_flags() } + + if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() } + if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 } + if (cmd == "dev") { return dispatch_dev(argn(2, "")) } + return -1 + } + + entry { + if (arg_count() < 2) { usage(); exit(1) } + let cmd = arg(1) + # `ludic mygame.ludic` — build and run that file + if is_ludic_file(cmd) { + let rc = cmd_run_file() + tmp_cleanup() + exit(rc) + } + let code = dispatch(cmd) + tmp_cleanup() + if code >= 0 { exit(code) } + err(`ludic: unknown command '{cmd}'\n`) + usage() + exit(1) + } +} diff --git a/tools/x/pkg.ludic b/tools/ludic-cli/pkg.ludic similarity index 94% rename from tools/x/pkg.ludic rename to tools/ludic-cli/pkg.ludic index dd52bfc0..1a89327f 100644 --- a/tools/x/pkg.ludic +++ b/tools/ludic-cli/pkg.ludic @@ -18,7 +18,7 @@ # infrastructure. A source package's Ludic is compiled together with the # consumer via the module-root import fallback in the compiler (do_import); a # prebuilt lib declares its shipped targets and is resolved against the build -# target. Everything is content-verified against the lockfile by `x verify`. +# target. Everything is content-verified against the lockfile by `ludic verify`. # ---- the manifest / lock model ---------------------------------------------- @@ -419,7 +419,7 @@ function collision(sels: []Manifest) -> pointer { # ---- lockfile writing -------------------------------------------------------- function write_lock(sels: []Manifest) -> bool { - var body = "# package.lock.ludic — generated by `x get`. Do not edit by hand." + nl() + var body = "# package.lock.ludic — generated by `ludic get`. Do not edit by hand." + nl() body = body + "lock 1" + nl() let sorted = sort_mans(sels) var i = 0 @@ -537,30 +537,30 @@ function set_require(module: pointer, ver: pointer) -> void { # ---- the commands ------------------------------------------------------------ -# x add [@version] — add/update a dependency then install +# ludic add [@version] — add/update a dependency then install function cmd_pkg_add() -> int { - if arg_count() < 3 { err("usage: x add [@version]\n"); return 1 } - let spec = split_spec(arg(2)) + if arg_total() < 3 { err("usage: ludic add [@version]\n"); return 1 } + let spec = split_spec(arg_n(2)) let module = spec[0] var ver = spec[1] if slen(ver) == 0 { ver = latest_version(module) if slen(ver) == 0 { err(`x: {module} has no published version tags (git tag vX.Y.Z to publish)\n`); return 1 } - print(`x add: {module} -> latest v{ver}`) + print(`ludic add: {module} -> latest v{ver}`) } set_require(module, ver) return do_install(read_root_manifest()) } -# x get — resolve + fetch + link every dependency in package.ludic, write lock +# ludic get — resolve + fetch + link every dependency in package.ludic, write lock function cmd_pkg_get() -> int { let txt = read_file("package.ludic") - if txt == null { err("x: no package.ludic in the current directory (x add to start one)\n"); return 1 } + if txt == null { err("x: no package.ludic in the current directory (ludic add to start one)\n"); return 1 } print("resolving dependencies (MVS)…") return do_install(parse_manifest(txt)) } -# x update [module] — bump a dep (or all) to its latest published version, relock +# ludic update [module] — bump a dep (or all) to its latest published version, relock function cmd_pkg_update() -> int { let root = read_root_manifest() if len(root.deps) == 0 { err("x: package.ludic declares no dependencies\n"); return 1 } @@ -571,7 +571,7 @@ function cmd_pkg_update() -> int { if only == "" or only == d.module { let latest = latest_version(d.module) if slen(latest) > 0 and ver_gt(latest, d.ver) { - print(`x update: {d.module} {d.ver} -> {latest}`) + print(`ludic update: {d.module} {d.ver} -> {latest}`) set_require(d.module, latest) } } @@ -580,11 +580,11 @@ function cmd_pkg_update() -> int { return do_install(read_root_manifest()) } -# x verify — check every locked package against the store by content hash and +# ludic verify — check every locked package against the store by content hash and # confirm the project view links to it function cmd_pkg_verify() -> int { let txt = read_file("package.lock.ludic") - if txt == null { err("x: no package.lock.ludic (run x get first)\n"); return 1 } + if txt == null { err("x: no package.lock.ludic (run ludic get first)\n"); return 1 } let locked = parse_lock(txt) if len(locked) == 0 { print("lockfile lists no packages"); return 0 } var bad_count = 0 @@ -602,7 +602,7 @@ function cmd_pkg_verify() -> int { if got == raw { let link = `ludic_modules/{m.module}` if file_exists(link) { print(` ok {m.module}@{m.ver}`) } - else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run x get)`); bad_count += 1 } + else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run ludic get)`); bad_count += 1 } } else { print(` TAMPERED {m.module}@{m.ver} (want sha256:{sslice(raw, 0, 12)}… got sha256:{sslice(got, 0, 12)}…)`) bad_count += 1 @@ -615,11 +615,11 @@ function cmd_pkg_verify() -> int { return 1 } -# x vendor — copy the resolved packages into ./vendor for hermetic/offline +# ludic vendor — copy the resolved packages into ./vendor for hermetic/offline # builds. Build against them with LUDIC_MODULES=vendor. function cmd_pkg_vendor() -> int { let txt = read_file("package.lock.ludic") - if txt == null { err("x: no package.lock.ludic (run x get first)\n"); return 1 } + if txt == null { err("x: no package.lock.ludic (run ludic get first)\n"); return 1 } let locked = parse_lock(txt) run("rm -rf vendor") var i = 0 @@ -627,7 +627,7 @@ function cmd_pkg_vendor() -> int { let m = locked[i] let raw = strip_prefix(m.hash, "sha256:") let dest = `{store_root()}{raw}` - if not file_exists(dest) { err(`x: {m.module}@{m.ver} not in the store — run x get\n`); return 1 } + if not file_exists(dest) { err(`x: {m.module}@{m.ver} not in the store — run ludic get\n`); return 1 } let vdir = `vendor/{m.module}` run(`mkdir -p "$(dirname {vdir})"`) run(`cp -R {dest} {vdir}`) @@ -640,7 +640,7 @@ function cmd_pkg_vendor() -> int { # ---- prebuilt binary packages (issue #64) ----------------------------------- -# x build-lib [module.ludic] — compile a package's module to a prebuilt dylib for +# ludic build-lib [module.ludic] — compile a package's module to a prebuilt dylib for # the current target, under lib//. The developer runs this in the package # repo, commits the artifact, and `git tag`s — consumers fetch and link it. # The compiler is $LUDICC (default bin/ludicc) so this works both in-repo and, @@ -648,7 +648,7 @@ function cmd_pkg_vendor() -> int { function cmd_pkg_build_lib() -> int { var src = argn(2, "") if src == "" { src = capture_line("ls *.ludic 2>/dev/null | grep -v package.ludic | head -1") } - if src == "" or not file_exists(src) { err("usage: x build-lib (run in the package directory)\n"); return 1 } + if src == "" or not file_exists(src) { err("usage: ludic build-lib (run in the package directory)\n"); return 1 } let man = read_root_manifest() var name = "" if man.module != "" { name = capture_line(`basename {man.module}`) } @@ -671,9 +671,9 @@ function cmd_pkg_build_lib() -> int { return 0 } -# x link-flags — print the clang link flags a consumer build needs to pull in the +# ludic link-flags — print the clang link flags a consumer build needs to pull in the # project's prebuilt module dylibs (for the current target). Portable escape from -# `x app` (which is in-repo only): any build system can splice `$(x link-flags)` +# `ludic build` (which is in-repo only): any build system can splice `$(ludic link-flags)` # into its link step. function cmd_pkg_link_flags() -> int { let f = prebuilt_link_flags() diff --git a/tools/x/pkg_test.ludic b/tools/ludic-cli/pkg_test.ludic similarity index 80% rename from tools/x/pkg_test.ludic rename to tools/ludic-cli/pkg_test.ludic index 11363af0..42fa5749 100644 --- a/tools/x/pkg_test.ludic +++ b/tools/ludic-cli/pkg_test.ludic @@ -3,7 +3,7 @@ # It stands up throwaway git repositories under /tmp and points the package # manager at them with LUDIC_PKG_PROXY, so the whole fetch → MVS resolve → # content-addressed store → project link → compile → run → verify pipeline runs -# with no network. Every check drives a fresh `bin/x` / `bin/ludicc` process the +# with no network. Every check drives a fresh `bin/ludic` / `bin/ludicc` process the # way a real project would. # write `body` to `path`, creating parent directories @@ -63,11 +63,11 @@ function cmd_test_pkg() -> int { let envp = `LUDIC_PKG_PROXY={proxy} LUDIC_STORE={store}` - # ---- x get: resolve, fetch, store, link, lock ----------------------------- - if shq(`( cd {proj} && {envp} {root}/bin/x get > {work}/get.out 2>&1 )`) { - ok("x get resolves + fetches the dependency graph") + # ---- ludic get: resolve, fetch, store, link, lock ----------------------------- + if shq(`( cd {proj} && {envp} {root}/bin/ludic get > {work}/get.out 2>&1 )`) { + ok("ludic get resolves + fetches the dependency graph") } else { - bad2("x get failed", capture_line(`tail -2 {work}/get.out`)) + bad2("ludic get failed", capture_line(`tail -2 {work}/get.out`)) } var lock = read_file(`{proj}/package.lock.ludic`) @@ -98,23 +98,23 @@ function cmd_test_pkg() -> int { bad2("consumer build failed", capture_line(`tail -2 {work}/build.out`)) } - # ---- x verify: content integrity against the store ------------------------ - if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify.out 2>&1 )`) { - ok("x verify passes on an untouched store") - } else { bad2("x verify failed unexpectedly", capture_line(`tail -2 {work}/verify.out`)) } + # ---- ludic verify: content integrity against the store ------------------------ + if shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify.out 2>&1 )`) { + ok("ludic verify passes on an untouched store") + } else { bad2("ludic verify failed unexpectedly", capture_line(`tail -2 {work}/verify.out`)) } # tamper with a stored file — verify must now flag it let uhash = capture_line(`grep 'example.test/util' {proj}/package.lock.ludic | sed 's/.*sha256://; s/\".*//'`) run(`echo tampered >> {store}/{uhash}/util.ludic`) - if not shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify2.out 2>&1 )`) { - ok("x verify detects a tampered store entry") - } else { bad("x verify missed a tampered store entry") } + if not shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify2.out 2>&1 )`) { + ok("ludic verify detects a tampered store entry") + } else { bad("ludic verify missed a tampered store entry") } # heal the store: a content-addressed entry is keyed by its hash-named dir, so - # `x get` trusts an existing one — drop it first, then re-fetch a clean copy. + # `ludic get` trusts an existing one — drop it first, then re-fetch a clean copy. run(`rm -rf {store}/{uhash}`) - run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`) - if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify3.out 2>&1 )`) { - ok("re-fetching heals a tampered store entry (x verify green again)") + run(`( cd {proj} && {envp} {root}/bin/ludic get > /dev/null 2>&1 )`) + if shq(`( cd {proj} && {envp} {root}/bin/ludic verify > {work}/verify3.out 2>&1 )`) { + ok("re-fetching heals a tampered store entry (ludic verify green again)") } else { bad2("store did not heal after re-fetch", capture_line(`tail -1 {work}/verify3.out`)) } # ---- a package that exports ECS: a component, a model and a system -------- @@ -122,7 +122,7 @@ function cmd_test_pkg() -> int { # consumer's compile-time ECS — no ABI seam. This proves a consumer *game* can # use a package's property (component), model, @OnSpawn handler (system) and a # plain function together. The engine runtime (runtime/native/*.ludic) resolves - # relative to the compile CWD, so — exactly like `x app` — the game is compiled + # relative to the compile CWD, so — exactly like `ludic build` — the game is compiled # from the repo root with the package linked in via an absolute LUDIC_MODULES. let epkg = `{proxy}/example.test/enginepkg` run(`mkdir -p {epkg} && git -C {epkg} init -q`) @@ -132,14 +132,14 @@ function cmd_test_pkg() -> int { let proj2 = `{work}/proj2` pt_write(`{proj2}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/enginepkg" "1.0.0"` + nl()) pt_write(`{proj2}/game.ludic`, "program G {\n import \"example.test/enginepkg/lib.ludic\"\n entry {\n spawn Unit { Health { max: 50 } }\n let H = world_prop_id(\"Health\")\n let hp = world_field_id(H, \"hp\")\n let e = world_query_next(H, 0)\n print(world_get(e, H, hp))\n print(bonus())\n }\n}\n") - if shq(`( cd {proj2} && {envp} {root}/bin/x get > {work}/get2.out 2>&1 )`) { + if shq(`( cd {proj2} && {envp} {root}/bin/ludic get > {work}/get2.out 2>&1 )`) { if shq(`( cd {root} && LUDIC_MODULES={proj2}/ludic_modules {root}/bin/ludicc --headless {proj2}/game.ludic -o {proj2}/game > {work}/gbuild.out 2>&1 )`) { let got = capture_line(`{proj2}/game`) if got == "50 7" { ok("a consumer game uses the package's component + model + system + function (\"50 7\")") } else { bad2("imported ECS produced wrong output", `got [{got}] want [50 7]`) } } else { bad2("game consuming an ECS package failed to build", capture_line(`tail -2 {work}/gbuild.out`)) } - } else { bad2("x get for the ECS package failed", capture_line(`tail -1 {work}/get2.out`)) } + } else { bad2("ludic get for the ECS package failed", capture_line(`tail -1 {work}/get2.out`)) } # ---- namespace collision policy (v1: hard error) -------------------------- let dupe = `{proxy}/example.test/dupe` @@ -147,7 +147,7 @@ function cmd_test_pkg() -> int { pt_write(`{dupe}/package.ludic`, `package "example.test/dupe"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl()) pt_write(`{dupe}/dupe.ludic`, "function dupe_tag() -> pointer { return \"d\" }\n") pt_commit_tag(dupe, "v1.0.0") - if not shq(`( cd {proj} && {envp} {root}/bin/x add example.test/dupe@1.0.0 > {work}/dupe.out 2>&1 )`) and shq(`grep -q collision {work}/dupe.out`) { + if not shq(`( cd {proj} && {envp} {root}/bin/ludic add example.test/dupe@1.0.0 > {work}/dupe.out 2>&1 )`) and shq(`grep -q collision {work}/dupe.out`) { ok("two packages claiming the same Foo.* namespace is a hard error") } else { bad2("collision not rejected", capture_line(`tail -1 {work}/dupe.out`)) } # drop the bad dep again so the manifest is clean @@ -159,16 +159,16 @@ function cmd_test_pkg() -> int { pt_write(`{pb}/package.ludic`, `package "example.test/prebuilt"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Blob"` + nl() + `targets "made-up-target"` + nl()) pt_write(`{pb}/README`, "a prebuilt lib shipping no artifact for this host\n") pt_commit_tag(pb, "v1.0.0") - if not shq(`( cd {proj} && {envp} {root}/bin/x add example.test/prebuilt@1.0.0 > {work}/pb.out 2>&1 )`) and shq(`grep -q 'prebuilt lib' {work}/pb.out`) { + if not shq(`( cd {proj} && {envp} {root}/bin/ludic add example.test/prebuilt@1.0.0 > {work}/pb.out 2>&1 )`) and shq(`grep -q 'prebuilt lib' {work}/pb.out`) { ok("a prebuilt lib with no artifact for the build target is a hard error") } else { bad2("prebuilt target matrix not enforced", capture_line(`tail -1 {work}/pb.out`)) } pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl()) - # ---- x vendor: hermetic copy --------------------------------------------- - run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`) - if shq(`( cd {proj} && {envp} {root}/bin/x vendor > {work}/vendor.out 2>&1 )`) and file_exists(`{proj}/vendor/example.test/util/util.ludic`) { - ok("x vendor copies the resolved packages into ./vendor") - } else { bad2("x vendor failed", capture_line(`tail -1 {work}/vendor.out`)) } + # ---- ludic vendor: hermetic copy --------------------------------------------- + run(`( cd {proj} && {envp} {root}/bin/ludic get > /dev/null 2>&1 )`) + if shq(`( cd {proj} && {envp} {root}/bin/ludic vendor > {work}/vendor.out 2>&1 )`) and file_exists(`{proj}/vendor/example.test/util/util.ludic`) { + ok("ludic vendor copies the resolved packages into ./vendor") + } else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) } # ---- package-declarable engine-system + namespace (issue #62) ------------- # A source package registers a compile-time engine system (@EngineSystem) and a @@ -182,7 +182,7 @@ function cmd_test_pkg() -> int { let proj4 = `{work}/proj4` pt_write(`{proj4}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/coachpack" "1.0.0"` + nl()) pt_write(`{proj4}/game.ludic`, "program Game {\n import \"example.test/coachpack/coach.ludic\"\n model Player { Score }\n @OnSpawn(Player) handler Init { }\n var frames: int = 0\n handler Boot phase Start { let e = world_spawn(world_model_id(\"Player\")) }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 5 {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n print(world_get(e, P, 0))\n print(Coach.bonus())\n quit()\n }\n }\n}\n") - if shq(`( cd {proj4} && {envp} {root}/bin/x get > {work}/get4.out 2>&1 )`) { + if shq(`( cd {proj4} && {envp} {root}/bin/ludic get > {work}/get4.out 2>&1 )`) { run(`LUDIC_HOME={root} LUDIC_MODULES={proj4}/ludic_modules {root}/bin/ludicc --headless {proj4}/game.ludic -o {proj4}/game 2>{work}/gc4.err`) if file_exists(`{proj4}/game`) { let got = capture_line(`printf 'aaaaa' | {proj4}/game`) @@ -190,10 +190,10 @@ function cmd_test_pkg() -> int { ok("a package registers an engine-system (@EngineSystem) + a namespace (@Namespace) with no compiler edit (\"4 99\")") } else { bad2("package hooks produced wrong output", `got [{got}] want [4 99]`) } } else { bad2("consumer of a @EngineSystem/@Namespace package failed to build", capture_line(`tail -1 {work}/gc4.err`)) } - } else { bad2("x get for the coach package failed", capture_line(`tail -1 {work}/get4.out`)) } + } else { bad2("ludic get for the coach package failed", capture_line(`tail -1 {work}/get4.out`)) } # ---- prebuilt binary package (issue #64): exported function + system + component - # A module is compiled to a dylib (x build-lib), fetched as a `kind prebuilt` + # A module is compiled to a dylib (ludic build-lib), fetched as a `kind prebuilt` # dependency, and linked into a consumer game that never saw its source. At load # the module registers a dynamic component and an @System(Update); the consumer # also calls an exported function via `extern`. Native/dylib only → macOS-gated. @@ -206,21 +206,21 @@ function cmd_test_pkg() -> int { run(`mkdir -p {mp} && git -C {mp} init -q`) pt_write(`{mp}/package.ludic`, `package "example.test/manapack"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Mana"` + nl() + `targets "{tgt}"` + nl()) pt_write(`{mp}/manapack.ludic`, "program ManaPack {\n function module_init() -> void { world_register_prop(\"Mana\", 2) }\n @System(Update) function mana_regen() -> void {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n if e >= 0 { let v = world_get(e, M, 0); world_set(e, M, 0, v + 1) }\n }\n function mana_bonus() -> int { return 42 }\n}\n") - if shq(`( cd {mp} && LUDICC={root}/bin/ludicc {root}/bin/x build-lib manapack.ludic > {work}/bl.out 2>&1 )`) and file_exists(`{mp}/lib/{tgt}/libmanapack.dylib`) { - ok("x build-lib compiles a package module to a per-target dylib") - } else { bad2("x build-lib failed", capture_line(`tail -1 {work}/bl.out`)) } + if shq(`( cd {mp} && LUDICC={root}/bin/ludicc {root}/bin/ludic build-lib manapack.ludic > {work}/bl.out 2>&1 )`) and file_exists(`{mp}/lib/{tgt}/libmanapack.dylib`) { + ok("ludic build-lib compiles a package module to a per-target dylib") + } else { bad2("ludic build-lib failed", capture_line(`tail -1 {work}/bl.out`)) } pt_commit_tag(mp, "v1.0.0") let proj3 = `{work}/proj3` pt_write(`{proj3}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/manapack" "1.0.0"` + nl()) pt_write(`{proj3}/game.ludic`, "program Game {\n property Dummy { x: int = 0 }\n model Ent { Dummy }\n @OnSpawn(Ent) handler Init { }\n extern function mana_bonus() -> int = \"fn_mana_bonus\"\n var frames: int = 0\n handler Boot phase Start {\n let e = world_spawn(world_model_id(\"Ent\"))\n let M = world_prop_id(\"Mana\")\n world_attach_dyn(e, M)\n world_set(e, M, 0, 0)\n }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 4 {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n print(world_get(e, M, 0))\n print(mana_bonus())\n quit()\n }\n }\n}\n") - if shq(`( cd {proj3} && {envp} {root}/bin/x get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) { - ok("x get fetches + links a prebuilt (kind prebuilt) package for the target") - } else { bad2("prebuilt x get failed", capture_line(`tail -1 {work}/get3.out`)) } + if shq(`( cd {proj3} && {envp} {root}/bin/ludic get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) { + ok("ludic get fetches + links a prebuilt (kind prebuilt) package for the target") + } else { bad2("prebuilt ludic get failed", capture_line(`tail -1 {work}/get3.out`)) } # compile the ECS consumer from the repo root (so runtime/native resolves), then - # link the module dylib via `x link-flags` (a real external build's link step). + # link the module dylib via `ludic link-flags` (a real external build's link step). run(`LUDIC_HOME={root} {root}/bin/ludicc --headless {proj3}/game.ludic --emit-llvm -o {proj3}/game.ll 2>{work}/gc.err`) - let flags = capture_line(`( cd {proj3} && LUDIC_STORE={store} {root}/bin/x link-flags )`) + let flags = capture_line(`( cd {proj3} && LUDIC_STORE={store} {root}/bin/ludic link-flags )`) if shq(`{cc()} -O2 -Wno-override-module {proj3}/game.ll {flags} -o {proj3}/game 2>{work}/gl.err`) { let got = capture_line(`printf 'aaaaaa' | {proj3}/game`) if got == "3 42" { diff --git a/tools/x/prelude.ludic b/tools/ludic-cli/prelude.ludic similarity index 57% rename from tools/x/prelude.ludic rename to tools/ludic-cli/prelude.ludic index 8fa2176b..a8d78538 100644 --- a/tools/x/prelude.ludic +++ b/tools/ludic-cli/prelude.ludic @@ -1,14 +1,16 @@ -# prelude.ludic — the shared runtime for `x`, the Ludic task runner. +# prelude.ludic — the shared runtime for `ludic`, the command-line interface. # -# `x` replaces every build/test/bootstrap shell script in the repo: it is a -# single native binary (bin/x) that drives clang, the self-host compiler and the -# unix tools the same way the old *.sh files did — only now it is written in -# Ludic and compiled by Ludic. 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. +# `ludic` is the one tool a user of the language ever runs: it creates projects, +# compiles and runs them, resolves packages, formats, tests, and (under +# `ludic dev`) drives every build/bootstrap/release task of the toolchain repo +# itself. It is a single native binary written in Ludic and compiled by Ludic, +# driving clang, the compiler and the unix tools through `run`. This fragment is +# the tiny standard library the commands lean on: process control, file IO, +# string trimming and a colored PASS/FAIL test harness. It carries no ECS, so it +# links as a plain CLI program. # -# Everything runs relative to the current directory, so `x` must be invoked from -# the repository root (the one-line bootstrap in README.md does exactly that). +# 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 ---------------------------------------------------------------- @@ -51,7 +53,7 @@ function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 } # ---- scratch files ------------------------------------------------------------ # Every scratch file the runner writes lives under one per-process directory -# (`$TMPDIR/x_`), so `x test` and an `x check-*` can run side by side without +# (`$TMPDIR/x_`), so `ludic dev test` and an `x check-*` can run side by side without # clobbering each other's captures. main removes it on the way out. var x_tmp: pointer = null @@ -59,7 +61,7 @@ function tmp_dir() -> pointer { if x_tmp == null { var base = Os.temp_dir() if len(base) > 1 and base[len(base) - 1] == '/' { base = base[0..len(base) - 1] } # macOS: TMPDIR ends in '/' - x_tmp = `{base}/x_{Os.pid()}` + x_tmp = `{base}/ludic_{Os.pid()}` run(`mkdir -p {x_tmp}`) } return x_tmp @@ -69,10 +71,10 @@ function tmp_dir() -> pointer { function tmp_path(name: pointer) -> pointer { return `{tmp_dir()}/{name}` } # remove the scratch directory (idempotent; a no-op when nothing was written). -# X_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case. +# LUDIC_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case. function tmp_cleanup() -> void { if x_tmp == null { return } - if getenv_or("X_KEEP_TMP", "0") == "1" { print(`scratch kept: {x_tmp}`) } + if getenv_or("LUDIC_KEEP_TMP", "0") == "1" { print(`scratch kept: {x_tmp}`) } else { run(`rm -rf {x_tmp}`) } x_tmp = null } @@ -172,3 +174,83 @@ function report() -> int { if (FAIL == 0) { return 0 } return 1 } + +# ---- command arguments ------------------------------------------------------- +# +# A command reads its own arguments as arg_n(1), arg_n(2)… whether it was +# reached as `ludic get` or as `ludic dev docs-check DIR`. main sets the shift +# once per dispatch, so no command has to know how deep its namespace is — +# getting that wrong is how `docs-check DIR` silently checked the wrong +# directory. +var g_shift: int = 0 + +function arg_n(i: int) -> pointer { return arg(i + g_shift) } +function arg_total() -> int { return arg_count() - g_shift } + +# ---- the toolchain install --------------------------------------------------- +# +# The user-facing commands run anywhere, so they cannot assume a `bin/` in the +# current directory the way the old repo-root-only task runner did. They ask +# here instead, and get the same answer in both layouts that exist: an install +# (~/.ludic/bin/ludic, root ~/.ludic) and a checkout of the toolchain repo +# (bin/ludic, root the repo). The compiler derives its own root by the identical +# rule (see ludic_home() in selfhost/frontend/parse.ludic), so the two never +# disagree about where the runtime and the bundled packages live. + +var g_home: pointer = null + +# the directory holding this binary, with a trailing '/'. argv[0] carries no +# directory when the CLI was found on $PATH, which is the normal case for an +# install — ask the shell where it found it. +function self_dir() -> pointer { + var a0 = arg(0) + var slash = -1 + var i = 0 + while a0[i] != 0 { if a0[i] == '/' { slash = i }; i += 1 } + if slash < 0 { + let found = capture_line(`command -v {a0}`) + if found == "" { return "" } + a0 = found + slash = -1 + i = 0 + while a0[i] != 0 { if a0[i] == '/' { slash = i }; i += 1 } + if slash < 0 { return "" } + } + return a0[0..slash + 1] +} + +# The toolchain install root, with a trailing '/', or "" for the current +# directory. $LUDIC_HOME wins; otherwise the binary's own directory decides, and +# a directory named `bin` means the root is its parent. +function ludic_home() -> pointer { + if g_home != null { return g_home } + let env = getenv("LUDIC_HOME") + if env != null { + var e = env + if len(e) > 0 and e[len(e) - 1] != '/' { e = e + "/" } + g_home = e + return g_home + } + var d = self_dir() + if len(d) >= 4 and d[len(d) - 4..len(d)] == "bin/" { d = d[0..len(d) - 4] } + # a source checkout the CLI was run from elsewhere in: prefer a ./bin here + if not is_exec(`{d}bin/ludicc`) and is_exec("bin/ludicc") { d = "" } + g_home = d + return g_home +} + +# the path to a toolchain binary (ludicc, ludic-fmt, ludic-lsp). Falls back to +# the bare name — on $PATH — when the install root has no bin/ of its own. +function tool(name: pointer) -> pointer { + let p = `{ludic_home()}bin/{name}` + if is_exec(p) { return p } + return name +} + +function ludicc() -> pointer { return tool("ludicc") } + +# true in a checkout of the toolchain repo itself, where the `ludic dev` tasks +# have something to work on. +function in_toolchain_repo() -> bool { + return file_exists("selfhost/ludicc.seed.ll") and file_exists("tools/ludic-cli/main.ludic") +} diff --git a/tools/ludic-cli/project.ludic b/tools/ludic-cli/project.ludic new file mode 100644 index 00000000..a4c5e823 --- /dev/null +++ b/tools/ludic-cli/project.ludic @@ -0,0 +1,397 @@ +# project.ludic — the commands a user of the language runs on their own project: +# new, build, run, test, fmt, lsp, doctor, upgrade. +# +# The shape of a project is a convention, not a configuration file: a manifest +# (package.ludic), the program under src/, tests under tests/, assets under +# assets/, output in build/. `ludic build` in such a directory needs no +# arguments; given a path it compiles that file instead, so a single .ludic +# lying around is still a one-command build. +# +# None of this assumes a checkout of the toolchain — the compiler, the engine +# runtime and the bundled ludic.* packages are found through ludic_home(). + +# ---- project discovery ------------------------------------------------------ + +# the `package "name"` line of ./package.ludic, or "" when there is no manifest +function manifest_name() -> pointer { + let txt = read_file("package.ludic") + if txt == null { return "" } + let m = parse_manifest(txt) + return m.module +} + +# the name to give the built binary: the manifest's module (its last dotted +# segment, so ludic.snake builds `snake`), else the entry file's base name. +function project_name(entry: pointer) -> pointer { + let mod = manifest_name() + if mod != "" { + var last = -1 + var i = 0 + while mod[i] != 0 { if mod[i] == '.' { last = i }; i += 1 } + if last >= 0 { return mod[last + 1..i] } + return mod + } + return capture_line(`basename {entry} .ludic`) +} + +# The program to compile. An explicit path always wins; otherwise the +# conventional entry points are tried in order, and finally a lone .ludic file in +# the current directory (so a scratch directory needs no ceremony). "" when +# nothing was found. +function find_entry(explicit: pointer) -> pointer { + if explicit != "" { return explicit } + if file_exists("src/main.ludic") { return "src/main.ludic" } + if file_exists("main.ludic") { return "main.ludic" } + let one = capture_line("ls *.ludic 2>/dev/null") + if one != "" and not shq("ls *.ludic 2>/dev/null | tail -n +2 | grep -q .") { return one } + return "" +} + +# report the "no entry point" case the same way everywhere +function no_entry() -> int { + err("ludic: no program to build here.\n") + err(" expected src/main.ludic (or main.ludic), or name a file: ludic build path/to/game.ludic\n") + err(" start a new project with: ludic new \n") + return 1 +} + +# ---- ludic new -------------------------------------------------------------- + +# The smallest program worth running: a window, an entity moving under the ECS, +# text, and a key to quit. A new project should do something the moment it is +# created — an empty main() teaches nothing about the language. +# +# Backticks interpolate, and this text is full of braces, so it is built by +# concatenation. The template lives here rather than in a data file because the +# CLI must scaffold from an install where no repo tree exists. +function template_main(name: pointer) -> pointer { + var b = "# src/main.ludic — the program. Build and play it with: ludic run\n" + b = b + "program " + name + " {\n" + b = b + "\n" + b = b + " property Pos { x: int = 0, y: int = 0 }\n" + b = b + " property Vel { dx: int = 0, dy: int = 0 }\n" + b = b + "\n" + b = b + " handler Boot phase Start {\n" + b = b + " spawn Ball { Pos { x: 152, y: 112 }, Vel { dx: 2, dy: 1 } }\n" + b = b + " }\n" + b = b + "\n" + b = b + " handler Move phase FixedUpdate {\n" + b = b + " for (p, v) in query [Pos, Vel] {\n" + b = b + " p.x = p.x + v.dx\n" + b = b + " p.y = p.y + v.dy\n" + b = b + " if p.x < 0 or p.x > 304 { v.dx = 0 - v.dx }\n" + b = b + " if p.y < 0 or p.y > 224 { v.dy = 0 - v.dy }\n" + b = b + " }\n" + b = b + " }\n" + b = b + "\n" + b = b + " handler Draw phase Render {\n" + b = b + " Screen.clear(0x0d1020)\n" + b = b + " for (p) in query [Pos] {\n" + b = b + " Screen.fill_rectangle(x: p.x, y: p.y, width: 16, height: 16, color: Color.Crimson)\n" + b = b + " }\n" + b = b + " Screen.draw_text(x: 8, y: 8, text: \"HELLO LUDIC\", color: Color.White, scale: 1)\n" + b = b + " Screen.show()\n" + b = b + " }\n" + b = b + "\n" + b = b + " handler Keys phase Input {\n" + b = b + " if Input.key() == 'q' { quit() }\n" + b = b + " }\n" + b = b + "}\n" + return b +} + +function template_test(name: pointer) -> pointer { + var b = "# A test program is a set of test blocks; ludic test compiles and runs every\n" + b = b + "# tests/*.ludic and reports the results.\n" + b = b + "program " + name + "Spec {\n" + b = b + " test \"arithmetic still works\" {\n" + b = b + " expect_eq(2 + 2, 4)\n" + b = b + " }\n" + b = b + "}\n" + return b +} + +function template_manifest(name: pointer) -> pointer { + return "package \"" + name + "\"\nversion \"0.1.0\"\nkind source\n" +} + +function template_gitignore() -> pointer { + return "build/\nludic_modules/\nvendor/\n*.ll\n" +} + +function template_readme(name: pointer) -> pointer { + var b = "# " + name + "\n" + b = b + "\n" + b = b + "A game written in Ludic (https://workshopsoft.pages.workshopsoft.io/ludic/).\n" + b = b + "\n" + b = b + " ludic run # build and play\n" + b = b + " ludic test # run the tests\n" + return b +} + +# ludic new — scaffold a project that builds and runs as it stands. +function cmd_new() -> int { + if arg_total() < 3 { + err("usage: ludic new \n") + return 1 + } + let name = arg_n(2) + if file_exists(name) { + err(`ludic new: {name} already exists\n`) + return 1 + } + run(`mkdir -p {name}/src {name}/tests {name}/assets`) + if not write_file(`{name}/package.ludic`, template_manifest(name)) { + err(`ludic new: cannot write {name}/package.ludic\n`) + return 1 + } + write_file(`{name}/src/main.ludic`, template_main(title_case(name))) + write_file(`{name}/tests/smoke.ludic`, template_test(title_case(name))) + write_file(`{name}/.gitignore`, template_gitignore()) + write_file(`{name}/README.md`, template_readme(name)) + print(`created {name}/`) + print(" package.ludic the manifest — name, version, dependencies") + print(" src/main.ludic the program") + print(" tests/smoke.ludic a test to grow") + print(" assets/ sprites, fonts and sounds") + print("") + print(`next: cd {name} && ludic run`) + return 0 +} + +# ---- ludic build / run ------------------------------------------------------ + +# shared flag parsing for build and run: returns the entry file ("" = none), +# filling the globals below. +var g_mode: int = 1 # 1 = windowed, 2 = headless +var g_out: pointer = "" # -o +var g_save: bool = false # --save-temps + +function parse_build_args(start: int) -> pointer { + var src = "" + g_mode = 1 + g_out = "" + g_save = false + var ai = start + while ai < arg_total() { + let a = arg_n(ai) + if a == "--headless" { g_mode = 2 } + else if a == "--windowed" { g_mode = 1 } + else if a == "--save-temps" { g_save = true } + else if a == "-o" { ai += 1; if ai < arg_total() { g_out = arg_n(ai) } } + else if a[0] != '-' { src = a } + ai += 1 + } + return find_entry(src) +} + +# where the binary lands: -o if given, else build/ (build/_headless +# for a headless build, so the two can coexist). +function output_path(entry: pointer) -> pointer { + if g_out != "" { return g_out } + let name = project_name(entry) + if g_mode == 2 { return `build/{name}_headless` } + return `build/{name}` +} + +# ludic build [file] [--headless] [-o out] [--save-temps] +function cmd_build() -> int { + let entry = parse_build_args(2) + if entry == "" { return no_entry() } + let out = output_path(entry) + if not compile_app(entry, out, g_mode, g_save) { return 1 } + if g_mode == 2 { + print(`built {out} (headless: reads one key per frame from stdin, writes build/out.ppm)`) + } else { + print(`built {out}`) + } + return 0 +} + +# ludic run [file] [--headless] — build, then run it from the project directory +# so assets/ resolves relative to the game. +function cmd_run() -> int { + let entry = parse_build_args(2) + if entry == "" { return no_entry() } + let out = output_path(entry) + if not compile_app(entry, out, g_mode, g_save) { return 1 } + return sh(`./{out}`) +} + +# `ludic mygame.ludic` — the file is argv[1], so the scan starts there. +function cmd_run_file() -> int { + let entry = parse_build_args(1) + if entry == "" { return no_entry() } + let out = output_path(entry) + if not compile_app(entry, out, g_mode, g_save) { return 1 } + return sh(`./{out}`) +} + +# ---- ludic test ------------------------------------------------------------- + +# every test program in the project: tests/*.ludic plus any *_test.ludic under +# src/ (both conventions are in use, and neither is worth arguing about). +function test_files() -> []pointer { + var files = split_lines(capture("ls tests/*.ludic 2>/dev/null")) + let more = split_lines(capture("find src -name '*_test.ludic' 2>/dev/null | sort")) + var i = 0 + while i < len(more) { push(files, more[i]); i += 1 } + return files +} + +# ludic test [file...] — compile each test program headlessly and run it. A test +# program's own runner prints ok/FAIL per test block and exits non-zero if any +# failed, so this reports one line per file and forwards the failure. +function cmd_test() -> int { + var files = new []pointer + var ai = 2 + while ai < arg_total() { if arg_n(ai)[0] != '-' { push(files, arg_n(ai)) }; ai += 1 } + if len(files) == 0 { files = test_files() } + if len(files) == 0 { + err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n") + return 1 + } + run("mkdir -p build") + var failed = 0 + var i = 0 + while i < len(files) { + let f = files[i] + let bin = `{tmp_dir()}/{flat(strip_ext(f))}` + if not compile_app(f, bin, 2, false) { + print(` {c_red()}FAIL{c_reset()} {f} (did not compile)`) + failed += 1 + } else { + # a test program prints one line per test block; that belongs on screen + # when something failed and nowhere when everything passed. + let log = `{bin}.out` + let rc = sh(`{bin} < /dev/null > {log} 2>&1`) + if rc == 0 { print(` {c_green()}PASS{c_reset()} {f}`) } + else { + print(` {c_red()}FAIL{c_reset()} {f} (exit {string(rc)})`) + let txt = read_file(log) + if txt != null { out(txt) } + failed += 1 + } + } + i += 1 + } + print("") + if failed == 0 { + print(`== {string(len(files))} test files passed ==`) + return 0 + } + print(`== {string(failed)} of {string(len(files))} test files failed ==`) + return 1 +} + +# drop the extension from a path ("tests/combat.ludic" -> "tests/combat") +function strip_ext(p: pointer) -> pointer { + let n = len(p) + if n > 6 and p[n - 6..n] == ".ludic" { return p[0..n - 6] } + return p +} + +# ---- ludic fmt / lsp -------------------------------------------------------- + +# ludic fmt [paths...] — the formatter over the project (src/ and tests/ by +# default), or over the paths named. +function cmd_fmt() -> int { + var args = "" + var ai = 2 + while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 } + if args == "" { + let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort") + if found == "" { + err("ludic fmt: nothing to format (no src/ or tests/ here — name the files instead)\n") + return 1 + } + args = ` {found}` + } + return sh(`{tool("ludic-fmt")}{args}`) +} + +# ludic lsp — the language server on stdio. Editors are configured to run this, +# so the server's location is the CLI's problem rather than the user's. +function cmd_lsp() -> int { + var args = "" + var ai = 2 + while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 } + return sh(`exec {tool("ludic-lsp")}{args}`) +} + +# ---- ludic doctor ----------------------------------------------------------- + +function doctor_line(label: pointer, good: bool, detail: pointer) -> void { + if good { print(` {c_green()}ok{c_reset()} {label} {detail}`) } + else { print(` {c_red()}no{c_reset()} {label} {detail}`) } +} + +# ludic doctor — answer "is my install healthy?" without making the user guess. +# Every line is something that has actually gone wrong for someone: a missing +# clang, a half-unpacked install, a PATH that finds nothing. +function cmd_doctor() -> int { + let home = ludic_home() + var shown = home + if shown == "" { shown = "(current directory)" } + print(`ludic doctor`) + print("") + doctor_line("install root ", true, shown) + var bad_count = 0 + + # $LUDIC_CC may carry flags (the Linux build injects a shim); the first word + # is the program to look for. + let cc_name = getenv_or("LUDIC_CC", "clang") + let cc_prog = capture_line(`printf '%s' '{cc_name}' | cut -d' ' -f1`) + let have_cc = shq(`command -v {cc_prog} >/dev/null 2>&1`) + doctor_line("C toolchain ", have_cc, `{cc_name} (assembles and links the emitted IR)`) + if not have_cc { bad_count += 1 } + + let have_cc2 = is_exec(ludicc()) or shq("command -v ludicc >/dev/null 2>&1") + doctor_line("compiler ", have_cc2, ludicc()) + if not have_cc2 { bad_count += 1 } + + let have_rt = file_exists(`{home}runtime/native/cocoa.ll`) + doctor_line("runtime ", have_rt, `{home}runtime/native/ (the engine, spliced into a game)`) + if not have_rt { bad_count += 1 } + + let have_pkgs = file_exists(`{home}packages/ludic.core/package.ludic`) + doctor_line("packages ", have_pkgs, `{home}packages/ (the bundled ludic.* modules)`) + + let ver = read_file(`{home}VERSION`) + var vs = "(unknown)" + if ver != null { vs = s_trim(ver) } + doctor_line("version ", ver != null, vs) + + let on_path = capture_line("command -v ludic") + doctor_line("on PATH ", on_path != "", on_path) + + if not is_darwin() { + print("") + print(" note windowing is macOS-only today; on this host a game builds headless") + print(" (ludic build --headless) but cannot open a window.") + } + print("") + if bad_count == 0 { print("everything checks out."); return 0 } + print(`{string(bad_count)} problem(s) above. Reinstall with:`) + print(` curl -fsSL {install_url()} | sh`) + return 1 +} + +# ---- ludic upgrade ---------------------------------------------------------- + +function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") } + +# ludic upgrade [version] — re-run the installer, which replaces the install in +# place. One code path for installing and updating means an upgrade can never +# drift from a fresh install. +function cmd_upgrade() -> int { + var ver = "" + if arg_total() >= 3 { ver = arg_n(2) } + if not shq("command -v curl >/dev/null 2>&1") { + err("ludic upgrade: needs curl\n") + return 1 + } + print(`upgrading from {install_url()}`) + if ver == "" { return sh(`curl -fsSL {install_url()} | sh`) } + return sh(`curl -fsSL {install_url()} | sh -s -- --version {ver}`) +} diff --git a/tools/x/release.ludic b/tools/ludic-cli/release.ludic similarity index 85% rename from tools/x/release.ludic rename to tools/ludic-cli/release.ludic index 295064bc..a88ca252 100644 --- a/tools/x/release.ludic +++ b/tools/ludic-cli/release.ludic @@ -1,13 +1,13 @@ # release.ludic — versioning + release cutting for the toolchain. # -# x version print the toolchain version (from the VERSION file) -# x release [level] cut a release: aggregate changes/ into CHANGELOG.md, +# ludic version print the toolchain version (from the VERSION file) +# ludic dev release [level] cut a release: aggregate changes/ into CHANGELOG.md, # bump VERSION, commit, and tag vX.Y.Z. `level` is # major|minor|patch; omitted, it is derived from the # highest `bump:` among the pending changesets. -# x 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. -# x release [level] --publish ...then push main + the tag and create a +# ludic dev release [level] --publish ...then push main + the tag and create a # Forgejo release with source + toolchain tarballs. # Needs FORGEJO_TOKEN in the environment. # @@ -49,7 +49,7 @@ function compute_next(cur: pointer, level: pointer) -> pointer { return `{maj}.{min}.{p2}` } -# x version — report the toolchain version. Prefer the compiler's own --version +# ludic version — report the toolchain version. Prefer the compiler's own --version # (proving that path works); fall back to the file if ludicc is not built yet. function cmd_version() -> int { if is_exec("bin/ludicc") { @@ -233,16 +233,16 @@ function build_section(ver: pointer) -> void { write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes")) } -# x changelog-render — print the CHANGELOG section that +# ludic dev changelog-render — print the CHANGELOG section that # `dir`'s changesets would produce. Used to re-render the sections of releases # cut before the renderer preserved markdown structure: check the changesets out # of the tag's parent commit, point this at them, and splice the result back in. function cmd_changelog_render() -> int { - if arg_count() < 5 { - err("usage: x changelog-render \n") + if arg_total() < 5 { + err("usage: ludic dev changelog-render \n") return 1 } - out(render_section(arg(2), arg(3), arg(4))) + out(render_section(arg_n(2), arg_n(3), arg_n(4))) return 0 } @@ -250,7 +250,7 @@ function cmd_changelog_render() -> int { # release section (or at the end of the header if this is the first release). function prepend_changelog() -> void { if not file_exists("CHANGELOG.md") { - run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by x release; do not edit released sections by hand.\n\n' > CHANGELOG.md") + run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md") } let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1") if (ln == "") { @@ -289,11 +289,11 @@ function changelog_section(ver: pointer) -> pointer { return sslice(text, start, n) } -# x changelog-section — print that release's CHANGELOG section. +# ludic dev changelog-section — print that release's CHANGELOG section. function cmd_changelog_section() -> int { - if arg_count() < 3 { err("usage: x changelog-section \n"); return 1 } - let sec = changelog_section(arg(2)) - if slen(sec) == 0 { err(`changelog-section: no section for v{arg(2)} in CHANGELOG.md\n`); return 1 } + if arg_total() < 3 { err("usage: ludic dev changelog-section \n"); return 1 } + let sec = changelog_section(arg_n(2)) + if slen(sec) == 0 { err(`changelog-section: no section for v{arg_n(2)} in CHANGELOG.md\n`); return 1 } out(sec) return 0 } @@ -305,8 +305,14 @@ function sha256_cmd() -> pointer { } # Build the release artifacts into dist/: a reproducible source+seed tarball from -# the tag, the toolchain built on this host, and one `.sha256` beside each — a -# release without checksums asks everyone downstream to trust the transport. +# the tag, a ready-to-run toolchain built on this host, and one `.sha256` beside +# each — a release without checksums asks everyone downstream to trust the +# transport. +# +# The toolchain tarball is a complete install root, not just binaries: bin/ next +# to the engine runtime, the bundled ludic.* packages and VERSION, laid out +# exactly as ludic_home() expects. install.sh unpacks it and is done; anything +# missing here is a game that will not link on a machine with no checkout. # # The checksums are per-artifact rather than a single SHA256SUMS on purpose. A # release is assembled from more than one host (a Linux runner cannot build the @@ -325,10 +331,24 @@ function build_artifacts(ver: pointer) -> bool { if is_exec("bin/ludicc") { let plat = capture_line("uname -s | tr '[:upper:]' '[:lower:]'") let arch = capture_line("uname -m") - if not shq(`tar -czf dist/ludic-{ver}-{plat}-{arch}.tar.gz bin selfhost/ludicc.seed.ll VERSION`) { + let name = `ludic-{ver}-{plat}-{arch}` + let stage = `{tmp_dir()}/{name}` + run(`rm -rf {stage} && mkdir -p {stage}/bin`) + if not shq(`cp bin/ludic bin/ludicc bin/ludic-fmt bin/ludic-lsp {stage}/bin/`) { + err("release: the toolchain is not built (run: ludic dev build)\n") + return false + } + run(`cp -R runtime {stage}/runtime`) + run(`cp -R packages {stage}/packages`) + run(`cp VERSION LICENSE README.md {stage}/`) + # the Linux link needs the Darwin-stdio shim injected through $LUDIC_CC; it + # travels with the toolchain that needs it (see tools/ci/linux_stdio_shim.ll). + if plat == "linux" { run(`mkdir -p {stage}/lib && cp tools/ci/linux_stdio_shim.ll {stage}/lib/`) } + if not shq(`tar -czf dist/{name}.tar.gz -C {tmp_dir()} {name}`) { err("release: building the toolchain tarball failed\n") return false } + run(`rm -rf {stage}`) } if not shq(`cd dist && for f in *.tar.gz; do {sha256_cmd()} "$f" > "$f.sha256" || exit 1; done`) { err("release: writing the per-artifact .sha256 files failed\n") @@ -338,10 +358,10 @@ function build_artifacts(ver: pointer) -> bool { return true } -# x publish [vX.Y.Z] — publish an already-tagged release: build the artifacts, +# ludic dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts, # take the notes from that version's CHANGELOG section, and create the Forgejo # release. Defaults to the version in VERSION. This is what CI runs on a tag -# push, and what `x release --publish` calls once it has tagged. +# push, and what `ludic dev release --publish` calls once it has tagged. function publish_tag(ver: pointer) -> int { if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 } let body = tmp_path("rel_notes.md") @@ -358,8 +378,8 @@ function publish_tag(ver: pointer) -> int { function cmd_publish() -> int { var ver = read_version_or("") - if arg_count() >= 3 { - var a = arg(2) + if arg_total() >= 3 { + var a = arg_n(2) if s_starts(a, "v") { a = sslice(a, 1, slen(a)) } ver = a } @@ -381,8 +401,8 @@ function cmd_release() -> int { var publish = false var dry = false var ai = 2 - while ai < arg_count() { - let a = arg(ai) + while ai < arg_total() { + let a = arg_n(ai) if (a == "--publish") { publish = true } else { if (a == "--dry-run") { dry = true } else { if (a == "major") or (a == "minor") or (a == "patch") { level = a } @@ -409,7 +429,7 @@ function cmd_release() -> int { let sec = read_file(tmp_path("rel_section.md")) if sec != null { out(sec) } print("") - print(` dry run — nothing written. cut it with: x release {level}`) + print(` dry run — nothing written. cut it with: ludic dev release {level}`) return 0 } prepend_changelog() @@ -424,7 +444,7 @@ function cmd_release() -> int { print(` committed + tagged v{ver}`) if publish { return publish_release(ver) } - print(` local release ready. publish with: FORGEJO_TOKEN=… x release {level} --publish`) + print(` local release ready. publish with: FORGEJO_TOKEN=… ludic dev release {level} --publish`) print(` (or push: git push origin HEAD && git push origin v{ver})`) return 0 } diff --git a/tools/x/selfhost.ludic b/tools/ludic-cli/selfhost.ludic similarity index 96% rename from tools/x/selfhost.ludic rename to tools/ludic-cli/selfhost.ludic index a6dfc012..be911287 100644 --- a/tools/x/selfhost.ludic +++ b/tools/ludic-cli/selfhost.ludic @@ -98,17 +98,18 @@ function line_count(path: pointer) -> pointer { # 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("x: could not assemble the seed\n"); exit(1) + err("ludic: could not assemble the seed\n"); exit(1) } } } # ---- selfhost-build: assemble + compile the self-host compiler --------------- -# usage: x selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost) +# usage: ludic dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost) function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int { run("mkdir -p build") let src = "build/selfhost.ludic" @@ -122,7 +123,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int { } # ---- sh-compile: compile one .ludic with a given selfhost binary and link ---- -# usage: x sh-compile +# usage: ludic dev sh-compile function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int { if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 } if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 } @@ -143,7 +144,7 @@ function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool { return true } -# usage: x game-build +# usage: ludic dev game-build function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int { if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 } print("build failed:") @@ -193,7 +194,7 @@ function cmd_bootstrap_cfree() -> int { print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler") return 0 } - print(" out.ll != seed.ll (seed is stale — regenerate with: x reseed)") + print(" out.ll != seed.ll (seed is stale — regenerate with: ludic dev reseed)") out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head")) return 1 } @@ -202,7 +203,7 @@ function cmd_bootstrap_cfree() -> int { # Uses the CURRENT seed to build the new one, compiling twice so the seed is a # fixed point of the NEW compiler, not a one-step image of the old. Falls back to # bin/ludicc only if the seed itself no longer assembles. Verify with -# `x bootstrap-cfree` afterwards. +# `ludic dev bootstrap-cfree` afterwards. function cmd_reseed() -> int { run("mkdir -p build/cfree") if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } diff --git a/tools/x/selfhost_test.ludic b/tools/ludic-cli/selfhost_test.ludic similarity index 92% rename from tools/x/selfhost_test.ludic rename to tools/ludic-cli/selfhost_test.ludic index 7fca8b94..aa7223c6 100644 --- a/tools/x/selfhost_test.ludic +++ b/tools/ludic-cli/selfhost_test.ludic @@ -19,7 +19,7 @@ function sh_case(name: pointer, exp: pointer) -> void { # framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256. # A hash is tiny and text-diffable — a regression shows as a changed hex line in a # review, not an opaque "binary files differ". Regenerate the baseline with -# `bin/x golden` (see cmd_golden below). +# `bin/ludic dev golden` (see cmd_golden below). function game_case(path: pointer, keys: pointer) -> void { let nm = flat(path) if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return } @@ -32,13 +32,13 @@ function game_case(path: pointer, keys: pointer) -> void { if not is_darwin() { skip(`{path} render (golden hash blessed on macOS)`); return } let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1") let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`) - if (want == "") { bad2(path, "no golden hash (run: bin/x golden)"); return } + if (want == "") { bad2(path, "no golden hash (run: bin/ludic dev golden)"); return } if (got == want) { ok(`{path} matches the golden render`) } else { bad2(path, `render hash {got} != golden {want}`) } } # render one golden game and return its " \n" manifest line (empty -# string on build failure). Shared by `bin/x golden` and nothing else. +# string on build failure). Shared by `bin/ludic dev golden` and nothing else. function bless_render(path: pointer, keys: pointer) -> pointer { let nm = flat(path) if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) { @@ -117,12 +117,12 @@ function cmd_selfhost_test() -> int { game_case("rendering/draw_prims", "") print("== the bootstrap fixpoint (seeded from bin/ludicc) ==") - if shq(`bin/x bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") } + if shq(`bin/ludic dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") } else { bad("fixpoint not reached"); out(capture(`cat {tmp_dir()}/boot.out`)) } print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==") - if shq(`bin/x 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: x reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) } + if shq(`bin/ludic dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") } + else { bad("C-free bootstrap failed (seed stale? run: ludic dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) } return report() } diff --git a/tools/x/test.ludic b/tools/ludic-cli/test.ludic similarity index 86% rename from tools/x/test.ludic rename to tools/ludic-cli/test.ludic index 22b908ed..2665ab30 100644 --- a/tools/x/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -4,7 +4,7 @@ # builds from the seed, compiles every example to its byte-exact golden render, # self-hosts to a fixpoint, and rebuilds itself with no C compiler. # -# It drives the other commands through `bin/x ` exactly where test.sh used +# It drives the other commands through `bin/ludic ` exactly where test.sh used # to spawn a sibling script, so each stage stays an independent, observable run. # compile a feature example as a game, run it (optional keys on stdin) and match @@ -23,7 +23,7 @@ function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) - # a feature example that consumes an in-repo controller package (packages/): same # as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a # `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine -# runtime. This is exactly how `x app` / an external project builds against a +# runtime. This is exactly how `ludic build` / an external project builds against a # fetched source package (the do_import module-root fallback, #63). function controller_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void { let nm = flat(path) @@ -59,6 +59,38 @@ function runtime_home_case() -> void { if (got == "7") { ok(lbl) } else { bad2(lbl, `got [{got}] want [7]`) } } +# The install layout: bin/ beside runtime/ and packages/, reached through $PATH +# from an unrelated directory, with nothing in the environment. This is what +# `curl https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh` lays down and the only shape most +# users will ever run, so it is asserted rather than assumed: a staged copy of +# the toolchain compiles and runs a project created by `ludic new`. +function install_layout_case() -> void { + let root = capture_line("pwd") + let home = `{tmp_dir()}/install` + let lbl = "an installed layout (bin/ + runtime/ + packages/, found on PATH) builds a new project" + run(`rm -rf {home} && mkdir -p {home}/bin`) + run(`cp bin/ludic bin/ludicc bin/ludic-fmt {home}/bin/`) + run(`cp -R runtime {home}/runtime && cp -R packages {home}/packages && cp VERSION {home}/VERSION`) + let work = `{tmp_dir()}/installwork` + run(`rm -rf {work} && mkdir -p {work}`) + # PATH only — no LUDIC_HOME, and the current directory has no bin/ of its own. + let envp = `env -u LUDIC_HOME PATH={home}/bin:/usr/bin:/bin:/usr/sbin:/sbin` + if not shq(`cd {work} && {envp} ludic new demo > {work}/new.out 2>&1`) { + bad2(lbl, capture_line(`tail -1 {work}/new.out`)); return + } + if not shq(`cd {work}/demo && {envp} ludic build --headless > {work}/build.out 2>&1`) { + bad2(lbl, capture_line(`tail -1 {work}/build.out`)); return + } + if not shq(`cd {work}/demo && {envp} ludic test > {work}/test.out 2>&1`) { + bad2(lbl, capture_line(`tail -1 {work}/test.out`)); return + } + let ver = capture_line(`{envp} ludic version`) + if ver != `ludic {s_trim(read_file("VERSION"))}` { + bad2("an installed `ludic version` reports the install's VERSION", `got [{ver}]`); return + } + ok(lbl) +} + # a "does it still compile" smoke test (parse -> lower -> link), no run function qsmoke(path: pointer) -> void { let nm = flat(path) @@ -126,7 +158,7 @@ function panic_case() -> void { else { bad2("panic", `rc={string(rc)} err=[{msg}]`) } } -# issue #45: `bin/x test --coverage`. Compile each test-spec with `--coverage`, +# issue #45: `bin/ludic dev test --coverage`. Compile each test-spec with `--coverage`, # run it with LUDIC_COVERAGE pointed at a per-file dump, then aggregate the dumps # into a clean per-file line-coverage report. The instrumentation is flag-gated, # so this reuses the same seed-built bin/ludicc the rest of the suite does. @@ -176,7 +208,7 @@ function cmd_test_coverage() -> int { } COV_COVERED = 0 COV_TOTAL = 0 - print("== line coverage (bin/x test --coverage) ==") + print("== line coverage (bin/ludic dev test --coverage) ==") cov_one("library/coverage", `{tmp_dir()}/cov_out`) cov_one("library/testing", `{tmp_dir()}/cov_out`) var pct = 100 @@ -186,7 +218,7 @@ function cmd_test_coverage() -> int { return 0 } -function cmd_test() -> int { +function cmd_dev_test() -> int { PASS = 0 FAIL = 0 @@ -199,7 +231,7 @@ function cmd_test() -> int { print("== the compiler builds from the IR seed (no C compiler) ==") run("rm -f bin/ludicc") - if shq("bin/x app examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") { + if shq("bin/ludic build examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") { ok("clang selfhost/ludicc.seed.ll -> bin/ludicc") } else { bad("compiler did not build from the seed") } @@ -209,11 +241,11 @@ function cmd_test() -> int { game_case("games/chronorift", "ddddwwwwaassK") print("== self-hosting and the bootstrap fixpoints ==") - run(`bin/x selfhost-test > {tmp_dir()}/sh.out 2>&1`) + run(`bin/ludic dev selfhost-test > {tmp_dir()}/sh.out 2>&1`) if shq(`grep -q 'gen2.ll == gen3.ll' {tmp_dir()}/sh.out`) { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") } if shq(`grep -q 'no C compiler' {tmp_dir()}/sh.out`) { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") } let corr = capture_line(`grep -c PASS {tmp_dir()}/sh.out`) - ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`) + ok(`self-host suite: {corr} checks passed (see: ludic dev selfhost-test)`) print("== documented syntax stays compilable (guards against spec/compiler drift) ==") qsmoke("lang/qdecl") @@ -367,12 +399,12 @@ function cmd_test() -> int { smoke("networking/net_rt") smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll) - print("== self-hosted front-end binaries (ludicc / ludic) ==") - # the two commands are one multi-call native binary built from the seed with - # clang alone. ludicc compiles; ludic compiles-and-runs. - if shq(`bin/x build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") and is_exec("bin/ludic") { - ok("x build-cli builds bin/ludicc and bin/ludic from the seed (clang-only)") - } else { bad2("x build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) } + print("== the compiler and the CLI (ludicc / ludic) ==") + # ludicc comes out of the IR seed with clang alone; the CLI is then compiled + # by it, from Ludic. + if shq(`bin/ludic dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") { + ok("ludic dev build-cli builds bin/ludicc from the seed (clang-only)") + } else { bad2("ludic dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) } # 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 @@ -398,37 +430,50 @@ function cmd_test() -> int { ok("ludicc --emit-llvm -> LLVM IR") } else { bad2("ludicc --emit-llvm", capture_line(`tail -1 {tmp_dir()}/cli.out`)) } - # ludic compiles, runs, and forwards the program's exit code. + # `ludic ` compiles, runs, and forwards the program's exit code. write_file(`{tmp_dir()}/cli_exit.ludic`, "entry { exit(42) }\n") - let rc = sh(`LUDIC_HOME=. bin/ludic {tmp_dir()}/cli_exit.ludic > {tmp_dir()}/cli.out 2>&1`) + let rc = sh(`bin/ludic {tmp_dir()}/cli_exit.ludic --headless > {tmp_dir()}/cli.out 2>&1`) if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") } else { bad2("ludic run: expected exit 42", `got {string(rc)}`) } + # The CLI has to work as an install does: from another directory, with no + # ./bin, finding its compiler, the engine runtime and the bundled packages + # from its own location. This is the shape `curl … | sh` produces. + install_layout_case() + + # install.sh is what the landing page tells people to pipe into sh, and it is + # published with the docs site — so it is checked here rather than discovered + # broken by a first-time user. `sh -n` parses it without running anything. + if not file_exists("install.sh") { bad("install.sh is missing") } + else if shq("sh -n install.sh 2>/dev/null") and shq("sh install.sh --help >/dev/null 2>&1") { + ok("install.sh parses under POSIX sh and answers --help") + } else { bad("install.sh does not parse under POSIX sh") } + # the package manager (issue #63): fetch + MVS resolve + content-addressed # store + ludic_modules/ links + build, run as its own hermetic suite (it # stands up throwaway git repos, so like selfhost-test it runs as a sub-process # and this stage checks its footer). - print("== package manager (x test-pkg) ==") - run(`bin/x test-pkg > {tmp_dir()}/pkg.out 2>&1`) + print("== package manager (ludic dev test-pkg) ==") + run(`bin/ludic dev test-pkg > {tmp_dir()}/pkg.out 2>&1`) if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) { let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`) - ok(`package-manager suite: {pc} checks passed (see: x test-pkg)`) - } else { bad2("x test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) } + ok(`package-manager suite: {pc} checks passed (see: ludic dev test-pkg)`) + } else { bad2("ludic dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) } # docs site generator + guard, in Ludic (no Python). docs-gen emits the whole # pages payload; docs-check is its coverage/integrity guard; docs-palette is # the named-colour source of truth (its --check mode regenerates the tracked # emit_color.ludic + palette.json into scratch files and compares). print("== docs site (Ludic generator, no Python) ==") - if shq(`bin/x docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) { - ok("x docs-gen -> the pages-branch payload") - } else { bad2("x docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) } - if shq(`bin/x docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) { - ok("x docs-check passes on the generated site") - } else { bad2("x docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) } - if shq(`bin/x docs-palette --check > {tmp_dir()}/pal.out 2>&1`) { - ok("x docs-palette regenerates emit_color.ludic + palette.json byte-identically") - } else { bad2("x docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) } + if shq(`bin/ludic dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) { + ok("ludic dev docs-gen -> the pages-branch payload") + } else { bad2("ludic dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) } + if shq(`bin/ludic dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) { + ok("ludic dev docs-check passes on the generated site") + } else { bad2("ludic dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) } + if shq(`bin/ludic dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) { + ok("ludic dev docs-palette regenerates emit_color.ludic + palette.json byte-identically") + } else { bad2("ludic dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) } return report() } diff --git a/tools/x/tools.ludic b/tools/ludic-cli/tools.ludic similarity index 90% rename from tools/x/tools.ludic rename to tools/ludic-cli/tools.ludic index 506f381d..42c00961 100644 --- a/tools/x/tools.ludic +++ b/tools/ludic-cli/tools.ludic @@ -27,12 +27,12 @@ function sync_vscode_grammar() -> void { # is `flag` present anywhere in argv? function has_flag(flag: pointer) -> bool { var i = 2 - while i < arg_count() { if (arg(i) == flag) { return true }; i += 1 } + while i < arg_total() { if (arg_n(i) == flag) { return true }; i += 1 } return false } # ---- tools: build the editor toolchain -------------------------------------- -# usage: x tools [--install] [--test] +# usage: ludic dev tools [--install] [--test] function cmd_tools() -> int { ensure_ludicc() print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)") @@ -57,8 +57,8 @@ function cmd_test_tools() -> int { PASS = 0 FAIL = 0 let fmt = "bin/ludic-fmt" - if not is_exec(fmt) { err("bin/ludic-fmt missing — run: x tools\n"); return 2 } - if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: x tools\n"); return 2 } + if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic dev tools\n"); return 2 } + if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic dev tools\n"); return 2 } print("formatter") @@ -129,7 +129,7 @@ function cmd_test_tools() -> int { # features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the # end-to-end protocol test on Darwin until the directory walk is portable. if is_darwin() { - let lrc = sh("bin/x test-lsp") + let lrc = sh("bin/ludic dev test-lsp") if lrc == 0 { ok("language server protocol") } else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") } else { bad("language server protocol") } @@ -160,10 +160,10 @@ function cmd_test_tools() -> int { # VS Code carries its own copy of the shared grammar; catch an edit to the copy if shq("cmp -s tools/editors/shared/ludic.tmLanguage.json tools/editors/vscode/syntaxes/ludic.tmLanguage.json") and shq("cmp -s tools/editors/shared/ludic.markdown-injection.json tools/editors/vscode/syntaxes/ludic.markdown-injection.json") and shq("cmp -s tools/editors/shared/language-configuration.json tools/editors/vscode/language-configuration.json") { ok("vscode grammar copies match tools/editors/shared") - } else { bad2("vscode grammar copies have drifted", "run: x tools to resync") } + } else { bad2("vscode grammar copies have drifted", "run: ludic dev tools to resync") } # the JetBrains plugin is off by default (its first build pulls a gigabyte of - # IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 x test-tools + # IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic dev test-tools if (getenv_or("LUDIC_TEST_JETBRAINS", "0") == "1") { if shq("test -x tools/editors/jetbrains/gradlew") { if shq("cd tools/editors/jetbrains && ./gradlew buildPlugin verifyPluginStructure --console=plain -q >/dev/null 2>&1 && test -f build/distributions/Ludic-1.0.0.zip") { ok("jetbrains plugin builds and verifies") } else { bad("jetbrains plugin build") } @@ -175,19 +175,19 @@ function cmd_test_tools() -> int { # the vocabulary lives in one place; drift between it and its copies (the # TextMate grammar, the Kotlin lexer) is the failure mode this layout prevents. - # The check is written in Ludic (tools/x/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. - if shq("bin/x check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") } + if shq("bin/ludic dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") } # every feature the compiler actually implements — namespace methods, keywords, # types, phases — must have a docs/language page. This reads the implementation # (emit_ns_call + ludic_syntax.h), so shipping a feature without docs fails here. # Ported to Ludic; runs through x. - if shq("bin/x check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") } + if shq("bin/ludic dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") } # every ```ludic fence in the docs must parse (or be marked skip/expect-error), # so a documented snippet can't drift from what the compiler accepts. - if shq("bin/x check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: x check-docs") } + if shq("bin/ludic dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic dev check-docs") } return report() } @@ -195,9 +195,9 @@ function cmd_test_tools() -> int { # JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python). function test_json(path: pointer) -> void { let bn = capture_line(`basename {path}`) - if shq(`bin/x lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) } + if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) } } function test_xml(path: pointer) -> void { let bn = capture_line(`basename {path}`) - if shq(`bin/x 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}`) } } diff --git a/tools/x/build.ludic b/tools/x/build.ludic deleted file mode 100644 index 444aa7fe..00000000 --- a/tools/x/build.ludic +++ /dev/null @@ -1,101 +0,0 @@ -# build.ludic — building the toolchain binaries and user programs. -# -# Replaces build-cli.sh (the ludicc/ludic front-end) and build.sh (compiling a -# Ludic program to a native app). All toolchain binaries land in bin/. - -# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2). -# Returns true on success. Used for x itself and the editor tools. -function build_tool(name: pointer, src: pointer) -> bool { - let ll = `build/{name}.ll` - if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } - # write to a temp then move, so a running bin/x 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_build_cli() -> int { - run("mkdir -p bin build") - print("cc: selfhost/ludicc.seed.ll -> bin/ludicc, bin/ludic (from the IR seed, no C compiler)") - if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: seed did not assemble\n"); return 1 } - run("cp bin/ludicc bin/ludic && chmod +x bin/ludicc bin/ludic") - print("done. built bin/ludicc and bin/ludic") - return 0 -} - -# ---- build: the whole toolchain (ludicc, ludic, x, ludic-fmt, ludic-lsp) ----- -function cmd_build() -> int { - if cmd_build_cli() != 0 { return 1 } - print("ludicc: tools/x/main.ludic -> bin/x (the task runner rebuilds itself)") - if not build_tool("x", "tools/x/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, x, ludic-fmt, ludic-lsp)") - return 0 -} - -# ---- app: compile a Ludic program to a native app ---------------------------- -# usage: x app [--headless|--windowed] [--save-temps] -# windowed is the default for a game; headless renders the last frame to -# build/out.ppm (never the repo root). -function cmd_app() -> int { - ensure_ludicc() - var src = null - var mode = 1 # 1 = windowed, 2 = headless - var save = false - var ai = 2 - while ai < arg_count() { - let a = arg(ai) - if (a == "--headless") { mode = 2 } - else if (a == "--windowed") { mode = 1 } - else if (a == "--save-temps") { save = true } - else if (a[0] != '-') { src = a } # 45 = '-', so skip other flags - ai += 1 - } - if (src == null) { err("usage: x app [--headless] [--save-temps]\n"); return 1 } - let name = capture_line(`basename {src} .ludic`) - - let pbf = prebuilt_link_flags() # #64: link resolved prebuilt module dylibs, if any - - if (mode == 2) { - let outbin = `build/{name}_headless` - let ll = `{outbin}.ll` - print(`ludicc: {src} -> {outbin} (renders the last frame to build/out.ppm)`) - if not shq(`bin/ludicc --headless {src} > {ll}`) { return 1 } - if not shq(`{cc()} -O2 {ll}{pbf} -o {outbin}`) { return 1 } - if not save { run(`rm -f {ll}`) } - print(`done. run: mkdir -p build && printf 'ddss' | ./{outbin} && open build/out.ppm`) - return 0 - } - - let outbin = `build/{name}` - let ll = `{outbin}.ll` - print(`ludicc: {src} -> {outbin} (Ludic-written compiler -> LLVM IR -> windowed binary)`) - if not shq(`bin/ludicc --windowed {src} > {ll}`) { return 1 } - # audio.ll (#22) is always linked here for the dev runner — unused snd_* are - # dead-stripped; the canonical `ludicc -o` path links it only when Audio.* is used. - if not shq(`{cc()} -O2 {ll} runtime/native/cocoa.ll runtime/native/audio.ll -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{pbf} -o {outbin}`) { return 1 } - if not save { run(`rm -f {ll}`) } - print(`done. run: ./{outbin} (from the repo root, so assets/ resolves)`) - return 0 -} - -# Remove every generated artifact and leave the tracked source untouched: the -# whole build/ tree (IR, objects, compiled apps, the headless render at -# 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. The -# toolchain binaries in bin/ are kept so the running `x` survives; rebuild them -# any time with `x build`. -function cmd_clean() -> int { - run("rm -rf build") - run("rm -f out.ppm bin/*.tmp") - print("cleaned: build/ (incl. build/out.ppm), bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)") - return 0 -} diff --git a/tools/x/main.ludic b/tools/x/main.ludic deleted file mode 100644 index 44503c7a..00000000 --- a/tools/x/main.ludic +++ /dev/null @@ -1,157 +0,0 @@ -# x — the Ludic task runner. One native binary (bin/x) that replaces every -# build/test/bootstrap shell script in the repo. It is written in Ludic and -# compiled by Ludic; the unix tools and clang it drives are invoked through the -# `run` intrinsic, the same way the old *.sh files shelled out. -# -# Bootstrap it once from a clean checkout (the only 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/x/main.ludic -o bin/x -# -# Thereafter `bin/x build` rebuilds the whole toolchain — including bin/x itself. -# Always run x from the repository root. -program X { - import "prelude.ludic" - import "selfhost.ludic" - import "build.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("x — the Ludic task runner (run from the repository root)") - print("") - print("build & run:") - print(" x build build the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)") - print(" x build-cli build just bin/ludicc and bin/ludic from the IR seed") - print(" x app [--headless] [--save-temps]") - print(" compile a Ludic program to a native app in build/") - print(" x tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)") - print(" x clean remove build/ (incl. build/out.ppm) and stray artifacts (keeps bin/)") - print("") - print("packages (issue #63 — MVS resolution, content-addressed store, ludic_modules/ links):") - print(" x add [@version] add a dependency to package.ludic, then resolve + fetch + link") - print(" x get resolve every package.ludic dependency, link them, write the lock") - print(" x update [module] bump a dep (or all) to its latest published version, then relock") - print(" x verify check every locked package against the store by content hash") - print(" x vendor copy the resolved packages into ./vendor for offline builds") - print(" x build-lib (#64) compile a package's module to a prebuilt dylib in lib//") - print(" x link-flags (#64) print the clang flags to link this project's prebuilt module dylibs") - print("") - print("test:") - print(" x test the full regression suite") - print(" x test-pkg the package-manager suite (hermetic git repos, offline)") - print(" x test --coverage per-file line coverage over the test specs") - print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)") - print(" x test-tools the editor-toolchain suite") - print(" x test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)") - print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)") - print("") - print("doc / lint checks (Ludic, no Python):") - print(" x check-docs every ```ludic doc fence parses (or is marked skip/expect-error)") - print(" x check-impl every implemented feature has a docs/language page") - print(" x check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") - print(" x lint-asset validate one editor .json / .xml asset") - print(" x docs-gen [--out DIR] generate the documentation site (default build/pages)") - print(" x docs-check [DIR] coverage/integrity guard over a generated docs site") - print(" x docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table (--check: compare only)") - print("") - print("release:") - print(" x version print the toolchain version (ludicc --version)") - print(" x release [major|minor|patch] [--dry-run] [--publish]") - print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)") - print(" x publish [vX.Y.Z] publish an already-tagged release (artifacts + notes); what CI runs on a tag push") - print(" x changelog-section print that release's CHANGELOG.md section") - print(" x changelog-render ") - print(" render a CHANGELOG section from a directory of changesets") - print("") - print("self-host internals:") - print(" x selfhost-build [ludicc] [out] assemble + compile the self-host compiler") - print(" x bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)") - print(" x bootstrap-cfree rebuild the compiler from the seed with no C compiler") - print(" x reseed regenerate selfhost/ludicc.seed.ll after a compiler change") - print(" x game-build compile a Ludic game (headless)") - print(" x sh-compile compile + link one .ludic file") - } - - # a positional argument, or a default when absent - function argn(i: int, dflt: pointer) -> pointer { - if (i < arg_count()) { return arg(i) } - return dflt - } - - # route one subcommand to its implementation and return its exit code; -1 when - # the name is unknown. Returning (rather than exiting in place) lets entry tear - # down the per-process scratch directory before the process ends. - function dispatch(cmd: pointer) -> int { - if (cmd == "build") { return cmd_build() } - if (cmd == "build-cli") { return cmd_build_cli() } - if (cmd == "app") { return cmd_app() } - 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_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 == "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: x sh-compile \n"); exit(1) } - return cmd_sh_compile(arg(2), arg(3), arg(4)) - } - if (cmd == "game-build") { - if (arg_count() < 5) { err("usage: x game-build \n"); exit(1) } - return cmd_game_build(arg(2), arg(3), arg(4)) - } - if (cmd == "add") { return cmd_pkg_add() } - if (cmd == "get") { return cmd_pkg_get() } - if (cmd == "update") { return cmd_pkg_update() } - if (cmd == "verify") { return cmd_pkg_verify() } - if (cmd == "vendor") { return cmd_pkg_vendor() } - if (cmd == "build-lib") { return cmd_pkg_build_lib() } - if (cmd == "link-flags") { return cmd_pkg_link_flags() } - if (cmd == "test-pkg") { return cmd_test_pkg() } - if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() } - 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(`x: unknown command '{cmd}'\n`) - usage() - exit(1) - } -}