chore(repo): DX cleanup — categorise examples, text-diffable golden, build/ output (#27 #28 #30)

Repository-cleanup / DX pass folding three tracker items into one coherent
change, verified green end to end (`bin/x test` 49/0, `bin/x selfhost-test`
29/0, `bin/x test-tools` 29/0).

#28 — curate & categorise examples/
- 42 flat entries regrouped into intent-revealing subdirs: games/, rendering/,
  ecs/, events/, networking/, lang/, library/ (was lib/).
- chronorift dir-vs-file duplication resolved: the entry file and its import
  modules now live together under games/chronorift(.ludic).
- Every path reference updated repo-wide (test runner, editor-tool drivers,
  docs/site, design docs).
- New examples/README.md indexes the whole set with run commands.
- Showcase examples without a self-asserting entry (hello, events, net_rt) now
  get a compile-only rot guard in `bin/x test`, so nothing here rots silently.

#30 — text-diffable golden baseline
- The 4 binary selfhost/golden/*.ppm blobs are replaced by a single
  selfhost/golden/renders.sha256 manifest (SHA-256 per render). Hashes are
  byte-identical to the old PPMs, so the baseline is unchanged — only its form.
- game_case now compares framebuffer hashes; a regression shows as a changed
  hex line in review, not "binary files differ".
- New `bin/x golden` regenerates the manifest deliberately (review with
  `git diff selfhost/golden/renders.sha256`).

#27 — PPM & asset handling
- Headless renders now write build/out.ppm, never the repo root; `x app`,
  `x clean`, messaging and .gitignore updated to match. Nothing is written to
  the working root any more.
- Redundant local Kenney .zip archives removed (the art ships extracted;
  .gitignore already excludes *.zip). CC0 License.txt files retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 18:54:21 +03:00
parent 0bc5d76952
commit fb728bbefe
73 changed files with 364 additions and 194 deletions

View file

@ -119,7 +119,7 @@ async function main() {
expect('scene in enter', ' enter Battle', 'Battle', 'entity.name.type.scene');
// A whole file must tokenise without the engine bailing out.
const sample = fs.readFileSync(path.join(ROOT, 'examples', 'menu.ludic'), 'utf8');
const sample = fs.readFileSync(path.join(ROOT, 'examples', 'games', 'menu.ludic'), 'utf8');
let state = vsctm.INITIAL;
let tokenCount = 0;
for (const line of sample.split('\n')) {
@ -127,8 +127,8 @@ async function main() {
state = r.ruleStack;
tokenCount += r.tokens.length;
}
if (tokenCount > 200) ok(`tokenises examples/menu.ludic (${tokenCount} tokens)`);
else bad('tokenises examples/menu.ludic', `only ${tokenCount} tokens`);
if (tokenCount > 200) ok(`tokenises examples/games/menu.ludic (${tokenCount} tokens)`);
else bad('tokenises examples/games/menu.ludic', `only ${tokenCount} tokens`);
// ---- the Markdown injection ---------------------------------------------
const mdGrammar = await registry.loadGrammar('markdown.ludic.codeblock');

View file

@ -111,7 +111,7 @@ def main():
check_true(f"advertises {cap}", cap in caps)
# ---- a fragment file: no `program` block, only reachable through an import --
frag = os.path.join(ROOT, "examples/chronorift/combat.ludic")
frag = os.path.join(ROOT, "examples/games/chronorift/combat.ludic")
text = c.open(frag)
uri = {"uri": "file://" + frag}

View file

@ -43,7 +43,8 @@ function cmd_build() -> int {
# ---- app: compile a Ludic program to a native app ----------------------------
# usage: x app <file.ludic> [--headless|--windowed] [--save-temps]
# windowed is the default for a game; headless renders the last frame to out.ppm.
# windowed is the default for a game; headless renders the last frame to
# build/out.ppm (never the repo root).
function cmd_app() -> int {
ensure_ludicc()
var src = null
@ -64,11 +65,11 @@ function cmd_app() -> int {
if (mode == 2) {
let outbin = `build/{name}_headless`
let ll = `{outbin}.ll`
print(`ludicc: {src} -> {outbin} (renders the last frame to out.ppm)`)
print(`ludicc: {src} -> {outbin} (renders the last frame to build/out.ppm)`)
if not shq(`bin/ludicc --headless {src} > {ll}`) { return 1 }
if not shq(`{cc()} -O2 {ll} -o {outbin}`) { return 1 }
if not save { run(`rm -f {ll}`) }
print(`done. run: printf 'ddss' | ./{outbin} && open out.ppm`)
print(`done. run: mkdir -p build && printf 'ddss' | ./{outbin} && open build/out.ppm`)
return 0
}
@ -83,13 +84,14 @@ function cmd_app() -> int {
}
# Remove every generated artifact and leave the tracked source untouched: the
# whole build/ tree (IR, objects, compiled apps), the headless render dropped in
# the root (out.ppm), and any stray *.tmp/*.ll left behind by a failed build.
# The toolchain binaries in bin/ are kept so the running `x` survives; rebuild
# them any time with `x build`.
# whole build/ tree (IR, objects, compiled apps, the headless render at
# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A
# legacy out.ppm in the root (from an older toolchain) is swept too. The
# toolchain binaries in bin/ are kept so the running `x` survives; rebuild them
# any time with `x build`.
function cmd_clean() -> int {
run("rm -rf build")
run("rm -f out.ppm bin/*.tmp")
print("cleaned: build/, out.ppm, bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)")
print("cleaned: build/ (incl. build/out.ppm), bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)")
return 0
}

View file

@ -27,12 +27,13 @@ program X {
print(" x app <file.ludic> [--headless] [--save-temps]")
print(" compile a Ludic program to a native app in build/")
print(" x tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
print(" x clean remove build/, out.ppm and stray artifacts (keeps bin/)")
print(" x clean remove build/ (incl. build/out.ppm) and stray artifacts (keeps bin/)")
print("")
print("test:")
print(" x test the full regression suite")
print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
print(" x test-tools the editor-toolchain suite")
print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
print("")
print("self-host internals:")
print(" x selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
@ -61,6 +62,7 @@ program X {
if (cmd == "test") { exit(cmd_test()) }
if (cmd == "selfhost-test") { exit(cmd_selfhost_test()) }
if (cmd == "test-tools") { exit(cmd_test_tools()) }
if (cmd == "golden") { exit(cmd_golden()) }
if (cmd == "bootstrap") { exit(cmd_bootstrap()) }
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) }
if (cmd == "reseed") { exit(cmd_reseed()) }

View file

@ -75,6 +75,21 @@ function getenv_or(name: pointer, dflt: pointer) -> pointer {
return v
}
# flatten a relative example path into a filesystem-safe token: '/' -> '_', so a
# categorised path like "games/snake" yields a single-segment temp name
# ("games_snake") that never implies a missing /tmp subdirectory.
function flat(p: pointer) -> pointer {
let n = len(p)
let b = bytes(n + 1)
for i in 0 .. n {
var c = p[i]
if (c == 47) { c = 95 } # '/' (47) -> '_' (95)
b[i] = c
}
b[n] = 0
return b
}
# ---- stdout helpers ---------------------------------------------------------
# write `s` with no trailing newline (print() always adds one)

View file

@ -15,12 +15,55 @@ function sh_case(name: pointer, exp: pointer) -> void {
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) }
}
# compile examples/<name>.ludic as a game and diff its render against the golden
function game_case(name: pointer, keys: pointer) -> void {
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_g_{name}`) { bad(`{name}: build`); return }
run(`printf '%s' '{keys}' | /tmp/x_g_{name} >/dev/null 2>&1`)
if shq(`cmp -s selfhost/golden/{name}.ppm out.ppm`) { ok(`{name} matches the golden render`) }
else { bad(`{name}: differs from golden`) }
# compile examples/<path>.ludic as a game, render it headless, and compare the
# framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256.
# A hash is tiny and text-diffable — a regression shows as a changed hex line in a
# review, not an opaque "binary files differ". Regenerate the baseline with
# `bin/x golden` (see cmd_golden below).
function game_case(path: pointer, keys: pointer) -> void {
let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_g_{nm}`) { bad(`{path}: build`); return }
run("mkdir -p build")
run(`printf '%s' '{keys}' | /tmp/x_g_{nm} >/dev/null 2>&1`)
let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`)
if (want == "") { bad2(path, "no golden hash (run: bin/x golden)"); return }
if (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/x 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/x_bg_{nm}`) {
err(`golden: {path} failed to build\n`); return ""
}
run("mkdir -p build")
run(`printf '%s' '{keys}' | /tmp/x_bg_{nm} >/dev/null 2>&1`)
let h = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
return `{path} {h}\n`
}
# regenerate the text render baseline. The keys here MUST match game_case's so
# the baseline and the checks render the same frame. Deliberate, reviewable:
# run it, then `git diff selfhost/golden/renders.sha256` shows exactly which
# render changed before you commit the new hash.
function cmd_golden() -> int {
ensure_ludicc()
if not is_exec("bin/ludicc") { err("golden: bin/ludicc unavailable\n"); return 1 }
var manifest = ""
manifest = manifest + bless_render("games/snake", "ddssaawwddss")
manifest = manifest + bless_render("games/menu", "ssss")
manifest = manifest + bless_render("games/chronorift", "ddddwwwwaassK")
manifest = manifest + bless_render("rendering/draw_prims", "")
if not write_file("selfhost/golden/renders.sha256", manifest) {
err("golden: could not write selfhost/golden/renders.sha256\n"); return 1
}
print("wrote selfhost/golden/renders.sha256:")
out(manifest)
print("review the change: git diff selfhost/golden/renders.sha256")
return 0
}
function cmd_selfhost_test() -> int {
@ -57,12 +100,13 @@ function cmd_selfhost_test() -> int {
sh_case("color", "16744512 1090486336 1090486336 8355711 8355711 8355711")
print("== the self-host compiler compiles real games (vs golden output) ==")
# the golden PPMs were produced by the original C compiler; the self-hosted
# compiler must reproduce them exactly.
game_case("snake", "ddssaawwddss")
game_case("menu", "ssss")
game_case("chronorift", "ddddwwwwaassK")
game_case("draw_prims", "")
# the golden render hashes were first produced by the original C compiler; the
# self-hosted compiler must reproduce the same framebuffer, byte for byte, so
# its SHA-256 matches the checked-in baseline in selfhost/golden/renders.sha256.
game_case("games/snake", "ddssaawwddss")
game_case("games/menu", "ssss")
game_case("games/chronorift", "ddddwwwwaassK")
game_case("rendering/draw_prims", "")
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==")
if shq("bin/x bootstrap > /tmp/x_boot.out 2>&1") { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }

View file

@ -9,38 +9,51 @@
# compile a feature example as a game, run it (optional keys on stdin) and match
# its stdout against the expected space-joined output.
function feat_case(name: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_e_{name}`) {
function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_e_{nm}`) {
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
}
var got = ""
if (keys == "") { got = capture_line(`/tmp/x_e_{name} < /dev/null`) }
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{name}`) }
if (keys == "") { got = capture_line(`/tmp/x_e_{nm} < /dev/null`) }
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{nm}`) }
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
}
# a "does it still compile" smoke test (parse -> lower -> link), no run
function qsmoke(name: pointer) -> void {
if game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_q_{name}`) {
ok(`{name} compiles (@Queries desugars to S_QUERY)`)
} else { bad2(name, capture_line("grep -i error /tmp/x_gb.err | head -1")) }
function qsmoke(path: pointer) -> void {
let nm = flat(path)
if game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_q_{nm}`) {
ok(`{path} compiles (@Queries desugars to S_QUERY)`)
} else { bad2(path, capture_line("grep -i error /tmp/x_gb.err | head -1")) }
}
# a plain compile-only rot guard for a showcase example that has no self-asserting
# `entry` (so net_case can't drive it): prove it still parses, lowers and links.
function smoke(path: pointer) -> void {
let nm = flat(path)
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o /tmp/x_sm_{nm}.ll > /tmp/x_sm_{nm}.err 2>&1`) {
bad2(path, capture_line(`tail -1 /tmp/x_sm_{nm}.err`)); return
}
ok(`{path} still compiles`)
}
# a pure-Ludic example that drives and asserts itself: compile headless to IR,
# let clang assemble it, run, and compare stdout. No C host and no C compiled.
function net_case(name: pointer, exp: pointer) -> void {
let ll = `/tmp/x_n_{name}.ll`
let log = `/tmp/x_n_{name}.out`
if not shq(`bin/ludicc --headless examples/{name}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
function net_case(path: pointer, exp: pointer) -> void {
let nm = flat(path)
let ll = `/tmp/x_n_{nm}.ll`
let log = `/tmp/x_n_{nm}.out`
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`)
bad2(name, `build ({t})`); return
bad2(path, `build ({t})`); return
}
if not shq(`{cc()} -O2 {ll} -o /tmp/x_nb_{name} >> {log} 2>&1`) {
if not shq(`{cc()} -O2 {ll} -o /tmp/x_nb_{nm} >> {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`)
bad2(name, `build ({t})`); return
bad2(path, `build ({t})`); return
}
let got = capture_line(`/tmp/x_nb_{name} < /dev/null`)
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) }
let got = capture_line(`/tmp/x_nb_{nm} < /dev/null`)
if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) }
}
function cmd_test() -> int {
@ -56,14 +69,14 @@ function cmd_test() -> int {
print("== the compiler builds from the IR seed (no C compiler) ==")
run("rm -f bin/ludicc")
if shq("bin/x app examples/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
if shq("bin/x app examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
ok("clang selfhost/ludicc.seed.ll -> bin/ludicc")
} else { bad("compiler did not build from the seed") }
print("== every example compiles to its golden render ==")
game_case("snake", "ddssaawwddss")
game_case("menu", "ssss")
game_case("chronorift", "ddddwwwwaassK")
game_case("games/snake", "ddssaawwddss")
game_case("games/menu", "ssss")
game_case("games/chronorift", "ddddwwwwaassK")
print("== self-hosting and the bootstrap fixpoints ==")
run("bin/x selfhost-test > /tmp/x_sh.out 2>&1")
@ -73,50 +86,57 @@ function cmd_test() -> int {
ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
qsmoke("qdecl")
feat_case("annotations", "", "3 25 0 0", "annotations.ludic (@Queries / @Computed / @OnSpawn / @Handles)")
feat_case("lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)")
feat_case("toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)")
feat_case("strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)")
feat_case("rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)")
feat_case("time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)")
feat_case("scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)")
feat_case("detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
feat_case("reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
qsmoke("lang/qdecl")
feat_case("lang/annotations", "", "3 25 0 0", "annotations.ludic (@Queries / @Computed / @OnSpawn / @Handles)")
feat_case("lang/lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)")
feat_case("lang/toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)")
feat_case("lang/strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)")
feat_case("lang/rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)")
feat_case("lang/time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)")
feat_case("lang/scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)")
feat_case("lang/detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
feat_case("lang/reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
net_case("offline_rewards", "13 650 2026-08-30 0")
net_case("lang/offline_rewards", "13 650 2026-08-30 0")
# EV0b + NETWORKING N0–N6: each a self-contained pure-Ludic program, driven and
# asserted from its own `entry`. The transport is the compiler's built-in
# loopback, so a networked game runs with zero foreign code.
net_case("mod_events", "10 32 42")
net_case("net_echo", "4 10 20 30 42")
net_case("net_snapshot", "50 7 50")
net_case("net_sync", "12 3 4 50 999")
net_case("net_owner", "-1 7 0 1")
net_case("net_rpc", "0 8")
net_case("net_roles", "1 102")
net_case("net_demo", "5 999 5")
net_case("events/mod_events", "10 32 42")
net_case("networking/net_echo", "4 10 20 30 42")
net_case("networking/net_snapshot", "50 7 50")
net_case("networking/net_sync", "12 3 4 50 999")
net_case("networking/net_owner", "-1 7 0 1")
net_case("networking/net_rpc", "0 8")
net_case("networking/net_roles", "1 102")
net_case("networking/net_demo", "5 999 5")
feat_case("promote", "", "100 101 200 201", "promote.ludic (@Public -> model_<M>_spawn/despawn events)")
feat_case("cancel", "", "0 1 92", "cancel.ludic (cancellable event + cancel + emit-as-expression)")
feat_case("events/promote", "", "100 101 200 201", "promote.ludic (@Public -> model_<M>_spawn/despawn events)")
feat_case("events/cancel", "", "0 1 92", "cancel.ludic (cancellable event + cancel + emit-as-expression)")
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("world_get", "50 1 7")
net_case("world_scan", "2 110")
net_case("world_spawn", "1 42 1")
net_case("world_mixed", "99")
net_case("world_query", "2 110")
net_case("ecs/world_get", "50 1 7")
net_case("ecs/world_scan", "2 110")
net_case("ecs/world_spawn", "1 42 1")
net_case("ecs/world_mixed", "99")
net_case("ecs/world_query", "2 110")
feat_case("recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("scoped", "2")
net_case("world_dyn", "0 1 30 100 1 30")
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("events/scoped", "2")
net_case("ecs/world_dyn", "0 1 30 100 1 30")
feat_case("prop_events", "", "300 400", "prop_events.ludic (@Public -> prop_<P>_attach/detach events)")
feat_case("scene_events", "", "10 1 20 2 30 3", "scene_events.ludic (public scene -> scene_<S>_enter/exit events)")
feat_case("program_events", "", "1 100 2 200", "program_events.ludic (@Public @OnStart/@OnQuit -> program_start/quit)")
feat_case("layer_events", "aaaa", "50 2 1 50", "layer_events.ludic (layer toggle + public layer -> layer_<L>_show/hide)")
feat_case("events/prop_events", "", "300 400", "prop_events.ludic (@Public -> prop_<P>_attach/detach events)")
feat_case("events/scene_events", "", "10 1 20 2 30 3", "scene_events.ludic (public scene -> scene_<S>_enter/exit events)")
feat_case("events/program_events", "", "1 100 2 200", "program_events.ludic (@Public @OnStart/@OnQuit -> program_start/quit)")
feat_case("events/layer_events", "aaaa", "50 2 1 50", "layer_events.ludic (layer toggle + public layer -> layer_<L>_show/hide)")
# showcase examples with no self-asserting `entry` — compile-only so they can't
# silently rot after a compiler or reorg change.
print("== showcase examples still compile ==")
smoke("ecs/hello")
smoke("events/events")
smoke("networking/net_rt")
print("== self-hosted front-end binaries (ludicc / ludic) ==")
# the two commands are one multi-call native binary built from the seed with
@ -127,12 +147,12 @@ function cmd_test() -> int {
# ludicc -o drives clang to a native binary; a game links windowed by default
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll).
if shq("LUDIC_HOME=. bin/ludicc examples/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("file /tmp/x_cli_snake 2>/dev/null | grep -q Mach-O") {
if shq("LUDIC_HOME=. bin/ludicc examples/games/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("file /tmp/x_cli_snake 2>/dev/null | grep -q Mach-O") {
ok("ludicc app.ludic -o bin -> native executable")
} else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) }
# --emit-llvm stops at the IR.
if shq("bin/ludicc examples/snake.ludic --emit-llvm -o /tmp/x_cli_snake.ll > /tmp/x_cli.out 2>&1") and shq("head -1 /tmp/x_cli_snake.ll | grep -q 'LLVM IR'") {
if shq("bin/ludicc examples/games/snake.ludic --emit-llvm -o /tmp/x_cli_snake.ll > /tmp/x_cli.out 2>&1") and shq("head -1 /tmp/x_cli_snake.ll | grep -q 'LLVM IR'") {
ok("ludicc --emit-llvm -> LLVM IR")
} else { bad2("ludicc --emit-llvm", capture_line("tail -1 /tmp/x_cli.out")) }

View file

@ -69,7 +69,7 @@ function cmd_test_tools() -> int {
} else { bad("idempotent on the source tree") }
# --- meaning preserved: reformatting a formatted file must be a no-op -------
if shq(`fail=0; for f in examples/hello.ludic examples/snake.ludic examples/menu.ludic examples/qdecl.ludic examples/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > /tmp/x_f1 2>/dev/null; {fmt} /tmp/x_f1 > /tmp/x_f2 2>/dev/null; cmp -s /tmp/x_f1 /tmp/x_f2 || fail=1; done; test $fail -eq 0`) {
if shq(`fail=0; for f in examples/ecs/hello.ludic examples/games/snake.ludic examples/games/menu.ludic examples/lang/qdecl.ludic examples/lang/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > /tmp/x_f1 2>/dev/null; {fmt} /tmp/x_f1 > /tmp/x_f2 2>/dev/null; cmp -s /tmp/x_f1 /tmp/x_f2 || fail=1; done; test $fail -eq 0`) {
ok("formatting is idempotent (fmt(fmt(x)) == fmt(x))")
} else { bad("formatting is not idempotent") }