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:
parent
0bc5d76952
commit
fb728bbefe
73 changed files with 364 additions and 194 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,11 +1,9 @@
|
|||
# Generated build tree: LLVM IR, objects, compiled apps and the docs site all
|
||||
# land under build/ (see `bin/x build` / `bin/x clean`). Root-anchored so a
|
||||
# source dir named "build" elsewhere is never accidentally ignored.
|
||||
# Generated build tree: LLVM IR, objects, compiled apps, the headless render
|
||||
# (build/out.ppm) and the docs site all land under build/ (see `bin/x build` /
|
||||
# `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/
|
||||
|
||||
# Headless render dropped in the working directory by a --headless run.
|
||||
/out.ppm
|
||||
|
||||
# Archives are never tracked: the Kenney art is committed *extracted* under
|
||||
# assets/kenney/**/ (PNG/tmx/License), while the original download .zips and any
|
||||
# packaged plugin .zip are local-only build inputs/outputs.
|
||||
|
|
|
|||
18
COMPILING.md
18
COMPILING.md
|
|
@ -20,8 +20,8 @@
|
|||
> 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/
|
||||
> # (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
||||
> bin/ludicc examples/snake.ludic -o bin/snake # compile
|
||||
> bin/ludic examples/snake.ludic # compile + run
|
||||
> bin/ludicc examples/games/snake.ludic -o bin/snake # compile
|
||||
> bin/ludic examples/games/snake.ludic # compile + run
|
||||
> bin/x help # list every command
|
||||
> ```
|
||||
>
|
||||
|
|
@ -76,10 +76,10 @@ self-hosted `ludicc`.
|
|||
`bin/x app` wraps the common cases:
|
||||
|
||||
```bash
|
||||
bin/x app examples/snake.ludic # -> build/snake (native)
|
||||
bin/x app examples/lib/combat.ludic --lib # -> build/libcombat.* (library)
|
||||
bin/x app examples/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 # -> build/snake (native)
|
||||
bin/x app examples/library/combat.ludic --lib # -> build/libcombat.* (library)
|
||||
bin/x app examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
||||
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
|
||||
|
|
@ -109,7 +109,7 @@ program Combat {
|
|||
```
|
||||
|
||||
```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
|
||||
# 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
|
||||
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
|
||||
|
|
@ -219,7 +219,7 @@ only the triple changes.
|
|||
```
|
||||
|
||||
```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/
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@
|
|||
> struct, a fixed-capacity listener array), proven by a C mod in
|
||||
> [`tests/mod_c/mod.c`](tests/mod_c/mod.c) binding
|
||||
> [`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:
|
||||
> **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`,
|
||||
> [`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.,
|
||||
> [`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`,
|
||||
> [`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`,
|
||||
> [`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).
|
||||
> - **EV2 / EV2b** — the world table: the reflection ABI, generated from the
|
||||
> 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.
|
||||
> The world table is complete: read, write, scan, identify, create, iterate.
|
||||
> - **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
|
||||
> unregister; dispatch skips tombstoned slots), `ludic_on_entity_<E>(entity, cb)`
|
||||
> (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
|
||||
> 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,
|
||||
> 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:
|
||||
> `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
|
||||
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`
|
||||
(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
|
||||
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
|
||||
|
|
|
|||
48
LANGUAGE.md
48
LANGUAGE.md
|
|
@ -133,7 +133,7 @@ handler Nav phase Update {
|
|||
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
|
||||
|
||||
|
|
@ -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
|
||||
entity). Keep whole-handler guards in the body of a handler with no `@Queries`,
|
||||
wrapping an inline query — as `CleanBattle` does in
|
||||
`examples/chronorift/combat.ludic`.
|
||||
`examples/games/chronorift/combat.ludic`.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
See [`examples/toggle.ludic`](examples/toggle.ludic) for the three enable/disable
|
||||
scopes, [`examples/detach.ludic`](examples/detach.ludic) for the structural
|
||||
attach/detach pair, and [`examples/reason.ludic`](examples/reason.ludic) for
|
||||
See [`examples/lang/toggle.ludic`](examples/lang/toggle.ludic) for the three enable/disable
|
||||
scopes, [`examples/lang/detach.ludic`](examples/lang/detach.ludic) for the structural
|
||||
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,
|
||||
query-membership edges, keyed effects) is in
|
||||
[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
|
||||
documentation; every declared handler still runs (registration is implicit).
|
||||
|
||||
See [`examples/annotations.ludic`](examples/annotations.ludic) (queries, computed
|
||||
fields, one hook) and [`examples/lifecycle.ludic`](examples/lifecycle.ludic) (the
|
||||
whole timeline), plus [`examples/toggle.ludic`](examples/toggle.ludic)
|
||||
See [`examples/lang/annotations.ludic`](examples/lang/annotations.ludic) (queries, computed
|
||||
fields, one hook) and [`examples/lang/lifecycle.ludic`](examples/lang/lifecycle.ludic) (the
|
||||
whole timeline), plus [`examples/lang/toggle.ludic`](examples/lang/toggle.ludic)
|
||||
(enable/disable). Scenes and their `on enter` / `on exit` lifecycle blocks are
|
||||
implemented — see "Scenes & layers" below. (An annotation spelling,
|
||||
`@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
|
||||
`emit`, at every scope:
|
||||
- **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)` →
|
||||
`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)` →
|
||||
`prop_<P>_attach` / `_detach` / `_enable` / `_disable`. See [`examples/prop_events.ludic`](examples/prop_events.ludic).
|
||||
- **scenes** — a `public` scene → `scene_<S>_enter` / `scene_<S>_exit`. See [`examples/scene_events.ludic`](examples/scene_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/events/scene_events.ludic`](examples/events/scene_events.ludic).
|
||||
- **layers** — a `public` layer, with `enable layer L` / `disable layer L`
|
||||
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` event may `cancel` it (a foreign listener sets the payload's
|
||||
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
|
||||
`preventDefault` shape. See [`examples/cancel.ludic`](examples/cancel.ludic).
|
||||
`preventDefault` shape. See [`examples/events/cancel.ludic`](examples/events/cancel.ludic).
|
||||
|
||||
```ludic
|
||||
# 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
|
||||
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
|
||||
another `.ludic` file (see `examples/lib/`).
|
||||
another `.ludic` file (see `examples/library/`).
|
||||
|
||||
## Statements
|
||||
|
||||
|
|
@ -636,7 +636,7 @@ match tile {
|
|||
`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
|
||||
state (no more `if phase == N` chains). See the co-op battle in
|
||||
`examples/chronorift/combat.ludic`:
|
||||
`examples/games/chronorift/combat.ludic`:
|
||||
|
||||
```ludic
|
||||
# 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
|
||||
— `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
|
||||
`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`.
|
||||
|
||||
## Expressions
|
||||
|
|
@ -792,15 +792,15 @@ Emacs, Sublime and Zed, are in `tools/editors/` — see
|
|||
|
||||
## 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`,
|
||||
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).
|
||||
- `examples/snake.ludic` — Snake, no assets — same compiler, proving generality.
|
||||
- `examples/games/snake.ludic` — Snake, no assets — same compiler, proving generality.
|
||||
|
||||
```bash
|
||||
bin/x app examples/snake.ludic && ./build/snake
|
||||
bin/x app examples/games/snake.ludic && ./build/snake
|
||||
```
|
||||
|
||||
## Not yet implemented
|
||||
|
|
@ -826,7 +826,7 @@ self-hosting; their lowerings are in
|
|||
## Scenes & layers
|
||||
|
||||
> **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
|
||||
> implicit active-scene register, states numbered by declaration order, and
|
||||
> `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
|
||||
`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.
|
||||
|
||||
## Queries in a handler signature
|
||||
|
|
@ -895,7 +895,7 @@ handler CleanBattle phase LateUpdate { despawn self() }
|
|||
This is exactly equivalent to wrapping the body in
|
||||
`for (Battle, Pos) in query [Battle, Pos, {Foe}] where Battle.hp <= 0 { … }` —
|
||||
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
|
||||
is the same loop: entities are visited by ascending id, `despawn` of the current
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
> **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach
|
||||
> pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are
|
||||
> implemented and tested ([`examples/detach.ludic`](examples/detach.ludic),
|
||||
> [`examples/reason.ludic`](examples/reason.ludic), `bin/x test` checks). The
|
||||
> implemented and tested ([`examples/lang/detach.ludic`](examples/lang/detach.ludic),
|
||||
> [`examples/lang/reason.ludic`](examples/lang/reason.ludic), `bin/x test` checks). The
|
||||
> extensions LC2–LC6 are research-informed proposals, not built. This document
|
||||
> distills a survey of lifecycle models across seven systems (§3) into a roadmap
|
||||
> 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
|
||||
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.
|
||||
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.
|
||||
Backward-compatible: the `reason:` binding is optional, and `@OnDespawn` without
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ them:
|
|||
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)) →
|
||||
`declare` emission ([emit_head.ludic:105](selfhost/emit_head.ludic:105)). Working
|
||||
examples: [examples/net_echo.ludic:12](examples/net_echo.ludic:12),
|
||||
[examples/lib/arena.ludic:14](examples/lib/arena.ludic:14). This is the single
|
||||
examples: [examples/networking/net_echo.ludic:12](examples/networking/net_echo.ludic:12),
|
||||
[examples/library/arena.ludic:14](examples/library/arena.ludic:14). This is the single
|
||||
most important fact in this document — see §7.
|
||||
|
||||
**What's missing (all of it must be built):**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
> 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
|
||||
> 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
|
||||
> Rust and Swift stand on.
|
||||
>
|
||||
|
|
@ -13,13 +13,13 @@
|
|||
> 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
|
||||
> 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
|
||||
> polling them back (`4 10 20 30 42`). **N1 (snapshot-to-buffer):**
|
||||
> `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` —
|
||||
> 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,
|
||||
> restore → `50 7 50`). Both are byte-identical when unused, so the offline dividend
|
||||
> (§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
|
||||
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).
|
||||
([`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`.)
|
||||
- **N1 — snapshot-to-buffer. ✅ SHIPPED.** Generalized `save()`/`load()` to a memory
|
||||
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
|
||||
(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.
|
||||
([`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`.)
|
||||
- **N2 — `@Sync` codegen. ✅ SHIPPED.** The three-level annotations → generated
|
||||
per-model `serialize_<M>`/`apply_<M>` + by-kind dispatchers (`ludic_serialize`/
|
||||
`apply`/`sync_size`, and the `serialize`/`apply`/`sync_size` builtins); the
|
||||
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).)
|
||||
- **N3 — ownership. ✅ SHIPPED.** `@Owned` + the `@L_owner_arr` array +
|
||||
`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 →
|
||||
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
|
||||
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
|
||||
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
|
||||
([`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
|
||||
the way the C mod proved the event ABI, but in pure Ludic over the built-in
|
||||
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**.
|
||||
|
||||
N0–N2 deliver "state can be declared, serialized, and moved." N3–N4 add ownership
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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/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 |
|
||||
| `examples/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/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) |
|
||||
| `examples/games/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) |
|
||||
| `examples/games/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) |
|
||||
| `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 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 |
|
||||
|
|
@ -74,14 +74,14 @@ to machine code via LLVM IR; see **[COMPILING.md](COMPILING.md)** for the
|
|||
pipeline and the runtime protocol.
|
||||
|
||||
```bash
|
||||
bin/x app examples/chronorift.ludic
|
||||
bin/x app examples/games/chronorift.ludic
|
||||
./build/chronorift # opens a native window
|
||||
```
|
||||
|
||||
Headless render (for testing / CI):
|
||||
|
||||
```bash
|
||||
bin/x app examples/chronorift.ludic --headless
|
||||
bin/x app examples/games/chronorift.ludic --headless
|
||||
printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm
|
||||
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
|
||||
|
||||
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
|
||||
PNG at runtime by the Ludic-written PNG/DEFLATE decoder — no zlib, no external
|
||||
dependency on any target.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` /
|
||||
> `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
|
||||
> layers, the overlay stack, scene-local state, transition parameters) are still
|
||||
> 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 |
|
||||
| `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.
|
||||
- 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
|
||||
exit`/`become` lowered to the implicit `machine`; the active scene is
|
||||
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.
|
||||
- **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists).
|
||||
- **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
|
||||
for toggled layers, so untouched scene programs stay byte-identical), and a
|
||||
`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
|
||||
hide` blocks.
|
||||
- **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
|
||||
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.*
|
||||
|
|
|
|||
|
|
@ -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`)*
|
||||
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)*
|
||||
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`).
|
||||
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
|
||||
highlighters, **never parsed**. *(✅ implemented in Phase 1 as an if-without-else alias)*
|
||||
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.)
|
||||
- ✅ **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
|
||||
`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).
|
||||
- ✅ **`when c { }` implemented** (#9) — as an `if`-without-else alias in
|
||||
[parse.ludic](selfhost/parse.ludic). Docs + editors already listed it; now the
|
||||
compiler agrees, so no editor-vocab churn was needed.
|
||||
- ✅ **`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).
|
||||
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
|
||||
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
|
||||
`@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
|
||||
`@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
|
||||
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
|
||||
|
|
@ -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
|
||||
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
|
||||
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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"intro": "Build the toolchain once, then your game. Everything runs from the repo root.",
|
||||
"steps": [
|
||||
{ "title": "Build the toolchain", "html": "<code>bin/x build</code> produces <code>ludicc</code>, the compiler you'll use for everything below." },
|
||||
{ "title": "Compile & 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 & 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": "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" },
|
||||
{ "blank": true },
|
||||
{ "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" },
|
||||
{ "blank": true },
|
||||
{ "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" },
|
||||
{ "out": "→ writes out.ppm" }
|
||||
]
|
||||
|
|
|
|||
101
examples/README.md
Normal file
101
examples/README.md
Normal 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. |
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# 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 Actor { kind: int = 0 }
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# ============================================================================
|
||||
# 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
|
||||
# entity per snake segment), named program state for the rest, and a Render
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
# 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20
|
||||
# 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
|
||||
program Detach {
|
||||
property Tag { v: int = 0 }
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# the deterministic Clock. Everything is integer seconds, so the result replays
|
||||
# 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 {
|
||||
entry {
|
||||
# A save records when the player last quit. It is hardcoded here so the demo
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
# 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500
|
||||
# 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000
|
||||
#
|
||||
# bin/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
|
||||
program Reasons {
|
||||
property Health { hp: int = 0 }
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
# 201 900 frame 3: Play.World.Step, then Hud.Draw
|
||||
# 202 900 frame 4: Step reaches 2 -> quit(); Hud.Draw paints the last frame
|
||||
#
|
||||
# bin/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
|
||||
program SceneDemo {
|
||||
var counter: int = 0
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# ============================================================================
|
||||
# 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/lib/arena.ludic -o build/arena -Lbuild -lcombat
|
||||
# ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
|
||||
# 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
|
||||
# symbols come from happens to be written in Ludic, but nothing here depends on
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
# `module` instead of `game` means: no entry point, no frame loop. ludicc
|
||||
# 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
|
||||
# .dylib/.so/.dll can call these: another Ludic program via `extern fn`, a game
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
# 5. rt_receive() — the client reconciles to the authoritative x=5
|
||||
#
|
||||
# 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"
|
||||
|
||||
program NetDemo {
|
||||
|
|
@ -55,7 +55,7 @@ function rt_shutdown() -> void {
|
|||
win_close()
|
||||
return
|
||||
}
|
||||
rt_dump_ppm("out.ppm")
|
||||
rt_dump_ppm("build/out.ppm")
|
||||
}
|
||||
|
||||
# ---- 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
4
selfhost/golden/renders.sha256
Normal file
4
selfhost/golden/renders.sha256
Normal 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
|
|
@ -119,7 +119,7 @@ async function main() {
|
|||
expect('scene in enter', ' enter Battle', 'Battle', 'entity.name.type.scene');
|
||||
|
||||
// A whole file must tokenise without the engine bailing out.
|
||||
const sample = fs.readFileSync(path.join(ROOT, 'examples', 'menu.ludic'), 'utf8');
|
||||
const sample = fs.readFileSync(path.join(ROOT, 'examples', 'games', 'menu.ludic'), 'utf8');
|
||||
let state = vsctm.INITIAL;
|
||||
let tokenCount = 0;
|
||||
for (const line of sample.split('\n')) {
|
||||
|
|
@ -127,8 +127,8 @@ async function main() {
|
|||
state = r.ruleStack;
|
||||
tokenCount += r.tokens.length;
|
||||
}
|
||||
if (tokenCount > 200) ok(`tokenises examples/menu.ludic (${tokenCount} tokens)`);
|
||||
else bad('tokenises examples/menu.ludic', `only ${tokenCount} tokens`);
|
||||
if (tokenCount > 200) ok(`tokenises examples/games/menu.ludic (${tokenCount} tokens)`);
|
||||
else bad('tokenises examples/games/menu.ludic', `only ${tokenCount} tokens`);
|
||||
|
||||
// ---- the Markdown injection ---------------------------------------------
|
||||
const mdGrammar = await registry.loadGrammar('markdown.ludic.codeblock');
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ def main():
|
|||
check_true(f"advertises {cap}", cap in caps)
|
||||
|
||||
# ---- a fragment file: no `program` block, only reachable through an import --
|
||||
frag = os.path.join(ROOT, "examples/chronorift/combat.ludic")
|
||||
frag = os.path.join(ROOT, "examples/games/chronorift/combat.ludic")
|
||||
text = c.open(frag)
|
||||
uri = {"uri": "file://" + frag}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ function cmd_build() -> int {
|
|||
|
||||
# ---- app: compile a Ludic program to a native app ----------------------------
|
||||
# usage: x app <file.ludic> [--headless|--windowed] [--save-temps]
|
||||
# windowed is the default for a game; headless renders the last frame to out.ppm.
|
||||
# windowed is the default for a game; headless renders the last frame to
|
||||
# build/out.ppm (never the repo root).
|
||||
function cmd_app() -> int {
|
||||
ensure_ludicc()
|
||||
var src = null
|
||||
|
|
@ -64,11 +65,11 @@ function cmd_app() -> int {
|
|||
if (mode == 2) {
|
||||
let outbin = `build/{name}_headless`
|
||||
let ll = `{outbin}.ll`
|
||||
print(`ludicc: {src} -> {outbin} (renders the last frame to out.ppm)`)
|
||||
print(`ludicc: {src} -> {outbin} (renders the last frame to build/out.ppm)`)
|
||||
if not shq(`bin/ludicc --headless {src} > {ll}`) { return 1 }
|
||||
if not shq(`{cc()} -O2 {ll} -o {outbin}`) { return 1 }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
print(`done. run: printf 'ddss' | ./{outbin} && open out.ppm`)
|
||||
print(`done. run: mkdir -p build && printf 'ddss' | ./{outbin} && open build/out.ppm`)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +84,14 @@ function cmd_app() -> int {
|
|||
}
|
||||
|
||||
# Remove every generated artifact and leave the tracked source untouched: the
|
||||
# whole build/ tree (IR, objects, compiled apps), the headless render dropped in
|
||||
# the root (out.ppm), and any stray *.tmp/*.ll left behind by a failed build.
|
||||
# The toolchain binaries in bin/ are kept so the running `x` survives; rebuild
|
||||
# them any time with `x build`.
|
||||
# whole build/ tree (IR, objects, compiled apps, the headless render at
|
||||
# build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A
|
||||
# legacy out.ppm in the root (from an older toolchain) is swept too. The
|
||||
# toolchain binaries in bin/ are kept so the running `x` survives; rebuild them
|
||||
# any time with `x build`.
|
||||
function cmd_clean() -> int {
|
||||
run("rm -rf build")
|
||||
run("rm -f out.ppm bin/*.tmp")
|
||||
print("cleaned: build/, out.ppm, bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)")
|
||||
print("cleaned: build/ (incl. build/out.ppm), bin/*.tmp (bin/ toolchain kept — `x build` to rebuild)")
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,13 @@ program X {
|
|||
print(" x app <file.ludic> [--headless] [--save-temps]")
|
||||
print(" compile a Ludic program to a native app in build/")
|
||||
print(" x tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
|
||||
print(" x clean remove build/, out.ppm and stray artifacts (keeps bin/)")
|
||||
print(" x clean remove build/ (incl. build/out.ppm) and stray artifacts (keeps bin/)")
|
||||
print("")
|
||||
print("test:")
|
||||
print(" x test the full regression suite")
|
||||
print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
|
||||
print(" x test-tools the editor-toolchain suite")
|
||||
print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
|
||||
print("")
|
||||
print("self-host internals:")
|
||||
print(" x selfhost-build [ludicc] [out] assemble + compile the self-host compiler")
|
||||
|
|
@ -61,6 +62,7 @@ program X {
|
|||
if (cmd == "test") { exit(cmd_test()) }
|
||||
if (cmd == "selfhost-test") { exit(cmd_selfhost_test()) }
|
||||
if (cmd == "test-tools") { exit(cmd_test_tools()) }
|
||||
if (cmd == "golden") { exit(cmd_golden()) }
|
||||
if (cmd == "bootstrap") { exit(cmd_bootstrap()) }
|
||||
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) }
|
||||
if (cmd == "reseed") { exit(cmd_reseed()) }
|
||||
|
|
|
|||
|
|
@ -75,6 +75,21 @@ function getenv_or(name: pointer, dflt: pointer) -> pointer {
|
|||
return v
|
||||
}
|
||||
|
||||
# flatten a relative example path into a filesystem-safe token: '/' -> '_', so a
|
||||
# categorised path like "games/snake" yields a single-segment temp name
|
||||
# ("games_snake") that never implies a missing /tmp subdirectory.
|
||||
function flat(p: pointer) -> pointer {
|
||||
let n = len(p)
|
||||
let b = bytes(n + 1)
|
||||
for i in 0 .. n {
|
||||
var c = p[i]
|
||||
if (c == 47) { c = 95 } # '/' (47) -> '_' (95)
|
||||
b[i] = c
|
||||
}
|
||||
b[n] = 0
|
||||
return b
|
||||
}
|
||||
|
||||
# ---- stdout helpers ---------------------------------------------------------
|
||||
|
||||
# write `s` with no trailing newline (print() always adds one)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,55 @@ function sh_case(name: pointer, exp: pointer) -> void {
|
|||
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) }
|
||||
}
|
||||
|
||||
# compile examples/<name>.ludic as a game and diff its render against the golden
|
||||
function game_case(name: pointer, keys: pointer) -> void {
|
||||
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_g_{name}`) { bad(`{name}: build`); return }
|
||||
run(`printf '%s' '{keys}' | /tmp/x_g_{name} >/dev/null 2>&1`)
|
||||
if shq(`cmp -s selfhost/golden/{name}.ppm out.ppm`) { ok(`{name} matches the golden render`) }
|
||||
else { bad(`{name}: differs from golden`) }
|
||||
# compile examples/<path>.ludic as a game, render it headless, and compare the
|
||||
# framebuffer's SHA-256 against the text baseline in selfhost/golden/renders.sha256.
|
||||
# A hash is tiny and text-diffable — a regression shows as a changed hex line in a
|
||||
# review, not an opaque "binary files differ". Regenerate the baseline with
|
||||
# `bin/x golden` (see cmd_golden below).
|
||||
function game_case(path: pointer, keys: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_g_{nm}`) { bad(`{path}: build`); return }
|
||||
run("mkdir -p build")
|
||||
run(`printf '%s' '{keys}' | /tmp/x_g_{nm} >/dev/null 2>&1`)
|
||||
let got = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
|
||||
let want = capture_line(`grep '^{path} ' selfhost/golden/renders.sha256 | cut -d' ' -f2`)
|
||||
if (want == "") { bad2(path, "no golden hash (run: bin/x golden)"); return }
|
||||
if (got == want) { ok(`{path} matches the golden render`) }
|
||||
else { bad2(path, `render hash {got} != golden {want}`) }
|
||||
}
|
||||
|
||||
# render one golden game and return its "<path> <sha256>\n" manifest line (empty
|
||||
# string on build failure). Shared by `bin/x golden` and nothing else.
|
||||
function bless_render(path: pointer, keys: pointer) -> pointer {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_bg_{nm}`) {
|
||||
err(`golden: {path} failed to build\n`); return ""
|
||||
}
|
||||
run("mkdir -p build")
|
||||
run(`printf '%s' '{keys}' | /tmp/x_bg_{nm} >/dev/null 2>&1`)
|
||||
let h = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
|
||||
return `{path} {h}\n`
|
||||
}
|
||||
|
||||
# regenerate the text render baseline. The keys here MUST match game_case's so
|
||||
# the baseline and the checks render the same frame. Deliberate, reviewable:
|
||||
# run it, then `git diff selfhost/golden/renders.sha256` shows exactly which
|
||||
# render changed before you commit the new hash.
|
||||
function cmd_golden() -> int {
|
||||
ensure_ludicc()
|
||||
if not is_exec("bin/ludicc") { err("golden: bin/ludicc unavailable\n"); return 1 }
|
||||
var manifest = ""
|
||||
manifest = manifest + bless_render("games/snake", "ddssaawwddss")
|
||||
manifest = manifest + bless_render("games/menu", "ssss")
|
||||
manifest = manifest + bless_render("games/chronorift", "ddddwwwwaassK")
|
||||
manifest = manifest + bless_render("rendering/draw_prims", "")
|
||||
if not write_file("selfhost/golden/renders.sha256", manifest) {
|
||||
err("golden: could not write selfhost/golden/renders.sha256\n"); return 1
|
||||
}
|
||||
print("wrote selfhost/golden/renders.sha256:")
|
||||
out(manifest)
|
||||
print("review the change: git diff selfhost/golden/renders.sha256")
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd_selfhost_test() -> int {
|
||||
|
|
@ -57,12 +100,13 @@ function cmd_selfhost_test() -> int {
|
|||
sh_case("color", "16744512 1090486336 1090486336 8355711 8355711 8355711")
|
||||
|
||||
print("== the self-host compiler compiles real games (vs golden output) ==")
|
||||
# the golden PPMs were produced by the original C compiler; the self-hosted
|
||||
# compiler must reproduce them exactly.
|
||||
game_case("snake", "ddssaawwddss")
|
||||
game_case("menu", "ssss")
|
||||
game_case("chronorift", "ddddwwwwaassK")
|
||||
game_case("draw_prims", "")
|
||||
# the golden render hashes were first produced by the original C compiler; the
|
||||
# self-hosted compiler must reproduce the same framebuffer, byte for byte, so
|
||||
# its SHA-256 matches the checked-in baseline in selfhost/golden/renders.sha256.
|
||||
game_case("games/snake", "ddssaawwddss")
|
||||
game_case("games/menu", "ssss")
|
||||
game_case("games/chronorift", "ddddwwwwaassK")
|
||||
game_case("rendering/draw_prims", "")
|
||||
|
||||
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==")
|
||||
if shq("bin/x bootstrap > /tmp/x_boot.out 2>&1") { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
|
||||
|
|
|
|||
|
|
@ -9,38 +9,51 @@
|
|||
|
||||
# compile a feature example as a game, run it (optional keys on stdin) and match
|
||||
# its stdout against the expected space-joined output.
|
||||
function feat_case(name: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
|
||||
if not game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_e_{name}`) {
|
||||
function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_e_{nm}`) {
|
||||
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
|
||||
}
|
||||
var got = ""
|
||||
if (keys == "") { got = capture_line(`/tmp/x_e_{name} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{name}`) }
|
||||
if (keys == "") { got = capture_line(`/tmp/x_e_{nm} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{nm}`) }
|
||||
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
|
||||
}
|
||||
|
||||
# a "does it still compile" smoke test (parse -> lower -> link), no run
|
||||
function qsmoke(name: pointer) -> void {
|
||||
if game_build_ok("bin/ludicc", `examples/{name}.ludic`, `/tmp/x_q_{name}`) {
|
||||
ok(`{name} compiles (@Queries desugars to S_QUERY)`)
|
||||
} else { bad2(name, capture_line("grep -i error /tmp/x_gb.err | head -1")) }
|
||||
function qsmoke(path: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_q_{nm}`) {
|
||||
ok(`{path} compiles (@Queries desugars to S_QUERY)`)
|
||||
} else { bad2(path, capture_line("grep -i error /tmp/x_gb.err | head -1")) }
|
||||
}
|
||||
|
||||
# a plain compile-only rot guard for a showcase example that has no self-asserting
|
||||
# `entry` (so net_case can't drive it): prove it still parses, lowers and links.
|
||||
function smoke(path: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o /tmp/x_sm_{nm}.ll > /tmp/x_sm_{nm}.err 2>&1`) {
|
||||
bad2(path, capture_line(`tail -1 /tmp/x_sm_{nm}.err`)); return
|
||||
}
|
||||
ok(`{path} still compiles`)
|
||||
}
|
||||
|
||||
# a pure-Ludic example that drives and asserts itself: compile headless to IR,
|
||||
# let clang assemble it, run, and compare stdout. No C host and no C compiled.
|
||||
function net_case(name: pointer, exp: pointer) -> void {
|
||||
let ll = `/tmp/x_n_{name}.ll`
|
||||
let log = `/tmp/x_n_{name}.out`
|
||||
if not shq(`bin/ludicc --headless examples/{name}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
|
||||
function net_case(path: pointer, exp: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
let ll = `/tmp/x_n_{nm}.ll`
|
||||
let log = `/tmp/x_n_{nm}.out`
|
||||
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
|
||||
let t = capture_line(`tail -1 {log}`)
|
||||
bad2(name, `build ({t})`); return
|
||||
bad2(path, `build ({t})`); return
|
||||
}
|
||||
if not shq(`{cc()} -O2 {ll} -o /tmp/x_nb_{name} >> {log} 2>&1`) {
|
||||
if not shq(`{cc()} -O2 {ll} -o /tmp/x_nb_{nm} >> {log} 2>&1`) {
|
||||
let t = capture_line(`tail -1 {log}`)
|
||||
bad2(name, `build ({t})`); return
|
||||
bad2(path, `build ({t})`); return
|
||||
}
|
||||
let got = capture_line(`/tmp/x_nb_{name} < /dev/null`)
|
||||
if (got == exp) { ok(`{name} ({got})`) } else { bad2(name, `got [{got}] want [{exp}]`) }
|
||||
let got = capture_line(`/tmp/x_nb_{nm} < /dev/null`)
|
||||
if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) }
|
||||
}
|
||||
|
||||
function cmd_test() -> int {
|
||||
|
|
@ -56,14 +69,14 @@ function cmd_test() -> int {
|
|||
|
||||
print("== the compiler builds from the IR seed (no C compiler) ==")
|
||||
run("rm -f bin/ludicc")
|
||||
if shq("bin/x app examples/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
|
||||
if shq("bin/x app examples/games/snake.ludic --headless >/dev/null 2>&1") and is_exec("bin/ludicc") {
|
||||
ok("clang selfhost/ludicc.seed.ll -> bin/ludicc")
|
||||
} else { bad("compiler did not build from the seed") }
|
||||
|
||||
print("== every example compiles to its golden render ==")
|
||||
game_case("snake", "ddssaawwddss")
|
||||
game_case("menu", "ssss")
|
||||
game_case("chronorift", "ddddwwwwaassK")
|
||||
game_case("games/snake", "ddssaawwddss")
|
||||
game_case("games/menu", "ssss")
|
||||
game_case("games/chronorift", "ddddwwwwaassK")
|
||||
|
||||
print("== self-hosting and the bootstrap fixpoints ==")
|
||||
run("bin/x selfhost-test > /tmp/x_sh.out 2>&1")
|
||||
|
|
@ -73,50 +86,57 @@ function cmd_test() -> int {
|
|||
ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
|
||||
|
||||
print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
|
||||
qsmoke("qdecl")
|
||||
feat_case("annotations", "", "3 25 0 0", "annotations.ludic (@Queries / @Computed / @OnSpawn / @Handles)")
|
||||
feat_case("lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)")
|
||||
feat_case("toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)")
|
||||
feat_case("strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)")
|
||||
feat_case("rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)")
|
||||
feat_case("time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)")
|
||||
feat_case("scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)")
|
||||
feat_case("detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
|
||||
feat_case("reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
|
||||
qsmoke("lang/qdecl")
|
||||
feat_case("lang/annotations", "", "3 25 0 0", "annotations.ludic (@Queries / @Computed / @OnSpawn / @Handles)")
|
||||
feat_case("lang/lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)")
|
||||
feat_case("lang/toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)")
|
||||
feat_case("lang/strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)")
|
||||
feat_case("lang/rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)")
|
||||
feat_case("lang/time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)")
|
||||
feat_case("lang/scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)")
|
||||
feat_case("lang/detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
|
||||
feat_case("lang/reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
|
||||
|
||||
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
|
||||
net_case("offline_rewards", "13 650 2026-08-30 0")
|
||||
net_case("lang/offline_rewards", "13 650 2026-08-30 0")
|
||||
|
||||
# EV0b + NETWORKING N0–N6: each a self-contained pure-Ludic program, driven and
|
||||
# asserted from its own `entry`. The transport is the compiler's built-in
|
||||
# loopback, so a networked game runs with zero foreign code.
|
||||
net_case("mod_events", "10 32 42")
|
||||
net_case("net_echo", "4 10 20 30 42")
|
||||
net_case("net_snapshot", "50 7 50")
|
||||
net_case("net_sync", "12 3 4 50 999")
|
||||
net_case("net_owner", "-1 7 0 1")
|
||||
net_case("net_rpc", "0 8")
|
||||
net_case("net_roles", "1 102")
|
||||
net_case("net_demo", "5 999 5")
|
||||
net_case("events/mod_events", "10 32 42")
|
||||
net_case("networking/net_echo", "4 10 20 30 42")
|
||||
net_case("networking/net_snapshot", "50 7 50")
|
||||
net_case("networking/net_sync", "12 3 4 50 999")
|
||||
net_case("networking/net_owner", "-1 7 0 1")
|
||||
net_case("networking/net_rpc", "0 8")
|
||||
net_case("networking/net_roles", "1 102")
|
||||
net_case("networking/net_demo", "5 999 5")
|
||||
|
||||
feat_case("promote", "", "100 101 200 201", "promote.ludic (@Public -> model_<M>_spawn/despawn events)")
|
||||
feat_case("cancel", "", "0 1 92", "cancel.ludic (cancellable event + cancel + emit-as-expression)")
|
||||
feat_case("events/promote", "", "100 101 200 201", "promote.ludic (@Public -> model_<M>_spawn/despawn events)")
|
||||
feat_case("events/cancel", "", "0 1 92", "cancel.ludic (cancellable event + cancel + emit-as-expression)")
|
||||
|
||||
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||
net_case("world_get", "50 1 7")
|
||||
net_case("world_scan", "2 110")
|
||||
net_case("world_spawn", "1 42 1")
|
||||
net_case("world_mixed", "99")
|
||||
net_case("world_query", "2 110")
|
||||
net_case("ecs/world_get", "50 1 7")
|
||||
net_case("ecs/world_scan", "2 110")
|
||||
net_case("ecs/world_spawn", "1 42 1")
|
||||
net_case("ecs/world_mixed", "99")
|
||||
net_case("ecs/world_query", "2 110")
|
||||
|
||||
feat_case("recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||
net_case("scoped", "2")
|
||||
net_case("world_dyn", "0 1 30 100 1 30")
|
||||
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||
net_case("events/scoped", "2")
|
||||
net_case("ecs/world_dyn", "0 1 30 100 1 30")
|
||||
|
||||
feat_case("prop_events", "", "300 400", "prop_events.ludic (@Public -> prop_<P>_attach/detach events)")
|
||||
feat_case("scene_events", "", "10 1 20 2 30 3", "scene_events.ludic (public scene -> scene_<S>_enter/exit events)")
|
||||
feat_case("program_events", "", "1 100 2 200", "program_events.ludic (@Public @OnStart/@OnQuit -> program_start/quit)")
|
||||
feat_case("layer_events", "aaaa", "50 2 1 50", "layer_events.ludic (layer toggle + public layer -> layer_<L>_show/hide)")
|
||||
feat_case("events/prop_events", "", "300 400", "prop_events.ludic (@Public -> prop_<P>_attach/detach events)")
|
||||
feat_case("events/scene_events", "", "10 1 20 2 30 3", "scene_events.ludic (public scene -> scene_<S>_enter/exit events)")
|
||||
feat_case("events/program_events", "", "1 100 2 200", "program_events.ludic (@Public @OnStart/@OnQuit -> program_start/quit)")
|
||||
feat_case("events/layer_events", "aaaa", "50 2 1 50", "layer_events.ludic (layer toggle + public layer -> layer_<L>_show/hide)")
|
||||
|
||||
# showcase examples with no self-asserting `entry` — compile-only so they can't
|
||||
# silently rot after a compiler or reorg change.
|
||||
print("== showcase examples still compile ==")
|
||||
smoke("ecs/hello")
|
||||
smoke("events/events")
|
||||
smoke("networking/net_rt")
|
||||
|
||||
print("== self-hosted front-end binaries (ludicc / ludic) ==")
|
||||
# the two commands are one multi-call native binary built from the seed with
|
||||
|
|
@ -127,12 +147,12 @@ function cmd_test() -> int {
|
|||
|
||||
# ludicc -o drives clang to a native binary; a game links windowed by default
|
||||
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll).
|
||||
if shq("LUDIC_HOME=. bin/ludicc examples/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("file /tmp/x_cli_snake 2>/dev/null | grep -q Mach-O") {
|
||||
if shq("LUDIC_HOME=. bin/ludicc examples/games/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("file /tmp/x_cli_snake 2>/dev/null | grep -q Mach-O") {
|
||||
ok("ludicc app.ludic -o bin -> native executable")
|
||||
} else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) }
|
||||
|
||||
# --emit-llvm stops at the IR.
|
||||
if shq("bin/ludicc examples/snake.ludic --emit-llvm -o /tmp/x_cli_snake.ll > /tmp/x_cli.out 2>&1") and shq("head -1 /tmp/x_cli_snake.ll | grep -q 'LLVM IR'") {
|
||||
if shq("bin/ludicc examples/games/snake.ludic --emit-llvm -o /tmp/x_cli_snake.ll > /tmp/x_cli.out 2>&1") and shq("head -1 /tmp/x_cli_snake.ll | grep -q 'LLVM IR'") {
|
||||
ok("ludicc --emit-llvm -> LLVM IR")
|
||||
} else { bad2("ludicc --emit-llvm", capture_line("tail -1 /tmp/x_cli.out")) }
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function cmd_test_tools() -> int {
|
|||
} else { bad("idempotent on the source tree") }
|
||||
|
||||
# --- meaning preserved: reformatting a formatted file must be a no-op -------
|
||||
if shq(`fail=0; for f in examples/hello.ludic examples/snake.ludic examples/menu.ludic examples/qdecl.ludic examples/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > /tmp/x_f1 2>/dev/null; {fmt} /tmp/x_f1 > /tmp/x_f2 2>/dev/null; cmp -s /tmp/x_f1 /tmp/x_f2 || fail=1; done; test $fail -eq 0`) {
|
||||
if shq(`fail=0; for f in examples/ecs/hello.ludic examples/games/snake.ludic examples/games/menu.ludic examples/lang/qdecl.ludic examples/lang/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > /tmp/x_f1 2>/dev/null; {fmt} /tmp/x_f1 > /tmp/x_f2 2>/dev/null; cmp -s /tmp/x_f1 /tmp/x_f2 || fail=1; done; test $fail -eq 0`) {
|
||||
ok("formatting is idempotent (fmt(fmt(x)) == fmt(x))")
|
||||
} else { bad("formatting is not idempotent") }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue