plum

#treesitter#compiler#wasm

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

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


5a6af08 — Peter John 2026-11-03T23:38:00+05:30
docs(plans): language restrictions review plan (immutable records + mut record, io effects, pattern casing, fusion before lowering, expression-bodied candidates)
docs/plans/2026-11-03-language-restrictions.review.md ADDED
@@ -0,0 +1,97 @@
1
+ # Language restrictions for a simpler supercompiler: review plan
2
+
3
+ **Builds on:** master after `2026-11-03-supercompiler` (fusion, uniqueness, reuse merged).
4
+ **Goal (user, 2026-11-03):** make the language stricter where that removes whole classes of supercompiler work and bugs. All five restrictions are accepted. Mutation stays available through an explicit `mut record`.
5
+
6
+ ## Summary
7
+ The supercompiler build showed where analysis gets hard. Each restriction below removes one of those problems:
8
+
9
+ | # | Restriction | What it removes |
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 |
12
+ | 2 | Effects are explicit: a function that does I/O says `io` | inferred purity and its mistakes |
13
+ | 3 | Fusion runs before union lowering | drive/simplify having to see through `List__Cons(Cons(..))` wrappers |
14
+ | 4 | Only expression-bodied functions (`= ...`, incl. `= match`) are fusion candidates | handling early `return`s, statement matches, wrappers in drive |
15
+ | 5 | In patterns, Capitalised names are tags and lowercase names are binders, enforced | the tag-read-as-binder class of bug (the 25-vs-26 miscompile) |
16
+
17
+ ## The new rules
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`").
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.
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.
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
+ 2. **Explicit effects.**
26
+ - A function that calls an `extern` or another `io` function is declared `io fun`. Methods too (`io fun draw(self)`).
27
+ - The checker enforces it both ways: a non-`io` function calling an `io` one is an error ("draw calls drawRect, which does I/O; mark it io fun"); an `io fun` that does no I/O gets a warning.
28
+ - Closures are `io` if their body is; a non-`io` function may receive an `io` closure only through a parameter typed `io fn(..)`.
29
+ - `export fun init/update/draw` and `test` blocks may do I/O implicitly.
30
+ - Purity for fusion becomes "not `io`": no inference.
31
+ 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.
32
+ 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.
33
+ 5. **Pattern casing.**
34
+ - In a pattern, a Capitalised name is always a tag (variant, union member, error name); a lowercase name always binds.
35
+ - A Capitalised name that is not a known tag is an error ("unknown tag Foo"); a lowercase name that matches a tag is an error too ("did you mean Foo?").
36
+ - The supercompiler passes use this rule directly; their ad-hoc `isTag` helpers go.
37
+
38
+ ## What will be built
39
+ | Part | What it does | Size |
40
+ |---|---|---|
41
+ | Casing rule (5) | checker errors + tests; simplify/drive use the rule | S |
42
+ | Fusion before lowering (3) | move the fusion stage in typedSource; drop wrapper handling in drive/simplify | M |
43
+ | Expression-bodied candidates (4) | eligibility check + explain note; std List/Picture checked or rewritten | S–M |
44
+ | `io` effects (2) | grammar `io fun` / `io fn(..)`, checker rule + warning, purity = not io; migrate std/examples/games/fixtures | L |
45
+ | `mut record` / `mut trait` (1a) | grammar, checker error on field assignment of immutable types, `mut` std collections; migrate std/examples/games | L |
46
+ | Spread in place without `own` (1b) | uniqueness covers all migrated pure code; own report re-run must be all proved | M |
47
+ | Remove `own` (1c) | grammar, AST, checker, ownership.rs, codegen own paths, docs; migrate remaining uses | M |
48
+ | Docs | README: immutability, `mut`, `io`, pattern casing, fusion eligibility | S |
49
+
50
+ ## How it fits together
51
+ ```mermaid
52
+ flowchart LR
53
+ C5["5 casing rule"] --> F["supercompiler passes simpler"]
54
+ L3["3 fuse before lowering"] --> F
55
+ E4["4 expression-bodied candidates"] --> F
56
+ IO["2 io effects"] --> PU["purity = not io"] --> F
57
+ M1["1a immutable + mut record"] --> U["1b uniqueness covers in-place"] --> R["1c remove own"]
58
+ ```
59
+
60
+ ## Decisions you can change
61
+ - **Effect keyword.**
62
+ - Default: `io fun`.
63
+ - Alternatives: `fun!` (Koka/Roc-like marker), or an `effects` clause.
64
+ - **Mutable keyword.**
65
+ - Default: `mut record` / `mut trait`.
66
+ - Alternative: `class` for mutable reference types.
67
+ - **Structs.**
68
+ - Default: local struct field assignment stays allowed (structs are values).
69
+ - Alternative: structs immutable too.
70
+ - **Order.**
71
+ - Default: 5, 3, 4, then 2, then 1a → 1b → 1c.
72
+ - Alternative: 1 first.
73
+
74
+ ## Risks
75
+ - **R1: migration size.**
76
+ - 2 and 1a touch every std module, example, game and fixture.
77
+ - Mitigation:
78
+ - the compiler error messages say exactly what to add;
79
+ - migration runs per folder;
80
+ - suites stay green at every step.
81
+ - **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.
82
+ - **R3: `io` and generic higher-order std functions** (`List.map(f)` with an `io` closure). Handled by `io fn(..)` parameter types; std's higher-order functions stay pure, and code that needs effects inside a loop uses `for`.
83
+ - **R4: `mut` creep.** If most records end up `mut`, the benefit shrinks. The plan reports how many types became `mut` and why.
84
+
85
+ ## Out of scope
86
+ - Structural `==` for records (deferred T7 of the state/view plan).
87
+ - Generalisation in the supercompiler; deterministic replays (queued separately).
88
+
89
+ ## How we will know it works
90
+ - New checker tests for each rule (errors and messages).
91
+ - Every suite green after each part, with fusion on and off.
92
+ - 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.
93
+ - drive/simplify shrink: wrapper handling, `isTag` helpers and block-body cases removed (line counts reported).
94
+ - Benchmark table versus the supercompiler's, no unexplained regression.
95
+
96
+ ## Rough time and cost
97
+ About 8 parts: three small ones first (5, 3, 4), then the two large migrations (2, 1a), then `own` removal. Rough agent time is 2–3 hours plus one headed benchmark run.