plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


8dead74 — Peter John 2026-11-03T20:34:10+05:30
docs(plans): pure scene graph on enums review plan
docs/plans/2026-11-03-pure-scene-enums.review.md ADDED
@@ -0,0 +1,94 @@
1
+ # Pure scene graph on enums: review plan
2
+
3
+ **Builds on:** master f2156fc (pure scene graph, `own record`, inline variants, unboxed variants, closure specialisation merged).
4
+ **Comes before:** `2026-11-03-supercompiler.review.md` (parked until this ships).
5
+
6
+ ## Summary
7
+ The pure scene graph was written before inline variants landed, so it models sum types the old way:
8
+ - `std/game/PureScene`: an `own trait Actor` with 14 fields, embedded by `own record`s `Group`, `Sprite`, `Label` and `Button`. Each record repeats the 14-field header, and behaviour is spread over `override fun`s.
9
+ - `game_pure_scene.plum`: five more `own record`s (`Panel`, `Ship`, `Orb`, `Mine`, `Star`) embed the same trait.
10
+ - `std/game/Actions`: six separate records (`MoveBy`, `RotateBy`, `Delay`, `Sequence`, `Parallel`, `Forever`) joined by a union enum.
11
+
12
+ This plan moves all of it to enums (user decision 2026-11-03: pure scene only; shape "Node record + kind enum"). The mutable `std/game/Scene`, `game_scene.plum`, `scene.plum` and the trait examples stay trait-based: they are the trait showcase.
13
+
14
+ ## The new shape
15
+ - **A node is a record plus a kind.**
16
+ - `own record Node[K]` holds the fields every node has: id, position, size, origin, scale, rotation, colour, visible, touchable and actions, plus `kind: Kind[K]`.
17
+ - `Kind[K]` is an inline-variant enum: `Group(children)`, `Sprite(texture region)`, `Label(text, size)`, `Button(text, textures, pressed, over)` and `Custom(body: K)`.
18
+ - `n.x` stays a plain field read, and there's no 14-field header to repeat.
19
+ - Narrowing back to a kind is a `match`. That fixes phase A's known gap: a trait value from the tree couldn't be narrowed back to its record.
20
+ - **The game brings its own kinds.**
21
+ - `game_pure_scene` defines `enum Body = Panel(..) | Ship(..) | Orb(..) | Mine(..) | Star(..)`, with only each kind's own fields (clock, velocity, phase, texture...), and builds a `Node[Body]` tree.
22
+ - Per-kind behaviour moves from overrides into two plain functions the game passes in: `act(node, dt) -> node` (drift, spin, animation clocks) and `draw(node, matrix, tint)`.
23
+ - `SceneTree.update(root, dt, act)` and `PureScene.drawTree(root, draw)` call them for `Custom` nodes. `Group`, `Sprite`, `Label` and `Button` are handled by std itself.
24
+ - Closure specialisation inlines the passed functions, so there's no indirect call per node.
25
+ - **Hit-testing and input are unchanged in behaviour.**
26
+ - A `Custom` node hits on its box, like today's default.
27
+ - `Button` keeps its press/enter/exit/release/click events.
28
+ - `SceneInput`'s `Stage` holds a `Node[K]` root.
29
+ - **Actions use the same "record plus enum" split.**
30
+ - `own record Action(elapsed, current, kind: ActionKind)` holds the state that changes while it runs.
31
+ - `ActionKind` is an inline-variant enum: `MoveBy(dx, dy, duration)`, `RotateBy(radians, duration)`, `Delay(duration)`, `Sequence(steps)`, `Parallel(steps)` and `Forever(template)`.
32
+ - Inline-variant fields can't be assigned, so this split is what keeps stepping in place and allocation-free, as it is today.
33
+ - The `Actions.moveBy` / `sequence` / `forever` builders keep their names and arguments.
34
+ - **Plain data stays records or structs:** `Pointer`, `UiEvent`, `Input`, `Capture`, `Handled`, `Stage`, `Pose`, `Step`. They are single-shape values, not sum types.
35
+
36
+ ## What you will get
37
+ - `PureScene`, `SceneTree`, `SceneInput` and `Actions` with no `trait`, no `override` and no union-of-records enums.
38
+ - `game_pure_scene.plum` and the `stress_pure_scene` web fixture on `Node[Body]`.
39
+ - The game reads like `game_state_view`: kinds are an enum and behaviour is a `match`.
40
+ - Same rules, keys, assets, sounds and scoring.
41
+ - README's pure-scene section and the std doc comments updated to the new shape.
42
+
43
+ ## What will be built
44
+ | Part | What it does | Size |
45
+ |---|---|---|
46
+ | Actions on enums | `Action` record + `ActionKind` enum; in-place stepping kept; golden-value tests unchanged in values | M |
47
+ | PureScene on enums | `Node[K]`, `Kind[K]`, builders (`group`, `sprite`, `label`, `button`, `custom`), drawing, hit test, Button's pointer handling | M |
48
+ | SceneTree and SceneInput | update/find/hit/stage over `Node[K]` with the game's `act` (and `draw` passed through drawing) | M |
49
+ | Orb Run on Node[Body] | `game_pure_scene.plum`: `enum Body`, `actBody`, `drawBody`; tests kept by name and value | M |
50
+ | Fixture, host checks, docs | `stress_pure_scene.plum`, host parity and draw-count tests, README | S |
51
+ | Benchmark | Headed Chromium run against the mutable scene, same bar as before | S |
52
+
53
+ ## How it fits together
54
+ ```mermaid
55
+ flowchart LR
56
+ G["game: enum Body, actBody, drawBody"] --> T["SceneTree.update(root, dt, actBody)"]
57
+ G --> D["PureScene.drawTree(root, drawBody)"]
58
+ T --> N["own record Node[K] + enum Kind[K]"]
59
+ D --> N
60
+ N --> A["own record Action + enum ActionKind"]
61
+ I["SceneInput Stage(root: Node[K])"] --> N
62
+ ```
63
+
64
+ ## Decisions you can change
65
+ - **Where per-kind behaviour lives.**
66
+ - Default: `act` and `draw` functions passed in by the game.
67
+ - Alternative: methods on the game's `Body` enum, which std would call through a fixed name. That needs the "static calls on type parameters" path and couples std to a naming convention.
68
+ - **Custom hit-testing.**
69
+ - Default: box hit only, as today.
70
+ - Alternative: also pass a `hit` function.
71
+ - **Actions shape.**
72
+ - Default: record + enum, which keeps stepping allocation-free.
73
+ - Alternative: a pure inline-variant `Action` rebuilt on every step. Simpler, but one allocation per running action per frame.
74
+
75
+ ## Risks
76
+ - **R1: generic own record with a generic enum field.** A probe compiled and ran `own record Node[K](.., kind: Kind[K])` with a `Slice[Node[K]]` of children, recursive update with a passed function, and in-place field writes. Deeper shapes (`Node[Body]` inside `Stage`, `Handled`, closures over nodes) may still hit compiler gaps. Any gap found is reported as an incident and fixed in the compiler, not worked around silently.
77
+ - **R2: ownership.** `act` takes the node by `own` and hands it back. The ownership checker must accept the matches, and the writes through `kind`, without special cases.
78
+ - **R3: speed.** A `match` on `kind` replaces trait dispatch. It should be no slower, but the benchmark decides. The bar stays: average within 10% and p95 within 15% of the mutable scene (last run: pure 1.78 ms vs mutable 1.83 ms).
79
+
80
+ ## Out of scope
81
+ - `std/game/Scene`, `game_scene.plum`, `scene.plum`, `trait_scene.plum`, `traits.plum` (stay on traits).
82
+ - `Picture` and `game_state_view` (already enums).
83
+ - Compiler changes beyond fixing a gap this work hits (see R1).
84
+ - The supercompiler (next plan).
85
+
86
+ ## How we will know it works
87
+ - `grep` finds no `trait`, `override` or record-union `enum X = A | B` in the four std modules or `game_pure_scene.plum`.
88
+ - Actions golden tests and every `game_pure_scene` test pass, with the same names and values.
89
+ - Host parity and draw-count tests pass: the pure scene draws the same count as the mutable scene. The `pureScene*` browser check passes.
90
+ - Full suites stay green: Plum std/examples (632+) and the Rust crates.
91
+ - Headed benchmark table against the mutable scene, judged on the bar.
92
+
93
+ ## Rough time and cost
94
+ About 6 parts. Actions and PureScene can run side by side, then SceneTree/SceneInput, then the game, then fixture/docs and the benchmark. Rough agent time is 50–80 minutes, plus one headed benchmark run.