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

10
.gitignore vendored
View file

@ -1,11 +1,9 @@
# Generated build tree: LLVM IR, objects, compiled apps and the docs site all # Generated build tree: LLVM IR, objects, compiled apps, the headless render
# land under build/ (see `bin/x build` / `bin/x clean`). Root-anchored so a # (build/out.ppm) and the docs site all land under build/ (see `bin/x build` /
# source dir named "build" elsewhere is never accidentally ignored. # `bin/x clean`). Root-anchored so a source dir named "build" elsewhere is never
# accidentally ignored. Nothing is written to the repo root any more.
/build/ /build/
# Headless render dropped in the working directory by a --headless run.
/out.ppm
# Archives are never tracked: the Kenney art is committed *extracted* under # Archives are never tracked: the Kenney art is committed *extracted* under
# assets/kenney/**/ (PNG/tmx/License), while the original download .zips and any # assets/kenney/**/ (PNG/tmx/License), while the original download .zips and any
# packaged plugin .zip are local-only build inputs/outputs. # packaged plugin .zip are local-only build inputs/outputs.

View file

@ -20,8 +20,8 @@
> clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x > clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
> bin/x build # rebuild the whole toolchain into bin/ > bin/x build # rebuild the whole toolchain into bin/
> # (ludicc, ludic, x, ludic-fmt, ludic-lsp) > # (ludicc, ludic, x, ludic-fmt, ludic-lsp)
> bin/ludicc examples/snake.ludic -o bin/snake # compile > bin/ludicc examples/games/snake.ludic -o bin/snake # compile
> bin/ludic examples/snake.ludic # compile + run > bin/ludic examples/games/snake.ludic # compile + run
> bin/x help # list every command > bin/x help # list every command
> ``` > ```
> >
@ -76,10 +76,10 @@ self-hosted `ludicc`.
`bin/x app` wraps the common cases: `bin/x app` wraps the common cases:
```bash ```bash
bin/x app examples/snake.ludic # -> build/snake (native) bin/x app examples/games/snake.ludic # -> build/snake (native)
bin/x app examples/lib/combat.ludic --lib # -> build/libcombat.* (library) bin/x app examples/library/combat.ludic --lib # -> build/libcombat.* (library)
bin/x app examples/snake.ludic --headless # -> build/snake_headless (out.ppm) bin/x app examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
bin/x app examples/snake.ludic --web # -> build/web/ (browser) bin/x app examples/games/snake.ludic --web # -> build/web/ (browser)
``` ```
The `--lib` and `--web` targets were part of the old C driver and are **not yet The `--lib` and `--web` targets were part of the old C driver and are **not yet
@ -109,7 +109,7 @@ program Combat {
``` ```
```bash ```bash
ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
nm -gU build/libcombat.dylib nm -gU build/libcombat.dylib
# T _damage T _hits_to_kill T _xp_for (no _curve) # T _damage T _hits_to_kill T _xp_for (no _curve)
``` ```
@ -122,7 +122,7 @@ extern function damage(attack: int, armour: int, roll: int) -> int = "damage"
``` ```
```bash ```bash
ludicc examples/lib/arena.ludic -o build/arena -Lbuild -lcombat ludicc examples/library/arena.ludic -o build/arena -Lbuild -lcombat
``` ```
Libraries are linked as `@rpath/…` (`$ORIGIN` on Linux) and executables search Libraries are linked as `@rpath/…` (`$ORIGIN` on Linux) and executables search
@ -219,7 +219,7 @@ only the triple changes.
``` ```
```bash ```bash
bin/x app examples/chronorift.ludic --web bin/x app examples/games/chronorift.ludic --web
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/ python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
``` ```

View file

@ -8,18 +8,18 @@
> struct, a fixed-capacity listener array), proven by a C mod in > struct, a fixed-capacity listener array), proven by a C mod in
> [`tests/mod_c/mod.c`](tests/mod_c/mod.c) binding > [`tests/mod_c/mod.c`](tests/mod_c/mod.c) binding
> [`examples/mod_host.ludic`](examples/mod_host.ludic). Byte-identical when no > [`examples/mod_host.ludic`](examples/mod_host.ludic). Byte-identical when no
> event is declared. ([`examples/events.ludic`](examples/events.ludic)) > event is declared. ([`examples/events/events.ludic`](examples/events/events.ludic))
> - **EV1** — public events across the **whole architecture**, every scope shipped: > - **EV1** — public events across the **whole architecture**, every scope shipped:
> **program** (`@Public @OnStart`/`@OnQuit` → `program_start`/`program_quit`, > **program** (`@Public @OnStart`/`@OnQuit` → `program_start`/`program_quit`,
> [`examples/program_events.ludic`](examples/program_events.ludic)); **models** > [`examples/events/program_events.ludic`](examples/events/program_events.ludic)); **models**
> (`@Public @OnSpawn`/`@OnDespawn` → `model_<M>_spawn`/`_despawn`, > (`@Public @OnSpawn`/`@OnDespawn` → `model_<M>_spawn`/`_despawn`,
> [`examples/promote.ludic`](examples/promote.ludic)); **properties** (`@Public > [`examples/events/promote.ludic`](examples/events/promote.ludic)); **properties** (`@Public
> @OnAttach`/`@OnDetach`/`@OnEnable`/`@OnDisable` → `prop_<P>_attach` etc., > @OnAttach`/`@OnDetach`/`@OnEnable`/`@OnDisable` → `prop_<P>_attach` etc.,
> [`examples/prop_events.ludic`](examples/prop_events.ludic)); **scenes** (a > [`examples/events/prop_events.ludic`](examples/events/prop_events.ludic)); **scenes** (a
> `public` scene → `scene_<S>_enter`/`_exit`, > `public` scene → `scene_<S>_enter`/`_exit`,
> [`examples/scene_events.ludic`](examples/scene_events.ludic)); and **layers** (a > [`examples/events/scene_events.ludic`](examples/events/scene_events.ludic)); and **layers** (a
> `public` layer + `enable/disable layer L` → `layer_<L>_show`/`_hide`, > `public` layer + `enable/disable layer L` → `layer_<L>_show`/`_hide`,
> [`examples/layer_events.ludic`](examples/layer_events.ludic)) — which also > [`examples/events/layer_events.ludic`](examples/events/layer_events.ludic)) — which also
> landed **SCENES E2 layer toggle** (`@LE_<L>` flag gating a layer's handlers). > landed **SCENES E2 layer toggle** (`@LE_<L>` flag gating a layer's handlers).
> - **EV2 / EV2b** — the world table: the reflection ABI, generated from the > - **EV2 / EV2b** — the world table: the reflection ABI, generated from the
> compile-time schema, so a mod reads, writes, scans, identifies, **and creates** > compile-time schema, so a mod reads, writes, scans, identifies, **and creates**
@ -35,7 +35,7 @@
> for an ECS program that declares events, so event-free games stay byte-exact. > for an ECS program that declares events, so event-free games stay byte-exact.
> The world table is complete: read, write, scan, identify, create, iterate. > The world table is complete: read, write, scan, identify, create, iterate.
> - **EV3** — `cancellable` events, the `cancel` verb, and `emit E(…)` as an > - **EV3** — `cancellable` events, the `cancel` verb, and `emit E(…)` as an
> expression returning the veto flag. ([`examples/cancel.ludic`](examples/cancel.ludic)) > expression returning the veto flag. ([`examples/events/cancel.ludic`](examples/events/cancel.ludic))
> - **EV5** — leak-proof scoped listeners: `ludic_off_<E>(token)` (explicit > - **EV5** — leak-proof scoped listeners: `ludic_off_<E>(token)` (explicit
> unregister; dispatch skips tombstoned slots), `ludic_on_entity_<E>(entity, cb)` > unregister; dispatch skips tombstoned slots), `ludic_on_entity_<E>(entity, cb)`
> (entity-scoped), and a generated `ludic_sweep_entity` called from `despawn` that > (entity-scoped), and a generated `ludic_sweep_entity` called from `despawn` that
@ -44,7 +44,7 @@
> - **EV6** — re-entrant `emit` is depth-bounded (`@ev_depth` vs `EV_DEPTH_CAP`): a > - **EV6** — re-entrant `emit` is depth-bounded (`@ev_depth` vs `EV_DEPTH_CAP`): a
> listener may emit another event, but an event cycle traps as an early return > listener may emit another event, but an event cycle traps as an early return
> instead of hanging the frame. Dispatch order was already deterministic (array, > instead of hanging the frame. Dispatch order was already deterministic (array,
> registration order). Proven by [`examples/recurse.ludic`](examples/recurse.ludic). > registration order). Proven by [`examples/events/recurse.ludic`](examples/events/recurse.ludic).
> >
> - **EV7 (schema opening)** — a mod defines a brand-new component at runtime: > - **EV7 (schema opening)** — a mod defines a brand-new component at runtime:
> `ludic_register_prop(name, nfields)` mallocs flat `[MAX_ENT × nfields × i32]` > `ludic_register_prop(name, nfields)` mallocs flat `[MAX_ENT × nfields × i32]`
@ -566,7 +566,7 @@ work (and how LIFECYCLE/SCENES sequence).
`N_EVENT`/`S_EMIT`; `parse_event` + `@On` annotation + `emit` statement (guarded `N_EVENT`/`S_EMIT`; `parse_event` + `@On` annotation + `emit` statement (guarded
by an identifier-lookahead so a bare `emit(...)` call still parses); registries by an identifier-lookahead so a bare `emit(...)` call still parses); registries
`g_events`/`g_onlisten` (emit_core); `emit_event_fns` (emit_game); `emit_emit` `g_events`/`g_onlisten` (emit_core); `emit_event_fns` (emit_game); `emit_emit`
(emit_stmt). [`examples/events.ludic`](examples/events.ludic) is a `bin/x test` check. (emit_stmt). [`examples/events/events.ludic`](examples/events/events.ludic) is a `bin/x test` check.
- **EV1 — `@Public` hook promotion.** ✅ *All scopes shipped.* `@Public` on a - **EV1 — `@Public` hook promotion.** ✅ *All scopes shipped.* `@Public` on a
lifecycle hook fires a public event at that hook's site (payload: entity, plus lifecycle hook fires a public event at that hook's site (payload: entity, plus
`EndReason` for despawn); `find_event(name)` doubles as the "is this hook `EndReason` for despawn); `find_event(name)` doubles as the "is this hook

