plum

#treesitter#compiler#wasm

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

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


9855142 — Peter John 2026-11-04T20:37:32+05:30
docs: language simplification review plan
docs/plans/2026-11-04-language-simplification.review.md ADDED
@@ -0,0 +1,135 @@
1
+ # Language simplification: review plan
2
+
3
+ ## Summary
4
+ Plum carries several features that each need their own compiler machinery but give little that the rest of the language can't already do. This plan removes them before the compiler is refactored further (node ids, file splits), so that later work has less to carry:
5
+ - traits
6
+ - union enums
7
+ - value enums
8
+ - generators
9
+ - variadics
10
+ - the duplicate error forms
11
+ - declare-on-first-use `=`
12
+ - the separate `record` keyword
13
+
14
+ What stays is a smaller language:
15
+ - one product type, `struct`, where embedding gives subtyping and `override`;
16
+ - one enum kind, with inline variants;
17
+ - one error model, `T!E` and `T?`;
18
+ - `:=` to declare and `=` to reassign.
19
+
20
+ Decided by you on 2026-11-04 (decision record §13).
21
+
22
+ ## What you will get
23
+ - **Traits are gone.** A struct that embeds another struct first is a subtype of it: you can pass a `Ship` wherever a `Node` is expected, and `override fun` on `Ship` runs when called through a `Node`. The scene graph's `trait Actor` becomes a base struct.
24
+ - **Union enums are gone.** `JsonParse` and the union examples become ordinary enums with inline variants (`A(..) | B(..)`), compiled directly as subtypes of the enum, with no hidden wrapper records.
25
+ - **Value enums are gone** (explicit numbers like `Red = 1`). Nothing uses them today.
26
+ - **Generators are gone** (`fun*` / `yield`). `Seq` and its 35 generator functions across 6 files are rewritten on plain functions and loops.
27
+ - **Variadics are gone** (`...T`). `List(1, 2, 3)` and `Map("a" => 1)` keep working as list literals passed to a normal parameter; the 10 uses in 6 files are updated.
28
+ - **One error model.** `T!E` with error sets, and `T?`. Whatever other error forms the checker still supports (the `?.` → `andThen` special case and older fallbacks) are removed. The 10 `?.` uses, all in one file, are rewritten.
29
+ - **`=` only reassigns.** Using `=` on a name that doesn't exist yet becomes a compile error that suggests `:=`, and every such use in std and the examples is fixed.
30
+ - **One product type.** `record` (211 uses in 55 files) and `struct` (37 uses in 19 files) become one `struct`: immutable, with the compiler choosing whether each value is boxed. `mut struct` keeps today's `mut record` meaning until FBIP removes it.
31
+ - **A much smaller compiler.** These go away:
32
+ - in plum-core: union lowering, inline-variant hoisting, and the trait stand-ins created in embeds/ctors and lowered by traits.rs;
33
+ - in the checker: the union plan, variadic intrinsics, generator typing and value-enum checks;
34
+ - in codegen: value-enum helpers and union forwarders.
35
+ - **Nothing lost in programs.** Every test (Rust and Plum) and every example still passes after the rewrite, and the games still run in the browser check.
36
+
37
+ ## What will be built
38
+ | Run | Part | What it does | Size | Built by |
39
+ |---|---|---|---|---|
40
+ | A | Value enums out | Removes the syntax, checker and codegen paths for explicit enum numbers | S | a careful helper |
41
+ | A | Variadics out | Removes `...T`; `List(..)`/`Map(..)` take a list literal; updates the 6 files | M | a careful helper |
42
+ | A | Generators out | Removes `fun*`/`yield`; rewrites Seq and its users on loops and functions | L | careful helpers |
43
+ | A | Error model | Inventories the error forms the checker supports, keeps `T!E` + `T?`, rewrites `?.` uses, deletes the rest | M | a careful helper |
44
+ | A | `=` reassigns only | Turns declare-on-first-use into an error with a `:=` hint and fixes every site | M | a careful helper + a fast helper for the mechanical fixes |
45
+ | B | One `struct` | Merges `record` into `struct` (keyword, checker, codegen boxing choice) and rewrites the 55 files | L | careful helpers |
46
+ | C | Embedding as subtyping | First-position embedding makes a subtype; `override` dispatches on the concrete type; box-free wasm-gc base/subtype layout | L | careful helpers |
47
+ | C | Traits out | Rewrites the scene graph's `Actor` and the trait examples on base structs; deletes trait syntax and lowering | M | a careful helper |
48
+ | C | Unions out, inline variants native | Rewrites `JsonParse` and the union examples; inline variants compile directly; deletes union lowering and the union plan | L | careful helpers |
49
+ | each run | Verify | All Rust tests, `plum test` over std and examples, browser game check; deleted-code line counts | S | a careful helper |
50
+
51
+ ## How it fits together
52
+ ```mermaid
53
+ flowchart LR
54
+ S["struct (one product type)"] --> E["embedding = subtyping + override"]
55
+ E --> A["scene Actor base struct"]
56
+ V["enum with inline variants (one enum kind)"] --> J["JsonParse, examples"]
57
+ R["T!E and T? (one error model)"] --> C["checker"]
58
+ D[":= declares, = reassigns"] --> C
59
+ E --> C
60
+ V --> C
61
+ C --> G["codegen: base/subtype structs, no forwarders"]
62
+ ```
63
+
64
+ ## How it gets built
65
+ ```mermaid
66
+ flowchart TB
67
+ subgraph RA["Run A: removals that touch only syntax and std"]
68
+ A1[value enums] --- A2[variadics] --- A3[generators + Seq] --- A4[error model] --- A5["= reassigns"]
69
+ end
70
+ subgraph RB["Run B: one struct"]
71
+ B1[record into struct]
72
+ end
73
+ subgraph RC["Run C: subtyping, no traits, no unions"]
74
+ C1[embedding subtyping + override] --> C2[traits out]
75
+ C1 --> C3[unions out, native inline variants]
76
+ end
77
+ RA --> RB --> RC --> N["then: explicit function generics, cleanup 3b"]
78
+ ```
79
+ Each run is its own branch, its own machine plan and its own approval, and each ends with everything green and merged before the next starts. Run B comes before C because subtyping is defined on the merged `struct`.
80
+
81
+ ## Decisions you can change
82
+ - **Which embedding subtypes:**
83
+ - (default) only the first embedded struct makes a subtype, today's rule for trait layouts;
84
+ - or any embedded struct does.
85
+ - **Behaviour-only interfaces (a function taking "anything that can draw"):**
86
+ - (default) a base struct with methods that must be overridden; a method without a body is abstract, and a subtype that doesn't override it is a compile error;
87
+ - or none at all.
88
+ - **`==` on structs:**
89
+ - (default) stays identity, with deep structural `==` remaining its own deferred item;
90
+ - or structural `==` is done in run B.
91
+ - **Boxing in run B:**
92
+ - (default) the compiler chooses: small immutable structs without embedding are unboxed, the rest boxed, shown by `--explain`;
93
+ - or keep the two layouts under an explicit hint.
94
+ - **`mut struct`:**
95
+ - (default) it stays until FBIP removes mutation;
96
+ - or remove `mut` now and rewrite the imperative games on functional updates.
97
+ - **Generators:**
98
+ - (default) `Seq` becomes a struct holding a step function, so laziness is kept;
99
+ - or `Seq` becomes eager lists, which is simpler but loses laziness.
100
+ - **Order:**
101
+ - (default) runs A, B, C, then explicit function generics;
102
+ - or explicit generics between A and B.
103
+
104
+ ## Risks
105
+ - **The scene graph and games depend on traits.** Run C ports `std/game/imperative/Scene`, `std/game/pure/PureScene` and the examples first, with the browser game check as the gate. Dispatch cost has to stay at today's box-free subtyped speed. The 2026-11-03 benchmarks showed boxed dispatch at +92% on 20k nodes, so the new layout reuses the box-free path; the benchmark is rerun before merging.
106
+ - **Seq laziness and fusion.** The supercompiler fuses `List` chains and parts of Seq. Rewriting Seq may change what fuses. The fusion allocation tests and the benchmark guard this, and a measured slowdown is reported, not hidden.
107
+ - **The `record` → `struct` rewrite is wide** (55 files). It is mostly a keyword change, done mechanically, but `record` and `struct` differ today in boxing and copy semantics. Every behaviour difference gets a test before the two are merged.
108
+ - **Removing declare-on-first-use** may surface many sites. The checker error lists them all, and a mechanical pass fixes them.
109
+ - **Two sessions on the compiler.** The kids-games `List.length` fix and these runs both touch the compiler. Each run starts from the newest `master`.
110
+ - **Wasm output will change.** Unlike the cleanup phases this is not byte-identical, so programs are judged by their test results and the game check, not by hashes.
111
+
112
+ ## Out of scope
113
+ - New features. Pattern-matching changes beyond what the removals need.
114
+ - FBIP and removing `mut`, unless you pick the alternative above.
115
+ - Explicit function generics, which follow as their own plan.
116
+ - `../kam_plum`. It uses none of the removed features except possibly `record`, which gets a mechanical rename note.
117
+ - Cleanup phases 3b to 6, which resume after this.
118
+
119
+ ## How we will know it works
120
+ - After each run: the full workspace tests and `plum test plum-std plum-examples` pass, and the browser game check passes.
121
+ - `grep` finds no `trait`, `fun*`, `yield`, `...T`, `record` keyword, value-enum or union-enum syntax in std, the examples or the grammar. The compiler rejects each with a clear message that points at the replacement.
122
+ - A `Ship` embedding `Node` passed as a `Node` calls `Ship`'s `override`. There's a test for this, and the scene examples use it.
123
+ - Lines removed from plum-core, the checker and codegen are counted per run. The expectation is several thousand lines, and the real number gets reported.
124
+ - The game benchmark is within noise of today's numbers (2026-11-03: 1.70–1.75 ms).
125
+
126
+ ## Rough time and cost
127
+ Rough, until each run's machine plan is linted:
128
+
129
+ | Run | Tasks | Time |
130
+ |---|---|---|
131
+ | A | 6–8 | 45–75 min |
132
+ | B | 5–7 | 40–60 min |
133
+ | C | 7–10 | 60–100 min |
134
+
135
+ About 2.5–4 hours in total, using Opus 5.5 at medium effort for agents as you asked.