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