View file

@ -133,7 +133,7 @@ handler Nav phase Update {
handler Draw phase Render { clear(0x0e0e16); ui_render(); present() } handler Draw phase Render { clear(0x0e0e16); ui_render(); present() }
``` ```
See `examples/menu.ludic` for a complete title screen. See `examples/games/menu.ludic` for a complete title screen.
## Types ## Types
@ -246,7 +246,7 @@ A constraint is evaluated **per candidate entity**, so it is the wrong place for
a guard that concerns the whole handler (re-reading `reg(R_MODE)` for every a guard that concerns the whole handler (re-reading `reg(R_MODE)` for every
entity). Keep whole-handler guards in the body of a handler with no `@Queries`, entity). Keep whole-handler guards in the body of a handler with no `@Queries`,
wrapping an inline query — as `CleanBattle` does in wrapping an inline query — as `CleanBattle` does in
`examples/chronorift/combat.ludic`. `examples/games/chronorift/combat.ludic`.
### Matching is lazy, not snapshotted ### Matching is lazy, not snapshotted
@ -391,9 +391,9 @@ disable Gravity # a whole model sits out every query
disable AiThink # a handler stops running each phase disable AiThink # a handler stops running each phase
``` ```
See [`examples/toggle.ludic`](examples/toggle.ludic) for the three enable/disable See [`examples/lang/toggle.ludic`](examples/lang/toggle.ludic) for the three enable/disable
scopes, [`examples/detach.ludic`](examples/detach.ludic) for the structural scopes, [`examples/lang/detach.ludic`](examples/lang/detach.ludic) for the structural
attach/detach pair, and [`examples/reason.ludic`](examples/reason.ludic) for attach/detach pair, and [`examples/lang/reason.ludic`](examples/lang/reason.ludic) for
reason-carrying teardown. The rest of the lifecycle roadmap (value-change hooks, reason-carrying teardown. The rest of the lifecycle roadmap (value-change hooks,
query-membership edges, keyed effects) is in query-membership edges, keyed effects) is in
[LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md). [LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md).
@ -402,9 +402,9 @@ query-membership edges, keyed effects) is in
`program`, `@Handles(Move)` names the handlers it uses. It parses and reads as `program`, `@Handles(Move)` names the handlers it uses. It parses and reads as
documentation; every declared handler still runs (registration is implicit). documentation; every declared handler still runs (registration is implicit).
See [`examples/annotations.ludic`](examples/annotations.ludic) (queries, computed See [`examples/lang/annotations.ludic`](examples/lang/annotations.ludic) (queries, computed
fields, one hook) and [`examples/lifecycle.ludic`](examples/lifecycle.ludic) (the fields, one hook) and [`examples/lang/lifecycle.ludic`](examples/lang/lifecycle.ludic) (the
whole timeline), plus [`examples/toggle.ludic`](examples/toggle.ludic) whole timeline), plus [`examples/lang/toggle.ludic`](examples/lang/toggle.ludic)
(enable/disable). Scenes and their `on enter` / `on exit` lifecycle blocks are (enable/disable). Scenes and their `on enter` / `on exit` lifecycle blocks are
implemented — see "Scenes & layers" below. (An annotation spelling, implemented — see "Scenes & layers" below. (An annotation spelling,
`@OnEnter(Scene)` / `@OnExit(Scene)`, is a designed but not-yet-built convenience `@OnEnter(Scene)` / `@OnExit(Scene)`, is a designed but not-yet-built convenience
@ -444,22 +444,22 @@ emit Hurt(entity: e, amount: 5) # fires every listener
architecture.** The game's own lifecycle becomes moddable with no hand-written architecture.** The game's own lifecycle becomes moddable with no hand-written
`emit`, at every scope: `emit`, at every scope:
- **program** — `@Public @OnStart`/`@OnQuit` → `program_start` / `program_quit` - **program** — `@Public @OnStart`/`@OnQuit` → `program_start` / `program_quit`
(the top-level mod entry/exit points). See [`examples/program_events.ludic`](examples/program_events.ludic). (the top-level mod entry/exit points). See [`examples/events/program_events.ludic`](examples/events/program_events.ludic).
- **models** — `@Public @OnSpawn(Enemy)`/`@OnDespawn(Enemy)` → - **models** — `@Public @OnSpawn(Enemy)`/`@OnDespawn(Enemy)` →
`model_Enemy_spawn` / `model_Enemy_despawn` (entity, + `EndReason` on despawn). `model_Enemy_spawn` / `model_Enemy_despawn` (entity, + `EndReason` on despawn).
See [`examples/promote.ludic`](examples/promote.ludic). See [`examples/events/promote.ludic`](examples/events/promote.ludic).
- **properties** — `@Public @OnAttach/@OnDetach/@OnEnable/@OnDisable(P)` → - **properties** — `@Public @OnAttach/@OnDetach/@OnEnable/@OnDisable(P)` →
`prop_<P>_attach` / `_detach` / `_enable` / `_disable`. See [`examples/prop_events.ludic`](examples/prop_events.ludic). `prop_<P>_attach` / `_detach` / `_enable` / `_disable`. See [`examples/events/prop_events.ludic`](examples/events/prop_events.ludic).
- **scenes** — a `public` scene → `scene_<S>_enter` / `scene_<S>_exit`. See [`examples/scene_events.ludic`](examples/scene_events.ludic). - **scenes** — a `public` scene → `scene_<S>_enter` / `scene_<S>_exit`. See [`examples/events/scene_events.ludic`](examples/events/scene_events.ludic).
- **layers** — a `public` layer, with `enable layer L` / `disable layer L` - **layers** — a `public` layer, with `enable layer L` / `disable layer L`
flipping the layer on and off (its handlers stop while hidden) → flipping the layer on and off (its handlers stop while hidden) →
`layer_<L>_show` / `layer_<L>_hide`. See [`examples/layer_events.ludic`](examples/layer_events.ludic). `layer_<L>_show` / `layer_<L>_hide`. See [`examples/events/layer_events.ludic`](examples/events/layer_events.ludic).
- **`cancellable` events are decisions, not just notifications.** A listener on a - **`cancellable` events are decisions, not just notifications.** A listener on a
`cancellable` event may `cancel` it (a foreign listener sets the payload's `cancellable` event may `cancel` it (a foreign listener sets the payload's
trailing `cancelled` flag); `emit E(…)` used as an *expression* yields that flag, trailing `cancelled` flag); `emit E(…)` used as an *expression* yields that flag,
so the caller applies the action only when it wasn't vetoed — the Bukkit/DOM so the caller applies the action only when it wasn't vetoed — the Bukkit/DOM
`preventDefault` shape. See [`examples/cancel.ludic`](examples/cancel.ludic). `preventDefault` shape. See [`examples/events/cancel.ludic`](examples/events/cancel.ludic).
```ludic ```ludic
# doc-check: skip — illustrative # doc-check: skip — illustrative
@ -567,7 +567,7 @@ extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C s
`extern fn … = "symbol"` declares a foreign function and binds it to a symbol `extern fn … = "symbol"` declares a foreign function and binds it to a symbol
resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how
Ludic calls anything with a C ABI — including a shared library built from Ludic calls anything with a C ABI — including a shared library built from
another `.ludic` file (see `examples/lib/`). another `.ludic` file (see `examples/library/`).
## Statements ## Statements
@ -636,7 +636,7 @@ match tile {
`machine` turns a register into an explicit state machine: it dispatches on the `machine` turns a register into an explicit state machine: it dispatches on the
register's value to the matching `state`, and `become` transitions to a named register's value to the matching `state`, and `become` transitions to a named
state (no more `if phase == N` chains). See the co-op battle in state (no more `if phase == N` chains). See the co-op battle in
`examples/chronorift/combat.ludic`: `examples/games/chronorift/combat.ludic`:
```ludic ```ludic
# doc-check: skip — illustrative: elided bodies # doc-check: skip — illustrative: elided bodies
@ -671,7 +671,7 @@ A variant is a **compile-time `int`** accessed as `Enum.Variant` (`Action.Guard`
is `1`), numbered from `0` by declaration order, so it works anywhere an int does is `1`), numbered from `0` by declaration order, so it works anywhere an int does
— `match` patterns, comparisons, `set_reg`. Enums are a naming layer over `int`: — `match` patterns, comparisons, `set_reg`. Enums are a naming layer over `int`:
there is no distinct enum runtime type yet, so an enum value lives in an ordinary there is no distinct enum runtime type yet, so an enum value lives in an ordinary
`int` or register (and is saved with it). See `examples/chronorift/combat.ludic`, `int` or register (and is saved with it). See `examples/games/chronorift/combat.ludic`,
whose battle menus dispatch on `KnightAct`/`MageAct` instead of `0..3`. whose battle menus dispatch on `KnightAct`/`MageAct` instead of `0..3`.
## Expressions ## Expressions
@ -792,15 +792,15 @@ Emacs, Sublime and Zed, are in `tools/editors/` — see
## Working programs ## Working programs
- `examples/chronorift.ludic` — a co-op JRPG (overworld, dungeon, boss, shop, - `examples/games/chronorift.ludic` — a co-op JRPG (overworld, dungeon, boss, shop,
save) using CC0 Kenney sprites. Split across `chronorift/*.ludic` via `import`, save) using CC0 Kenney sprites. Split across `chronorift/*.ludic` via `import`,
built on models. built on models.
- `examples/menu.ludic` — a retained-UI title screen (9-slice panel, TrueType - `examples/games/menu.ludic` — a retained-UI title screen (9-slice panel, TrueType
labels, focusable buttons). labels, focusable buttons).
- `examples/snake.ludic` — Snake, no assets — same compiler, proving generality. - `examples/games/snake.ludic` — Snake, no assets — same compiler, proving generality.
```bash ```bash
bin/x app examples/snake.ludic && ./build/snake bin/x app examples/games/snake.ludic && ./build/snake
``` ```
## Not yet implemented ## Not yet implemented
@ -826,7 +826,7 @@ self-hosting; their lowerings are in
## Scenes & layers ## Scenes & layers
> **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks > **Implemented (S0).** `scene`, `layer`, and the `on enter` / `on exit` hooks
> compile; [`examples/scenes.ludic`](examples/scenes.ludic) runs and is checked > compile; [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) runs and is checked
> by `bin/x test`. A scene lowers to a `machine` the compiler writes for you: one > by `bin/x test`. A scene lowers to a `machine` the compiler writes for you: one
> implicit active-scene register, states numbered by declaration order, and > implicit active-scene register, states numbered by declaration order, and
> `become` as two direct calls plus a store. Richer scene features (the overlay > `become` as two direct calls plus a store. Richer scene features (the overlay
@ -878,7 +878,7 @@ scene Overworld {
any single phase, and a `become` in `Update` is visible to that same frame's any single phase, and a `become` in `Update` is visible to that same frame's
`Render`. `Render`.
[`examples/scenes.ludic`](examples/scenes.ludic) is a runnable, tested example [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) is a runnable, tested example
of these rules. of these rules.
## Queries in a handler signature ## Queries in a handler signature
@ -895,7 +895,7 @@ handler CleanBattle phase LateUpdate { despawn self() }
This is exactly equivalent to wrapping the body in This is exactly equivalent to wrapping the body in
`for (Battle, Pos) in query [Battle, Pos, {Foe}] where Battle.hp <= 0 { … }` — `for (Battle, Pos) in query [Battle, Pos, {Foe}] where Battle.hp <= 0 { … }` —
same lowering, same semantics. The body runs once per matching entity and same lowering, same semantics. The body runs once per matching entity and
`self()` is that entity. `examples/qdecl.ludic` is a working example. `self()` is that entity. `examples/lang/qdecl.ludic` is a working example.
Mutation during iteration follows the same rules as an inline query, because it Mutation during iteration follows the same rules as an inline query, because it
is the same loop: entities are visited by ascending id, `despawn` of the current is the same loop: entities are visited by ascending id, `despawn` of the current

View file

@ -2,8 +2,8 @@
> **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach > **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach
> pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are > pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are
> implemented and tested ([`examples/detach.ludic`](examples/detach.ludic), > implemented and tested ([`examples/lang/detach.ludic`](examples/lang/detach.ludic),
> [`examples/reason.ludic`](examples/reason.ludic), `bin/x test` checks). The > [`examples/lang/reason.ludic`](examples/lang/reason.ludic), `bin/x test` checks). The
> extensions LC2–LC6 are research-informed proposals, not built. This document > extensions LC2–LC6 are research-informed proposals, not built. This document
> distills a survey of lifecycle models across seven systems (§3) into a roadmap > distills a survey of lifecycle models across seven systems (§3) into a roadmap
> for Ludic. §13 lists the open decisions. > for Ludic. §13 lists the open decisions.
@ -121,7 +121,7 @@ Lowering: `attach` guards on the has-flag and, when absent, reuses the existing
`emit_init_component` (seed + `@OnAttach`); `detach` guards on presence, clears the `emit_init_component` (seed + `@OnAttach`); `detach` guards on presence, clears the
flag, and fires `@OnDetach` with the property bound by name — the same binding the flag, and fires `@OnDetach` with the property bound by name — the same binding the
`@OnDisable` path already uses. No new runtime; POD data stays in `@S_` storage. `@OnDisable` path already uses. No new runtime; POD data stays in `@S_` storage.
See [`examples/detach.ludic`](examples/detach.ludic). See [`examples/lang/detach.ludic`](examples/lang/detach.ludic).
--- ---
@ -166,7 +166,7 @@ survey promises. The despawn hook compiles to `@on_despawn_<Model>(i32 %e, i32
works without a user declaration; a user enum of the same name still shadows it. works without a user declaration; a user enum of the same name still shadows it.
Backward-compatible: the `reason:` binding is optional, and `@OnDespawn` without Backward-compatible: the `reason:` binding is optional, and `@OnDespawn` without
it is unchanged. `@OnDetach` and scene `on exit` do **not** yet take reasons it is unchanged. `@OnDetach` and scene `on exit` do **not** yet take reasons
(§13.1). See [`examples/reason.ludic`](examples/reason.ludic). (§13.1). See [`examples/lang/reason.ludic`](examples/lang/reason.ludic).
--- ---

View file

@ -36,8 +36,8 @@ them:
parse ([parse_game.ludic:236](selfhost/parse_game.ludic:236)) → call lowering to a parse ([parse_game.ludic:236](selfhost/parse_game.ludic:236)) → call lowering to a
direct `call @<sym>` ([emit_expr.ludic:168](selfhost/emit_expr.ludic:168)) → direct `call @<sym>` ([emit_expr.ludic:168](selfhost/emit_expr.ludic:168)) →
`declare` emission ([emit_head.ludic:105](selfhost/emit_head.ludic:105)). Working `declare` emission ([emit_head.ludic:105](selfhost/emit_head.ludic:105)). Working
examples: [examples/net_echo.ludic:12](examples/net_echo.ludic:12), examples: [examples/networking/net_echo.ludic:12](examples/networking/net_echo.ludic:12),
[examples/lib/arena.ludic:14](examples/lib/arena.ludic:14). This is the single [examples/library/arena.ludic:14](examples/library/arena.ludic:14). This is the single
most important fact in this document — see §7. most important fact in this document — see §7.
**What's missing (all of it must be built):** **What's missing (all of it must be built):**

View file

@ -5,7 +5,7 @@
> self-contained **pure-Ludic** program (no `.c`, no foreign host): a built-in > self-contained **pure-Ludic** program (no `.c`, no foreign host): a built-in
> loopback transport fills the seam, and each `examples/net_*.ludic` drives and > loopback transport fills the seam, and each `examples/net_*.ludic` drives and
> asserts itself from its own `entry`. See `bin/x test` (checks `net_echo` … `net_demo`) > asserts itself from its own `entry`. See `bin/x test` (checks `net_echo` … `net_demo`)
> and `examples/net_demo.ludic` for a full RPC→authority→replicate→reconcile loop. > and `examples/networking/net_demo.ludic` for a full RPC→authority→replicate→reconcile loop.
> clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor > clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor
> Rust and Swift stand on. > Rust and Swift stand on.
> >
@ -13,13 +13,13 @@
> checks. **N0 (transport seam):** `extern fn` now lowers end to end — a direct > checks. **N0 (transport seam):** `extern fn` now lowers end to end — a direct
> `@<sym>` call plus a `declare`, no networking logic in the compiler — so the whole > `@<sym>` call plus a `declare`, no networking logic in the compiler — so the whole
> transport is two externs (`net_send`/`net_poll`) a host fills. Proven by > transport is two externs (`net_send`/`net_poll`) a host fills. Proven by
> [`examples/net_echo.ludic`](examples/net_echo.ludic) sending four bytes through > [`examples/networking/net_echo.ludic`](examples/networking/net_echo.ludic) sending four bytes through
> the loopback host in [`tests/net_c/loopback.c`](tests/net_c/loopback.c) and > the loopback host in [`tests/net_c/loopback.c`](tests/net_c/loopback.c) and
> polling them back (`4 10 20 30 42`). **N1 (snapshot-to-buffer):** > polling them back (`4 10 20 30 42`). **N1 (snapshot-to-buffer):**
> `world_size()`/`world_save(buf)`/`world_load(buf, len)` generalize `save()`/`load()` > `world_size()`/`world_save(buf)`/`world_load(buf, len)` generalize `save()`/`load()`
> from a file to a caller-owned memory buffer — the same block layout via `memcpy` — > from a file to a caller-owned memory buffer — the same block layout via `memcpy` —
> so the whole ECS world round-trips through bytes. Proven by > so the whole ECS world round-trips through bytes. Proven by
> [`examples/net_snapshot.ludic`](examples/net_snapshot.ludic) + > [`examples/networking/net_snapshot.ludic`](examples/networking/net_snapshot.ludic) +
> [`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) (snapshot, mutate, > [`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) (snapshot, mutate,
> restore → `50 7 50`). Both are byte-identical when unused, so the offline dividend > restore → `50 7 50`). Both are byte-identical when unused, so the offline dividend
> (§8) holds. This is a companion to > (§8) holds. This is a companion to
@ -427,7 +427,7 @@ work (and how EVENTS-DESIGN sequenced EV0–EV7).
header emits a matching `declare`, so any C/Rust/Zig library (a socket, here the header emits a matching `declare`, so any C/Rust/Zig library (a socket, here the
loopback) binds through the same seam windowing uses. `find_extern` (emit_core), loopback) binds through the same seam windowing uses. `find_extern` (emit_core),
the extern branch in emit_expr's call path, `emit_extern_decls` (emit_head). the extern branch in emit_expr's call path, `emit_extern_decls` (emit_head).
([`examples/net_echo.ludic`](examples/net_echo.ludic), ([`examples/networking/net_echo.ludic`](examples/networking/net_echo.ludic),
[`tests/net_c/loopback.c`](tests/net_c/loopback.c) → `4 10 20 30 42`.) [`tests/net_c/loopback.c`](tests/net_c/loopback.c) → `4 10 20 30 42`.)
- **N1 — snapshot-to-buffer. ✅ SHIPPED.** Generalized `save()`/`load()` to a memory - **N1 — snapshot-to-buffer. ✅ SHIPPED.** Generalized `save()`/`load()` to a memory
buffer: `world_size()` (exact snapshot bytes), `world_save(buf) -> int`, buffer: `world_size()` (exact snapshot bytes), `world_save(buf) -> int`,
@ -435,31 +435,31 @@ work (and how EVENTS-DESIGN sequenced EV0–EV7).
kind, vars, per-component `@S_`/`@H_`) now feeds a file (fwrite/fread) *or* a buffer kind, vars, per-component `@S_`/`@H_`) now feeds a file (fwrite/fread) *or* a buffer
(memcpy over a threaded i64 offset), chosen by `g_snap_mode` in emit_save.ludic; (memcpy over a threaded i64 offset), chosen by `g_snap_mode` in emit_save.ludic;
no rt_ hook (the ECS world only). The rollback/replication substrate. no rt_ hook (the ECS world only). The rollback/replication substrate.
([`examples/net_snapshot.ludic`](examples/net_snapshot.ludic), ([`examples/networking/net_snapshot.ludic`](examples/networking/net_snapshot.ludic),
[`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) → `50 7 50`.) [`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) → `50 7 50`.)
- **N2 — `@Sync` codegen. ✅ SHIPPED.** The three-level annotations → generated - **N2 — `@Sync` codegen. ✅ SHIPPED.** The three-level annotations → generated
per-model `serialize_<M>`/`apply_<M>` + by-kind dispatchers (`ludic_serialize`/ per-model `serialize_<M>`/`apply_<M>` + by-kind dispatchers (`ludic_serialize`/
`apply`/`sync_size`, and the `serialize`/`apply`/`sync_size` builtins); the `apply`/`sync_size`, and the `serialize`/`apply`/`sync_size` builtins); the
POD-scalar compile error and the empty-participation warning. The declarative POD-scalar compile error and the empty-participation warning. The declarative
core. ([`examples/net_sync.ludic`](examples/net_sync.ludic) → `12 3 4 50 999`, core. ([`examples/networking/net_sync.ludic`](examples/networking/net_sync.ludic) → `12 3 4 50 999`,
emit in [`selfhost/emit_net.ludic`](selfhost/emit_net.ludic).) emit in [`selfhost/emit_net.ludic`](selfhost/emit_net.ludic).)
- **N3 — ownership. ✅ SHIPPED.** `@Owned` + the `@L_owner_arr` array + - **N3 — ownership. ✅ SHIPPED.** `@Owned` + the `@L_owner_arr` array +
`owner()`/`set_owner()`/`is_owner()`; owners are part of the world snapshot. `owner()`/`set_owner()`/`is_owner()`; owners are part of the world snapshot.
([`examples/net_owner.ludic`](examples/net_owner.ludic) → `-1 7 0 1`.) ([`examples/networking/net_owner.ludic`](examples/networking/net_owner.ludic) → `-1 7 0 1`.)
- **N4 — remote events (RPCs). ✅ SHIPPED.** `@ToServer`/`@ToClients` on `event`s → - **N4 — remote events (RPCs). ✅ SHIPPED.** `@ToServer`/`@ToClients` on `event`s →
payload serialize (`[event id][fields]`) + directional `net_send` + `net_pump()` payload serialize (`[event id][fields]`) + directional `net_send` + `net_pump()`
far-side re-`emit`. ([`examples/net_rpc.ludic`](examples/net_rpc.ludic) → `0 8`.) far-side re-`emit`. ([`examples/networking/net_rpc.ludic`](examples/networking/net_rpc.ludic) → `0 8`.)
- **N5 — roles + drivable sim. ✅ SHIPPED.** `@Server`/`@Predicted` role-guarded - **N5 — roles + drivable sim. ✅ SHIPPED.** `@Server`/`@Predicted` role-guarded
dispatch driven by the `@L_role` register (`set_role`/`is_server`/`local_id`); dispatch driven by the `@L_role` register (`set_role`/`is_server`/`local_id`);
the opt-in `entry`-owns-the-loop with `tick_fixed()`/`tick_render()`. Together the opt-in `entry`-owns-the-loop with `tick_fixed()`/`tick_render()`. Together
these let prediction/rollback be written in developer/library code. these let prediction/rollback be written in developer/library code.
([`examples/net_roles.ludic`](examples/net_roles.ludic) → `1 102`.) ([`examples/networking/net_roles.ludic`](examples/networking/net_roles.ludic) → `1 102`.)
- **N6 — a blessed reference netcode runtime. ✅ SHIPPED.** A Ludic library - **N6 — a blessed reference netcode runtime. ✅ SHIPPED.** A Ludic library
([`examples/net_rt.ludic`](examples/net_rt.ludic)) — server-authoritative state ([`examples/networking/net_rt.ludic`](examples/networking/net_rt.ludic)) — server-authoritative state
replication over the primitives — plus a full end-to-end demo, proving the seams replication over the primitives — plus a full end-to-end demo, proving the seams
the way the C mod proved the event ABI, but in pure Ludic over the built-in the way the C mod proved the event ABI, but in pure Ludic over the built-in
transport. Library policy, swappable for lockstep+rollback. transport. Library policy, swappable for lockstep+rollback.
([`examples/net_demo.ludic`](examples/net_demo.ludic) → `5 999 5`.) A built-in ([`examples/networking/net_demo.ludic`](examples/networking/net_demo.ludic) → `5 999 5`.) A built-in
loopback transport (N0) means all of this needs **no foreign code at all**. loopback transport (N0) means all of this needs **no foreign code at all**.
N0–N2 deliver "state can be declared, serialized, and moved." N3–N4 add ownership N0–N2 deliver "state can be declared, serialized, and moved." N3–N4 add ownership

View file

@ -48,9 +48,9 @@ self-hosted native toolchain.)
| `runtime/web/platform.js` | the browser's window — the same five `win_*` functions `cocoa.ll` implements, against a `<canvas>` | | `runtime/web/platform.js` | the browser's window — the same five `win_*` functions `cocoa.ll` implements, against a `<canvas>` |
| `runtime/web/index.html` | the page a web build is served from | | `runtime/web/index.html` | the page a web build is served from |
| `tools/ludic-web/run.mjs` | runs a headless wasm build under Node, so native and wasm output can be diffed | | `tools/ludic-web/run.mjs` | runs a headless wasm build under Node, so native and wasm output can be diffed |
| `examples/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) | | `examples/games/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) |
| `examples/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) | | `examples/games/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) |
| `examples/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) | | `examples/games/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) |
| `bin/x` | the task runner — `bin/x app examples/<name>.ludic` compiles a program; `bin/x help` lists every command | | `bin/x` | the task runner — `bin/x app examples/<name>.ludic` compiles a program; `bin/x help` lists every command |
| `bin/x test` | regression suite: builds the compiler, compiles/runs all examples, checks save/load + diagnostics | | `bin/x test` | regression suite: builds the compiler, compiles/runs all examples, checks save/load + diagnostics |
| `tools/ludic-tools/` | the editor toolchain, **in Ludic**: `ludic-fmt` (source formatter) and `ludic-lsp` (language server) — one lexer and one vocabulary shared by both | | `tools/ludic-tools/` | the editor toolchain, **in Ludic**: `ludic-fmt` (source formatter) and `ludic-lsp` (language server) — one lexer and one vocabulary shared by both |
@ -74,14 +74,14 @@ to machine code via LLVM IR; see **[COMPILING.md](COMPILING.md)** for the
pipeline and the runtime protocol. pipeline and the runtime protocol.
```bash ```bash
bin/x app examples/chronorift.ludic bin/x app examples/games/chronorift.ludic
./build/chronorift # opens a native window ./build/chronorift # opens a native window
``` ```
Headless render (for testing / CI): Headless render (for testing / CI):
```bash ```bash
bin/x app examples/chronorift.ludic --headless bin/x app examples/games/chronorift.ludic --headless
printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm
sips -s format png out.ppm --out frame.png sips -s format png out.ppm --out frame.png
``` ```
@ -141,7 +141,7 @@ same highlighting, checking and formatting as the source tree.
## The game: Chrono Rift ## The game: Chrono Rift
A playable co-op JRPG in `examples/chronorift.ludic`, using CC0 A playable co-op JRPG in `examples/games/chronorift.ludic`, using CC0
[Kenney](https://kenney.nl) sprites (Tiny Town + Tiny Dungeon), decoded from [Kenney](https://kenney.nl) sprites (Tiny Town + Tiny Dungeon), decoded from
PNG at runtime by the Ludic-written PNG/DEFLATE decoder — no zlib, no external PNG at runtime by the Ludic-written PNG/DEFLATE decoder — no zlib, no external
dependency on any target. dependency on any target.

View file

@ -2,7 +2,7 @@
> **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` / > **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` /
> `layer` / `on enter` / `on exit` / `become`, lowered to the implicit machine of > `layer` / `on enter` / `on exit` / `become`, lowered to the implicit machine of
> §3 and §9 — is implemented and tested ([`examples/scenes.ludic`](examples/scenes.ludic), > §3 and §9 — is implemented and tested ([`examples/lang/scenes.ludic`](examples/lang/scenes.ludic),
> a `bin/x test` check). The extensions in §4–§8 (scene-owned entities, richer > a `bin/x test` check). The extensions in §4–§8 (scene-owned entities, richer
> layers, the overlay stack, scene-local state, transition parameters) are still > layers, the overlay stack, scene-local state, transition parameters) are still
> design targets. This document reaches deliberately past the thin sketch so we > design targets. This document reaches deliberately past the thin sketch so we
@ -22,7 +22,7 @@ the language today, and a third is sketched:
| `machine`/`state`/`become` over a register | works | dispatch on the register each frame; still one flat register, no per-state handlers or lifecycle | | `machine`/`state`/`become` over a register | works | dispatch on the register each frame; still one flat register, no per-state handlers or lifecycle |
| `scene`/`layer`/`on enter`/`on exit` | **sketch only** | — | | `scene`/`layer`/`on enter`/`on exit` | **sketch only** | — |
The sketch ([`examples/scenes.ludic`](examples/scenes.ludic)) specs: The sketch ([`examples/lang/scenes.ludic`](examples/lang/scenes.ludic)) specs:
- Exactly **one scene active**; the `start` scene runs first. - Exactly **one scene active**; the `start` scene runs first.
- A scene's handlers run only while it is active; handlers outside any scene are - A scene's handlers run only while it is active; handlers outside any scene are
@ -309,7 +309,7 @@ Each is independently shippable and testable, matching how the repo phases work.
- **S0 — parse & lower the sketch.** ✅ **Done.** `scene`/`layer`/`on enter`/`on - **S0 — parse & lower the sketch.** ✅ **Done.** `scene`/`layer`/`on enter`/`on
exit`/`become` lowered to the implicit `machine`; the active scene is exit`/`become` lowered to the implicit `machine`; the active scene is
snapshotted per phase so exactly one scene's layers dispatch in any phase. snapshotted per phase so exactly one scene's layers dispatch in any phase.
[`examples/scenes.ludic`](examples/scenes.ludic) compiles, runs, and is checked [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) compiles, runs, and is checked
by `bin/x test`. This is the floor everything else builds on. by `bin/x test`. This is the floor everything else builds on.
- **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists). - **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists).
- **S2 — layer toggle & pause** (E2) on top of the existing `enable`/`disable`. - **S2 — layer toggle & pause** (E2) on top of the existing `enable`/`disable`.
@ -317,7 +317,7 @@ Each is independently shippable and testable, matching how the repo phases work.
layer L` flips an `@LE_<L>` flag that gates the layer's handlers (emitted only layer L` flips an `@LE_<L>` flag that gates the layer's handlers (emitted only
for toggled layers, so untouched scene programs stay byte-identical), and a for toggled layers, so untouched scene programs stay byte-identical), and a
`public` layer fires `layer_<L>_show`/`_hide` — see `public` layer fires `layer_<L>_show`/`_hide` — see
[`examples/layer_events.ludic`](examples/layer_events.ludic). Still open: the [`examples/events/layer_events.ludic`](examples/events/layer_events.ludic). Still open: the
*pause* half (keep drawing while `Update` handlers suspend) and `on show`/`on *pause* half (keep drawing while `Update` handlers suspend) and `on show`/`on
hide` blocks. hide` blocks.
- **S3 — the scene stack** (E3): `push`/`pop`/`overlay`/`passthrough`. The big one. - **S3 — the scene stack** (E3): `push`/`pop`/`overlay`/`passthrough`. The big one.
@ -350,5 +350,5 @@ actually lands; S4–S5 are ergonomics; S6 is a performance destination.
--- ---
*Companion to [LANGUAGE.md §"Scenes & layers"](LANGUAGE.md) and the ordering *Companion to [LANGUAGE.md §"Scenes & layers"](LANGUAGE.md) and the ordering
sketch in [`examples/scenes.ludic`](examples/scenes.ludic). Supersedes nothing sketch in [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic). Supersedes nothing
until the compiler work in §10 lands.* until the compiler work in §10 lands.*

