plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
279200c
— Peter John
2026-11-04T10:05:30+05:30
docs: record the November 2026 language and scene decisions
docs/decisions/2026-11-language-and-scene-decisions.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Language and scene decisions, November 2026
|
|
2
|
+
|
|
3
|
+
A record of the design decisions taken in the 2026-11-03/04 sessions, with the reasoning and the options that were rejected. Each entry links the plan that carries it out. "User" marks a decision the user took; the rest are recommendations the user accepted.
|
|
4
|
+
|
|
5
|
+
## 1. Direction: the most functional style, not the fastest
|
|
6
|
+
|
|
7
|
+
- **Decision (user):** prefer the most functional approach, where a frame is a recursive value built from state and walked recursively (`State -> view -> draw`), over matching the mutable scene's speed. "I don't want the fastest, otherwise I would just stick with mutate."
|
|
8
|
+
- **Consequence:** performance is measured and reported, not chased. The only hard bar for games is staying well inside the frame budget (state/view p95 2.5 ms against a 4 ms bar at 120 Hz).
|
|
9
|
+
- **Measured** (headed Chromium, M3 Pro, ms per frame): mutable scene 1.93, PureScene 1.65, state/view with Ui and Anim 2.07. With fusion, state/view draw is about 7% faster.
|
|
10
|
+
|
|
11
|
+
## 2. Three scene styles, three folders
|
|
12
|
+
|
|
13
|
+
- **Decision (user):** each style lives in its own folder, because one shared folder was confusing.
|
|
14
|
+
- `plum-std/game/`: shared basics (`Game`, `Vec2`, `Color`, `Affine`) and the mutable `Scene`.
|
|
15
|
+
- `plum-std/game/pure/`: PureScene, SceneTree, SceneInput, Actions (pure updates, in place via `own` today).
|
|
16
|
+
- `plum-std/game/functional/`: Picture, Anim, Ui (state/view).
|
|
17
|
+
- `plum-examples/games/pure/` and `plum-examples/games/functional/` for their example games. `plum game` finds assets in the nearest parent `assets/` folder.
|
|
18
|
+
|
|
19
|
+
## 3. Pure scene on enums
|
|
20
|
+
|
|
21
|
+
- **Decision (user):** the pure scene graph models sum types with enums, not a 14-field trait embedded by records.
|
|
22
|
+
- `own record Node` holds the shared fields and embeds `record Transform` (position, size, origin, scale, rotation, plus `localMatrix()` and the box hit test). Its `kind: Kind` is an enum: `Group(children: Slice[Node]) | Sprite | Label | Button | Custom(actor: Actor)`.
|
|
23
|
+
- `Custom(actor: Actor)` is Plum's `Custom(Box<dyn Actor>)`. `Actor` is a trait with no fields, only `act` and `draw`. The game's own kinds (Ship, Orb, Mine, Star, Panel) are records embedding it. Calls are resolved statically because Plum compiles the whole program.
|
|
24
|
+
- Fixed lists are built with variadic init, `Slice(a, b, c)` / `List(a, b)`; Plum has no `[a, b]` literal (dropped earlier by the user).
|
|
25
|
+
- Children stay a `Slice` (not the cons `List`) so updates can run in place with `mapInPlace`.
|
|
26
|
+
- **Rejected:** a generic `Node[K]` with game functions passed in, a union of the game's records with a `match`, and generic traits (`Actor[Msg]`). The user said no `Msg` parameter is needed: "Generics can be tried later".
|
|
27
|
+
- **Plan:** `docs/plans/2026-11-03-pure-scene-enums.md` (merged).
|
|
28
|
+
|
|
29
|
+
## 4. State/view at feature parity
|
|
30
|
+
|
|
31
|
+
- **Decision (user):** give the functional style PureScene's features as pure functions.
|
|
32
|
+
- `Anim`: animations are functions of time, `Anim.at(anim, t) -> AnimPose`, with `MoveBy | RotateBy | ScaleTo | Delay | Seq | Par | Forever`. State stores only when an animation started; nothing is stepped, so nothing drifts.
|
|
33
|
+
- `Picture`: `Tagged(id, child)` plus `Picture.hitId`.
|
|
34
|
+
- `Ui`: `Ui.step(ui, input, hit)` returns the new UI state and `UiEvent`s, using PureScene's press/enter/exit/release/click and capture rules; `Ui.button` builds a button from the UI state.
|
|
35
|
+
- **Deferred (user):** static subtree reuse (build unchanging parts of a picture once, share them every frame). The user expects it isn't a big cost and can be optimised later.
|
|
36
|
+
- **Deferred (user):** structural `==` for records. The user chose deep structural equality (like Rust's `#[derive(PartialEq)]`, not cycle-safe), then deferred building it. Until then `==` on records is identity, and events are compared field by field.
|
|
37
|
+
- **Plan:** `docs/plans/2026-11-03-state-view-parity.md` (merged).
|
|
38
|
+
|
|
39
|
+
## 5. Functional scene graphs: notes from the discussion
|
|
40
|
+
|
|
41
|
+
- Picture already follows the textbook functional design: a recursive ADT, the matrix passed down instead of parent pointers, top-down hit paths, assets referenced by handle.
|
|
42
|
+
- Structural sharing pays off only for parts that don't change (UI, terrain). In a game where everything moves, rebuilding from state costs about the same and is simpler.
|
|
43
|
+
- Folding a tree into a flat list of render commands is redundant here. The host already batches draws, and `Picture.draw` calling the host directly is the fused form; the supercompiler removes the tree as well.
|
|
44
|
+
- Zippers are not needed in state/view, because code edits `State` and rebuilds rather than editing the tree.
|
|
45
|
+
|
|
46
|
+
## 6. The supercompiler
|
|
47
|
+
|
|
48
|
+
- **Goal:** remove data that is built only to be taken apart: List pipelines and `Picture.draw(view(s))`.
|
|
49
|
+
- **Design:**
|
|
50
|
+
- Passes in `plum-core`: purity, simplify (inlining, known-constructor `match`, case-of-case), whistle (homeomorphic embedding), drive (fold recursive producer/consumer pairs, chains, wrappers), uniqueness (in-place spreads), reuse (same-shape rebuilds reuse the old cell).
|
|
51
|
+
- They run after closure specialisation and before TRMC. Every rewrite is checked again, and the original is kept if that check fails.
|
|
52
|
+
- **Correction during the build:** only the producer must be pure and its result used once; the consumer may do I/O (`draw` calls the host). Interleaving a pure producer's evaluation with the consumer's effects can't be observed.
|
|
53
|
+
- Stops on growth (whistle, size budget, a cap of 64 new functions, nesting depth 6); no generalisation. Every refused site gets a reason note.
|
|
54
|
+
- **Switches:** `--no-fusion` and `--explain-fusion` compiler flags (user: flags, not environment variables).
|
|
55
|
+
- **Results:** `map -> filter -> fold` on the cons List fully fuses (1M elements: 4 ms against 15 ms); view->draw fuses into allocation-free functions, except the game-over overlay, whose `Filter` genuinely reads its child twice.
|
|
56
|
+
- **Plan:** `docs/plans/2026-11-03-supercompiler.md` (merged); reports `...benchmark.md`, `...own-report.md`.
|
|
57
|
+
|
|
58
|
+
## 7. `own`
|
|
59
|
+
|
|
60
|
+
- **Decision (user):** keep `own` until the compiler can prove for itself that nobody else holds a value; then it becomes unnecessary.
|
|
61
|
+
- **Finding:** `own` mainly guards in-place field writes (`n.x = ...`). Uniqueness inference covered only spreads, so `own` could not be removed yet. Uniqueness did already give a plain record the same in-place spread `own` gives.
|
|
62
|
+
- **Measured earlier:** `own` barely moved scene speed. Copying was never the bottleneck; allocation-free actions and direct traversal were.
|
|
63
|
+
- **Path to removal:** the language restrictions below (immutable fields, uniqueness for method parameters and `self`), then delete `own`.
|
|
64
|
+
|
|
65
|
+
## 8. Language restrictions for a simpler compiler
|
|
66
|
+
|
|
67
|
+
- **Decision (user):** the language may become stricter or lose features if that makes the supercompiler simpler and more reliable. All five restrictions were accepted:
|
|
68
|
+
1. **Immutable fields; `mut` per field.** Fields are immutable unless declared `mut` (OCaml style), for example `record Slice[T](mut data: Array[T], start: Int, mut size: Int, mut cap: Int)`. Other updates go through spread, `T(..x, f: v)`. The user chose per-field `mut` over `mut record` because it is more precise: unchanging fields stay stable for the optimiser, and one counter doesn't make a whole type mutable.
|
|
69
|
+
2. **Effects through an `Io` capability (Zig `std.Io` style).** Host I/O happens only through an `Io` value (`io.drawRect(..)`, `io.printLn(..)`). Entry points receive it, and a function is pure if no `Io` is reachable from its parameters or captures. The user rejected an `io fun` keyword and `fun!` markers ("I don't want function coloring") and rejected implicit inference as hidden. A fake or recording `Io` serves tests and replays.
|
|
70
|
+
3. **Fusion runs before union lowering** (internal).
|
|
71
|
+
4. **Only expression-bodied functions** (`= ...`) are fusion candidates.
|
|
72
|
+
5. **Pattern casing is enforced:** Capitalised names are tags, lowercase names are binders. This removes the bug class behind a miscompile where an error name was read as a variable.
|
|
73
|
+
- **Plan:** `docs/plans/2026-11-03-language-restrictions.md` (in progress).
|
|
74
|
+
|
|
75
|
+
## 9. Next: no heap mutation at all (Koka-style FBIP)
|
|
76
|
+
|
|
77
|
+
- **Decision (user):** after the restrictions, remove heap mutation entirely, in the spirit of Koka's FBIP ("functional but in-place").
|
|
78
|
+
- Persistent `Slice`/`Map`/`Array`/`Buffer`: `xs.set(i, v)` returns the new collection and is written in place when the old one is provably unique.
|
|
79
|
+
- Koka-style `fip` functions: a checked guarantee that every update is in place, a compile error otherwise.
|
|
80
|
+
- Game shells return the new world (`update(io, w, dt) -> World`). Local variable reassignment in loops stays.
|
|
81
|
+
- **Why not plain Koka:** Koka checks for a reference count of 1 at runtime (Perceus). Plum's heap is wasm-gc, which has no reference counts, so Plum proves uniqueness statically first. A small reference count used only as a uniqueness check (the GC still frees memory) is the fallback if static proof falls short on the games.
|
|
82
|
+
- **Risk:** an array write the analysis can't prove unique silently becomes a copy every frame; `fip` turns that into a compile error. The `mut` fields left after the restrictions plan are this plan's worklist.
|
|
83
|
+
|
|
84
|
+
## 10. Next after that: deterministic games
|
|
85
|
+
|
|
86
|
+
- **Discussion:** pure updates make `step(state, input)` repeatable, but full determinism also needs a fixed timestep, a seeded random generator kept in game state, and recorded inputs. Plum's `Math.sin`/`cos` are written in Plum, and wasm `f64` gives the same results in every engine.
|
|
87
|
+
- **Decision (user):** a separate plan, ported from `kam_macroquad`'s design: a fixed-timestep accumulator, replays that record inputs rather than state, state checksums, and a seeded `KaMRandom`. The `Io` capability makes recording and replaying host input straightforward.
|
|
88
|
+
|
|
89
|
+
## 11. Where Plum sits among languages
|
|
90
|
+
|
|
91
|
+
Gleam's data model (immutable records with `T(..x, f: v)` updates, custom types, enforced constructor/variable casing), Zig's effects and errors (`std.Io`-style capability, `!T`, error sets, `T?`, per-instantiation generics), Rust-style closed-world traits with static dispatch, an explicit `mut`-field escape hatch (to be removed by FBIP), and a whole-program wasm-gc compiler with a supercompiler. Unlike Gleam it tracks effects, keeps loops, and has traits.
|
|
92
|
+
|
|
93
|
+
## 12. Smaller decisions
|
|
94
|
+
|
|
95
|
+
- **wasmtime:** the GC heap starts at 64 MiB in every host, and the dev profile compiles wasmtime at opt-level 3.
|
|
96
|
+
- **Tests:** superlite's per-file test mode now really runs the named files (`.superlite/run-test-files.sh`); it used to filter `cargo test` by path and run nothing.
|
|
97
|
+
- **Fixed during the builds** (each with a regression test): borrowed values passed to trait methods; trait forwarders keep `own`; field-less records (`Panel()`); a bare pattern on a variant with fields is a compile error; views into `own` values are checked after a move; TRMC handles `if`/`else` branches and methods; and several fusion and uniqueness soundness holes found in review.
|