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