View file

@ -37,10 +37,10 @@ Verified against the self-hosted compiler ([selfhost/parse.ludic](selfhost/parse
5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented. *(✅ Phase 3d: now `@pure`)* 5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented. *(✅ Phase 3d: now `@pure`)*
6. `@anno` + `reads/writes/needs/uses [..]` — parsed then thrown away 6. `@anno` + `reads/writes/needs/uses [..]` — parsed then thrown away
([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented. *(✅ Phase 3c: `needs`/`uses` dropped)* ([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented. *(✅ Phase 3c: `needs`/`uses` dropped)*
7. `scene`/`layer`/`on enter` — full LANGUAGE.md section + [examples/scenes.ludic](examples/scenes.ludic), 7. `scene`/`layer`/`on enter` — full LANGUAGE.md section + [examples/lang/scenes.ludic](examples/lang/scenes.ludic),
**does not compile** (`expected declaration`). **does not compile** (`expected declaration`).
8. `query (v) [..]` in a system signature — two LANGUAGE.md sections + 8. `query (v) [..]` in a system signature — two LANGUAGE.md sections +
[examples/qdecl.ludic](examples/qdecl.ludic), **does not compile** (`parse error: {`). *(✅ implemented in Phase 1)* [examples/lang/qdecl.ludic](examples/lang/qdecl.ludic), **does not compile** (`parse error: {`). *(✅ implemented in Phase 1)*
9. `when cond {}` — documented ([LANGUAGE.md:329](LANGUAGE.md)) + in all three editor 9. `when cond {}` — documented ([LANGUAGE.md:329](LANGUAGE.md)) + in all three editor
highlighters, **never parsed**. *(✅ implemented in Phase 1 as an if-without-else alias)* highlighters, **never parsed**. *(✅ implemented in Phase 1 as an if-without-else alias)*
10. CLI `--emit-llvm`/`-o`/`--shared`/`--fmt` — documented, but `ludicc` only 10. CLI `--emit-llvm`/`-o`/`--shared`/`--fmt` — documented, but `ludicc` only
@ -187,13 +187,13 @@ Made spec ⇄ compiler agree **before** any grammar change. What landed:
(`edge` is a pure marker; the emitter never lowered it differently.) (`edge` is a pure marker; the emitter never lowered it differently.)
- ✅ **Signature-`query` implemented** (#8) — `query (vars) [terms] where c` in a - ✅ **Signature-`query` implemented** (#8) — `query (vars) [terms] where c` in a
system header desugars to the same `S_QUERY` node the inline `for` builds, so system header desugars to the same `S_QUERY` node the inline `for` builds, so
`examples/qdecl.ludic` compiles and runs. Also fixed multi-line clause parsing `examples/lang/qdecl.ludic` compiles and runs. Also fixed multi-line clause parsing
(clauses may now span lines). (clauses may now span lines).
- ✅ **`when c { }` implemented** (#9) — as an `if`-without-else alias in - ✅ **`when c { }` implemented** (#9) — as an `if`-without-else alias in
[parse.ludic](selfhost/parse.ludic). Docs + editors already listed it; now the [parse.ludic](selfhost/parse.ludic). Docs + editors already listed it; now the
compiler agrees, so no editor-vocab churn was needed. compiler agrees, so no editor-vocab churn was needed.
- ✅ **`scene`/`layer` marked not-yet-implemented** (#7) — prominent note in - ✅ **`scene`/`layer` marked not-yet-implemented** (#7) — prominent note in
LANGUAGE.md §"Scenes & layers" + a header on [examples/scenes.ludic](examples/scenes.ludic). LANGUAGE.md §"Scenes & layers" + a header on [examples/lang/scenes.ludic](examples/lang/scenes.ludic).
Full scene front-end + emission deferred (real work, out of Phase 1 scope). Full scene front-end + emission deferred (real work, out of Phase 1 scope).
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section - ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now
@ -272,7 +272,7 @@ are retired; declaration modifiers are now leading `@annotations`: `@export fn`,
`@edge system`, `@pure`, `@deterministic`. `parse_one_decl` collects a leading `@edge system`, `@pure`, `@deterministic`. `parse_one_decl` collects a leading
`@anno` run and `@export` sets the fn export flag ([parse.ludic](selfhost/parse.ludic)); `@anno` run and `@export` sets the fn export flag ([parse.ludic](selfhost/parse.ludic));
the dead `edge`-dispatch was removed from `parse_system`. Migrated the one the dead `edge`-dispatch was removed from `parse_system`. Migrated the one
`@export` user ([examples/lib/combat.ludic](examples/lib/combat.ludic)); old `@export` user ([examples/library/combat.ludic](examples/library/combat.ludic)); old
prefix forms now rejected. Behavior-identical: the export flag is parse-only in prefix forms now rejected. Behavior-identical: the export flag is parse-only in
the self-hosted emitter (it emits `@fn_<name>` for every function and never reads the self-hosted emitter (it emits `@fn_<name>` for every function and never reads
the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so
@ -359,7 +359,7 @@ Added an **annotation DSL**: `@Queries(these: [Prop{constraint}, …], on: Model
a handler desugars to the existing `S_QUERY` loop (each property binds by its own a handler desugars to the existing `S_QUERY` loop (each property binds by its own
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}` name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
tag), and `@Handles(…)` on a program parses as documentation. See tag), and `@Handles(…)` on a program parses as documentation. See
[examples/annotations.ludic](examples/annotations.ludic); bin/x test 15/15. All thirteen findings are resolved or resolved by an [examples/lang/annotations.ludic](examples/lang/annotations.ludic); bin/x test 15/15. All thirteen findings are resolved or resolved by an
explicit, documented decision. explicit, documented decision.
--- ---

View file

@ -79,7 +79,7 @@
"intro": "Build the toolchain once, then your game. Everything runs from the repo root.", "intro": "Build the toolchain once, then your game. Everything runs from the repo root.",
"steps": [ "steps": [
{ "title": "Build the toolchain", "html": "<code>bin/x build</code> produces <code>ludicc</code>, the compiler you'll use for everything below." }, { "title": "Build the toolchain", "html": "<code>bin/x build</code> produces <code>ludicc</code>, the compiler you'll use for everything below." },
{ "title": "Compile &amp; run an example", "html": "<code>bin/x app examples/snake.ludic</code> turns a <code>.ludic</code> file into a native binary. Run it to open a real window." }, { "title": "Compile &amp; run an example", "html": "<code>bin/x app examples/games/snake.ludic</code> turns a <code>.ludic</code> file into a native binary. Run it to open a real window." },
{ "title": "Go headless for tests", "html": "<code>--headless</code> renders frames to a <code>.ppm</code> from piped input — deterministic output you can diff." }, { "title": "Go headless for tests", "html": "<code>--headless</code> renders frames to a <code>.ppm</code> from piped input — deterministic output you can diff." },
{ "title": "Edit with full tooling", "html": "<code>bin/x tools</code> builds the formatter and language server; every editor gets completion, diagnostics and go-to-definition." } { "title": "Edit with full tooling", "html": "<code>bin/x tools</code> builds the formatter and language server; every editor gets completion, diagnostics and go-to-definition." }
], ],
@ -89,11 +89,11 @@
{ "cmd": "bin/x build" }, { "cmd": "bin/x build" },
{ "blank": true }, { "blank": true },
{ "comment": "build and run an example (opens a window)" }, { "comment": "build and run an example (opens a window)" },
{ "cmd": "bin/x app examples/snake.ludic" }, { "cmd": "bin/x app examples/games/snake.ludic" },
{ "cmd": "./build/snake" }, { "cmd": "./build/snake" },
{ "blank": true }, { "blank": true },
{ "comment": "deterministic headless render for tests" }, { "comment": "deterministic headless render for tests" },
{ "cmd": "bin/x app examples/snake.ludic --headless" }, { "cmd": "bin/x app examples/games/snake.ludic --headless" },
{ "cmd": "printf 'ddddwww' | ./build/snake_headless" }, { "cmd": "printf 'ddddwww' | ./build/snake_headless" },
{ "out": "→ writes out.ppm" } { "out": "→ writes out.ppm" }
] ]

101
examples/README.md Normal file
View file

@ -0,0 +1,101 @@
# Examples
A curated tour of Ludic, grouped by intent. Every example here is exercised by
the test suite (`bin/x test` / `bin/x selfhost-test`), so nothing in this
directory silently rots.
Run any program straight from the repository root (so `assets/` resolves):
```sh
bin/x app examples/games/snake.ludic # compile + open a real window
bin/x app examples/games/snake.ludic --headless # render one frame to build/out.ppm
bin/ludic examples/lang/offline_rewards.ludic # compile + run a plain program
```
## `games/` — complete, windowed games
| Example | What it shows |
|---|---|
| [`games/snake.ludic`](games/snake.ludic) | A full game from primitives: input, grid, growth, collision, score. |
| [`games/menu.ludic`](games/menu.ludic) | A retained-UI title screen — the UI declared as data. |
| [`games/chronorift.ludic`](games/chronorift.ludic) | A 2D co-op JRPG: overworld, dungeon, random encounters, turn battle, boss, save. The entry file `import`s the modules under [`games/chronorift/`](games/chronorift/). |
## `rendering/` — drawing primitives
| Example | What it shows |
|---|---|
| [`rendering/draw_prims.ludic`](rendering/draw_prims.ludic) | The extended `Screen.*` primitives — line, circle, triangle, fill, sprite. |
## `ecs/` — the entity-component world
| Example | What it shows |
|---|---|
| [`ecs/hello.ludic`](ecs/hello.ludic) | The smallest program that exercises the ECS pipeline. |
| [`ecs/world_get.ludic`](ecs/world_get.ludic) | Reflection: read/write a component by name. |
| [`ecs/world_scan.ludic`](ecs/world_scan.ludic) | Reflection: scan the world and identify each entity. |
| [`ecs/world_query.ludic`](ecs/world_query.ludic) | Reflection: iterate the world by property. |
| [`ecs/world_spawn.ludic`](ecs/world_spawn.ludic) | Reflection: a mod spawns a fresh entity by model id. |
| [`ecs/world_mixed.ludic`](ecs/world_mixed.ludic) | Reflection: get/set through real struct offsets. |
| [`ecs/world_dyn.ludic`](ecs/world_dyn.ludic) | Reflection: register a brand-new component at runtime. |
## `events/` — the event bus
| Example | What it shows |
|---|---|
| [`events/events.ludic`](events/events.ludic) | EV0: the event-bus core — declare, `emit`, `@On`. |
| [`events/mod_events.ludic`](events/mod_events.ludic) | Two listeners on one event, driven from a mod. |
| [`events/cancel.ludic`](events/cancel.ludic) | EV3: cancellable (decision) events. |
| [`events/recurse.ludic`](events/recurse.ludic) | EV6: re-entrant `emit` is depth-bounded — no runaway cycle. |
| [`events/promote.ludic`](events/promote.ludic) | EV1: `@Public` promotes a lifecycle hook to a public event. |
| [`events/prop_events.ludic`](events/prop_events.ludic) | EV1 for properties: attach/detach events. |
| [`events/scene_events.ludic`](events/scene_events.ludic) | EV1 for scenes: on-enter / on-exit events. |
| [`events/program_events.ludic`](events/program_events.ludic) | EV1 for the program scope: start/quit events. |
| [`events/layer_events.ludic`](events/layer_events.ludic) | EV1 for layers + a `public` layer's show/hide events. |
| [`events/scoped.ludic`](events/scoped.ludic) | A despawned entity drops out of subsequent event work. |
## `networking/` — deterministic multiplayer (N0–N6)
Each maps to a stage of [`NETWORKING-DESIGN.md`](../NETWORKING-DESIGN.md); all run
over the compiler's built-in loopback transport with zero foreign code.
| Example | What it shows |
|---|---|
| [`networking/net_echo.ludic`](networking/net_echo.ludic) | N0: the transport seam. |
| [`networking/net_snapshot.ludic`](networking/net_snapshot.ludic) | N1: whole-world snapshot to a memory buffer. |
| [`networking/net_sync.ludic`](networking/net_sync.ludic) | N2: `@Sync` replication codegen. |
| [`networking/net_owner.ludic`](networking/net_owner.ludic) | N3: entity ownership. |
| [`networking/net_rpc.ludic`](networking/net_rpc.ludic) | N4: remote events / RPCs. |
| [`networking/net_roles.ludic`](networking/net_roles.ludic) | N5: handler roles + the drivable sim. |
| [`networking/net_demo.ludic`](networking/net_demo.ludic) | N6: a networked game end to end, in pure Ludic. |
| [`networking/net_rt.ludic`](networking/net_rt.ludic) | A blessed server-authoritative replication runtime. |
## `lang/` — language & standard-library tour
| Example | What it shows |
|---|---|
| [`lang/annotations.ludic`](lang/annotations.ludic) | The annotation-first style: `@Queries` / `@Computed` / `@OnSpawn` / `@Handles`. |
| [`lang/qdecl.ludic`](lang/qdecl.ludic) | `@Queries` desugaring to the query system. |
| [`lang/lifecycle.ludic`](lang/lifecycle.ludic) | The whole game lifecycle as `@`-hooks, in firing order. |
| [`lang/detach.ludic`](lang/detach.ludic) | Structural attach/detach + `@OnAttach` / `@OnDetach`. |
| [`lang/reason.ludic`](lang/reason.ludic) | Reason-carrying teardown (`@OnDespawn`: Despawned vs Quit). |
| [`lang/toggle.ludic`](lang/toggle.ludic) | enable/disable at the three ECS scopes. |
| [`lang/scenes.ludic`](lang/scenes.ludic) | One active scene at a time, handlers grouped into layers. |
| [`lang/strings.ludic`](lang/strings.ludic) | Strings as values: compare, join, interpolate, slice. |
| [`lang/rng_demo.ludic`](lang/rng_demo.ludic) | The `Random.*` extensions (value/int/sign). |
| [`lang/time_demo.ludic`](lang/time_demo.ludic) | `Time.frame` / `elapsed` / `delta`. |
| [`lang/offline_rewards.ludic`](lang/offline_rewards.ludic) | A worked idle-game example over the Time/Date/Duration stdlib. |
## `library/` — building and linking a shared library
A how-to for compiling a `.ludic` module to a native `.dylib`/`.so`/`.dll` and
linking a second program against it via `extern fn`:
```sh
bin/ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
bin/ludicc examples/library/arena.ludic -o build/arena -Lbuild -lcombat
```
| Example | What it shows |
|---|---|
| [`library/combat.ludic`](library/combat.ludic) | Exported functions callable across the C ABI. |
| [`library/arena.ludic`](library/arena.ludic) | A program that links against the shared library. |

View file

@ -1,5 +1,5 @@
# world.ludic — the data model: components, entity archetypes, constants. # world.ludic — the data model: components, entity archetypes, constants.
# Imported by examples/chronorift.ludic. No `game` wrapper (this is a fragment). # Imported by examples/games/chronorift.ludic. No `game` wrapper (this is a fragment).
property Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
property Actor { kind: int = 0 } property Actor { kind: int = 0 }

View file

@ -1,6 +1,6 @@
# ============================================================================ # ============================================================================
# snake.ludic — a small, complete game in Ludic. No sprites or assets: every # snake.ludic — a small, complete game in Ludic. No sprites or assets: every
# pixel is drawn from primitives. Build: bin/x app examples/snake.ludic # pixel is drawn from primitives. Build: bin/x app examples/games/snake.ludic
# #
# It shows the everyday shape of a Ludic game: an ECS for the moving parts (one # It shows the everyday shape of a Ludic game: an ECS for the moving parts (one
# entity per snake segment), named program state for the rest, and a Render # entity per snake segment), named program state for the rest, and a Render

View file

@ -11,7 +11,7 @@
# 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20 # 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20
# 0 the Shield is gone — nothing matches # 0 the Shield is gone — nothing matches
# #
# bin/x game-build bin/ludicc examples/detach.ludic /tmp/detach # bin/x game-build bin/ludicc examples/lang/detach.ludic /tmp/detach
# /tmp/detach </dev/null # /tmp/detach </dev/null
program Detach { program Detach {
property Tag { v: int = 0 } property Tag { v: int = 0 }

View file

@ -3,7 +3,7 @@
# the deterministic Clock. Everything is integer seconds, so the result replays # the deterministic Clock. Everything is integer seconds, so the result replays
# identically — no wall clock, no floating point. # identically — no wall clock, no floating point.
# #
# bin/ludic examples/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0 # bin/ludic examples/lang/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0
program OfflineRewards { program OfflineRewards {
entry { entry {
# A save records when the player last quit. It is hardcoded here so the demo # A save records when the player last quit. It is hardcoded here so the demo

View file

@ -9,7 +9,7 @@
# 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500 # 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500
# 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000 # 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000
# #
# bin/x game-build bin/ludicc examples/reason.ludic /tmp/reason # bin/x game-build bin/ludicc examples/lang/reason.ludic /tmp/reason
# /tmp/reason </dev/null # /tmp/reason </dev/null
program Reasons { program Reasons {
property Health { hp: int = 0 } property Health { hp: int = 0 }

View file

@ -12,7 +12,7 @@
# 201 900 frame 3: Play.World.Step, then Hud.Draw # 201 900 frame 3: Play.World.Step, then Hud.Draw
# 202 900 frame 4: Step reaches 2 -> quit(); Hud.Draw paints the last frame # 202 900 frame 4: Step reaches 2 -> quit(); Hud.Draw paints the last frame
# #
# bin/x game-build bin/ludicc examples/scenes.ludic /tmp/scenes # bin/x game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes
# printf 'aaaa' | /tmp/scenes # printf 'aaaa' | /tmp/scenes
program SceneDemo { program SceneDemo {
var counter: int = 0 var counter: int = 0

View file

@ -1,8 +1,8 @@
# ============================================================================ # ============================================================================
# arena.ludic — a game that links the Ludic shared library next to it. # arena.ludic — a game that links the Ludic shared library next to it.
# #
# ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib # ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
# ludicc examples/lib/arena.ludic -o build/arena -Lbuild -lcombat # ludicc examples/library/arena.ludic -o build/arena -Lbuild -lcombat
# #
# `extern fn` binds a name to a symbol resolved at link time. The library the # `extern fn` binds a name to a symbol resolved at link time. The library the
# symbols come from happens to be written in Ludic, but nothing here depends on # symbols come from happens to be written in Ludic, but nothing here depends on

View file

@ -4,7 +4,7 @@
# `module` instead of `game` means: no entry point, no frame loop. ludicc # `module` instead of `game` means: no entry point, no frame loop. ludicc
# compiles this to a real shared object — # compiles this to a real shared object —
# #
# ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib # ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
# #
# — whose `export fn`s are ordinary C-ABI symbols. Anything that can call a # — whose `export fn`s are ordinary C-ABI symbols. Anything that can call a
# .dylib/.so/.dll can call these: another Ludic program via `extern fn`, a game # .dylib/.so/.dll can call these: another Ludic program via `extern fn`, a game

View file

@ -14,7 +14,7 @@
# 5. rt_receive() — the client reconciles to the authoritative x=5 # 5. rt_receive() — the client reconciles to the authoritative x=5
# #
# Prints 5 / 999 / 5. Build & run with the Ludic toolchain only: # Prints 5 / 999 / 5. Build & run with the Ludic toolchain only:
# bin/x app examples/net_demo.ludic --headless && ./build/net_demo_headless # bin/x app examples/networking/net_demo.ludic --headless && ./build/net_demo_headless
import "net_rt.ludic" import "net_rt.ludic"
program NetDemo { program NetDemo {

View file

@ -55,7 +55,7 @@ function rt_shutdown() -> void {
win_close() win_close()
return return
} }
rt_dump_ppm("out.ppm") rt_dump_ppm("build/out.ppm")
} }
# ---- framebuffer ---------------------------------------------------------- # ---- framebuffer ----------------------------------------------------------

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
games/snake 5812c6ad93e85a53576d3c0f116246509ca79c854cf0c46ca4e20b504ccbef0f
games/menu 937232b0b9723fd3d9096b67293ebdc6cd3b3352b742adcd6f29183a187d30f0
games/chronorift b52accaea66f3080646dedaa7d073df705ca89e43ee953283ba7d106defc50eb
rendering/draw_prims b744b5c8e7ee0ce503f8acb3d0375517d00f35c623073b1b2ae34b95d84fad98

File diff suppressed because one or more lines are too long

View file

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

View file

@ -111,7 +111,7 @@ def main():
check_true(f"advertises {cap}", cap in caps) check_true(f"advertises {cap}", cap in caps)
# ---- a fragment file: no `program` block, only reachable through an import -- # ---- 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) text = c.open(frag)
uri = {"uri": "file://" + frag} uri = {"uri": "file://" + frag}

View file

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

View file

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

View file

@ -75,6 +75,21 @@ function getenv_or(name: pointer, dflt: pointer) -> pointer {
return v 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 --------------------------------------------------------- # ---- stdout helpers ---------------------------------------------------------
# write `s` with no trailing newline (print() always adds one) # 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}]`) } 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 # compile examples/<path>.ludic as a game, render it headless, and compare the
function game_case(name: pointer, keys: pointer) -> void { # framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256.
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_g_{name}`) { bad(`{name}: build`); return } # A hash is tiny and text-diffable — a regression shows as a changed hex line in a
run(`printf '%s' '{keys}' | /tmp/x_g_{name} >/dev/null 2>&1`) # review, not an opaque "binary files differ". Regenerate the baseline with
if shq(`cmp -s selfhost/golden/{name}.ppm out.ppm`) { ok(`{name} matches the golden render`) } # `bin/x golden` (see cmd_golden below).
else { bad(`{name}: differs from golden`) } 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 { function cmd_selfhost_test() -> int {
@ -57,12 +100,13 @@ function cmd_selfhost_test() -> int {
sh_case("color", "16744512 1090486336 1090486336 8355711 8355711 8355711") sh_case("color", "16744512 1090486336 1090486336 8355711 8355711 8355711")
print("== the self-host compiler compiles real games (vs golden output) ==") print("== the self-host compiler compiles real games (vs golden output) ==")
# the golden PPMs were produced by the original C compiler; the self-hosted # the golden render hashes were first produced by the original C compiler; the
# compiler must reproduce them exactly. # self-hosted compiler must reproduce the same framebuffer, byte for byte, so
game_case("snake", "ddssaawwddss") # its SHA-256 matches the checked-in baseline in selfhost/golden/renders.sha256.
game_case("menu", "ssss") game_case("games/snake", "ddssaawwddss")
game_case("chronorift", "ddddwwwwaassK") game_case("games/menu", "ssss")
game_case("draw_prims", "") game_case("games/chronorift", "ddddwwwwaassK")
game_case("rendering/draw_prims", "")
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==") 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)") } 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 # compile a feature example as a game, run it (optional keys on stdin) and match
# its stdout against the expected space-joined output. # its stdout against the expected space-joined output.
function feat_case(name: pointer, keys: pointer, exp: pointer, label: pointer) -> void { function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_e_{name}`) { 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 bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
} }
var got = "" var got = ""
if (keys == "") { got = capture_line(`/tmp/x_e_{name} < /dev/null`) } if (keys == "") { got = capture_line(`/tmp/x_e_{nm} < /dev/null`) }
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{name}`) } else { got = capture_line(`printf '{keys}' | /tmp/x_e_{nm}`) }
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) } if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
} }
# a "does it still compile" smoke test (parse -> lower -> link), no run # a "does it still compile" smoke test (parse -> lower -> link), no run
function qsmoke(name: pointer) -> void { function qsmoke(path: pointer) -> void {
if game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_q_{name}`) { let nm = flat(path)
ok(`{name} compiles (@Queries desugars to S_QUERY)`) if game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_q_{nm}`) {
} else { bad2(name, capture_line("grep -i error /tmp/x_gb.err | head -1")) } 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, # 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. # let clang assemble it, run, and compare stdout. No C host and no C compiled.
function net_case(name: pointer, exp: pointer) -> void { function net_case(path: pointer, exp: pointer) -> void {
let ll = `/tmp/x_n_{name}.ll` let nm = flat(path)
let log = `/tmp/x_n_{name}.out` let ll = `/tmp/x_n_{nm}.ll`
if not shq(`bin/ludicc --headless examples/{name}.ludic --emit-llvm -o {ll} > {log} 2>&1`) { 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}`) 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}`) 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`) let got = capture_line(`/tmp/x_nb_{nm} < /dev/null`)
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) } if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) }
} }
function cmd_test() -> int { function cmd_test() -> int {
@ -56,14 +69,14 @@ function cmd_test() -> int {
print("== the compiler builds from the IR seed (no C compiler) ==") print("== the compiler builds from the IR seed (no C compiler) ==")
run("rm -f bin/ludicc") 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") ok("clang selfhost/ludicc.seed.ll -> bin/ludicc")
} else { bad("compiler did not build from the seed") } } else { bad("compiler did not build from the seed") }
print("== every example compiles to its golden render ==") print("== every example compiles to its golden render ==")
game_case("snake", "ddssaawwddss") game_case("games/snake", "ddssaawwddss")
game_case("menu", "ssss") game_case("games/menu", "ssss")
game_case("chronorift", "ddddwwwwaassK") game_case("games/chronorift", "ddddwwwwaassK")
print("== self-hosting and the bootstrap fixpoints ==") print("== self-hosting and the bootstrap fixpoints ==")
run("bin/x selfhost-test > /tmp/x_sh.out 2>&1") 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)`) ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
print("== documented syntax stays compilable (guards against spec/compiler drift) ==") print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
qsmoke("qdecl") qsmoke("lang/qdecl")
feat_case("annotations", "", "3 25 0 0", "annotations.ludic (@Queries / @Computed / @OnSpawn / @Handles)") feat_case("lang/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("lang/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("lang/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("lang/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("lang/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("lang/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("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("detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)") feat_case("lang/detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
feat_case("reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)") 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`. # 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 # 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 # asserted from its own `entry`. The transport is the compiler's built-in
# loopback, so a networked game runs with zero foreign code. # loopback, so a networked game runs with zero foreign code.
net_case("mod_events", "10 32 42") net_case("events/mod_events", "10 32 42")
net_case("net_echo", "4 10 20 30 42") net_case("networking/net_echo", "4 10 20 30 42")
net_case("net_snapshot", "50 7 50") net_case("networking/net_snapshot", "50 7 50")
net_case("net_sync", "12 3 4 50 999") net_case("networking/net_sync", "12 3 4 50 999")
net_case("net_owner", "-1 7 0 1") net_case("networking/net_owner", "-1 7 0 1")
net_case("net_rpc", "0 8") net_case("networking/net_rpc", "0 8")
net_case("net_roles", "1 102") net_case("networking/net_roles", "1 102")
net_case("net_demo", "5 999 5") 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("events/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/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. # EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("world_get", "50 1 7") net_case("ecs/world_get", "50 1 7")
net_case("world_scan", "2 110") net_case("ecs/world_scan", "2 110")
net_case("world_spawn", "1 42 1") net_case("ecs/world_spawn", "1 42 1")
net_case("world_mixed", "99") net_case("ecs/world_mixed", "99")
net_case("world_query", "2 110") net_case("ecs/world_query", "2 110")
feat_case("recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)") feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("scoped", "2") net_case("events/scoped", "2")
net_case("world_dyn", "0 1 30 100 1 30") 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("events/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("events/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("events/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/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) ==") print("== self-hosted front-end binaries (ludicc / ludic) ==")
# the two commands are one multi-call native binary built from the seed with # 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 # ludicc -o drives clang to a native binary; a game links windowed by default
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll). # (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") ok("ludicc app.ludic -o bin -> native executable")
} else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) } } else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) }
# --emit-llvm stops at the IR. # --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") ok("ludicc --emit-llvm -> LLVM IR")
} else { bad2("ludicc --emit-llvm", capture_line("tail -1 /tmp/x_cli.out")) } } 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") } } else { bad("idempotent on the source tree") }
# --- meaning preserved: reformatting a formatted file must be a no-op ------- # --- 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))") ok("formatting is idempotent (fmt(fmt(x)) == fmt(x))")
} else { bad("formatting is not idempotent") } } else { bad("formatting is not idempotent") }