plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ad3ec48
— Peter John
2026-11-03T20:37:53+05:30
docs(plans): pure scene enums: Custom(actor: Actor), no Msg; ownership fix task
docs/plans/2026-11-03-pure-scene-enums.review.md
CHANGED
|
@@ -6,29 +6,33 @@
|
|
|
6
6
|
## Summary
|
|
7
7
|
The pure scene graph was written before inline variants landed, so it models sum types the old way:
|
|
8
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.
|
|
9
|
+
- `game_pure_scene.plum`: five more `own record`s (`Panel`, `Ship`, `Orb`, `Mine`, `Star`) embed the same 14-field trait.
|
|
10
10
|
- `std/game/Actions`: six separate records (`MoveBy`, `RotateBy`, `Delay`, `Sequence`, `Parallel`, `Forever`) joined by a union enum.
|
|
11
11
|
|
|
12
|
-
This plan moves
|
|
12
|
+
This plan moves the std node kinds to an enum. A trait is used only where Rust would use `Box<dyn Actor>`: the game's own kinds. User decisions, 2026-11-03:
|
|
13
|
+
- pure scene only;
|
|
14
|
+
- a `Node` record plus a `Kind` enum;
|
|
15
|
+
- `Custom(actor: Actor)` like `Custom(Box<dyn Actor>)`, with no `Msg` type parameter.
|
|
16
|
+
|
|
17
|
+
The mutable `std/game/Scene`, `game_scene.plum`, `scene.plum` and the trait examples stay as they are.
|
|
13
18
|
|
|
14
19
|
## The new shape
|
|
15
20
|
- **A node is a record plus a kind.**
|
|
16
|
-
- `own record Node
|
|
21
|
+
- `own record Node` holds the fields every node has: id, position, size, origin, scale, rotation, colour, visible, touchable and actions, plus `kind: Kind`.
|
|
17
|
-
- `Kind
|
|
22
|
+
- `Kind` is an inline-variant enum: `Group(children: Slice[Node])`, `Sprite(texture region)`, `Label(text, size)`, `Button(text, textures, pressed, over)` and `Custom(actor: Actor)`.
|
|
18
23
|
- `n.x` stays a plain field read, and there's no 14-field header to repeat.
|
|
19
|
-
- Narrowing
|
|
20
|
-
- **The game
|
|
21
|
-
- `
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- `
|
|
27
|
-
- Closure specialisation inlines the passed functions, so there's no indirect call per node.
|
|
24
|
+
- Narrowing to a std kind is a `match` on `n.kind`.
|
|
25
|
+
- **The game's kinds are records behind a behaviour-only trait** (Rust's `Box<dyn Actor>`).
|
|
26
|
+
- `trait Actor` in std has no fields, only two methods:
|
|
27
|
+
- `act(self, own n: Node, dt) -> Node`: the node is handed in and back, so the actor can move it;
|
|
28
|
+
- `draw(self, n: Node, m: Affine, tint: Color)`.
|
|
29
|
+
- `game_pure_scene` keeps `Panel`, `Ship`, `Orb`, `Mine` and `Star` as records that embed `Actor` and hold only their own fields (clock, velocity, phase, texture...). The 14-field header is gone from all of them.
|
|
30
|
+
- `SceneTree.update` and `PureScene.drawTree` call `actor.act` / `actor.draw` for `Custom` nodes. Plum compiles the whole program at once, so these calls are resolved statically, like today's trait calls: no vtable, no `Node` type parameter, no functions passed in.
|
|
31
|
+
- `Group`, `Sprite`, `Label` and `Button` are handled by std itself.
|
|
28
32
|
- **Hit-testing and input are unchanged in behaviour.**
|
|
29
33
|
- A `Custom` node hits on its box, like today's default.
|
|
30
34
|
- `Button` keeps its press/enter/exit/release/click events.
|
|
31
|
-
- `
|
|
35
|
+
- Events stay `UiEvent(id, kind)`, which the game maps by id.
|
|
32
36
|
- **Actions use the same "record plus enum" split.**
|
|
33
37
|
- `own record Action(elapsed, current, kind: ActionKind)` holds the state that changes while it runs.
|
|
34
38
|
- `ActionKind` is an inline-variant enum: `MoveBy(dx, dy, duration)`, `RotateBy(radians, duration)`, `Delay(duration)`, `Sequence(steps)`, `Parallel(steps)` and `Forever(template)`.
|
|
@@ -37,61 +41,70 @@ This plan moves all of it to enums (user decision 2026-11-03: pure scene only; s
|
|
|
37
41
|
- **Plain data stays records or structs:** `Pointer`, `UiEvent`, `Input`, `Capture`, `Handled`, `Stage`, `Pose`, `Step`. They are single-shape values, not sum types.
|
|
38
42
|
|
|
39
43
|
## What you will get
|
|
40
|
-
- `PureScene`, `SceneTree`, `SceneInput` and `Actions` with no
|
|
44
|
+
- `PureScene`, `SceneTree`, `SceneInput` and `Actions` with no union-of-records enums. The only trait left is the small `Actor`.
|
|
41
|
-
- `game_pure_scene.plum` and the `stress_pure_scene` web fixture on `Node
|
|
45
|
+
- `game_pure_scene.plum` and the `stress_pure_scene` web fixture on the new `Node`:
|
|
42
|
-
-
|
|
46
|
+
- each game kind is a small record embedding `Actor`;
|
|
43
|
-
-
|
|
47
|
+
- same rules, keys, assets, sounds and scoring.
|
|
44
48
|
- README's pure-scene section and the std doc comments updated to the new shape.
|
|
49
|
+
- One compiler fix found while probing (below).
|
|
45
50
|
|
|
46
51
|
## What will be built
|
|
47
52
|
| Part | What it does | Size |
|
|
48
53
|
|---|---|---|
|
|
54
|
+
| Ownership fix | Passing a borrowed `own` value to a trait method's borrowed param is wrongly reported as "cannot store borrowed" (probe: `.superlite/repros/custom_actor_probe2.plum`). Plain functions already accept it | S–M |
|
|
49
55
|
| Actions on enums | `Action` record + `ActionKind` enum; in-place stepping kept; golden-value tests unchanged in values | M |
|
|
50
|
-
| PureScene on enums | `Node
|
|
56
|
+
| PureScene on enums | `Node`, `Kind`, `trait Actor`, builders (`group`, `sprite`, `label`, `button`, `custom`), drawing, hit test, Button's pointer handling | M |
|
|
51
|
-
| SceneTree and SceneInput | update/find/hit/stage over `Node
|
|
57
|
+
| SceneTree and SceneInput | update/find/hit/stage over `Node`, calling `Custom` actors | M |
|
|
52
|
-
| Orb Run on Node
|
|
58
|
+
| Orb Run on the new Node | `game_pure_scene.plum`: kind records embedding `Actor`; tests kept by name and value | M |
|
|
53
59
|
| Fixture, host checks, docs | `stress_pure_scene.plum`, host parity and draw-count tests, README | S |
|
|
54
60
|
| Benchmark | Headed Chromium run against the mutable scene, same bar as before | S |
|
|
55
61
|
|
|
56
62
|
## How it fits together
|
|
57
63
|
```mermaid
|
|
58
64
|
flowchart LR
|
|
59
|
-
G["game: records Ship, Orb, ...
|
|
65
|
+
G["game: records Ship, Orb, ... embedding trait Actor"] --> C["Custom(actor: Actor)"]
|
|
60
|
-
G --> D["PureScene.drawTree(root, drawBody)"]
|
|
61
|
-
|
|
66
|
+
C --> N["own record Node + enum Kind"]
|
|
67
|
+
T["SceneTree.update"] --> N
|
|
62
|
-
D --> N
|
|
68
|
+
D["PureScene.drawTree"] --> N
|
|
63
69
|
N --> A["own record Action + enum ActionKind"]
|
|
64
|
-
I["SceneInput Stage(root: Node
|
|
70
|
+
I["SceneInput Stage(root: Node)"] --> N
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
## Decisions you can change
|
|
68
|
-
- **Where per-kind behaviour lives.**
|
|
69
|
-
- Default: `act` and `draw` functions passed in by the game.
|
|
70
|
-
- 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.
|
|
71
74
|
- **Custom hit-testing.**
|
|
72
75
|
- Default: box hit only, as today.
|
|
73
|
-
- Alternative:
|
|
76
|
+
- Alternative: an optional `hit` method on `Actor` with the box test as its default body.
|
|
74
77
|
- **Actions shape.**
|
|
75
78
|
- Default: record + enum, which keeps stepping allocation-free.
|
|
76
79
|
- Alternative: a pure inline-variant `Action` rebuilt on every step. Simpler, but one allocation per running action per frame.
|
|
77
80
|
|
|
78
81
|
## Risks
|
|
79
|
-
- **R1:
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
+
- **R1: compiler gaps.**
|
|
83
|
+
- Probes compiled and ran a `Node` record with a `Kind` enum, a `Slice[Node]` of children, recursive in-place update and `Custom(actor: Actor)` dispatch. The one gap found is the ownership fix above.
|
|
84
|
+
- Deeper shapes (`Node` inside `Stage`, `Handled`, closures over nodes) may hit more. Each gap is reported as an incident and fixed in the compiler, not worked around silently.
|
|
85
|
+
- **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.
|
|
86
|
+
- **R3: speed.** A `match` on `kind` replaces trait dispatch for std kinds. 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).
|
|
82
87
|
|
|
83
88
|
## Out of scope
|
|
84
|
-
- `std/game/Scene`, `game_scene.plum`, `scene.plum`, `trait_scene.plum`, `traits.plum`
|
|
89
|
+
- `std/game/Scene`, `game_scene.plum`, `scene.plum`, `trait_scene.plum`, `traits.plum`.
|
|
85
90
|
- `Picture` and `game_state_view` (already enums).
|
|
91
|
+
- Generic traits (`Actor[Msg]`): not needed.
|
|
86
|
-
- Compiler changes beyond fixing
|
|
92
|
+
- Compiler changes beyond fixing gaps this work hits.
|
|
87
93
|
- The supercompiler (next plan).
|
|
88
94
|
|
|
89
95
|
## How we will know it works
|
|
90
|
-
-
|
|
96
|
+
- No union-of-records enum and no 14-field trait left in the four std modules or `game_pure_scene.plum`; `trait Actor` has no fields.
|
|
97
|
+
- The ownership fix has a regression test, and the probe passes.
|
|
91
98
|
- Actions golden tests and every `game_pure_scene` test pass, with the same names and values.
|
|
92
99
|
- Host parity and draw-count tests pass: the pure scene draws the same count as the mutable scene. The `pureScene*` browser check passes.
|
|
93
100
|
- Full suites stay green: Plum std/examples (632+) and the Rust crates.
|
|
94
101
|
- Headed benchmark table against the mutable scene, judged on the bar.
|
|
95
102
|
|
|
96
103
|
## Rough time and cost
|
|
97
|
-
About
|
|
104
|
+
About 7 parts:
|
|
105
|
+
- in parallel first: the ownership fix, Actions and PureScene;
|
|
106
|
+
- then SceneTree/SceneInput;
|
|
107
|
+
- then the game;
|
|
108
|
+
- then fixture/docs and the benchmark.
|
|
109
|
+
|
|
110
|
+
Rough agent time is 55–85 minutes, plus one headed benchmark run.
|