plum
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.
|