plum

#treesitter#compiler#wasm

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

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


60b292c — Peter John 2026-11-04T09:50:09+05:30
docs(plans): language restrictions: Io capability instead of io fun
docs/plans/2026-11-03-language-restrictions.review.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Language restrictions for a simpler supercompiler: review plan
2
2
 
3
- **Builds on:** master after `2026-11-03-supercompiler` (fusion, uniqueness, reuse merged).
3
+ **Builds on:** master b3aeaba (supercompiler merged; `--no-fusion` / `--explain-fusion` flags).
4
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
5
 
6
6
  ## Summary
@@ -9,7 +9,7 @@ The supercompiler build showed where analysis gets hard. Each restriction below
9
9
  | # | Restriction | What it removes |
10
10
  |---|---|---|
11
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 |
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 |
15
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) |
@@ -22,12 +22,13 @@ The supercompiler build showed where analysis gets hard. Each restriction below
22
22
  - Structs stay plain values: assigning a field of a local struct is a local update, as today.
23
23
  - Std collections (`Slice`, `Array`, `Map`, `Buffer`) keep their mutating methods: they become `mut` types.
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
- 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.
25
+ 2. **Effects through an `Io` capability (user decision 2026-11-04: Zig `std.Io` style, no function colouring).**
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.
27
+ - `export fun init(io: Io)`, `update(io: Io, w: World, dt: Float)`, `draw(io: Io, w: World)`, `main(io: Io)` and `test` blocks receive an `Io` from the host.
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
+ - 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
+ - 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
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.
32
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.
33
34
  5. **Pattern casing.**
@@ -41,11 +42,11 @@ The supercompiler build showed where analysis gets hard. Each restriction below
41
42
  | Casing rule (5) | checker errors + tests; simplify/drive use the rule | S |
42
43
  | Fusion before lowering (3) | move the fusion stage in typedSource; drop wrapper handling in drive/simplify | M |
43
44
  | 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
+ | `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 |
45
46
  | `mut record` / `mut trait` (1a) | grammar, checker error on field assignment of immutable types, `mut` std collections; migrate std/examples/games | L |
46
47
  | Spread in place without `own` (1b) | uniqueness covers all migrated pure code; own report re-run must be all proved | M |
47
48
  | 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
+ | Docs | README: immutability, `mut`, the `Io` capability, pattern casing, fusion eligibility | S |
49
50
 
50
51
  ## How it fits together
51
52
  ```mermaid
@@ -53,14 +54,14 @@ flowchart LR
53
54
  C5["5 casing rule"] --> F["supercompiler passes simpler"]
54
55
  L3["3 fuse before lowering"] --> F
55
56
  E4["4 expression-bodied candidates"] --> F
56
- IO["2 io effects"] --> PU["purity = not io"] --> F
57
+ IO["2 Io capability"] --> PU["purity = no Io reachable"] --> F
57
58
  M1["1a immutable + mut record"] --> U["1b uniqueness covers in-place"] --> R["1c remove own"]
58
59
  ```
59
60
 
60
61
  ## Decisions you can change
61
- - **Effect keyword.**
62
+ - **Effects.**
62
- - Default: `io fun`.
63
+ - Chosen: `Io` capability parameter (user, 2026-11-04).
63
- - Alternatives: `fun!` (Koka/Roc-like marker), or an `effects` clause.
64
+ - Rejected: `io fun` keyword and `fun!` markers (function colouring), implicit inference (hidden).
64
65
  - **Mutable keyword.**
65
66
  - Default: `mut record` / `mut trait`.
66
67
  - Alternative: `class` for mutable reference types.
@@ -79,7 +80,7 @@ flowchart LR
79
80
  - migration runs per folder;
80
81
  - suites stay green at every step.
81
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.
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
+ - **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.
83
84
  - **R4: `mut` creep.** If most records end up `mut`, the benefit shrinks. The plan reports how many types became `mut` and why.
84
85
 
85
86
  ## Out of scope