Networking N2–N6, and a fully C-free toolchain

Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.

Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
  compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
  entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
  extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().

C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
  programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
  parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.

Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 15:08:23 +03:00
parent 96d01e45ab
commit bca8f126fc
67 changed files with 24066 additions and 9309 deletions

354
SCENES-DESIGN.md Normal file
View file

@ -0,0 +1,354 @@
# Scenes, expanded — a design doc
> **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),
> a `test.sh` 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
> can decide the shape before building each one. §11 lists the open decisions.
---
## 1. Where we are
A Ludic program is almost always several mutually-exclusive states — a title
screen, the overworld, a battle, a pause menu. Two ways to write that exist in
the language today, and a third is sketched:
| Approach | Status | Cost |
|---|---|---|
| Mode register consulted at the top of every handler (`if reg(R_MODE) == …`) | works | a guard re-read per handler per frame; state is a magic number; nothing scopes to it |
| `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:
- Exactly **one scene active**; the `start` scene runs first.
- A scene's handlers run only while it is active; handlers outside any scene are
global.
- **Layers group handlers; declaration order is draw order** — within a phase,
globals first, then the active scene's layers in written order.
- `on enter` / `on exit` are lifecycle hooks (not phases).
- `become Name` runs the old scene's `on exit`, switches, runs the new `on enter`
— two direct calls and a store, no dispatch table.
That's a good spine. The problem is it's specced as **sugar over a mode
register**: it tidies the syntax but adds little the register didn't already
have. The compiler knows *much* more at a scene boundary than a register does,
and this doc is about spending that knowledge.
---
## 2. Design principles
1. **The scene boundary is a compile-time fact — use it.** The set of handlers,
layers, and owned state for each scene is known statically. Transitions should
be direct calls and a single store, never a table walk. (The sketch already
promises this; the extensions must preserve it.)
2. **Structure, not registers.** Anything you'd track with a hand-managed
register alongside the mode — which entities belong to this state, which layers
are drawn, what's paused — should be expressible *as* scene structure and
enforced by the compiler.
3. **Reuse the machinery we already have.** Layers pausing, scenes tearing down
their entities, and hooks firing are all expressible in terms of
`enable`/`disable` (cheap flag flips), `despawn`, and the lifecycle-hook
lowering. Scenes should *compose* those, not introduce a parallel runtime.
4. **One active-scene path stays hot; overlays are the exception, not the rule.**
The common case (one full-screen scene at a time) must lower to the cheapest
possible dispatch. Richer shapes (a pause menu over a frozen world) are opt-in
and pay only for what they use.
---
## 3. Core model (firmed up from the sketch)
```ludic
# doc-check: skip — illustrative
scene Title start {
on enter { ui_open(UI_Menu) }
on exit { ui_visible(UI_Menu, 0) }
layer Main {
handler Choose phase Update {
if ui_clicked(UI_NewGame) { become Overworld }
}
}
}
scene Overworld {
on enter { spawn_party() }
layer World { handler Move phase Update { … } }
layer Hud { handler Draw phase Render { … } }
}
```
Unchanged from the sketch, made precise:
- **Scenes number themselves by declaration order**, exactly like `machine`
states — `Title` is `0`, `Overworld` is `1`. The active scene lives in one
implicit register (`__scene`). This makes `scene` a `machine` the compiler
writes for you, which is the right mental model and the right lowering.
- **A layer handler may not use phase `Start`.** `Start` runs once at boot,
before any scene is entered; scene setup goes in `on enter`.
- **Global handlers still run every frame**, before any scene's layers, in every
phase. A scene's layers run only while it is active.
Everything below is new.
---
## 4. Extension E1 — scene-owned entities (scoped lifetime)
The single biggest thing a mode register cannot do: **own the entities that only
make sense in this state, and tear them down automatically on exit.** Today a
battle scene spawns combatants in `on enter` and must remember to despawn every
one in `on exit` — miss one and it leaks into the overworld.
Proposal: entities spawned *by a scene's handlers or `on enter`* are tagged with
that scene, and `on exit` despawns them by default.
```ludic
# doc-check: skip
scene Battle {
on enter { spawn Foe; spawn Foe; spawn Foe } # tagged @Battle
# on exit: implicit `despawn all @Battle` — no manual cleanup
layer World { handler Fight phase Update { … } }
}
```
- Implemented as an implicit **scene tag** (a `{Battle}`-style kind bit) added at
`spawn` time while a scene is active, plus a generated `despawn`-by-tag in the
synthesized `on exit`. Reuses the existing tag-filter and despawn-hook
machinery — no new runtime.
- **Opt out** for entities that should outlive the scene: `spawn Foe persist` (or
spawn it from a global handler). Persisted entities keep their data across the
transition, matching how `disable` keeps field data.
- Composes with `@OnDespawn(Model)`: the destructor hook fires for each
scene-owned entity as it's torn down, so `drop_loot`-style cleanup still runs.
**Open:** does a re-`become Battle` get fresh entities (fresh tag generation) or
resume the old ones? Default: fresh. See §9.
---
## 5. Extension E2 — layers are more than draw order
The sketch uses layers only to order `Render`. Layers are the natural unit for
three more things, all built on the existing `enable`/`disable` flag flips:
1. **Per-layer toggle.** `disable Hud` / `enable Hud` flips one flag; the layer's
handlers stop running and drawing. This is `disable Handler` generalized to a
named group — same one-flag-flip cost.
2. **Pause vs. tear-down.** A layer can keep drawing while its *update* handlers
are suspended:
```ludic
# doc-check: skip
scene Overworld {
layer World { handler Move phase Update { … } handler Draw phase Render { … } }
layer Hud { handler DrawHud phase Render { … } }
}
```
When a pause menu opens over the Overworld (see E3), `World`'s `Update`
handlers suspend but its `Render` handler still paints the frozen world behind
the menu. Today that requires a `if !paused` guard in every update handler;
with layers it's structural.
3. **Layer lifecycle hooks.** `on show` / `on hide` per layer, mirroring scene
`on enter`/`on exit`, for the toggle points. (Naming TBD — could fold into the
`@OnEnable`/`@OnDisable` annotations, which already exist for properties.)
---
## 6. Extension E3 — the scene *stack* (the headline)
The sketch says "exactly one scene is active." That's the right default and the
wrong constraint. The states a mode register handles *worst* are the ones that
**overlay without replacing**: a pause menu over live gameplay, a dialog box, an
inventory screen, a confirmation prompt. With one register you either lose the
underlying state or hand-roll a "previous mode" variable and restore it.
Proposal: keep "one *base* scene," but allow scenes to be **pushed as overlays**.
```ludic
# doc-check: skip
scene Overworld {
layer World { handler Move phase Update { … } handler Draw phase Render { … } }
layer Hud { handler DrawHud phase Render { … } }
on enter { … }
handler PauseKey phase Input { if pressed(KEY_ESC) { push Pause } }
}
scene Pause overlay { # `overlay` = pushed, not swapped
on enter { dim_backdrop() }
layer Menu {
handler Nav phase Update {
if pressed(KEY_ESC) { pop } # back to Overworld, untouched
}
handler Draw phase Render { ui_render() }
}
}
```
- `push Name` runs `Name`'s `on enter` and makes it the top scene **without**
running the base scene's `on exit`. `pop` runs the overlay's `on exit` and
returns to whatever was beneath.
- **Update belongs to the top of the stack; render walks the whole stack bottom
to top.** So `Pause`'s `Menu` layer draws over `Overworld`'s frozen `World` and
`Hud`. This is the default that makes pause menus "just work." An overlay that
should let the layer beneath keep updating opts in with `push Name passthrough`.
- **The stack is a small fixed-capacity array of scene ids** (say 8) in a
compiler-owned buffer — not heap, not a linked structure. `push`/`pop` are an
index bump and an `on enter`/`on exit` call. Depth overflow is a compile-time
or trap decision (§9).
- `become` still exists and still means "swap the base scene" (full `on exit` →
`on enter`, stack cleared). `push`/`pop` are the overlay verbs. Keeping the two
distinct is what lets the common single-scene path stay a single register.
This is the extension that turns `scene` from "nicer mode register" into
something with no clean equivalent in the register world.
---
## 7. Extension E4 — scene-local state
A scene almost always has state that exists only while it's active — a battle's
turn counter, a menu's cursor index. Today that's a global register that other
scenes could stomp. Proposal: **`var` / `const` declared inside a `scene` is
scoped to it**, storage shared across scenes that are never simultaneously active
(the compiler can overlap their storage since only one base scene runs at a
time — an arena-per-scene, or a union).
```ludic
# doc-check: skip
scene Battle {
var turn = 0 # visible only inside Battle; reset by `on enter` if desired
layer World { handler Step phase Update { turn += 1 } }
}
```
- Reads/writes lower to a fixed offset in the scene's state block, no register
indirection.
- Overlay scenes (E3) that *can* be live simultaneously with their base cannot
share storage — the compiler keeps their blocks distinct. Base scenes that
never coexist share.
---
## 8. Extension E5 — parameterized transitions, and the reserved annotations
**Parameters on transitions.** `become`/`push` can carry arguments that the
target's `on enter` binds — so a battle knows which foes, a dialog knows which
line:
```ludic
# doc-check: skip
scene Battle {
on enter (foe_kind: int, count: int) { for i in 0 .. count { spawn_foe(foe_kind) } }
}
# elsewhere:
become Battle(FOE_GOBLIN, 3)
```
Lowers to argument stores into the scene's state block (E4) immediately before
the `on enter` call. No variadic runtime; the arity is checked at compile time.
**The already-reserved annotation form.** [LANGUAGE.md:374](LANGUAGE.md:374)
reserves `@OnEnter` / `@OnExit` as handler annotations "waiting on scene support."
This doc adopts them as the annotation spelling of `on enter` / `on exit`,
mirroring how `@OnStart` is the annotation form of `phase Start`:
```ludic
# doc-check: skip
@OnEnter(Battle) handler Setup { … } # == Battle's `on enter`
@OnExit(Battle) handler Teardown { … }
```
Both spellings desugar to the same synthesized scene-lifecycle function; a scene
may use either, not both, for a given hook.
**`reads`/`writes` + scenes (forward-looking).** The `reads`/`writes` clauses are
parsed but unconsumed ([LANGUAGE.md:717](LANGUAGE.md:717)). Once an analysis pass
exists, a scene's layers declare which state they touch, and the scheduler can run
independent layers of the active scene in parallel within a phase — the scene
boundary gives the pass a natural scope to reason about. Noted as a destination,
not part of the first cut.
---
## 9. Lowering summary
Everything above reduces to existing runtime concepts:
| Construct | Lowers to |
|---|---|
| active base scene | one implicit register `__scene`, states numbered by decl order — literally a compiler-written `machine` |
| `become Name` | `on exit` call · `set __scene` · `on enter` call (two direct calls + store, as the sketch promises) |
| scene layers in a phase | the phase scheduler, after global handlers, dispatches on `__scene` to that scene's layer handlers in declaration order |
| `push`/`pop` (E3) | fixed-capacity scene-id array + index; render walks it, update reads its top |
| scene-owned entities (E1) | implicit kind tag at `spawn`; generated `despawn`-by-tag in synthesized `on exit`; reuses despawn hooks |
| layer toggle / pause (E2) | the same one-flag-flip as `disable Handler`, keyed per layer |
| scene-local `var` (E4) | fixed offsets in a per-scene state block; non-coexisting scenes share storage |
| transition args (E5) | arg stores into the state block before the `on enter` call |
| `@OnEnter`/`@OnExit` (E5) | the same synthesized lifecycle functions as `on enter`/`on exit` |
No heap, no dispatch tables, no new allocator. The active-scene path is a
register read and a static branch; the stack adds a small array only for programs
that push overlays.
---
## 10. Suggested implementation phases
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
by `test.sh`. 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`.
✅ *Toggle shipped* (via EVENTS-DESIGN EV1 layers): `enable layer L` / `disable
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
*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.
- **S4 — scene-owned entities** (E1) and **scene-local state** (E4).
- **S5 — transition parameters** (E5).
- **S6 (later) — `reads`/`writes` scheduling** (E5), gated on the analysis pass.
S0–S1 deliver the sketch as promised; S2–S3 are where the "great potential"
actually lands; S4–S5 are ergonomics; S6 is a performance destination.
---
## 11. Open decisions
1. **Re-entering a scene:** fresh entities/state, or resume? (Default proposed:
`become` = fresh, `push`/`pop` = the pushed scene is fresh each push, the base
underneath is untouched.)
2. **Stack depth:** compile-time cap with an error on overflow, or a runtime trap?
What capacity (8? configurable)?
3. **`passthrough` granularity:** does a passthrough overlay let *all* lower
layers update, or can it name which phases fall through?
4. **Layer hook naming:** `on show`/`on hide`, or reuse `@OnEnable`/`@OnDisable`?
5. **Scene-local storage sharing:** union non-coexisting scenes automatically, or
require an explicit opt-in so the sharing is visible in source?
6. **Global handlers and overlays:** do globals run once per frame regardless of
stack depth (proposed: yes), or per active scene?
7. **`become` from inside an overlay:** does it clear the stack (proposed: yes) or
is it an error while overlays are pushed?
---
*Companion to [LANGUAGE.md §"Scenes & layers"](LANGUAGE.md) and the ordering
sketch in [`examples/scenes.ludic`](examples/scenes.ludic). Supersedes nothing
until the compiler work in §10 lands.*