plum

#treesitter#compiler#wasm

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 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.
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[K]` holds the fields every node has: id, position, size, origin, scale, rotation, colour, visible, touchable and actions, plus `kind: Kind[K]`.
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[K]` is an inline-variant enum: `Group(children)`, `Sprite(texture region)`, `Label(text, size)`, `Button(text, textures, pressed, over)` and `Custom(body: K)`.
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 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, as records (user decision 2026-11-03).**
21
- - `game_pure_scene` keeps `Panel`, `Ship`, `Orb`, `Mine` and `Star` as plain records. Each holds only its own fields (clock, velocity, phase, texture...) and its own `act` and `draw` methods. They no longer embed a trait or repeat the 14-field header.
22
- - The records are joined by a union enum, `enum Body = Panel | Ship | Orb | Mine | Star`. A call like `b.act(node, dt)` on a `Body` dispatches to the right record with no `match` (checked by a probe).
23
- - The game builds a `Node[Body]` tree.
24
- - std finds the body in a `Custom(body)` node and calls two functions the game passes in, giving each the body and the node: `act(body, node, dt)` and `draw(body, node, matrix, tint)`.
25
- - In the game these are one-liners: `fun actBody(b: Body, n: Node[Body], dt: Float) = b.act(n, dt)`. A body's method can move the node (`n.x = ...`) and change its own fields in place.
26
- - `SceneTree.update(root, dt, actBody)` and `PureScene.drawTree(root, drawBody)` call them for `Custom` nodes. `Group`, `Sprite`, `Label` and `Button` are handled by std itself.
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
- - `SceneInput`'s `Stage` holds a `Node[K]` root.
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 `trait`, no `override` and no union-of-records enums.
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[Body]`.
45
+ - `game_pure_scene.plum` and the `stress_pure_scene` web fixture on the new `Node`:
42
- - Each kind is a small record with its own methods, joined by a union enum `Body`.
46
+ - each game kind is a small record embedding `Actor`;
43
- - Same rules, keys, assets, sounds and scoring.
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[K]`, `Kind[K]`, builders (`group`, `sprite`, `label`, `button`, `custom`), drawing, hit test, Button's pointer handling | M |
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[K]` with the game's `act` (and `draw` passed through drawing) | M |
57
+ | SceneTree and SceneInput | update/find/hit/stage over `Node`, calling `Custom` actors | M |
52
- | Orb Run on Node[Body] | `game_pure_scene.plum`: kind records with `act`/`draw` methods, `enum Body = Panel \| Ship \| ...`, one-line `actBody`/`drawBody`; tests kept by name and value | M |
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, ... + enum Body, actBody, drawBody"] --> T["SceneTree.update(root, dt, actBody)"]
65
+ G["game: records Ship, Orb, ... embedding trait Actor"] --> C["Custom(actor: Actor)"]
60
- G --> D["PureScene.drawTree(root, drawBody)"]
61
- T --> N["own record Node[K] + enum Kind[K]"]
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[K])"] --> N
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: also pass a `hit` function.
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: 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.
80
- - **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.
81
- - **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).
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` (stay on traits).
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 a gap this work hits (see R1).
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
- - `grep` finds no `trait` or `override` in the four std modules or `game_pure_scene.plum`, and no record-union enum in the std modules (the game's `Body` union is intended).
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 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.
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.