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