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