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

`ludic help` ended with a section titled "contributing to the toolchain itself",
listing bootstrap, reseed, docs-gen and release tasks. None of that is available
to someone who installed the language — those tasks need the repository — so the
shipped tool was advertising work its user cannot do, in a namespace they have to
read past to find `new` and `run`.

The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a
checkout and excluded from every release artifact. `ludic` keeps the project and
package commands and nothing else; `ludic dev …` now explains where the tasks
went instead of failing as an unknown command.

What this shook out: the two programs share prelude/build/project/pkg, so the
helpers each had accreted in whichever file first needed them — cc(),
ensure_ludicc, the string functions, title_case, cmd_version — moved to where
both can see them. The argument-shift indirection added for the `dev` namespace
is gone with the namespace, so commands read argv directly again.

`ludic-dev test` asserts the split rather than trusting it: the staged install
must build a project, and `ludic dev build` there must fail while naming
ludic-dev. install.sh keeps building older tags, whose bootstrap goes through
main.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 23:15:12 +03:00
parent f369fbd227
commit e175619543
46 changed files with 630 additions and 558 deletions

View file

@ -10,7 +10,7 @@ docs/
language/<category>/<id>.md one file per symbol — keyword, type, phase,
builtin, namespace method, operator, annotation
language/<category>/_section.md section title + blurb + order
language/colors/palette.json the 221 named colors (generated by `ludic dev docs-palette`)
language/colors/palette.json the 221 named colors (generated by `ludic-dev docs-palette`)
site/site.json landing-page messaging (hero, features, …)
site/snippets/*.ludic the code shown on the landing page (real programs)
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks
@ -66,9 +66,9 @@ no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and
`inventory.json` are the only inputs the tools here still read directly.
```bash
bin/ludic dev docs-gen --out build/pages # generate the whole site
bin/ludic dev docs-check build/pages # coverage + duplicate-token + link guard
bin/ludic dev check-docs # parse every ```ludic doc fence
bin/ludic-dev docs-gen --out build/pages # generate the whole site
bin/ludic-dev docs-check build/pages # coverage + duplicate-token + link guard
bin/ludic-dev check-docs # parse every ```ludic doc fence
```
`docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token
@ -79,13 +79,13 @@ built `bin/ludic` (bootstrapped from the IR seed with clang alone).
## Publish
`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs
`ludic dev docs-gen` + `ludic dev docs-check` on every push to `main` that touches `docs/**`,
`ludic-dev docs-gen` + `ludic-dev docs-check` on every push to `main` that touches `docs/**`,
`tools/docgen/**` or `tools/ludic-cli/**`, and publishes the result to the `pages` branch
root. `index.html` + `.nojekyll` always stay at the root.
## Colors
`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated
by `bin/ludic dev docs-palette` from a single palette table — the `pal_add(...)` rows in
by `bin/ludic-dev docs-palette` from a single palette table — the `pal_add(...)` rows in
`tools/ludic-cli/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
generated files.

View file

@ -31,11 +31,11 @@ it needs no path configuration.
From a checkout, build them with:
```bash
bin/ludic dev tools
bin/ludic-dev tools
```
Produces `bin/ludic-fmt` and `bin/ludic-lsp`. Add `--install` to symlink both
into `~/.local/bin`, `--test` to run `bin/ludic dev test-tools` afterwards.
into `~/.local/bin`, `--test` to run `bin/ludic-dev test-tools` afterwards.
## What you get, in any editor
@ -118,7 +118,7 @@ ln -sf ../../tools/git-hooks/pre-commit .git/hooks/pre-commit
The hook runs `ludic-fmt --check` over the staged `.ludic` and `.md` files only,
and does nothing at all when `bin/ludic-fmt` has not been built — so it never
blocks a commit on a machine that has not run `bin/ludic dev tools`.
blocks a commit on a machine that has not run `bin/ludic-dev tools`.
## Why the formatter is not `ludicc --fmt`
@ -140,7 +140,7 @@ both spellings are what the language documents and uses:
(`const R_DIR: int = 0 # 0 up`) survive a save;
- `id=Root` inside a `ui` block and `{Enemy}` inside a query stay tight.
`bin/ludic dev test-tools` checks the property that matters: formatting every file in
`bin/ludic-dev test-tools` checks the property that matters: formatting every file in
the tree and re-running the *compiler's* canonical dump produces byte-identical
output. The formatter cannot change what a program means.
@ -149,8 +149,8 @@ output. The formatter cannot change what a program means.
The vocabulary is written down in five places that cannot include each other —
the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the
JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent
failure, so `bin/ludic dev check-vocabulary` (written in Ludic) compares all five, and
`bin/ludic dev test-tools` runs it.
failure, so `bin/ludic-dev check-vocabulary` (written in Ludic) compares all five, and
`bin/ludic-dev test-tools` runs it.
When you add a keyword or builtin: put it in `ludic_syntax.h`, then run
`bin/ludic dev test-tools` and let it tell you which copies still need it.
`bin/ludic-dev test-tools` and let it tell you which copies still need it.

View file

@ -43,7 +43,7 @@ The first build downloads an IntelliJ IDEA Community distribution (over 1 GB),
so expect it to take a while; later builds take seconds. Once the cache is warm,
```bash
LUDIC_TEST_JETBRAINS=1 bin/ludic dev test-tools
LUDIC_TEST_JETBRAINS=1 bin/ludic-dev test-tools
```
includes the plugin build in the toolchain's own test run.
@ -55,4 +55,4 @@ LSP4IJ is a required dependency; the IDE offers to install it for you.
**Settings -> Languages & Frameworks -> Ludic** — the path to `ludic-lsp` and to
`ludicc`. Both default to `bin/` under the project root, which is where
`bin/ludic dev tools` puts them.
`bin/ludic-dev tools` puts them.

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ async function start(context) {
// Not an error worth a modal: the grammar still highlights, and plenty of
// people open a .ludic file without having built the toolchain.
output.appendLine(
'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic dev tools`, or set "ludic.server.path". ' +
'ludic-lsp not found. Install the toolchain (curl -fsSL https://ludic.dev/install.sh | sh), build it from a checkout with `bin/ludic-dev tools`, or set "ludic.server.path". ' +
'Syntax highlighting works without it; completion and diagnostics do not.'
);
return;

View file

@ -1,56 +1,30 @@
# build.ludic — compiling: a user's program (ludic build / ludic run) and the
# toolchain's own binaries (ludic dev build).
# build.ludic — turning a .ludic file into an executable.
#
# compile_app is the one place a .ludic file becomes an executable, so a project
# build and the repo's own example builds cannot drift apart. Everything the
# toolchain owns — the compiler, the engine runtime it splices — is addressed
# through ludic_home(), so the same code path works from a checkout and from an
# installed ~/.ludic.
# compile_app is the one place that happens, shared by `ludic build` / `ludic
# run` and by the contributor tool, so a user's build and the repo's own example
# builds cannot drift apart. Everything the toolchain owns — the compiler, the
# engine runtime it splices — is addressed through ludic_home(), so the same code
# path works from a checkout and from an installed ~/.ludic.
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2).
# Returns true on success. Used for x itself and the editor tools.
function build_tool(name: pointer, src: pointer) -> bool {
let ll = `build/{name}.ll`
if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
# write to a temp then move, so a running bin/ludic can rebuild itself in place
if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false }
run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`)
return true
}
# ---- build-cli: the self-hosted front-end binaries ---------------------------
# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed
# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just
# compiles.
function cmd_dev_build_cli() -> int {
# ensure bin/ludicc exists and is current with the seed. Built from the checked-in
# IR seed with clang alone — no C compiler is ever involved.
function ensure_ludicc() -> void {
if not in_toolchain_repo() { return }
run("mkdir -p bin build")
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 }
run("chmod +x bin/ludicc")
print("done. built bin/ludicc")
return 0
}
# ---- build: the whole toolchain (ludicc, ludic, ludic-fmt, ludic-lsp) -------
function cmd_dev_build() -> int {
if cmd_dev_build_cli() != 0 { return 1 }
print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI rebuilds itself)")
if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 }
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt")
if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 }
print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp")
if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 }
sync_vscode_grammar()
print("done. toolchain in bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)")
return 0
if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) {
err("ludic: could not assemble the seed\n"); exit(1)
}
}
}
# ---- compiling one program to a native binary -------------------------------
#
# mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is
# rendered to build/out.ppm from piped input — deterministic output for tests).
# Returns true on success. `ludic build`, `ludic run` and `ludic dev app` all go
# through here.
# Returns true on success. `ludic build`, `ludic run` and the contributor tool all
# go through here.
function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool {
ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale
let home = ludic_home()
@ -91,7 +65,7 @@ function dir_of_path(p: pointer) -> pointer {
# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A
# legacy out.ppm in the root (from an older toolchain) is swept too. Binaries in
# bin/ are kept so the running CLI survives; in the toolchain repo, rebuild them
# any time with `ludic dev build`.
# any time with `ludic-dev build`.
function cmd_clean() -> int {
run("rm -rf build")
run("rm -f out.ppm bin/*.tmp")

View file

@ -9,75 +9,6 @@
# plain indexing; `==` on pointers is a byte-string compare (the whole toolchain
# leans on this).
# ---- small string helpers ---------------------------------------------------
# length of a NUL-terminated buffer
function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n }
# a fresh NUL-terminated copy of s[start .. end) (end exclusive)
function sslice(s: pointer, start: int, end: int) -> pointer {
if end < start { return "" }
let n = end - start
let b = bytes(n + 1)
var i = 0
while i < n { b[i] = s[start + i]; i += 1 }
b[n] = 0
return b
}
# index of the first byte of `needle` in `hay` at or after `from`, else -1
function s_index(hay: pointer, needle: pointer, from: int) -> int {
let hn = slen(hay)
let nn = slen(needle)
if nn == 0 { return from }
var i = from
while i + nn <= hn {
var j = 0
while j < nn and hay[i + j] == needle[j] { j += 1 }
if j == nn { return i }
i += 1
}
return -1
}
function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 }
# does `hay` contain `needle` exactly at position `at`?
function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool {
let nn = slen(needle)
var i = 0
while i < nn { if hay[at + i] != needle[i] { return false }; i += 1 }
return true
}
# does `s` (a whole line) begin with `pre`?
function s_starts(s: pointer, pre: pointer) -> bool {
let pn = slen(pre)
var i = 0
while i < pn { if s[i] != pre[i] { return false }; i += 1 }
return true
}
# is byte c an ASCII space/tab?
function is_ws(c: int) -> bool { return c == ' ' or c == '\t' }
# the substring from `start` up to the next '\n' (or end)
function line_at(s: pointer, start: int) -> pointer {
var e = start
while s[e] != 0 and s[e] != '\n' { e += 1 }
return sslice(s, start, e)
}
# trim leading/trailing ASCII whitespace (space, tab, cr, nl)
function s_trim(s: pointer) -> pointer {
let n = slen(s)
var a = 0
while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 }
var b = n
while b > a and (is_ws(s[b - 1]) or s[b - 1] == '\n' or s[b - 1] == '\r') { b -= 1 }
return sslice(s, a, b)
}
# ============================================================================
# check-docs — every ```ludic fence in the docs must parse (or be marked)
# ============================================================================
@ -177,7 +108,7 @@ function count_nl(s: pointer, upto: int) -> int {
return n
}
# usage: ludic dev check-docs (scans the user-facing docs + docs/language/**)
# usage: ludic-dev check-docs (scans the user-facing docs + docs/language/**)
function cmd_check_docs() -> int {
ensure_ludicc()
DC_OK = 0; DC_BAD = 0; DC_SKIP = 0; DC_FAILS = ""
@ -361,7 +292,7 @@ var CI_PROB: pointer = ""
var CI_NPROB: int = 0
function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 }
# usage: ludic dev check-impl
# usage: ludic-dev check-impl
function cmd_check_impl() -> int {
CI_PROB = ""; CI_NPROB = 0
let allsrc = read_all_selfhost()
@ -568,7 +499,7 @@ function set_union(a: []pointer, b: []pointer) -> []pointer {
return out
}
# usage: ludic dev check-vocabulary
# usage: ludic-dev check-vocabulary
function cmd_check_vocab() -> int {
CV_PROB = ""; CV_N = 0
let h = read_file("tools/ludic-tools/ludic_syntax.h")
@ -702,10 +633,10 @@ function xml_valid(text: pointer) -> bool {
return sp == 0
}
# usage: ludic dev lint-asset <file> (validates one .json or .xml editor asset)
# usage: ludic-dev lint-asset <file> (validates one .json or .xml editor asset)
function cmd_lint_asset() -> int {
if arg_total() < 3 { err("usage: ludic dev lint-asset <file.json|file.xml>\n"); return 2 }
let path = arg_n(2)
if arg_count() < 3 { err("usage: ludic-dev lint-asset <file.json|file.xml>\n"); return 2 }
let path = arg(2)
let t = read_file(path)
if t == null { err("lint-asset: cannot read " + path + "\n"); return 1 }
if s_index(path, ".json", 0) >= 0 {

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

@ -0,0 +1,137 @@
# ludic-dev — the contributor tool for the Ludic toolchain itself: building the
# compiler from its IR seed, the regression suites, the docs site, releases.
#
# It is a separate binary from `ludic` on purpose. Those two have different
# audiences and nothing in common but the compile step: a person who installed
# the language cannot bootstrap a compiler they do not have the source of, and
# should not have to read past a screen of tasks that only mean something inside
# this repository. So `ludic` ships and `ludic-dev` does not — it is built from a
# checkout, lives in bin/, and is left out of every release artifact.
#
# From a clean checkout (the one step Ludic cannot do for itself, since compiling
# Ludic needs a compiler):
#
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
# bin/ludic-dev build
#
# Thereafter `ludic-dev build` rebuilds the whole toolchain — bin/ludic, this
# tool, the compiler, the editor tools. Run it from the repository root.
program LudicDev {
import "prelude.ludic"
import "build.ludic"
import "toolchain.ludic"
import "selfhost.ludic"
import "tools.ludic"
import "selfhost_test.ludic"
import "test.ludic"
import "json.ludic"
import "lsp_test.ludic"
import "forgejo.ludic"
import "checks.ludic"
import "docgen.ludic"
import "docgen_gen.ludic"
import "docgen_check.ludic"
import "release.ludic"
import "pkg.ludic"
import "pkg_test.ludic"
function usage() -> void {
print("ludic-dev — tasks for a checkout of the Ludic toolchain (run from the repository root)")
print("")
print("This is the contributor tool. The language's own command line is `ludic`,")
print("which ships to users and carries none of the below.")
print("")
print("build:")
print(" build build the whole toolchain into bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)")
print(" build-cli build just bin/ludicc from the IR seed")
print(" tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
print(" clean remove build/")
print("")
print("test:")
print(" test the full regression suite")
print(" test --coverage per-file line coverage over the test specs")
print(" test-pkg the package-manager suite (hermetic git repos, offline)")
print(" selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
print(" test-tools the editor-toolchain suite")
print(" test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)")
print(" golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
print("")
print("doc / lint checks (Ludic, no Python):")
print(" check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
print(" check-impl every implemented feature has a docs/language page")
print(" check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
print(" lint-asset <file> validate one editor .json / .xml asset")
print(" docs-gen [--out DIR] generate the documentation site (default build/pages)")
print(" docs-check [DIR] coverage/integrity guard over a generated docs site")
print(" docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table")
print("")
print("release:")
print(" release [major|minor|patch] [--dry-run] [--publish]")
print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)")
print(" publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push")
print(" changelog-section <ver> print that release's CHANGELOG.md section")
print(" changelog-render <ver> <date> <dir>")
print(" render a CHANGELOG section from a directory of changesets")
print("")
print("self-host internals:")
print(" selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
print(" bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)")
print(" bootstrap-cfree rebuild the compiler from the seed with no C compiler")
print(" reseed regenerate selfhost/ludicc.seed.ll after a compiler change")
print(" game-build <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
print(" sh-compile <ludicc> <in.ludic> <out> compile + link one .ludic file")
}
function dispatch(cmd: pointer) -> int {
if (cmd == "build") { return cmd_dev_build() }
if (cmd == "build-cli") { return cmd_dev_build_cli() }
if (cmd == "tools") { return cmd_tools() }
if (cmd == "clean") { return cmd_clean() }
if (cmd == "test") {
if (argn(2, "") == "--coverage") { return cmd_test_coverage() }
return cmd_dev_test()
}
if (cmd == "selfhost-test") { return cmd_selfhost_test() }
if (cmd == "test-tools") { return cmd_test_tools() }
if (cmd == "test-lsp") { return cmd_test_lsp() }
if (cmd == "test-pkg") { return cmd_test_pkg() }
if (cmd == "check-docs") { return cmd_check_docs() }
if (cmd == "check-impl") { return cmd_check_impl() }
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
if (cmd == "lint-asset") { return cmd_lint_asset() }
if (cmd == "docs-palette") { return cmd_docs_palette() }
if (cmd == "docs-gen") { return cmd_docs_gen() }
if (cmd == "docs-check") { return cmd_docs_check() }
if (cmd == "golden") { return cmd_golden() }
if (cmd == "bootstrap") { return cmd_bootstrap() }
if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
if (cmd == "reseed") { return cmd_reseed() }
if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost")) }
if (cmd == "sh-compile") {
if (arg_count() < 5) { err("usage: ludic-dev sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) }
return cmd_sh_compile(arg(2), arg(3), arg(4))
}
if (cmd == "game-build") {
if (arg_count() < 5) { err("usage: ludic-dev game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
return cmd_game_build(arg(2), arg(3), arg(4))
}
if (cmd == "release") { return cmd_release() }
if (cmd == "changelog-render") { return cmd_changelog_render() }
if (cmd == "changelog-section") { return cmd_changelog_section() }
if (cmd == "publish") { return cmd_publish() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
return -1
}
entry {
if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1)
let code = dispatch(cmd)
tmp_cleanup()
if code >= 0 { exit(code) }
err(`ludic-dev: unknown task '{cmd}'\n`)
usage()
exit(1)
}
}

View file

@ -1,17 +1,17 @@
# docgen.ludic — the Ludic documentation-site generator and its guards, ported
# off Python (gen.py / check.py / palette.py). Three x subcommands:
#
# ludic dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic +
# ludic-dev docs-palette emit selfhost/backend/stdlib/emit_color.ludic +
# docs/language/colors/palette.json from the palette
# table below (the named-colour source of truth).
# ludic dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset
# ludic-dev docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset
# templates, emit every page + symbols.json + the
# highlighter + .nojekyll into DIR (default build/pages).
# ludic dev docs-check [DIR] the coverage/integrity guard over a generated site.
# ludic-dev docs-check [DIR] the coverage/integrity guard over a generated site.
#
# String work is over NUL-terminated byte buffers; helpers here mirror gen.py's
# html.escape / json.dumps / front-matter parsing closely enough to reproduce the
# Python generator's output byte-for-byte (verified against the oracle in ludic dev test).
# Python generator's output byte-for-byte (verified against the oracle in ludic-dev test).
# ---- a growable byte buffer (avoids O(n^2) string building) ------------------
property Sb { data: pointer = null, len: int = 0, cap: int = 0 }
@ -87,40 +87,6 @@ function lstrip_nl(s: pointer) -> pointer {
while s[i] == '\n' { i += 1 }
return sslice(s, i, slen(s))
}
# lowercase ASCII A-Z
function lower_ascii(s: pointer) -> pointer {
let n = slen(s)
let b = bytes(n + 1)
var i = 0
while i < n {
var c = s[i]
if c >= 'A' and c <= 'Z' { c += 32 }
b[i] = c
i += 1
}
b[n] = 0
return b
}
# Python str.title(): capitalise the first letter of each alpha run, lower the rest
function title_case(s: pointer) -> pointer {
let n = slen(s)
let b = bytes(n + 1)
var i = 0
var prev_alpha = false
while i < n {
var c = s[i]
let al = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')
if al {
if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } }
else { if c >= 'a' and c <= 'z' { c -= 32 } }
}
b[i] = c
prev_alpha = al
i += 1
}
b[n] = 0
return b
}
# lexicographic byte compare: <0, 0, >0
function str_cmp(a: pointer, b: pointer) -> int {
var i = 0
@ -563,7 +529,7 @@ function palette_emit_ludic(path: pointer) -> bool {
sb_puts(b, "# no allocation, identical codegen to writing the hex by hand. Unknown names are\n")
sb_puts(b, "# a compile error (color_lookup returns -1, which emit_expr reports).\n")
sb_puts(b, "#\n")
sb_puts(b, "# GENERATED by `ludic dev docs-palette` from the single source-of-truth palette table\n")
sb_puts(b, "# GENERATED by `ludic-dev docs-palette` from the single source-of-truth palette table\n")
sb_puts(b, "# in tools/ludic-cli/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n")
sb_puts(b, "# ============================================================================\n")
sb_puts(b, "\n")
@ -620,11 +586,11 @@ function palette_emit_json(path: pointer) -> bool {
return write_file(path, sb_str(b))
}
# `ludic dev docs-palette` rewrites the two tracked outputs; `ludic dev docs-palette --check`
# `ludic-dev docs-palette` rewrites the two tracked outputs; `ludic-dev docs-palette --check`
# regenerates into scratch files and compares them to the tracked ones instead,
# so the drift guard judges the working tree, not git HEAD.
function cmd_docs_palette() -> int {
let check = arg_total() > 2 and arg_n(2) == "--check"
let check = arg_count() > 2 and arg(2) == "--check"
palette_init()
# guard: duplicate names
let seen = new []pointer
@ -640,8 +606,8 @@ function cmd_docs_palette() -> int {
if not palette_emit_ludic(ludic_out) { err("cannot write emit_color.ludic\n"); return 1 }
if not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 }
if check {
if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 }
if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic dev docs-palette)\n"); return 1 }
if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: ludic-dev docs-palette)\n"); return 1 }
if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: ludic-dev docs-palette)\n"); return 1 }
}
print(`OK {string(len(pal_names))} colors`)
return 0

View file

@ -1,4 +1,4 @@
# docgen_check.ludic — `ludic dev docs-check [DIR]`: the coverage / integrity guard over
# docgen_check.ludic — `ludic-dev docs-check [DIR]`: the coverage / integrity guard over
# a generated docs site, ported from tools/docgen/check.py. Fails (exit 1) if the
# pages contract is broken, an inventory symbol lacks a source file or page, a
# token is documented on two pages, a namespace/section is split across dirs, or
@ -94,7 +94,7 @@ function drop_md(fn: pointer) -> pointer {
function cmd_docs_check() -> int {
var site = "build/pages"
if arg_total() >= 3 { site = arg_n(2) }
if arg_count() >= 3 { site = arg(2) }
let problems = new []pointer
let warnings = new []pointer

View file

@ -1,9 +1,9 @@
# docgen_gen.ludic — `ludic dev docs-gen`: the documentation-site generator, ported from
# docgen_gen.ludic — `ludic-dev docs-gen`: the documentation-site generator, ported from
# tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and
# docs/site/*, reads the asset templates in tools/docgen/assets, and emits the
# whole pages-branch payload (per-symbol pages, namespace/color/api pages, the
# landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output
# is byte-for-byte identical to the Python generator (gated in ludic dev test).
# is byte-for-byte identical to the Python generator (gated in ludic-dev test).
#
# Shared helpers (Sb, hesc/hescattr, the JSON writer, codeify, string utils)
# live in docgen.ludic; the JSON reader (json_parse/j_get) in json.ludic.
@ -951,11 +951,11 @@ function render_highlighter(sym: JVal) -> pointer {
function cmd_docs_gen() -> int {
var out = "build/pages"
var i = 2
while i < arg_total() {
let a = arg_n(i)
while i < arg_count() {
let a = arg(i)
if a == "--out" {
i += 1
if i < arg_total() { out = arg_n(i) }
if i < arg_count() { out = arg(i) }
}
i += 1
}

View file

@ -1,5 +1,5 @@
# lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real
# source tree (`ludic dev test-lsp`; replaces tools/test-lsp.py). Every assertion is
# source tree (`ludic-dev test-lsp`; replaces tools/test-lsp.py). Every assertion is
# about a feature an editor actually calls: if this passes, VS Code, JetBrains,
# Neovim and the rest are exercising code paths that have been checked, because
# they all speak to this one binary.

View file

@ -1,13 +1,13 @@
# ludic — the command-line interface to the Ludic toolchain. One native binary
# ludic — the command-line interface to the Ludic language. One native binary
# (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and
# the compiler it drives are invoked through the `run` intrinsic.
#
# Two audiences, one binary. A user of the language sees a small surface —
# new, build, run, test, add, fmt — that works from any directory against an
# installed toolchain. A contributor to the toolchain itself gets everything
# else under `ludic dev`: bootstrapping the compiler from its IR seed, the
# regression suites, the docs site, releases. Splitting them by namespace keeps
# `ludic help` about the language rather than about the repository.
# This is everything a user of the language does: create a project, build it, run
# it, test it, add a package, format, upgrade. Nothing here is about the
# toolchain's own repository — building the compiler, reseeding it, generating
# the docs site, cutting a release live in a separate tool (dev.ludic ->
# bin/ludic-dev) that only exists in a checkout and is never installed. Someone
# who installs Ludic should not have to read past commands they cannot use.
#
# Installing (macOS, Linux):
#
@ -17,29 +17,13 @@
# compiling Ludic needs a compiler:
#
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
# bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
# bin/ludic dev build
#
# Thereafter `ludic dev build` rebuilds the whole toolchain — including the CLI
# itself. The `dev` tasks run from the repository root.
# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
# bin/ludic-dev build
program Ludic {
import "prelude.ludic"
import "selfhost.ludic"
import "build.ludic"
import "project.ludic"
import "tools.ludic"
import "selfhost_test.ludic"
import "test.ludic"
import "json.ludic"
import "lsp_test.ludic"
import "forgejo.ludic"
import "checks.ludic"
import "docgen.ludic"
import "docgen_gen.ludic"
import "docgen_check.ludic"
import "release.ludic"
import "pkg.ludic"
import "pkg_test.ludic"
function usage() -> void {
print("ludic — the toolchain for the Ludic language")
@ -71,61 +55,9 @@ program Ludic {
print(" lsp run the language server on stdio (what editors spawn)")
print(" help this message")
print("")
print("contributing to the toolchain itself:")
print(" dev <task> build, test, bootstrap, docs, release (ludic dev help)")
print("")
print("A file argument runs it: ludic mygame.ludic")
}
function dev_usage() -> void {
print("ludic dev — tasks for a checkout of the Ludic toolchain (run from the repository root)")
print("")
print("build:")
print(" dev build build the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)")
print(" dev build-cli build just bin/ludicc from the IR seed")
print(" dev tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
print("")
print("test:")
print(" dev test the full regression suite")
print(" dev test --coverage per-file line coverage over the test specs")
print(" dev test-pkg the package-manager suite (hermetic git repos, offline)")
print(" dev selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
print(" dev test-tools the editor-toolchain suite")
print(" dev test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)")
print(" dev golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
print("")
print("doc / lint checks (Ludic, no Python):")
print(" dev check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
print(" dev check-impl every implemented feature has a docs/language page")
print(" dev check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
print(" dev lint-asset <file> validate one editor .json / .xml asset")
print(" dev docs-gen [--out DIR] generate the documentation site (default build/pages)")
print(" dev docs-check [DIR] coverage/integrity guard over a generated docs site")
print(" dev docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table")
print("")
print("release:")
print(" dev release [major|minor|patch] [--dry-run] [--publish]")
print(" cut a release: CHANGELOG + VERSION bump + tag (+ Forgejo release)")
print(" dev publish [vX.Y.Z] publish an already-tagged release; what CI runs on a tag push")
print(" dev changelog-section <ver> print that release's CHANGELOG.md section")
print(" dev changelog-render <ver> <date> <dir>")
print(" render a CHANGELOG section from a directory of changesets")
print("")
print("self-host internals:")
print(" dev selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
print(" dev bootstrap the self-hosting fixpoint proof (seeded from bin/ludicc)")
print(" dev bootstrap-cfree rebuild the compiler from the seed with no C compiler")
print(" dev reseed regenerate selfhost/ludicc.seed.ll after a compiler change")
print(" dev game-build <ludicc> <game.ludic> <out> compile a Ludic game (headless)")
print(" dev sh-compile <ludicc> <in.ludic> <out> compile + link one .ludic file")
}
# a positional argument, or a default when absent
function argn(i: int, dflt: pointer) -> pointer {
if (i < arg_count()) { return arg(i) }
return dflt
}
# does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape
# the compiler-runner had before the CLI grew subcommands.
function is_ludic_file(s: pointer) -> bool {
@ -134,55 +66,6 @@ program Ludic {
return sslice(s, n - 6, n) == ".ludic"
}
# ---- ludic dev <task> — the toolchain repo's own tasks ---------------------
# Every task here reads its arguments one position further along than a
# top-level command, so the shift is set once, here.
function dispatch_dev(cmd: pointer) -> int {
g_shift = 1
if (cmd == "build") { return cmd_dev_build() }
if (cmd == "build-cli") { return cmd_dev_build_cli() }
if (cmd == "tools") { return cmd_tools() }
if (cmd == "clean") { return cmd_clean() }
if (cmd == "test") {
if (argn(3, "") == "--coverage") { return cmd_test_coverage() }
return cmd_dev_test()
}
if (cmd == "selfhost-test") { return cmd_selfhost_test() }
if (cmd == "test-tools") { return cmd_test_tools() }
if (cmd == "test-lsp") { return cmd_test_lsp() }
if (cmd == "test-pkg") { return cmd_test_pkg() }
if (cmd == "check-docs") { return cmd_check_docs() }
if (cmd == "check-impl") { return cmd_check_impl() }
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
if (cmd == "lint-asset") { return cmd_lint_asset() }
if (cmd == "docs-palette") { return cmd_docs_palette() }
if (cmd == "docs-gen") { return cmd_docs_gen() }
if (cmd == "docs-check") { return cmd_docs_check() }
if (cmd == "golden") { return cmd_golden() }
if (cmd == "bootstrap") { return cmd_bootstrap() }
if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
if (cmd == "reseed") { return cmd_reseed() }
if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(3, "bin/ludicc"), argn(4, "build/selfhost")) }
if (cmd == "sh-compile") {
if (arg_count() < 6) { err("usage: ludic dev sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) }
return cmd_sh_compile(arg(3), arg(4), arg(5))
}
if (cmd == "game-build") {
if (arg_count() < 6) { err("usage: ludic dev game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
return cmd_game_build(arg(3), arg(4), arg(5))
}
if (cmd == "release") { return cmd_release() }
if (cmd == "changelog-render") { return cmd_changelog_render() }
if (cmd == "changelog-section") { return cmd_changelog_section() }
if (cmd == "publish") { return cmd_publish() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") or (cmd == "") { dev_usage(); return 0 }
# reported here rather than by entry, so the message names the dev task the
# user actually typed and the usage shown is the dev one
err(`ludic dev: unknown task '{cmd}'\n`)
dev_usage()
return 1
}
# route one subcommand to its implementation and return its exit code; -1 when
# the name is unknown. Returning (rather than exiting in place) lets entry tear
# down the per-process scratch directory before the process ends.
@ -206,13 +89,23 @@ program Ludic {
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
if (cmd == "dev") { return dispatch_dev(argn(2, "")) }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
# The toolchain's own tasks used to live here under `ludic dev`. Say where
# they went rather than printing "unknown command" at someone who read an
# older README or a commit message.
if (cmd == "dev") {
err("ludic: the toolchain's own tasks live in `ludic-dev`, which is built from a\n")
err(" checkout of the Ludic repository and is not part of an install:\n")
err(" clang selfhost/ludicc.seed.ll -o bin/ludicc\n")
err(" bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev help\n")
return 1
}
return -1
}
entry {
if (arg_count() < 2) { usage(); exit(1) }
if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1)
# `ludic mygame.ludic` — build and run that file
if is_ludic_file(cmd) {

View file

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

View file

@ -1,16 +1,14 @@
# prelude.ludic — the shared runtime for `ludic`, the command-line interface.
# prelude.ludic — the shared runtime for both command-line programs: `ludic`,
# which a user of the language runs, and `ludic-dev`, the contributor tool that
# builds the toolchain itself. Both are native binaries written in Ludic and
# compiled by Ludic, driving clang, the compiler and the unix tools through
# `run`. This fragment is the tiny standard library their commands lean on:
# process control, file IO, strings and a colored PASS/FAIL test harness. It
# carries no ECS, so both link as plain CLI programs.
#
# `ludic` is the one tool a user of the language ever runs: it creates projects,
# compiles and runs them, resolves packages, formats, tests, and (under
# `ludic dev`) drives every build/bootstrap/release task of the toolchain repo
# itself. It is a single native binary written in Ludic and compiled by Ludic,
# driving clang, the compiler and the unix tools through `run`. This fragment is
# the tiny standard library the commands lean on: process control, file IO,
# string trimming and a colored PASS/FAIL test harness. It carries no ECS, so it
# links as a plain CLI program.
#
# The user-facing commands work from any directory. The `ludic dev` tasks run
# relative to the current directory and expect the toolchain repo root.
# `ludic`'s commands work from any directory, against whatever toolchain is
# installed. `ludic-dev`'s tasks run relative to the current directory and expect
# the toolchain repo root.
# ---- file IO ----------------------------------------------------------------
@ -53,7 +51,7 @@ function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 }
# ---- scratch files ------------------------------------------------------------
# Every scratch file the runner writes lives under one per-process directory
# (`$TMPDIR/x_<pid>`), so `ludic dev test` and an `x check-*` can run side by side without
# (`$TMPDIR/x_<pid>`), so `ludic-dev test` and an `x check-*` can run side by side without
# clobbering each other's captures. main removes it on the way out.
var x_tmp: pointer = null
@ -175,17 +173,11 @@ function report() -> int {
return 1
}
# ---- command arguments -------------------------------------------------------
#
# A command reads its own arguments as arg_n(1), arg_n(2)… whether it was
# reached as `ludic get` or as `ludic dev docs-check DIR`. main sets the shift
# once per dispatch, so no command has to know how deep its namespace is —
# getting that wrong is how `docs-check DIR` silently checked the wrong
# directory.
var g_shift: int = 0
function arg_n(i: int) -> pointer { return arg(i + g_shift) }
function arg_total() -> int { return arg_count() - g_shift }
# a positional argument, or a default when it is absent
function argn(i: int, dflt: pointer) -> pointer {
if (i < arg_count()) { return arg(i) }
return dflt
}
# ---- the toolchain install ---------------------------------------------------
#
@ -249,8 +241,121 @@ function tool(name: pointer) -> pointer {
function ludicc() -> pointer { return tool("ludicc") }
# true in a checkout of the toolchain repo itself, where the `ludic dev` tasks
# have something to work on.
# true in a checkout of the toolchain repo itself, where a seed is present to
# assemble the compiler from.
function in_toolchain_repo() -> bool {
return file_exists("selfhost/ludicc.seed.ll") and file_exists("tools/ludic-cli/main.ludic")
}
# ---- strings ----------------------------------------------------------------
# Shared by every command, so they live here rather than in whichever file
# happened to need them first.
# length of a NUL-terminated buffer
function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n }
# a fresh NUL-terminated copy of s[start .. end) (end exclusive)
function sslice(s: pointer, start: int, end: int) -> pointer {
if end < start { return "" }
let n = end - start
let b = bytes(n + 1)
var i = 0
while i < n { b[i] = s[start + i]; i += 1 }
b[n] = 0
return b
}
# index of the first byte of `needle` in `hay` at or after `from`, else -1
function s_index(hay: pointer, needle: pointer, from: int) -> int {
let hn = slen(hay)
let nn = slen(needle)
if nn == 0 { return from }
var i = from
while i + nn <= hn {
var j = 0
while j < nn and hay[i + j] == needle[j] { j += 1 }
if j == nn { return i }
i += 1
}
return -1
}
function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 }
# does `hay` contain `needle` exactly at position `at`?
function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool {
let nn = slen(needle)
var i = 0
while i < nn { if hay[at + i] != needle[i] { return false }; i += 1 }
return true
}
# does `s` (a whole line) begin with `pre`?
function s_starts(s: pointer, pre: pointer) -> bool {
let pn = slen(pre)
var i = 0
while i < pn { if s[i] != pre[i] { return false }; i += 1 }
return true
}
# is byte c an ASCII space/tab?
function is_ws(c: int) -> bool { return c == ' ' or c == '\t' }
# the substring from `start` up to the next '\n' (or end)
function line_at(s: pointer, start: int) -> pointer {
var e = start
while s[e] != 0 and s[e] != '\n' { e += 1 }
return sslice(s, start, e)
}
# trim leading/trailing ASCII whitespace (space, tab, cr, nl)
function s_trim(s: pointer) -> pointer {
let n = slen(s)
var a = 0
while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 }
var b = n
while b > a and (is_ws(s[b - 1]) or s[b - 1] == '\n' or s[b - 1] == '\r') { b -= 1 }
return sslice(s, a, b)
}
# lowercase ASCII A-Z
function lower_ascii(s: pointer) -> pointer {
let n = slen(s)
let b = bytes(n + 1)
var i = 0
while i < n {
var c = s[i]
if c >= 'A' and c <= 'Z' { c += 32 }
b[i] = c
i += 1
}
b[n] = 0
return b
}
# Python str.title(): capitalise the first letter of each alpha run, lower the rest
function title_case(s: pointer) -> pointer {
let n = slen(s)
let b = bytes(n + 1)
var i = 0
var prev_alpha = false
while i < n {
var c = s[i]
let al = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')
if al {
if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } }
else { if c >= 'a' and c <= 'z' { c -= 32 } }
}
b[i] = c
prev_alpha = al
i += 1
}
b[n] = 0
return b
}
# ---- the C toolchain --------------------------------------------------------
# clang assembles the emitted IR and drives the linker. -Wno-override-module for
# the same reason the compiler itself passes it (see selfhost/main.ludic): the IR
# names no target triple, so clang substitutes the host's and warns every time.
function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") + " -Wno-override-module" }

View file

@ -131,11 +131,11 @@ function template_readme(name: pointer) -> pointer {
# ludic new <name> — scaffold a project that builds and runs as it stands.
function cmd_new() -> int {
if arg_total() < 3 {
if arg_count() < 3 {
err("usage: ludic new <name>\n")
return 1
}
let name = arg_n(2)
let name = arg(2)
if file_exists(name) {
err(`ludic new: {name} already exists\n`)
return 1
@ -173,12 +173,12 @@ function parse_build_args(start: int) -> pointer {
g_out = ""
g_save = false
var ai = start
while ai < arg_total() {
let a = arg_n(ai)
while ai < arg_count() {
let a = arg(ai)
if a == "--headless" { g_mode = 2 }
else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true }
else if a == "-o" { ai += 1; if ai < arg_total() { g_out = arg_n(ai) } }
else if a == "-o" { ai += 1; if ai < arg_count() { g_out = arg(ai) } }
else if a[0] != '-' { src = a }
ai += 1
}
@ -245,7 +245,7 @@ function test_files() -> []pointer {
function cmd_test() -> int {
var files = new []pointer
var ai = 2
while ai < arg_total() { if arg_n(ai)[0] != '-' { push(files, arg_n(ai)) }; ai += 1 }
while ai < arg_count() { if arg(ai)[0] != '-' { push(files, arg(ai)) }; ai += 1 }
if len(files) == 0 { files = test_files() }
if len(files) == 0 {
err("ludic test: no tests found (expected tests/*.ludic or src/**/*_test.ludic)\n")
@ -298,7 +298,7 @@ function strip_ext(p: pointer) -> pointer {
function cmd_fmt() -> int {
var args = ""
var ai = 2
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 }
if args == "" {
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
if found == "" {
@ -315,7 +315,7 @@ function cmd_fmt() -> int {
function cmd_lsp() -> int {
var args = ""
var ai = 2
while ai < arg_total() { args = `{args} {arg_n(ai)}`; ai += 1 }
while ai < arg_count() { args = `{args} {arg(ai)}`; ai += 1 }
return sh(`exec {tool("ludic-lsp")}{args}`)
}
@ -377,6 +377,25 @@ function cmd_doctor() -> int {
return 1
}
# ludic version — report the version of the toolchain this CLI belongs to.
#
# It asks the compiler (proving that path works), addressed through ludic_home()
# rather than as a relative bin/ludicc: `ludic version` is run from a user's
# project, not from the toolchain's own directory, and looking beside the current
# directory found nothing there and reported "(version unknown)" for a perfectly
# good install.
function cmd_version() -> int {
let cc = ludicc()
if is_exec(cc) {
run(`{cc} --version`)
return 0
}
let v = read_file(`{ludic_home()}VERSION`)
if v != null { print(`ludic {s_trim(v)}`); return 0 }
print("ludic (version unknown)")
return 0
}
# ---- ludic upgrade ----------------------------------------------------------
function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https://workshopsoft.pages.workshopsoft.io/ludic/install.sh") }
@ -386,7 +405,7 @@ function install_url() -> pointer { return getenv_or("LUDIC_INSTALL_URL", "https
# drift from a fresh install.
function cmd_upgrade() -> int {
var ver = ""
if arg_total() >= 3 { ver = arg_n(2) }
if arg_count() >= 3 { ver = arg(2) }
if not shq("command -v curl >/dev/null 2>&1") {
err("ludic upgrade: needs curl\n")
return 1

View file

@ -1,13 +1,13 @@
# release.ludic — versioning + release cutting for the toolchain.
#
# ludic version print the toolchain version (from the VERSION file)
# ludic dev release [level] cut a release: aggregate changes/ into CHANGELOG.md,
# ludic-dev release [level] cut a release: aggregate changes/ into CHANGELOG.md,
# bump VERSION, commit, and tag vX.Y.Z. `level` is
# major|minor|patch; omitted, it is derived from the
# highest `bump:` among the pending changesets.
# ludic dev release [level] --dry-run render the changelog section to stdout and
# ludic-dev release [level] --dry-run render the changelog section to stdout and
# stop: nothing is written, committed, tagged or pushed.
# ludic dev release [level] --publish ...then push main + the tag and create a
# ludic-dev release [level] --publish ...then push main + the tag and create a
# Forgejo release with source + toolchain tarballs.
# Needs FORGEJO_TOKEN in the environment.
#
@ -49,24 +49,6 @@ function compute_next(cur: pointer, level: pointer) -> pointer {
return `{maj}.{min}.{p2}`
}
# ludic version — report the version of the toolchain this CLI belongs to.
#
# It asks the compiler (proving that path works), addressed through ludic_home()
# rather than as a relative bin/ludicc: `ludic version` is run from a user's
# project, not from the toolchain's own directory, and looking beside the current
# directory found nothing there and reported "(version unknown)" for a perfectly
# good install.
function cmd_version() -> int {
let cc = ludicc()
if is_exec(cc) {
run(`{cc} --version`)
return 0
}
let v = read_file(`{ludic_home()}VERSION`)
if v != null { print(`ludic {s_trim(v)}`); return 0 }
print(`ludic {read_version_or("(version unknown)")}`)
return 0
}
# ---- changelog rendering ----------------------------------------------------
#
@ -241,16 +223,16 @@ function build_section(ver: pointer) -> void {
write_file(tmp_path("rel_section.md"), render_section(ver, date, "changes"))
}
# ludic dev changelog-render <version> <date> <dir> — print the CHANGELOG section that
# ludic-dev changelog-render <version> <date> <dir> — print the CHANGELOG section that
# `dir`'s changesets would produce. Used to re-render the sections of releases
# cut before the renderer preserved markdown structure: check the changesets out
# of the tag's parent commit, point this at them, and splice the result back in.
function cmd_changelog_render() -> int {
if arg_total() < 5 {
err("usage: ludic dev changelog-render <version> <date> <changesets-dir>\n")
if arg_count() < 5 {
err("usage: ludic-dev changelog-render <version> <date> <changesets-dir>\n")
return 1
}
out(render_section(arg_n(2), arg_n(3), arg_n(4)))
out(render_section(arg(2), arg(3), arg(4)))
return 0
}
@ -258,7 +240,7 @@ function cmd_changelog_render() -> int {
# release section (or at the end of the header if this is the first release).
function prepend_changelog() -> void {
if not file_exists("CHANGELOG.md") {
run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md")
run("printf '# Changelog\n\nAll notable changes to the Ludic toolchain, newest first. Generated from the\nchangesets under changes/ by ludic-dev release; do not edit released sections by hand.\n\n' > CHANGELOG.md")
}
let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1")
if (ln == "") {
@ -297,11 +279,11 @@ function changelog_section(ver: pointer) -> pointer {
return sslice(text, start, n)
}
# ludic dev changelog-section <version> — print that release's CHANGELOG section.
# ludic-dev changelog-section <version> — print that release's CHANGELOG section.
function cmd_changelog_section() -> int {
if arg_total() < 3 { err("usage: ludic dev changelog-section <version>\n"); return 1 }
let sec = changelog_section(arg_n(2))
if slen(sec) == 0 { err(`changelog-section: no section for v{arg_n(2)} in CHANGELOG.md\n`); return 1 }
if arg_count() < 3 { err("usage: ludic-dev changelog-section <version>\n"); return 1 }
let sec = changelog_section(arg(2))
if slen(sec) == 0 { err(`changelog-section: no section for v{arg(2)} in CHANGELOG.md\n`); return 1 }
out(sec)
return 0
}
@ -343,7 +325,7 @@ function build_artifacts(ver: pointer) -> bool {
let stage = `{tmp_dir()}/{name}`
run(`rm -rf {stage} && mkdir -p {stage}/bin`)
if not shq(`cp bin/ludic bin/ludicc bin/ludic-fmt bin/ludic-lsp {stage}/bin/`) {
err("release: the toolchain is not built (run: ludic dev build)\n")
err("release: the toolchain is not built (run: ludic-dev build)\n")
return false
}
run(`cp -R runtime {stage}/runtime`)
@ -366,10 +348,10 @@ function build_artifacts(ver: pointer) -> bool {
return true
}
# ludic dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts,
# ludic-dev publish [vX.Y.Z] — publish an already-tagged release: build the artifacts,
# take the notes from that version's CHANGELOG section, and create the Forgejo
# release. Defaults to the version in VERSION. This is what CI runs on a tag
# push, and what `ludic dev release --publish` calls once it has tagged.
# push, and what `ludic-dev release --publish` calls once it has tagged.
function publish_tag(ver: pointer) -> int {
if getenv_or("FORGEJO_TOKEN", "") == "" { err("publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
let body = tmp_path("rel_notes.md")
@ -386,8 +368,8 @@ function publish_tag(ver: pointer) -> int {
function cmd_publish() -> int {
var ver = read_version_or("")
if arg_total() >= 3 {
var a = arg_n(2)
if arg_count() >= 3 {
var a = arg(2)
if s_starts(a, "v") { a = sslice(a, 1, slen(a)) }
ver = a
}
@ -409,8 +391,8 @@ function cmd_release() -> int {
var publish = false
var dry = false
var ai = 2
while ai < arg_total() {
let a = arg_n(ai)
while ai < arg_count() {
let a = arg(ai)
if (a == "--publish") { publish = true }
else { if (a == "--dry-run") { dry = true }
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a }
@ -437,7 +419,7 @@ function cmd_release() -> int {
let sec = read_file(tmp_path("rel_section.md"))
if sec != null { out(sec) }
print("")
print(` dry run — nothing written. cut it with: ludic dev release {level}`)
print(` dry run — nothing written. cut it with: ludic-dev release {level}`)
return 0
}
prepend_changelog()
@ -452,7 +434,7 @@ function cmd_release() -> int {
print(` committed + tagged v{ver}`)
if publish { return publish_release(ver) }
print(` local release ready. publish with: FORGEJO_TOKEN=… ludic dev release {level} --publish`)
print(` local release ready. publish with: FORGEJO_TOKEN=… ludic-dev release {level} --publish`)
print(` (or push: git push origin HEAD && git push origin v{ver})`)
return 0
}

View file

@ -6,10 +6,6 @@
# now lives here once, in selfhost_frags().
# the C toolchain driver (clang) that assembles and links the emitted IR
# The C toolchain used to assemble and link IR. -Wno-override-module for the
# same reason the compiler itself passes it (see selfhost/main.ludic): the IR
# names no target triple, so clang substitutes the host's and warns every time.
function cc() -> pointer { return getenv_or("LUDIC_CC", "clang") + " -Wno-override-module" }
# the self-host compiler's source fragments, in link order. This is THE list;
# the old scripts each carried their own copy.
@ -95,21 +91,9 @@ function line_count(path: pointer) -> pointer {
return capture_line(`wc -l < {path}`)
}
# ensure bin/ludicc exists and is current with the seed. Built from the checked-in
# IR seed with clang alone — no C compiler is ever involved.
function ensure_ludicc() -> void {
if not in_toolchain_repo() { return }
run("mkdir -p bin build")
if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) {
err("ludic: could not assemble the seed\n"); exit(1)
}
}
}
# ---- selfhost-build: assemble + compile the self-host compiler ---------------
# usage: ludic dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
# usage: ludic-dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
run("mkdir -p build")
let src = "build/selfhost.ludic"
@ -123,7 +107,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
}
# ---- sh-compile: compile one .ludic with a given selfhost binary and link ----
# usage: ludic dev sh-compile <selfhost-binary> <input.ludic> <output-binary>
# usage: ludic-dev sh-compile <selfhost-binary> <input.ludic> <output-binary>
function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 }
if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 }
@ -144,7 +128,7 @@ function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
return true
}
# usage: ludic dev game-build <selfhost-binary> <game.ludic> <out-binary>
# usage: ludic-dev game-build <selfhost-binary> <game.ludic> <out-binary>
function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int {
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
print("build failed:")
@ -194,7 +178,7 @@ function cmd_bootstrap_cfree() -> int {
print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
return 0
}
print(" out.ll != seed.ll (seed is stale — regenerate with: ludic dev reseed)")
print(" out.ll != seed.ll (seed is stale — regenerate with: ludic-dev reseed)")
out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head"))
return 1
}
@ -203,7 +187,7 @@ function cmd_bootstrap_cfree() -> int {
# Uses the CURRENT seed to build the new one, compiling twice so the seed is a
# fixed point of the NEW compiler, not a one-step image of the old. Falls back to
# bin/ludicc only if the seed itself no longer assembles. Verify with
# `ludic dev bootstrap-cfree` afterwards.
# `ludic-dev bootstrap-cfree` afterwards.
function cmd_reseed() -> int {
run("mkdir -p build/cfree")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }

View file

@ -19,7 +19,7 @@ function sh_case(name: pointer, exp: pointer) -> void {
# framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256.
# A hash is tiny and text-diffable — a regression shows as a changed hex line in a
# review, not an opaque "binary files differ". Regenerate the baseline with
# `bin/ludic dev golden` (see cmd_golden below).
# `bin/ludic-dev golden` (see cmd_golden below).
function game_case(path: pointer, keys: pointer) -> void {
let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return }
@ -32,13 +32,13 @@ function game_case(path: pointer, keys: pointer) -> void {
if not is_darwin() { skip(`{path} render (golden hash blessed on macOS)`); return }
let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`)
if (want == "") { bad2(path, "no golden hash (run: bin/ludic dev golden)"); return }
if (want == "") { bad2(path, "no golden hash (run: bin/ludic-dev golden)"); return }
if (got == want) { ok(`{path} matches the golden render`) }
else { bad2(path, `render hash {got} != golden {want}`) }
}
# render one golden game and return its "<path> <sha256>\n" manifest line (empty
# string on build failure). Shared by `bin/ludic dev golden` and nothing else.
# string on build failure). Shared by `bin/ludic-dev golden` and nothing else.
function bless_render(path: pointer, keys: pointer) -> pointer {
let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) {
@ -117,12 +117,12 @@ function cmd_selfhost_test() -> int {
game_case("rendering/draw_prims", "")
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==")
if shq(`bin/ludic dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
if shq(`bin/ludic-dev bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
else { bad("fixpoint not reached"); out(capture(`cat {tmp_dir()}/boot.out`)) }
print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==")
if shq(`bin/ludic dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") }
else { bad("C-free bootstrap failed (seed stale? run: ludic dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
if shq(`bin/ludic-dev bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") }
else { bad("C-free bootstrap failed (seed stale? run: ludic-dev reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
return report()
}

View file

@ -91,6 +91,16 @@ function install_layout_case() -> void {
if ver != `ludic {s_trim(read_file("VERSION"))}` {
bad2("an installed `ludic version` reports the install's VERSION", `got [{ver}]`); return
}
# and the shipped CLI carries none of the contributor tasks: `ludic dev …` is
# not a command there, it is a pointer at the tool that has them.
let devlog = `{work}/dev.out`
let devrc = sh(`cd {work}/demo && {envp} ludic dev build > {devlog} 2>&1`)
var devout = read_file(devlog)
if devout == null { devout = "" }
if devrc == 0 or not s_contains(devout, "ludic-dev") {
bad2("an installed `ludic` must not carry the toolchain's own tasks", `rc {string(devrc)}, said [{s_trim(devout)}]`)
return
}
ok(lbl)
}
@ -161,7 +171,7 @@ function panic_case() -> void {
else { bad2("panic", `rc={string(rc)} err=[{msg}]`) }
}
# issue #45: `bin/ludic dev test --coverage`. Compile each test-spec with `--coverage`,
# issue #45: `bin/ludic-dev test --coverage`. Compile each test-spec with `--coverage`,
# run it with LUDIC_COVERAGE pointed at a per-file dump, then aggregate the dumps
# into a clean per-file line-coverage report. The instrumentation is flag-gated,
# so this reuses the same seed-built bin/ludicc the rest of the suite does.
@ -211,7 +221,7 @@ function cmd_test_coverage() -> int {
}
COV_COVERED = 0
COV_TOTAL = 0
print("== line coverage (bin/ludic dev test --coverage) ==")
print("== line coverage (bin/ludic-dev test --coverage) ==")
cov_one("library/coverage", `{tmp_dir()}/cov_out`)
cov_one("library/testing", `{tmp_dir()}/cov_out`)
var pct = 100
@ -244,11 +254,11 @@ function cmd_dev_test() -> int {
game_case("games/chronorift", "ddddwwwwaassK")
print("== self-hosting and the bootstrap fixpoints ==")
run(`bin/ludic dev selfhost-test > {tmp_dir()}/sh.out 2>&1`)
run(`bin/ludic-dev selfhost-test > {tmp_dir()}/sh.out 2>&1`)
if shq(`grep -q 'gen2.ll == gen3.ll' {tmp_dir()}/sh.out`) { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") }
if shq(`grep -q 'no C compiler' {tmp_dir()}/sh.out`) { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") }
let corr = capture_line(`grep -c PASS {tmp_dir()}/sh.out`)
ok(`self-host suite: {corr} checks passed (see: ludic dev selfhost-test)`)
ok(`self-host suite: {corr} checks passed (see: ludic-dev selfhost-test)`)
print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
qsmoke("lang/qdecl")
@ -405,9 +415,9 @@ function cmd_dev_test() -> int {
print("== the compiler and the CLI (ludicc / ludic) ==")
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled
# by it, from Ludic.
if shq(`bin/ludic dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") {
ok("ludic dev build-cli builds bin/ludicc from the seed (clang-only)")
} else { bad2("ludic dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
if shq(`bin/ludic-dev build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") {
ok("ludic-dev build-cli builds bin/ludicc from the seed (clang-only)")
} else { bad2("ludic-dev build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
# ludicc -o drives clang to a native binary. A game links windowed by default
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll) — that path is
@ -456,27 +466,27 @@ function cmd_dev_test() -> int {
# store + ludic_modules/ links + build, run as its own hermetic suite (it
# stands up throwaway git repos, so like selfhost-test it runs as a sub-process
# and this stage checks its footer).
print("== package manager (ludic dev test-pkg) ==")
run(`bin/ludic dev test-pkg > {tmp_dir()}/pkg.out 2>&1`)
print("== package manager (ludic-dev test-pkg) ==")
run(`bin/ludic-dev test-pkg > {tmp_dir()}/pkg.out 2>&1`)
if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) {
let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`)
ok(`package-manager suite: {pc} checks passed (see: ludic dev test-pkg)`)
} else { bad2("ludic dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) }
ok(`package-manager suite: {pc} checks passed (see: ludic-dev test-pkg)`)
} else { bad2("ludic-dev test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) }
# docs site generator + guard, in Ludic (no Python). docs-gen emits the whole
# pages payload; docs-check is its coverage/integrity guard; docs-palette is
# the named-colour source of truth (its --check mode regenerates the tracked
# emit_color.ludic + palette.json into scratch files and compares).
print("== docs site (Ludic generator, no Python) ==")
if shq(`bin/ludic dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) {
ok("ludic dev docs-gen -> the pages-branch payload")
} else { bad2("ludic dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
if shq(`bin/ludic dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) {
ok("ludic dev docs-check passes on the generated site")
} else { bad2("ludic dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
if shq(`bin/ludic dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) {
ok("ludic dev docs-palette regenerates emit_color.ludic + palette.json byte-identically")
} else { bad2("ludic dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
if shq(`bin/ludic-dev docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) {
ok("ludic-dev docs-gen -> the pages-branch payload")
} else { bad2("ludic-dev docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
if shq(`bin/ludic-dev docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) {
ok("ludic-dev docs-check passes on the generated site")
} else { bad2("ludic-dev docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
if shq(`bin/ludic-dev docs-palette --check > {tmp_dir()}/pal.out 2>&1`) {
ok("ludic-dev docs-palette regenerates emit_color.ludic + palette.json byte-identically")
} else { bad2("ludic-dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
return report()
}

View file

@ -0,0 +1,44 @@
# toolchain.ludic — building the toolchain's own binaries. Contributor-only:
# this is what `ludic-dev build` does, and none of it ships to a user.
# compile a Ludic source to a native binary in bin/ via ludicc + clang (-O2).
# Returns true on success. Used for the CLI, the contributor tool and the editor
# tools — every binary the toolchain builds of itself.
function build_tool(name: pointer, src: pointer) -> bool {
let ll = `build/{name}.ll`
if not shq(`bin/ludicc {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
# write to a temp then move, so a running bin/ludic can rebuild itself in place
if not shq(`{cc()} -O2 {ll} -o bin/{name}.tmp`) { print(`build failed: {name} (link)`); return false }
run(`mv -f bin/{name}.tmp bin/{name} && rm -f {ll}`)
return true
}
# ---- build-cli: the self-hosted front-end binaries ---------------------------
# Both ludicc and ludic are the SAME multi-call binary assembled from the IR seed
# with clang alone; invoked as `ludic` it compiles-and-runs, as `ludicc` it just
# compiles.
function cmd_dev_build_cli() -> int {
run("mkdir -p bin build")
print("cc: selfhost/ludicc.seed.ll -> bin/ludicc (from the IR seed, no C compiler)")
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("ludic: the seed did not assemble\n"); return 1 }
run("chmod +x bin/ludicc")
print("done. built bin/ludicc")
return 0
}
# ---- build: the whole toolchain --------------------------------------------
# bin/ludic is what a user installs; bin/ludic-dev is this tool, which exists
# only in a checkout and is never part of a release.
function cmd_dev_build() -> int {
if cmd_dev_build_cli() != 0 { return 1 }
print("ludicc: tools/ludic-cli/main.ludic -> bin/ludic (the CLI users install)")
if not build_tool("ludic", "tools/ludic-cli/main.ludic") { return 1 }
print("ludicc: tools/ludic-cli/dev.ludic -> bin/ludic-dev (this tool, rebuilding itself)")
if not build_tool("ludic-dev", "tools/ludic-cli/dev.ludic") { return 1 }
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt")
if not build_tool("ludic-fmt", "tools/ludic-tools/fmt.ludic") { return 1 }
print("ludicc: tools/ludic-tools/lsp.ludic -> bin/ludic-lsp")
if not build_tool("ludic-lsp", "tools/ludic-tools/lsp.ludic") { return 1 }
sync_vscode_grammar()
print("done. toolchain in bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)")
return 0
}

View file

@ -27,12 +27,12 @@ function sync_vscode_grammar() -> void {
# is `flag` present anywhere in argv?
function has_flag(flag: pointer) -> bool {
var i = 2
while i < arg_total() { if (arg_n(i) == flag) { return true }; i += 1 }
while i < arg_count() { if (arg(i) == flag) { return true }; i += 1 }
return false
}
# ---- tools: build the editor toolchain --------------------------------------
# usage: ludic dev tools [--install] [--test]
# usage: ludic-dev tools [--install] [--test]
function cmd_tools() -> int {
ensure_ludicc()
print("ludicc: tools/ludic-tools/fmt.ludic -> bin/ludic-fmt (Ludic -> LLVM IR -> binary, no C)")
@ -57,8 +57,8 @@ function cmd_test_tools() -> int {
PASS = 0
FAIL = 0
let fmt = "bin/ludic-fmt"
if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic dev tools\n"); return 2 }
if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic dev tools\n"); return 2 }
if not is_exec(fmt) { err("bin/ludic-fmt missing — run: ludic-dev tools\n"); return 2 }
if not is_exec("bin/ludic-lsp") { err("bin/ludic-lsp missing — run: ludic-dev tools\n"); return 2 }
print("formatter")
@ -129,7 +129,7 @@ function cmd_test_tools() -> int {
# features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the
# end-to-end protocol test on Darwin until the directory walk is portable.
if is_darwin() {
let lrc = sh("bin/ludic dev test-lsp")
let lrc = sh("bin/ludic-dev test-lsp")
if lrc == 0 { ok("language server protocol") }
else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") }
else { bad("language server protocol") }
@ -160,10 +160,10 @@ function cmd_test_tools() -> int {
# VS Code carries its own copy of the shared grammar; catch an edit to the copy
if shq("cmp -s tools/editors/shared/ludic.tmLanguage.json tools/editors/vscode/syntaxes/ludic.tmLanguage.json") and shq("cmp -s tools/editors/shared/ludic.markdown-injection.json tools/editors/vscode/syntaxes/ludic.markdown-injection.json") and shq("cmp -s tools/editors/shared/language-configuration.json tools/editors/vscode/language-configuration.json") {
ok("vscode grammar copies match tools/editors/shared")
} else { bad2("vscode grammar copies have drifted", "run: ludic dev tools to resync") }
} else { bad2("vscode grammar copies have drifted", "run: ludic-dev tools to resync") }
# the JetBrains plugin is off by default (its first build pulls a gigabyte of
# IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic dev test-tools
# IntelliJ SDK). One env var away: LUDIC_TEST_JETBRAINS=1 ludic-dev test-tools
if (getenv_or("LUDIC_TEST_JETBRAINS", "0") == "1") {
if shq("test -x tools/editors/jetbrains/gradlew") {
if shq("cd tools/editors/jetbrains && ./gradlew buildPlugin verifyPluginStructure --console=plain -q >/dev/null 2>&1 && test -f build/distributions/Ludic-1.0.0.zip") { ok("jetbrains plugin builds and verifies") } else { bad("jetbrains plugin build") }
@ -177,17 +177,17 @@ function cmd_test_tools() -> int {
# TextMate grammar, the Kotlin lexer) is the failure mode this layout prevents.
# The check is written in Ludic (tools/ludic-cli/checks.ludic) and runs through x — no
# Python in the loop.
if shq("bin/ludic dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") }
if shq("bin/ludic-dev check-vocabulary 2>/dev/null") { ok("vocabulary in sync across grammar/lexer/header") } else { bad("vocabulary drifted") }
# every feature the compiler actually implements — namespace methods, keywords,
# types, phases — must have a docs/language page. This reads the implementation
# (emit_ns_call + ludic_syntax.h), so shipping a feature without docs fails here.
# Ported to Ludic; runs through x.
if shq("bin/ludic dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") }
if shq("bin/ludic-dev check-impl > /dev/null 2>&1") { ok("docs cover every implemented feature") } else { bad("docs drifted from the implementation") }
# every ```ludic fence in the docs must parse (or be marked skip/expect-error),
# so a documented snippet can't drift from what the compiler accepts.
if shq("bin/ludic dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic dev check-docs") }
if shq("bin/ludic-dev check-docs > /dev/null 2>&1") { ok("every doc fence parses") } else { bad2("a doc fence drifted", "run: ludic-dev check-docs") }
return report()
}
@ -195,9 +195,9 @@ function cmd_test_tools() -> int {
# JSON/XML validity, checked by the Ludic validators in checks.ludic (no Python).
function test_json(path: pointer) -> void {
let bn = capture_line(`basename {path}`)
if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) }
if shq(`bin/ludic-dev lint-asset {path}`) { ok(`valid JSON: {bn}`) } else { bad(`invalid JSON: {path}`) }
}
function test_xml(path: pointer) -> void {
let bn = capture_line(`basename {path}`)
if shq(`bin/ludic dev lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) }
if shq(`bin/ludic-dev lint-asset {path}`) { ok(`valid XML: {bn}`) } else { bad(`invalid XML: {path}`) }
}