plum

#treesitter#compiler#wasm

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

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


72f01b5 — Peter John 2026-11-04T10:04:32+05:30
docs(plans): language restrictions: per-field mut instead of mut record; FBIP is the next plan
docs/plans/2026-11-03-language-restrictions.md ADDED
@@ -0,0 +1,277 @@
1
+ # Language restrictions
2
+
3
+ **Review plan:** docs/plans/2026-11-03-language-restrictions.review.md
4
+ **Goal:** pattern casing enforced; fusion before union lowering; expression-bodied fusion candidates; host effects only through a std `Io` capability (purity = no `Io` reachable); record fields immutable unless declared `mut`; then remove `own`; the remaining `mut` fields are the worklist for the follow-up FBIP plan.
5
+ **Architecture:** checker rules in plum-checker; fusion stage moved earlier in typedSource; std `Io` record wraps the externs (externs callable only inside std/Io.plum); codegen synthesises host-facing exports that pass the singleton `Io`; grammar gains `mut` on record/trait fields; uniqueness extended to method params/self so pure code stays in place; `own` deleted last.
6
+ **Tech stack:** Rust workspace (plum-core, plum-checker, plum-wasm-codegen, plum-cli, plum-lsp), tree-sitter grammar, Plum std/examples, plum-game (Rust host + JS bridge).
7
+ **Spec:** docs/plans/2026-11-03-language-restrictions.review.md
8
+ **Detail:** contracts
9
+ **Target-min:** 60
10
+ **Budget-usd:** 80
11
+
12
+ Global constraints:
13
+ - Repo /Users/pyrossh/Code/plum, branch superlite/sessions/language-restrictions (base master b3aeaba).
14
+ - Run tests ONLY via `bash "$SUPERLITE/skills/swarm/scripts/superlite-cli" run_tests --fast <files>` (real runs: `<crate>/tests/x.rs` -> cargo test -p crate --test x; `.plum`/plum-std/plum-examples -> plum test; plum-game paths -> its manifests; output .superlite/test-output.log). Raw cargo test is hook-blocked. `touch` edited .rs first.
15
+ - Fusion switches are CLI flags: `--no-fusion`, `--explain-fusion`; in Rust tests use `plum_checker::withFusionOptions`.
16
+ - Grammar: `cd plum-tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate --disable-optimizations`, then `npx --yes tree-sitter-cli test`; every std/example file must still parse. Never `plum format` a real file.
17
+ - Plum test sources: single-letter type names parse as generics; `test "name"` + `assert`.
18
+ - Every suite green after each task (full suite = cargo workspace + plum-std + plum-examples + plum-game host/web tests), fusion on and off.
19
+ - Error messages say exactly what to write instead (they drive the migrations).
20
+ - A compiler gap found while migrating: incident + minimal repro in .superlite/repros/, fixed in the compiler, never worked around silently.
21
+
22
+ ## Main diagram
23
+ ```mermaid
24
+ flowchart LR
25
+ A["T1 casing"] --> B["T2 fuse before lowering"] --> C["T3 expression-bodied"]
26
+ D["T4 std Io + entry points"] --> E["T5 purity = no Io"] --> F["T6 migrate to io"]
27
+ G["T7 mut fields + rule"] --> H["T8 migrate mutation"] --> I["T9 uniqueness: params/self"] --> J["T10 remove own"] --> K["T11 docs + bench"]
28
+ C --> K
29
+ F --> K
30
+ ```
31
+
32
+ ## File structure
33
+ Each task's Owns line is its file list.
34
+
35
+ Spec map: Summary, The new rules, What will be built -> T1-T11; How it fits together -> main diagram; Decisions you can change -> chosen defaults (`Io` capability, per-field `mut`, structs stay value-mutable, order 5,3,4,2,1); Risks -> R1 migration per folder T6/T8, R2 bench T11, R3 io threading T6, R4 mut-field count reported T8/T14; Out of scope -> not built; How we will know it works -> task tests + T11; Rough time and cost -> schedule.
36
+
37
+ ## Task graph
38
+ ```mermaid
39
+ flowchart LR
40
+ T1[casing]
41
+ T2[fuse early]
42
+ T3[expr bodied]
43
+ T4[io type]
44
+ T5[purity io]
45
+ T6[migrate io]
46
+ T7[mut rule]
47
+ T8[migrate mut]
48
+ T9[uniq params]
49
+ T10[migrate own]
50
+ T11[docs bench]
51
+ T12[io entry]
52
+ T13[migrate io ex]
53
+ T14[migrate mut ex]
54
+ T15[delete own]
55
+ %% taskgraph
56
+ T1 --> T2
57
+ T2 --> T3
58
+ T3 --> T4
59
+ T4 --> T12
60
+ T12 --> T5
61
+ T5 --> T6
62
+ T6 --> T13
63
+ T13 --> T7
64
+ T7 --> T8
65
+ T8 --> T14
66
+ T14 --> T9
67
+ T9 --> T10
68
+ T10 --> T15
69
+ T15 --> T11
70
+ ```
71
+
72
+ Ruling: sequential [chain-lr] - T1 T2 T4 T5 T7 T9 T10 all change plum-checker/src/lib.rs and/or plum-core passes, and each migration task compiles against the previous rule; one shared checkout - cost if wrong: ~30 min longer wall clock.
73
+
74
+ ## Predicted schedule
75
+ Generated by `superlite-cli plan_tools lint`.
76
+
77
+ ## Small rules
78
+
79
+ **Probe entry:** run=./target/debug/plum test plum-std plum-examples
80
+ Package: compiler; categories: deep, state.
81
+
82
+ ### T1 pattern casing enforced · Complexity 3 (O-1) · Est. context 40k
83
+ **Owns:** plum-checker/src/lib.rs, plum-checker/tests/pattern_casing_test.rs, plum-core/src/simplify.rs, plum-core/src/drive.rs
84
+ **Components:** 2
85
+ **LOC owned:** 90
86
+ **Tests:** fast
87
+ **Model:** sonnet
88
+ **Est. time:** 3 min
89
+ **Consumes:** none
90
+ **Produces:** checker errors: Capitalised pattern name that is no known tag -> "unknown tag Foo"; lowercase pattern name equal to a tag -> "did you mean Foo?"; simplify/drive `isTag`/`isTagName` replaced by the casing rule
91
+ **Review Focus:** deep: nested patterns, union members, error names.
92
+ **Test code:** `unknown_capitalised_tag_rejected`, `lowercase_tag_name_rejected`, `error_names_are_tags`; existing suites green.
93
+
94
+ ### T2 fusion before union lowering · Complexity 5 (O-1) · Est. context 50k
95
+ **Owns:** plum-checker/src/lib.rs, plum-core/src/drive.rs, plum-core/src/simplify.rs, plum-checker/tests/fusion_pipeline_test.rs
96
+ **Components:** 2
97
+ **LOC owned:** 140
98
+ **Tests:** fast
99
+ **Model:** sonnet
100
+ **Est. time:** 5 min
101
+ **Consumes:** none
102
+ **Produces:** typedSource runs the fusion stage on the checked, not-yet-union-lowered program; union lowering runs on the fused result; drive/simplify drop `List__Cons(Cons(..))` wrapper handling
103
+ **Review Focus:** state: re-check fallback still works; fusion_alloc results unchanged.
104
+ **Test code:** `fusion_runs_before_union_lowering` (explain note for a union-member pipeline); plum-wasm-codegen/tests/fusion_alloc_test.rs unchanged and green; LOC removed from drive/simplify reported.
105
+
106
+ ### T3 fusion candidates must be expression-bodied · Complexity 3 (O-1) · Est. context 40k
107
+ **Owns:** plum-core/src/drive.rs, plum-core/tests/drive_test.rs, plum-std/List.plum, plum-std/game/functional/Picture.plum
108
+ **Components:** 1
109
+ **LOC owned:** 90
110
+ **Tests:** fast
111
+ **Model:** sonnet
112
+ **Est. time:** 3 min
113
+ **Consumes:** none
114
+ **Produces:** drive considers only fns whose body is one expression (`= match` / `= if` / `= Ctor(..)` / `= call`); others get note "not expression-bodied"; block-body/early-return handling removed; std List and Picture fns that should fuse are expression-bodied
115
+ **Review Focus:** state: List chain and view->draw still fuse (fusion_alloc_test green).
116
+ **Test code:** `block_bodied_producer_not_fused_note`; fusion_alloc_test green; drive LOC removed reported.
117
+
118
+ ## Io capability
119
+
120
+ **Probe entry:** run=./target/debug/plum test plum-std plum-examples
121
+ Package: compiler + std + host; categories: protocol, state.
122
+
123
+ ### T4 std `Io` type, extern restriction, entry points · Complexity 6 (O-1) · Est. context 60k
124
+ **Owns:** plum-std/Io.plum, plum-std/game/Game.plum, plum-std/Os.plum, plum-std/Time.plum, plum-std/Random.plum, plum-std/Http.plum, plum-checker/src/lib.rs, plum-checker/tests/io_capability_test.rs
125
+ **Components:** 2
126
+ **LOC owned:** 140
127
+ **Tests:** fast
128
+ **Model:** sonnet
129
+ **Est. time:** 5 min
130
+ **Consumes:** none
131
+ **Produces:** `record Io` in plum-std/Io.plum (zero fields; constructor usable only from std) with methods wrapping every extern (game host API, printLn, file/env/time/random/http); checker errors: an `extern fun` called outside plum-std/Io.plum ("printLn needs io: call io.printLn(..)"), `Io()` constructed outside std; old std APIs (Os.printLn etc.) stay as deprecated thin wrappers until T6/T13 migrate callers, and the extern restriction is behind a temporary `--allow-raw-io` option until then
132
+ **Review Focus:** state: no std file outside Io.plum calls an extern once migrated.
133
+ **Test code:** `extern_outside_io_rejected`, `io_constructor_outside_std_rejected`.
134
+
135
+ ### T12 entry points receive `io` · Complexity 4 (O-1) · Est. context 45k
136
+ **Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/io_entry_e2e_test.rs
137
+ **Components:** 1
138
+ **LOC owned:** 80
139
+ **Tests:** fast
140
+ **Model:** sonnet
141
+ **Est. time:** 3 min
142
+ **Consumes:** none
143
+ **Produces:** `export fun` / `main` / `test` entry points may declare a first param `io: Io`; codegen passes a singleton `Io` and keeps the host ABI unchanged (wasm exports keep today's signatures, without the io param)
144
+ **Review Focus:** protocol: host ABI unchanged (plum-game host_test, web_check).
145
+ **Test code:** `entry_point_receives_io`, `export_abi_unchanged_with_io_param`.
146
+
147
+ ### T5 purity = no `Io` reachable · Complexity 4 (O-1) · Est. context 45k
148
+ **Owns:** plum-core/src/purity.rs, plum-core/tests/purity_test.rs
149
+ **Components:** 1
150
+ **LOC owned:** 120
151
+ **Tests:** fast
152
+ **Model:** sonnet
153
+ **Est. time:** 4 min
154
+ **Consumes:** none
155
+ **Produces:** plum_core::purity::pureFns(src: &Source) -> HashSet<String> now: a fn is pure iff no `Io` is reachable from its param types (records/enums/closures containing Io, transitively) or its captures, and it calls no extern; field writes on params stay impure until T8
156
+ e.g. `fun view(s: State) -> SceneNode = ...` -> pure; `fun render(io: Io, n: SceneNode) = io.drawRect(..)` -> impure; `fun f(w: World)` where World has field `io: Io` -> impure
157
+ **Review Focus:** deep: Io inside generics / closures / records.
158
+ **Test code:** the e.g. cases; fusion_alloc_test green.
159
+
160
+ ### T6 migrate std to `io` · Complexity 4 (O-1) · Est. context 50k
161
+ **Owns:** plum-std/game/functional/Picture.plum, plum-std/game/Scene.plum, plum-std/game/pure/PureScene.plum, plum-std/game/pure/SceneTree.plum, plum-std/game/pure/SceneInput.plum, plum-std/Testing.plum
162
+ **Components:** 1
163
+ **LOC owned:** 110
164
+ **Tests:** fast
165
+ **Model:** sonnet
166
+ **Est. time:** 4 min
167
+ **Consumes:** none
168
+ **Produces:** std draw/input paths take `io` (`Picture.draw(io, node)`, Scene draw, `SceneInput.pollPointer(io, ..)`); std calls no extern outside Io.plum
169
+ **Review Focus:** state: pure std code (view helpers, step, Anim, Ui logic) takes no io.
170
+ **Test code:** plum-std green; `grep -rn "extern fun" plum-std` only in Io.plum.
171
+
172
+ ### T13 migrate examples, games, fixtures, host tests to `io` · Complexity 4 (O-1) · Est. context 55k
173
+ **Owns:** plum-examples, plum-game/web-test/fixtures, plum-game/tests/host_test.rs, README.md, plum-std/Io.plum, plum-checker/src/lib.rs
174
+ **Components:** 2
175
+ **LOC owned:** 100
176
+ **Tests:** slow
177
+ **Model:** sonnet
178
+ **Est. time:** 5 min
179
+ **Consumes:** none
180
+ **Produces:** examples' printLn -> `io.printLn` with `main(io: Io)` / test io; game `init/update/draw` take `io`; deprecated wrappers and `--allow-raw-io` removed
181
+ **Review Focus:** protocol: host parity + browser checks; state: pure code (view/step/rules) takes no io.
182
+ **Test code:** full suite green; host_test + web_check green; `grep -rn "extern fun" plum-std` only in Io.plum.
183
+
184
+ ## Immutable records
185
+
186
+ **Probe entry:** run=./target/debug/plum test plum-std plum-examples
187
+ Package: compiler + std; categories: state, deep.
188
+
189
+ ### T7 `mut` fields grammar and the immutability rule · Complexity 5 (O-1) · Est. context 50k
190
+ **Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src, plum-tooling/tree-sitter-plum/test/corpus/mut_fields.txt, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-checker/src/lib.rs, plum-checker/tests/mutability_test.rs
191
+ **Components:** 3
192
+ **LOC owned:** 150
193
+ **Tests:** fast
194
+ **Model:** sonnet
195
+ **Est. time:** 5 min
196
+ **Consumes:** none
197
+ **Produces:** `mut name: T` parses in record/trait field lists (AST field flag `isMut`, carried through embedding); assigning a non-`mut` field is an error "Node.x is immutable; use Node(..n, x: v), or declare the field `mut`"; structs keep value-local field assignment; behind a temporary `--allow-mutation` CLI flag/option until T8 finishes migrating (so the tree stays green)
198
+ **Review Focus:** state: embedded records/traits inherit mutability rules; generics.
199
+ **Test code:** `immutable_field_assign_rejected`, `mut_field_assign_ok`, `embedded_mut_field_ok`, `struct_local_assign_ok`, corpus mut_fields.txt.
200
+
201
+ ### T8 migrate std mutation · Complexity 5 (O-1) · Est. context 55k
202
+ **Owns:** plum-std
203
+ **Components:** 1
204
+ **LOC owned:** 130
205
+ **Tests:** fast
206
+ **Model:** sonnet
207
+ **Est. time:** 5 min
208
+ **Consumes:** none
209
+ **Produces:** std: mutable state fields of Slice/Map/Buffer and other array-wrapping records and of the mutable std/game/Scene become `mut` fields; std/game/pure (PureScene, SceneTree, SceneInput, Actions) and functional switch field writes to spreads; mut-field table (type.field, why) in the progress log
210
+ **Review Focus:** state: results identical; R4 mut count.
211
+ **Test code:** `plum test plum-std` green without `--allow-mutation`.
212
+
213
+ ### T14 migrate examples and fixtures; drop the escape hatch · Complexity 4 (O-1) · Est. context 50k
214
+ **Owns:** plum-examples, plum-game/web-test/fixtures, plum-checker/src/lib.rs
215
+ **Components:** 2
216
+ **LOC owned:** 100
217
+ **Tests:** slow
218
+ **Model:** sonnet
219
+ **Est. time:** 5 min
220
+ **Consumes:** none
221
+ **Produces:** examples/games/fixtures: game shells' changing fields (World.state etc.) `mut`, pure game code on spreads; `--allow-mutation` removed from the checker/CLI
222
+ **Review Focus:** state: game behaviour identical (host parity, browser checks).
223
+ **Test code:** full suite green; host_test + web_check green.
224
+
225
+ ### T9 uniqueness covers method params and `self` · Complexity 6 (O-1) · Est. context 55k
226
+ **Owns:** plum-core/src/uniqueness.rs, plum-core/tests/uniqueness_test.rs
227
+ **Components:** 1
228
+ **LOC owned:** 180
229
+ **Tests:** fast
230
+ **Model:** sonnet
231
+ **Est. time:** 5 min
232
+ **Consumes:** none
233
+ **Produces:** markUnique proves spreads whose source is a method param or `self` (whole-program fixpoint over method call sites incl. trait dispatch), so pure scene updates stay in place without `own`
234
+ **Review Focus:** state: soundness (aliasing via receivers, trait dispatch, closures).
235
+ **Test code:** `self_spread_in_method_unique`, `trait_dispatch_param_unique`, `aliased_receiver_not_unique`; the own report recount: every former own site proved or `mut`.
236
+
237
+ ### T10 migrate away from `own` · Complexity 5 (O-1) · Est. context 55k
238
+ **Owns:** plum-std, plum-examples, plum-game/web-test/fixtures, plum-checker/tests, plum-wasm-codegen/tests
239
+ **Components:** 2
240
+ **LOC owned:** 130
241
+ **Tests:** fast
242
+ **Model:** sonnet
243
+ **Est. time:** 5 min
244
+ **Consumes:** none
245
+ **Produces:** no `own` left in std/examples/fixtures (own_list.plum etc. migrated); own tests deleted or converted to uniqueness tests with equal struct.new counts
246
+ **Review Focus:** state: in-place counts preserved.
247
+ **Test code:** full suite green with `own` still accepted by the compiler.
248
+
249
+ ### T15 delete `own` from the compiler · Complexity 4 (O-1) · Est. context 50k
250
+ **Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src, plum-tooling/tree-sitter-plum/test/corpus/own.txt, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-checker/src/lib.rs, plum-checker/src/ownership.rs, plum-wasm-codegen/src/lib.rs
251
+ **Components:** 2
252
+ **LOC owned:** 140
253
+ **Tests:** fast
254
+ **Model:** sonnet
255
+ **Est. time:** 5 min
256
+ **Consumes:** none
257
+ **Produces:** no `own` keyword, AST flags (isOwn, ownSelf), ownership.rs or codegen own paths
258
+ **Review Focus:** state: nothing else depended on own flags (uniqueness, union_lower forwarders).
259
+ **Test code:** full suite green; `grep -rn "\bown\b"` finds no language use.
260
+
261
+ ### T11 docs and benchmark · Complexity 3 (O-1) · Est. context 35k
262
+ **Owns:** README.md, docs/plans/2026-11-03-language-restrictions.benchmark.md
263
+ **Components:** 1
264
+ **LOC owned:** 80
265
+ **Tests:** slow
266
+ **Model:** sonnet
267
+ **Est. time:** 4 min
268
+ **Consumes:** none
269
+ **Produces:** README: immutability + spread, `mut`, `Io` capability, pattern casing, fusion eligibility, own removed; headed benchmark (stress_scene, stress_pure_scene, stress_state_view) vs the supercompiler's table
270
+ **Review Focus:** numeric: no unexplained regression.
271
+ **Test code:** README snippets parse (plum-lsp test readmeSnippetsHaveNoSyntaxDiagnostics); benchmark table.
272
+
273
+ ## Review Focus
274
+ Per package lines above.
275
+
276
+ ## Self-review
277
+ Spec map covers every section; tasks are sequential (one ruling); no two concurrent tasks share files.
docs/plans/2026-11-03-language-restrictions.review.md CHANGED
@@ -8,7 +8,7 @@ The supercompiler build showed where analysis gets hard. Each restriction below
8
8
 
9
9
  | # | Restriction | What it removes |
10
10
  |---|---|---|
11
- | 1 | Records are immutable; `mut record` (and `mut trait`) opt in to field assignment | `own`, the ownership checker, alias analysis for in-place field writes |
11
+ | 1 | Record and trait fields are immutable unless declared `mut` (OCaml-style `mut` fields) | `own`, the ownership checker, alias analysis for in-place field writes |
12
12
  | 2 | Effects go through an `Io` capability value (Zig `std.Io` style) | inferred purity and its mistakes, with no function colouring |
13
13
  | 3 | Fusion runs before union lowering | drive/simplify having to see through `List__Cons(Cons(..))` wrappers |
14
14
  | 4 | Only expression-bodied functions (`= ...`, incl. `= match`) are fusion candidates | handling early `return`s, statement matches, wrappers in drive |
@@ -16,11 +16,11 @@ The supercompiler build showed where analysis gets hard. Each restriction below
16
16
 
17
17
  ## The new rules
18
18
  1. **Immutable records by default.**
19
- - `record` and `trait` fields can't be assigned (`n.x = 1` is a compile error: "Node is immutable; use Node(..n, x: 1), or declare it `mut record`").
19
+ - `record` and `trait` fields can't be assigned (`n.x = 1` is a compile error: "Node is immutable; use Node(..n, x: 1), or declare the field `mut`").
20
20
  - Updates use spread: `Node(..n, x: n.x + 1)`. When the compiler proves `n` is unique and dead after the spread (the uniqueness pass from the supercompiler), the spread is written in place. That is the job `own` does today.
21
- - `mut record` / `mut trait` keep today's mutable behaviour, for the mutable `std/game/Scene`, `Buffer`, game shells and similar.
21
+ - A field declared `mut` (`record Slice[T](mut data: Array[T], start: Int, mut size: Int, mut cap: Int)`) can be assigned; all other fields can only change through spread. Embedding copies each field's `mut`. Used for the mutable `std/game/Scene`, `Buffer`, game shells and similar (user decision 2026-11-04: per-field `mut`, not `mut record`).
22
22
  - Structs stay plain values: assigning a field of a local struct is a local update, as today.
23
- - Std collections (`Slice`, `Array`, `Map`, `Buffer`) keep their mutating methods: they become `mut` types.
23
+ - Std collections (`Slice`, `Array`, `Map`, `Buffer`) keep their mutating methods: their mutable state fields are `mut` (e.g. `mut size`, `mut data`).
24
24
  - **`own` is removed** (keyword, `own record`/`own trait`/`own` params, plum-checker/src/ownership.rs) once the migrated pure code is proven in place by uniqueness. The `own` report from the supercompiler (docs/plans/2026-11-03-supercompiler.own-report.md) becomes the acceptance check: every former `own` in-place site is either an in-place spread proved unique, or now lives in a `mut` type.
25
25
  2. **Effects through an `Io` capability (user decision 2026-11-04: Zig `std.Io` style, no function colouring).**
26
26
  - Host I/O is reachable only through a value of the std type `Io`: the host API becomes methods on it (`io.drawRect(..)`, `io.isKeyDown(Key.Left)`, `io.printLn(..)`, `io.soundPlay(..)`, `io.time()`, `io.random()` …). Raw `extern fun`s are only callable from std's `Io` implementation.
@@ -28,7 +28,7 @@ The supercompiler build showed where analysis gets hard. Each restriction below
28
28
  - No keyword and no split between kinds of function: `Io` is an ordinary value any function, generic or closure can take or capture.
29
29
  - Purity is a type fact: a function is pure if no `Io` is reachable from its parameters (including inside records/closures passed in) or its captures. Fusion uses this directly: no inference.
30
30
  - Tests and the queued deterministic-replay plan can pass a fake/recording `Io` (the host-parity native backend already has the shape).
31
- - `Io` itself is a `mut` type (it wraps host state).
31
+ - `Io` has no fields: host state lives in the host.
32
32
  3. **Fusion before union lowering.** The fusion stage moves to the checked, not-yet-lowered program; lowering runs after on the fused result. No language change.
33
33
  4. **Fusion candidates are expression-bodied.** A producer or consumer is considered only if its body is a single expression (`fun f(..) -> T = match ...` / `= if ...` / `= Ctor(..)`). Block-bodied functions are compiled as today and get an explain note "not expression-bodied". `Picture` and std `List` are already (or get rewritten to be) expression-bodied.
34
34
  5. **Pattern casing.**
@@ -43,7 +43,7 @@ The supercompiler build showed where analysis gets hard. Each restriction below
43
43
  | Fusion before lowering (3) | move the fusion stage in typedSource; drop wrapper handling in drive/simplify | M |
44
44
  | Expression-bodied candidates (4) | eligibility check + explain note; std List/Picture checked or rewritten | S–M |
45
45
  | `Io` capability (2) | std `Io` type wrapping the host externs; externs callable only from it; export/test entry points receive `io`; purity = no reachable `Io`; migrate std/examples/games/fixtures and plum-game host bindings | L |
46
- | `mut record` / `mut trait` (1a) | grammar, checker error on field assignment of immutable types, `mut` std collections; migrate std/examples/games | L |
46
+ | `mut` fields (1a) | grammar `mut name: T` in record/trait field lists, checker error on assigning a non-`mut` field; migrate std/examples/games | L |
47
47
  | Spread in place without `own` (1b) | uniqueness covers all migrated pure code; own report re-run must be all proved | M |
48
48
  | Remove `own` (1c) | grammar, AST, checker, ownership.rs, codegen own paths, docs; migrate remaining uses | M |
49
49
  | Docs | README: immutability, `mut`, the `Io` capability, pattern casing, fusion eligibility | S |
@@ -55,7 +55,7 @@ flowchart LR
55
55
  L3["3 fuse before lowering"] --> F
56
56
  E4["4 expression-bodied candidates"] --> F
57
57
  IO["2 Io capability"] --> PU["purity = no Io reachable"] --> F
58
- M1["1a immutable + mut record"] --> U["1b uniqueness covers in-place"] --> R["1c remove own"]
58
+ M1["1a immutable + mut fields"] --> U["1b uniqueness covers in-place"] --> R["1c remove own"]
59
59
  ```
60
60
 
61
61
  ## Decisions you can change
@@ -63,8 +63,8 @@ flowchart LR
63
63
  - Chosen: `Io` capability parameter (user, 2026-11-04).
64
64
  - Rejected: `io fun` keyword and `fun!` markers (function colouring), implicit inference (hidden).
65
65
  - **Mutable keyword.**
66
+ - Chosen: per-field `mut` (user, 2026-11-04).
66
- - Default: `mut record` / `mut trait`.
67
+ - Rejected: `mut record` / `mut trait` (whole-type), `class`.
67
- - Alternative: `class` for mutable reference types.
68
68
  - **Structs.**
69
69
  - Default: local struct field assignment stays allowed (structs are values).
70
70
  - Alternative: structs immutable too.
@@ -81,16 +81,17 @@ flowchart LR
81
81
  - suites stay green at every step.
82
82
  - **R2: performance after removing `own`.** In-place updates now depend on the uniqueness proof. The benchmark is re-run (pure scene, state/view, mutable), and any update that falls back to a copy is listed with its reason.
83
83
  - **R3: threading `io`.** Call chains that do I/O pass `io` along (`Picture.draw(io, node)`); pure code (view, step, rules) never sees it. Std higher-order functions stay generic: a closure capturing `io` is simply impure and won't fuse.
84
- - **R4: `mut` creep.** If most records end up `mut`, the benefit shrinks. The plan reports how many types became `mut` and why.
84
+ - **R4: `mut` creep.** If many fields end up `mut`, the benefit shrinks. The plan reports how many fields became `mut` and why; each one is a target for the follow-up FBIP plan.
85
85
 
86
86
  ## Out of scope
87
+ - Removing heap mutation entirely (Koka-style FBIP: persistent std collections updated in place when unique, `fip` functions as a checked in-place guarantee, a reference count as a uniqueness check only if static proof falls short). This is the next plan (user, 2026-11-04); the `mut` fields left after this plan are its worklist.
87
88
  - Structural `==` for records (deferred T7 of the state/view plan).
88
89
  - Generalisation in the supercompiler; deterministic replays (queued separately).
89
90
 
90
91
  ## How we will know it works
91
92
  - New checker tests for each rule (errors and messages).
92
93
  - Every suite green after each part, with fusion on and off.
93
- - The `own` report re-run shows every former `own` site proved in place or moved to a `mut` type; then `own` is gone from the grammar and repo.
94
+ - The `own` report re-run shows every former `own` site proved in place or now relies on a `mut` field; then `own` is gone from the grammar and repo.
94
95
  - drive/simplify shrink: wrapper handling, `isTag` helpers and block-body cases removed (line counts reported).
95
96
  - Benchmark table versus the supercompiler's, no unexplained regression.
96
97