plum
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 |
|
|
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
|
|
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
|
|
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:
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|