plum

#treesitter#compiler#wasm

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

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


12f598b — Peter John 2026-11-06T10:53:22+05:30
plans: FBIP stage 1
Files changed (1) hide show
  1. docs/plans/2026-11-06-fbip-stage1.md +108 -0
docs/plans/2026-11-06-fbip-stage1.md ADDED
@@ -0,0 +1,108 @@
1
+ # FBIP stage 1: the proof, in-place arrays, fip, and a pilot
2
+
3
+ **Review plan:** docs/plans/2026-11-05-fbip.review.md, approved by the user 2026-11-06 ("Yes implement it and keep mut for now"). Decisions: `mut` stays for game shells and the imperative scene; static proof first; no ownership types; `kam_plum` out of scope.
4
+ **Goal:** a stronger uniqueness proof, an in-place `Array.with`, `fip fun`, and a `--no-fbip` switch, then a pilot persistent collection that shows whether in-place updates really happen in the shapes games use. Stage 2, porting `Slice`, `Map` and the rest, is planned only after this pilot's result.
5
+ **Architecture:** compiler/core (uniqueness), compiler/driver, compiler/cli, compiler/wasm-codegen (intrinsics), compiler/tooling/tree-sitter-plum, std/Array.plum, tests/.
6
+ **Tech stack:** Rust 2021, Plum, tree-sitter.
7
+ **Detail:** contracts
8
+ **Target-min:** 120
9
+
10
+ ## Global constraints
11
+ - Work on `master` in the main checkout, default shared `target/`. Don't commit. Never run `plum format` on real files. No `sed -i -E`. Don't touch docs/ except the files a task owns.
12
+ - No `cargo test` (hook): `cargo build --workspace --tests`, then run every test executable that build reports (14 binaries, listed by `cargo test --no-run --workspace 2>&1 | grep Executable`, which only compiles). Sweep `target/debug/plum test std examples tests` (779 before this plan). `compiler/game` is a standalone crate with its own build.
13
+ - `plum format` isn't a check. Tree-sitter regeneration needs `--disable-optimizations`.
14
+ - Wasm: `bash .superlite/wasm-tree.sh check` compares against the baseline at this plan's start. T2 onward: with `--no-fbip` every example stays byte-identical to the baseline. Without the flag, bytes may change when the stronger proof finds more in-place spreads, and the behaviour must not.
15
+ - **Soundness is the first priority.** A wrong uniqueness proof silently changes a program: writes reach another holder of the value. Anything unclear stays a copy. Every proof extension gets aliasing tests: a program that would give a different answer if the update were wrongly in place, run with fusion on and with `--no-fbip`, with the same result.
16
+ - Differential check at each task's end: `plum test std examples tests` passes both with the default and with `--no-fbip`.
17
+ - Design (coordinator ruling): no runtime reference counts or flags. `Array.with(self, i, v) -> Array[T]` is a compiler-known method that writes in place when its receiver is proved unique and copies otherwise, marked the same way `ClassCall.unique` marks a spread. `fip fun` is a compile-time guarantee only: it adds no runtime behaviour. Existing mutating APIs (`Array.set`, `Slice.push`) stay unchanged in this stage.
18
+
19
+ ## Task graph
20
+ ```mermaid
21
+ flowchart LR
22
+ T1[baseline and probes]
23
+ T2[no-fbip switch]
24
+ T3[proof: methods and loops]
25
+ T4[Array.with]
26
+ T5[fip fun]
27
+ T6[pilot and verdict]
28
+ %% taskgraph
29
+ T1 --> T2
30
+ T2 --> T3
31
+ T3 --> T4
32
+ T4 --> T5
33
+ T5 --> T6
34
+ ```
35
+ Ruling: chain - each builds on the last, and all share the uniqueness pass - cost: about 2 hours.
36
+
37
+ ## Package A: the switch and the proof
38
+ **Probe entry:** run=plum test std examples tests
39
+
40
+ ### T1 baseline and probes · Complexity 3 · Est. context 35k
41
+ **Owns:** docs/plans/2026-11-06-fbip.benchmark.md, tests/fbip_probes/*.plum
42
+ **Components:** 1
43
+ **LOC owned:** 120
44
+ **Tests:** fast
45
+ **Produces:** `docs/plans/2026-11-06-fbip.benchmark.md` records today's speed for the stress fixtures and for collection probes; `tests/fbip_probes/` holds the probe programs
46
+ e.g. the doc gives ms for 1M `Slice.push` on a local, on a struct field, and through a helper function
47
+ **Notes:** Release builds. Stress fixtures as in docs/plans/2026-11-05-game-engine.results.md, 3 runs each, with `--uncapped` if it gives steadier numbers: record the load average and how noisy it is. Probes, via a release `plum run`: 1M `Slice.push` on a local, on a field, through a helper; 1M `Array.set`; Map set and get, 100k; `Buffer` writes; a struct spread in a loop. Also record compile time for a game and `.wasm` size for one game.
48
+ **Test code:** the probes run under `plum test` with an answer check, so they stay correct.
49
+
50
+ ### T2 no-fbip switch · Complexity 2 · Est. context 30k
51
+ **Owns:** compiler/driver/src/*.rs, compiler/cli/src/*.rs, compiler/cli/tests/cli/*.rs, compiler/core/src/uniqueness.rs, compiler/game/web-test/src/*.rs
52
+ **Components:** 1
53
+ **LOC owned:** 60
54
+ **Tests:** fast
55
+ **Consumes:** `docs/plans/2026-11-06-fbip.benchmark.md` records today's speed for the stress fixtures and for collection probes; `tests/fbip_probes/` holds the probe programs (T1)
56
+ **Produces:** `PassOptions.fbip`, default on, and `--no-fbip` on `plum run`, `build`, `test`; with it off, no spread or array write is marked unique
57
+ e.g. `plum run --no-fbip game.plum` reports no `unique spread` under `--explain-fusion`
58
+ **Notes:** Today uniqueness runs as part of fusion, so `--no-fusion` also turns it off; keep that. Off means every spread copies. Add a `--no-fbip` option to `plum-web-test` so games can be compared.
59
+ **Test code:** a cli test: an aliasing-free spread program gives the same output either way, and `--explain-fusion` differs as expected.
60
+
61
+ ### T3 proof: methods and loops · Complexity 5 · Est. context 60k
62
+ **Owns:** compiler/core/src/uniqueness.rs, compiler/core/tests/core/uniqueness_test.rs, compiler/core/tests/core/main.rs, tests/fbip/*.plum
63
+ **Components:** 1
64
+ **LOC owned:** 200
65
+ **Tests:** fast
66
+ **Consumes:** `PassOptions.fbip`, default on, and `--no-fbip` on `plum run`, `build`, `test`; with it off, no spread or array write is marked unique (T2)
67
+ **Produces:** the proof covers method `self` and typed receivers, including overrides, `x = x.m(..)` hand-over in loops and fields, and functions that return their unique argument; `tests/fbip/` holds aliasing tests
68
+ e.g. `v = v.bumped()` in a `for` loop, with `bumped(self) = Cell(..self, n: self.n + 1)`, is reported as a unique spread
69
+ **Notes:** Today parameters of free functions only. A method's `self` is unique when every call site passes a unique value; an overridable method is the union of all overrides, as purity does now. A function called with both unique and shared values is not unique: no per-call-site specialisation in this stage, and say how often that blocks real code. Report what stays unproved, and why, through `--explain-fusion`.
70
+ **Test code:** `tests/fbip/aliasing.plum`: keep an alias then update; store in a list then update; capture in a closure; return both; a value in two fields; a shared value through a method; an override keeping `self`; update in only one branch. Each gives the copying answer, and matches `--no-fbip`. Plus positive tests in `uniqueness_test.rs`.
71
+
72
+ ### T4 Array.with · Complexity 5 · Est. context 55k
73
+ **Owns:** std/Array.plum, compiler/core/src/*.rs, compiler/core/tests/core/*.rs, compiler/wasm-codegen/src/*.rs, compiler/wasm-codegen/tests/wasm_codegen/**/*.rs, compiler/checker/src/*.rs, tests/fbip/*.plum
74
+ **Components:** 2
75
+ **LOC owned:** 180
76
+ **Tests:** fast
77
+ **Consumes:** the proof covers method `self` and typed receivers, including overrides, `x = x.m(..)` hand-over in loops and fields, and functions that return their unique argument; `tests/fbip/` holds aliasing tests (T3)
78
+ **Produces:** `Array.with(self, i, v) -> Array[T]` returns the array with one element changed, in place when the receiver is proved unique and through a copy otherwise, with no cost for the check at runtime
79
+ e.g. `a = a.with(i, i * 2)` in a loop on a fresh array allocates nothing per step, shown by `--explain-fusion` and a byte-count or allocation test
80
+ **Notes:** Works for flat-struct element arrays too: `Array[Vec2]` is stored field by field. A proved site compiles exactly like `Array.set`. Out-of-range behaves as `set` does.
81
+ **Test code:** aliasing tests for arrays: alias then `with`, the original is unchanged; array in a struct field; array returned from a helper; `Array[Vec2]`. An `opsOf`-style wasm test that a proved site has no `array.copy`, and an unproved one does.
82
+
83
+ ### T5 fip fun · Complexity 4 · Est. context 50k
84
+ **Owns:** compiler/tooling/tree-sitter-plum/**, compiler/tooling/vscode-plum/**, compiler/core/src/*.rs, compiler/checker/src/*.rs, compiler/driver/src/*.rs, compiler/checker/tests/**/*.rs, compiler/core/tests/core/*.rs, tests/fbip/*.plum
85
+ **Components:** 2
86
+ **LOC owned:** 150
87
+ **Tests:** fast
88
+ **Consumes:** `Array.with(self, i, v) -> Array[T]` returns the array with one element changed, in place when the receiver is proved unique and through a copy otherwise, with no cost for the check at runtime (T4)
89
+ **Produces:** `fip fun` is a compile error, at the exact spot and giving the reason, when a spread or `Array.with` in that function isn't proved in place
90
+ e.g. `fip fun f(a: Array[Int]) -> Array[Int] = { b := a; a.with(0, 1) }` errors at `a.with`, saying `a` is also held by `b`
91
+ **Notes:** Check at the end of the pipeline, because the proof runs after fusion: the error comes from the driver with the node's span. Under `--no-fbip` or `--no-fusion`, `fip` isn't enforced, and say so. Calls from a `fip fun` to other functions aren't required to be `fip`, so it's a local check. Formatter (`format.scm`) and LSP keyword completion learn the keyword.
92
+ **Test code:** checker or driver tests for a passing case and each failure reason, with exact positions. Parser corpus for the keyword.
93
+
94
+ ### T6 pilot and verdict · Complexity 4 · Est. context 45k
95
+ **Owns:** tests/fbip/*.plum, docs/plans/2026-11-06-fbip.benchmark.md, docs/plans/2026-11-06-fbip-stage1.results.md
96
+ **Components:** 1
97
+ **LOC owned:** 150
98
+ **Tests:** slow
99
+ **Consumes:** `fip fun` is a compile error, at the exact spot and giving the reason, when a spread or `Array.with` in that function isn't proved in place (T5)
100
+ **Produces:** a verdict on stage 2, with numbers: a pilot persistent `Vec[T]` written only in Plum shows which real shapes update in place
101
+ e.g. the results doc has a table of shape, proved or not, ms versus `Slice`
102
+ **Notes:** `tests/fbip/vec.plum`: `struct Vec[T](data: Array[T], size: Int)` with `push(self, v) -> Vec[T]`, `set`, `at`, growing. Shapes: local in a loop; struct field `self.items = self.items.push(x)` through a `mut` field; field of a nested struct; a helper taking and returning it; generic instantiation; a value also read between updates; two lists in a struct; a list of lists; a game-like `update` loop over 20,000 items. For each: proved or not, reason, and time against the same shape on `Slice`. Then count, across `std` and `examples`, how many of the 363 mutating call sites look like shapes that work. Report honestly, including if the answer is no: the verdict is the point of this stage.
103
+ **Test code:** the pilot tests under `plum test`, and under `--no-fbip` for same answers; results doc with the verdict.
104
+
105
+ ## Review Focus
106
+ - A: state - no wrongly proved uniqueness; every alias shape stays a copy.
107
+ - B: state - `--no-fbip` output matches the baseline; default output behaves the same.
108
+ - C: encoding - `Array.with` on flat-struct arrays writes every field array.