plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
9117837
— Peter John
2026-11-03T15:21:52+05:30
docs(plans): remove outdated and unbuilt plans; fix stale status lines
- docs/plans/2026-10-25-erased-generics.md +0 -228
- docs/plans/2026-10-25-erased-generics.progress.md +0 -17
- docs/plans/2026-10-25-roc-errors.incidents.jsonl +0 -5
- docs/plans/2026-10-25-roc-errors.md +0 -190
- docs/plans/2026-10-25-roc-errors.progress.md +0 -26
- docs/plans/2026-10-26-game-engine.md +0 -171
- docs/plans/2026-11-01-struct-value-types.review.md +1 -1
- docs/plans/2026-11-02-render-experiments.md +0 -72
- docs/plans/2026-11-03-inline-variants-match-body.review.md +1 -1
docs/plans/2026-10-25-erased-generics.md
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
# Erased generics + dyn dispatch (remove monomorphization)
|
|
2
|
-
|
|
3
|
-
**Goal:** Make the compiler simpler and faster by type-checking generics TypeScript-style and compiling each generic fn/type once. Primitives are boxed only at generic edges. Every class/enum value carries a type id for trait dispatch. List and Array are built into the compiler. `monomorphize.rs` is deleted.
|
|
4
|
-
**Architecture:** tree-sitter → `plum-core` AST (unchanged syntax) → `plum-checker` (structural generic types, no monomorphization) → `plum-wasm-codegen` (erased wasm-gc) → hosts.
|
|
5
|
-
**Tech stack:** Rust workspace, wasm-gc via wasm-encoder, wasmtime.
|
|
6
|
-
**Spec path:** this file, section "Design".
|
|
7
|
-
**Detail:** tests
|
|
8
|
-
**Target-min:** 40
|
|
9
|
-
|
|
10
|
-
## Design
|
|
11
|
-
|
|
12
|
-
What the user decided:
|
|
13
|
-
- Simpler compiler, with types erased like TypeScript.
|
|
14
|
-
- Values are boxed only at generic edges.
|
|
15
|
-
- Per-value vtables ("every type like dyn Trait").
|
|
16
|
-
- List/Map/Array built into the compiler.
|
|
17
|
-
- `[T]` generics kept as they are (Zig-style comptime was rejected).
|
|
18
|
-
|
|
19
|
-
Baseline (release build, scratchpad bench): a typical compile is 380–500 ms, and monomorphization is 80% of it (it runs twice, and the global env is cloned for every expression it infers). `Bool.plum` expands from 316 items to 947.
|
|
20
|
-
|
|
21
|
-
1. **Structural types.** `PlumType` gains `TApp(String, Vec<PlumType>)` (e.g. `List[Int]`) and `TParam(String)` (a rigid generic parameter inside a generic body). `plumTypeFromAst` builds them from `ast::Type.generics`, and mangled `$` names disappear. `TTags` stays the same. `unify` compares `TApp` args pairwise. E stays covariant for tag unions (the subset rule from the Roc errors plan).
|
|
22
|
-
2. **Generic checking without monomorphization.**
|
|
23
|
-
- A generic fn or type body is checked once, with its params as `TParam`.
|
|
24
|
-
- A call site instantiates the callee's scheme with fresh `TVar`s and unifies them with the args, the expected type and the closure return types.
|
|
25
|
-
- Field and variant payload types are substituted from the receiver's `TApp` args.
|
|
26
|
-
- A method's own generics (`map(cb: fn(T) -> U)`) are instantiated per call.
|
|
27
|
-
- A method call on a `TParam` resolves through the param's bounds (`K: Hashable` gives `hash()`). Calling a method with no bound is an error: `'T' has no method 'x' (add a bound)`.
|
|
28
|
-
3. **Trait types (dyn).** A trait name can be used as a type (`s: Shape`, `List[Shape]`). A value of any class/enum that implements every trait method coerces to it, and `checkTraitConformance` reuses its existing per-receiver method table.
|
|
29
|
-
4. **Erased runtime representation.**
|
|
30
|
-
- Every class/enum GC struct gets field 0 `typeId: i32`, unique per declared type and stable in declaration order. `Str` is included.
|
|
31
|
-
- `TParam` and trait types lower to `ref null $Obj`, the common supertype `struct {typeId: i32}`, which every class/enum struct subtypes. `Box[T]`'s `T` field is `ref null $Obj`.
|
|
32
|
-
- Boxing: an `Int`/`Float`/`Byte`/`Bool` flowing into a `TParam` or trait slot (arg, return, field, payload, closure arg/ret) is boxed into `$BoxI64`/`$BoxF64`/`$BoxI32` (each has a typeId header). Where the instantiated type is primitive, reading back casts and unboxes. The existing `$Tag` boxes are reused and get headers too. `Bool` stays an enum ref and needs no box.
|
|
33
|
-
- A generic fn, type or method compiles to exactly one wasm function or struct. A closure passed to a generic param is compiled with erased signatures, and a call site whose concrete closure types are primitive gets a wrapper thunk that boxes and unboxes.
|
|
34
|
-
5. **Trait dispatch.** For each trait method, codegen emits one dispatcher `$dyn$<Trait>$<method>(self: ref $Obj, args…)`. It reads `typeId` and `br_table`s to the concrete method, with boxed primitives dispatching to the primitive's methods (`Int.hash`, `Str.toStr`). Calls on a concrete receiver stay direct calls.
|
|
35
|
-
6. **Built-in collections.** Ruling: List and Array are compiler intrinsics. Map stays Plum code in std on top of them, erased and compiled once, because a hand-written wasm hash table is a lot of codegen for no benefit once Map is no longer specialized.
|
|
36
|
-
- `List[T]` = `struct {typeId, len: i32, data: ref $AnyArray}`, growable with doubling. Its intrinsic methods are `List()`/variadic `List(a, b)`, `len`, `get` (→ `Try[T, [OutOfBounds]]`), `set`, `push`, `pop` (→ `[ListWasEmpty]`) and `clear`.
|
|
37
|
-
- `Array[T]` = the existing `array<anyref>` + `len`/`get`/`set`/`push`.
|
|
38
|
-
- The rest of List's API (map, filter, reduce, each, chunk, toStr…) stays Plum code in `List.plum`, built on the intrinsics. List is no longer a linked list.
|
|
39
|
-
7. **Removed:** `monomorphize.rs`, `monomorphize_tests.rs`, `erasedEnumName`, `tagUnionSegmentsCompatible` and its mangled-name parsing, and every `$`-mangling path in codegen. Comments in std that describe monomorphization workarounds (eager per-specialization compilation, `toTryStr` callbacks) are updated or removed, with behaviour kept the same.
|
|
40
|
-
8. **Out of scope:** generic tag payloads (still non-generic), separate compilation, removing boxing via specialization.
|
|
41
|
-
|
|
42
|
-
Success criteria:
|
|
43
|
-
- All of `plum test plum-std plum-examples` (1951) and `cargo test --workspace` stay green after every task.
|
|
44
|
-
- At the end, a typical compile is ≤ 100 ms and wasm for `Bool.plum` is smaller than 96 KB, measured with `scratchpad/bench`.
|
|
45
|
-
|
|
46
|
-
## Global constraints
|
|
47
|
-
|
|
48
|
-
- Run `cargo test --workspace` and `cargo run -q -p plum-cli -- test plum-std plum-examples` after every task, and both must be green. T1–T3 keep the monomorphizer feeding codegen, so behaviour can't regress before the switch in T4.
|
|
49
|
-
- `touch` edited `.rs` files before trusting cargo. Never run `plum format` on real files. Regenerate tree-sitter only with `--disable-optimizations`, though no grammar change is expected.
|
|
50
|
-
- Type id 0 is reserved for `$Obj` itself. Box type ids: Int=1, Float=2, Byte=3, Tag=4. Declared classes/enums start at 16.
|
|
51
|
-
- Error wording: `'T' has no method 'x' (add a bound)`. `type 'X' does not implement trait 'Y': missing 'm'`.
|
|
52
|
-
|
|
53
|
-
## Main diagram
|
|
54
|
-
|
|
55
|
-
```mermaid
|
|
56
|
-
flowchart LR
|
|
57
|
-
P["parser AST (unchanged)"] --> C["checker: TApp/TParam, instantiate per call, bounds, trait types"]
|
|
58
|
-
C -->|"typed AST + expr types"| G["codegen: erased structs, typeId header, box at generic edges"]
|
|
59
|
-
G --> D["dispatchers: dyn Trait.method via typeId"]
|
|
60
|
-
G --> L["intrinsics: List / Array"]
|
|
61
|
-
L --> S["std: List API + Map in Plum, compiled once"]
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## File structure
|
|
65
|
-
|
|
66
|
-
- `plum-checker/src/types.rs`: TApp/TParam, substitution, instantiation.
|
|
67
|
-
- `plum-checker/src/lib.rs`: generic checking, bounds, trait types.
|
|
68
|
-
- `plum-checker/src/monomorphize.rs`: deleted in T5.
|
|
69
|
-
- `plum-wasm-codegen/src/lib.rs`: erasure, boxing, typeId, dispatchers, intrinsics.
|
|
70
|
-
- `plum-std/List.plum`, `Array.plum`, `Map.plum`: rewritten on the intrinsics.
|
|
71
|
-
- `README.md`: a "How generics compile" section.
|
|
72
|
-
|
|
73
|
-
Ruling: sequential [all] - every task edits plum-checker/src/lib.rs or plum-wasm-codegen/src/lib.rs, and each task's tests compile real Plum through the previous stage - cost: no parallelism.
|
|
74
|
-
|
|
75
|
-
```mermaid
|
|
76
|
-
flowchart LR
|
|
77
|
-
T1[types]
|
|
78
|
-
T2[generic checking]
|
|
79
|
-
T3[trait types + typeId]
|
|
80
|
-
T4[erased codegen switch]
|
|
81
|
-
T5[intrinsics + std + delete mono]
|
|
82
|
-
T6[docs + bench]
|
|
83
|
-
%% taskgraph
|
|
84
|
-
T1 --> T2
|
|
85
|
-
T2 --> T3
|
|
86
|
-
T3 --> T4
|
|
87
|
-
T4 --> T5
|
|
88
|
-
T5 --> T6
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
```mermaid
|
|
92
|
-
%% schedule
|
|
93
|
-
gantt
|
|
94
|
-
title Predicted schedule (mm:ss)
|
|
95
|
-
dateFormat X
|
|
96
|
-
axisFormat %M:%S
|
|
97
|
-
section lane 1
|
|
98
|
-
T1 structural types :t1, 0, 118s
|
|
99
|
-
T2 generic checking :t2, 118, 163s
|
|
100
|
-
T3 trait types + typeId :t3, 281, 153s
|
|
101
|
-
T4 erased codegen switch :t4, 434, 166s
|
|
102
|
-
T5 intrinsics std rewrite delete monom :t5, 600, 196s
|
|
103
|
-
T6 docs LSP benchmark :t6, 795, 141s
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
critical path: T1 → T2 → T3 → T4 → T5 → T6 (15.6 min) · peak width 1 · predicted build 15.6 min · predicted total ≈ 19.1 min (target ✓)
|
|
107
|
-
|
|
108
|
-
## Package: checker
|
|
109
|
-
|
|
110
|
-
**App type:** library / evaluator. **Categories:** deep, state.
|
|
111
|
-
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
112
|
-
|
|
113
|
-
### T1 structural types · Complexity 5 · Est. context 50k
|
|
114
|
-
**Owns:** plum-checker/src/types.rs, plum-checker/tests/types_tests.rs
|
|
115
|
-
**Components:** 1
|
|
116
|
-
**LOC owned:** 220
|
|
117
|
-
**Tests:** fast
|
|
118
|
-
**Model:** sonnet
|
|
119
|
-
**Est. time:** 3 min
|
|
120
|
-
**Consumes:** none
|
|
121
|
-
**Produces:** PlumType::TApp(String, Vec<PlumType>) · PlumType::TParam(String) · types::substitute(ty: &PlumType, s: &BTreeMap<String, PlumType>) -> PlumType · types::instantiate(params: &[String], ty: &PlumType, fresh: &mut u32) -> (PlumType, BTreeMap<String, PlumType>) · Display: TApp("Try",[TInt,TTags{A}]) -> "Try[Int, [A]]"
|
|
122
|
-
**Review Focus:** `substitute_nested`: substitute(TApp(List,[TApp(Try,[TParam T, TTags{A}])]), {T: Int}) gives List[Try[Int, [A]]]; `instantiate_fresh_distinct`: two instantiations of `fn(T) -> T` produce different TVars.
|
|
123
|
-
**Test code:** Rust unit tests for the two named cases plus Display round-trips.
|
|
124
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
125
|
-
**Notes:** Adding the variants breaks exhaustive matches elsewhere. Add minimal compile-only arms there (allowed exception), treating TApp like TNamed of its base name for now, so every existing suite stays green.
|
|
126
|
-
|
|
127
|
-
### T2 generic checking · Complexity 9 · Est. context 80k
|
|
128
|
-
**Owns:** plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs
|
|
129
|
-
**Components:** 2
|
|
130
|
-
**LOC owned:** 380
|
|
131
|
-
**Tests:** fast
|
|
132
|
-
**Model:** sonnet
|
|
133
|
-
**Est. time:** 6 min
|
|
134
|
-
**Consumes:** PlumType::TApp, PlumType::TParam, types::substitute, types::instantiate (T1)
|
|
135
|
-
**Produces:** plum_checker::checkSource runs on the ORIGINAL source (no monomorphizeSource call inside) · plum_checker::typedSource(source: &ast::Source) -> CheckResult<TypedInfo>, where TypedInfo maps an expr pointer (usize) to its instantiated PlumType, for codegen
|
|
136
|
-
**Review Focus:** (deep) `method_own_generic_instantiated_per_call`: `xs.map(|n| n * 2)` on List[Int] is List[Int], and `xs.map(|n| "{n}")` is List[Str]; `nested_generic_field`: Box[Try[Int,[A]]].value is Try[Int, [A]]; (state) `unbounded_tparam_method_error`: `fun f[T](x: T) -> Int = x.hash()` reports `'T' has no method 'hash' (add a bound)`; `bounded_ok`: with `T: Hashable` it passes; `tags_covariant_in_tapp`: Try[Int,[A]] passes where Try[Int,[A,B]] is expected.
|
|
137
|
-
**Test code:** Rust tests for the named cases. All of plum-std and plum-examples must check with 0 errors on the un-monomorphized source.
|
|
138
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
139
|
-
**Notes:** Codegen still calls monomorphizeSource itself, so runtime behaviour doesn't change. Keep the old mangled-name checker paths only where codegen still calls into them, through `inferExpr` on monomorphized source.
|
|
140
|
-
|
|
141
|
-
### T3 trait types + typeId · Complexity 7 · Est. context 75k
|
|
142
|
-
**Owns:** plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs, plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs
|
|
143
|
-
**Components:** 2
|
|
144
|
-
**LOC owned:** 300
|
|
145
|
-
**Tests:** fast
|
|
146
|
-
**Model:** sonnet
|
|
147
|
-
**Est. time:** 5.5 min
|
|
148
|
-
**Consumes:** plum_checker::checkSource (T2)
|
|
149
|
-
**Produces:** `$Obj {typeId: i32}` supertype with typeId field 0 on every class/enum struct · dispatcher fns `$dyn$<Trait>$<method>` · trait names usable as types (checker coercion + codegen)
|
|
150
|
-
**Review Focus:** (deep) `heterogeneous_list_dispatch`: `List[Shape]` holding Circle and Square, summed `area()` is exact; (state) `missing_trait_method_rejected`: coercing a type without `area` reports `type 'X' does not implement trait 'Shape': missing 'area'`; `boxed_int_dispatch`: an Int passed as `ToStr` gives "5" from toStr().
|
|
151
|
-
**Test code:** codegen tests that run through wasmtime for the three named cases, plus one example `plum-examples/traits.plum` test block (Owns: add plum-examples/traits.plum).
|
|
152
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
153
|
-
**Notes:** Monomorphization is still on here, and trait-typed values work under it, because a dyn value isn't generic.
|
|
154
|
-
|
|
155
|
-
## Package: codegen
|
|
156
|
-
|
|
157
|
-
**App type:** library / evaluator. **Categories:** deep, numeric, state.
|
|
158
|
-
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
159
|
-
|
|
160
|
-
### T4 erased codegen switch · Complexity 9 · Est. context 80k
|
|
161
|
-
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-cli/src/main.rs
|
|
162
|
-
**Components:** 2
|
|
163
|
-
**LOC owned:** 400
|
|
164
|
-
**Tests:** fast
|
|
165
|
-
**Model:** sonnet
|
|
166
|
-
**Est. time:** 6 min
|
|
167
|
-
**Consumes:** plum_checker::typedSource(source) -> CheckResult<TypedInfo> (T2) · `$Obj` header and dispatchers (T3)
|
|
168
|
-
**Produces:** compileSource(source) compiles the un-monomorphized AST: one wasm fn per generic fn or method, TParam → ref null $Obj, primitive boxing and unboxing at generic edges, closure wrapper thunks
|
|
169
|
-
**Review Focus:** (numeric) `boxed_i64_extremes_through_generic`: identity[T] on i64::MIN, 2^53+1 and -0.0 Float round-trips exactly; (deep) `generic_closure_thunk`: `List(1,2,3).map(|n| n * 2)` sums to 12; `try_of_list_of_try`: Try[List[Try[Int,[A]]],[B]] round-trips; (state) `one_copy_per_generic`: a module using List[Int] and List[Str] has exactly one compiled `map` function.
|
|
170
|
-
**Test code:** codegen tests for the named cases. Both full plum suites green.
|
|
171
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
172
|
-
**Notes:** Stop calling monomorphizeSource in codegen and checkSource. Leave monomorphize.rs on disk, unused, for T5 to delete. List/Map are still the Plum std code here, now compiled erased.
|
|
173
|
-
|
|
174
|
-
### T5 intrinsics, std rewrite, delete monomorphizer · Complexity 7 · Est. context 75k
|
|
175
|
-
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-std/, plum-checker/src/monomorphize.rs, plum-checker/tests/monomorphize_tests.rs, plum-checker/src/lib.rs
|
|
176
|
-
**Components:** 2
|
|
177
|
-
**LOC owned:** 350
|
|
178
|
-
**Tests:** slow
|
|
179
|
-
**Model:** sonnet
|
|
180
|
-
**Est. time:** 6 min
|
|
181
|
-
**Consumes:** erased compileSource (T4)
|
|
182
|
-
**Produces:** List/Array intrinsics (Design item 6) · List.plum API on the intrinsics (no linked list) · Map.plum on erased generics with Hashable dispatch · monomorphize.rs and monomorphize_tests.rs deleted, plus dead mangled-name helpers
|
|
183
|
-
**Review Focus:** (state) `list_get_set_push_pop`: push 1000, get(999) is Ok(999), pop empties, get(0) on an empty list is Err(OutOfBounds); (numeric) `list_growth`: len stays exact across the doubling boundaries 16/17 and 1024/1025; (deep) `map_collisions_erased`: Map[Str,Int] with 500 keys round-trips.
|
|
184
|
-
**Test code:** `test` blocks in List.plum/Map.plum for the named cases. All existing std tests still green.
|
|
185
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
186
|
-
|
|
187
|
-
### T6 docs, LSP, benchmark · Complexity 3 · Est. context 40k
|
|
188
|
-
**Owns:** README.md, plum-lsp/src/, plum-examples/, docs/plans/2026-10-25-erased-generics.md
|
|
189
|
-
**Components:** 1
|
|
190
|
-
**LOC owned:** 120
|
|
191
|
-
**Tests:** slow
|
|
192
|
-
**Model:** sonnet
|
|
193
|
-
**Est. time:** 2.5 min
|
|
194
|
-
**Consumes:** final compiler (T5)
|
|
195
|
-
**Produces:** a README "How generics compile" section (erasure, boxing at edges, dyn traits, built-in List/Array), the bench results table appended to this plan, and the LSP compiling against the new checker API
|
|
196
|
-
**Review Focus:** `bench_targets`: a typical compile ≤ 100 ms and the Bool.plum wasm < 96 KB, or the numbers recorded with the reason they missed.
|
|
197
|
-
**Test code:** plum test suites green. The bench binary at the scratchpad path is rebuilt against the workspace.
|
|
198
|
-
**Steps:** 1 run the bench, 2 write docs, 3 verify
|
|
199
|
-
|
|
200
|
-
## Review Focus
|
|
201
|
-
|
|
202
|
-
Checker: deep (nested TApp substitution, per-call method generics), state (bounds, trait coercion). Codegen: numeric (boxing precision), deep (closure thunks, nested erased generics), state (one copy per generic, typeId stability).
|
|
203
|
-
|
|
204
|
-
## Self-review
|
|
205
|
-
|
|
206
|
-
Design items 1→T1, 2→T2, 3,5→T3, 4→T4, 6,7→T5, success criteria→T6. There are no placeholders. Each Consumes line matches a Produces line. Tasks are sequential by ruling, and each one leaves both suites green.
|
|
207
|
-
|
|
208
|
-
## Results
|
|
209
|
-
|
|
210
|
-
Release build, best of 5, milliseconds.
|
|
211
|
-
|
|
212
|
-
| Measure | Before (monomorphizing) | After (erased) |
|
|
213
|
-
|---|---|---|
|
|
214
|
-
| Typical program, total | 380-500 | 20-25 (load ~9, check ~7, codegen ~5) |
|
|
215
|
-
| List.plum | 911 | typical range |
|
|
216
|
-
| Largest (oop_visitor, dop_visitor, Http, Os) | n/a | 32-35 |
|
|
217
|
-
| Bool.plum wasm size | 96.3 KB | 22.7 KB |
|
|
218
|
-
| `plum test` std + examples | ~89 s | ~21-28 s |
|
|
219
|
-
|
|
220
|
-
Targets (<=100 ms, <96 KB) met.
|
|
221
|
-
|
|
222
|
-
### Open follow-ups
|
|
223
|
-
|
|
224
|
-
- A generic fn returning a closure is unparseable (`-> fn(Int) -> Int`).
|
|
225
|
-
- A closure capturing a fn-typed param fails with "unknown function 'cb'".
|
|
226
|
-
- A bare `-> Box` return inside Box's own methods leaves `T` unsubstituted.
|
|
227
|
-
- `List` is not its own intrinsic; it is Plum code on top of `Array`.
|
|
228
|
-
- A built-in `Bool` is planned next.
|
docs/plans/2026-10-25-erased-generics.progress.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
- start cf9e06f3c64fb5fe2d51a2d87291c5ac9d5c90a0 (erased-generics)
|
|
2
|
-
- unblocked: dispatched T1 at 18:49:15; waiting on fixes: none
|
|
3
|
-
- Ruling: T0 skipped - test-cmd.json reused from roc-errors run - cost: none
|
|
4
|
-
- Ruling: Map stays Plum std code on List/Array intrinsics (user chose built-in collections) - hand-written wasm hash table adds lots of codegen for no gain once Map isn't specialized - cost: Map slightly slower than a native one
|
|
5
|
-
- T1 dd5f31d
|
|
6
|
-
- tests fast passed=1 failed=0 skipped=0 seconds=13.4
|
|
7
|
-
- T2 eaf49cb; Ruling: locals may shadow top-level fns (side effect of T2) - cost: a typo'd local could hide a fn
|
|
8
|
-
- review T2 9fbf75a; residual: unconstrained vars default to Unit in typedSource (watch in T4); free fns can't declare bounds (grammar)
|
|
9
|
-
- T3 6c41056; Ruling: builtin ids 0-15 reserved (Str=5, Buffer=6 fixed so hosts need no lookup) - cost: none
|
|
10
|
-
- T4 55dceee; Ruling: uniform anyref closure ABI (no thunks) - cost: primitive closure args box per call
|
|
11
|
-
- review T4: no fixes; open: bare generic type in signature unsound (folded into T5); generic fn returning a closure unparseable (-> fn(Int) -> Int), left out of scope
|
|
12
|
-
- T5 dispatched 09:51:10
|
|
13
|
-
- NOTE (user: defer): grammar can't express a fn type as a return type (-> fn(Int) -> Int); generic fn returning a closure unparseable
|
|
14
|
-
- T5 7d25f15; Ruling: List stays Plum code over the Array intrinsic (not its own intrinsic) - matches 'compiler owns representations, std owns what Plum can express' - cost: one extra indirection per List op
|
|
15
|
-
- NOTE: closure capturing a fn-typed param fails 'unknown function cb' (pre-existing codegen bug, open)
|
|
16
|
-
- NOTE: follow-up plan = built-in Bool only (user)
|
|
17
|
-
- T6 16cd722
|
docs/plans/2026-10-25-roc-errors.incidents.jsonl
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{"file": "plum-core/src/builtin_usage.rs", "line": 0, "context": "T0: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "15:54:00"}
|
|
2
|
-
{"file": "plum-core/src/builtin_usage.rs", "line": 0, "context": "T1: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "15:56:13"}
|
|
3
|
-
{"file": "plum-checker/src/lib.rs", "line": 1265, "context": "refutable payload pattern B(0) counted as covering tag B in exhaustiveness", "agent": "reviewer", "kind": "exception", "resolution": "fixed", "confidence": "high", "time": "16:02:03"}
|
|
4
|
-
{"file": "-", "line": 0, "context": "tag_literal_does_not_shadow_declared_enum_variant: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "16:39:56"}
|
|
5
|
-
{"file": "-", "line": 0, "context": "unpinnedOkIsNotATagLiteral: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "16:39:56"}
|
docs/plans/2026-10-25-roc-errors.md
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# Roc-style error handling (Try + structural error tags)
|
|
2
|
-
|
|
3
|
-
**Goal:** Replace `Option[T]`/`Result[T,E]` with one `Try[T, E]` type whose errors are structural tag unions (`[NotFound, BadNum(Str)]`). `?` merges different error sets into the caller's declared union, `expr ? |e| Tag` maps an error inline, `??` gives a default, and `crash "msg"` stops the program.
|
|
4
|
-
**Architecture:** tree-sitter grammar → `plum-core` parser/AST → `plum-checker` (monomorphize, then check) → `plum-wasm-codegen` (wasm-gc) → hosts (`plum-cli`, `plum-runtime`).
|
|
5
|
-
**Tech stack:** Rust workspace, tree-sitter (regenerate with `tree-sitter generate --disable-optimizations`), wasm-gc via wasm-encoder, wasmtime hosts.
|
|
6
|
-
**Spec path:** this file, section "Design".
|
|
7
|
-
**Detail:** tests
|
|
8
|
-
**Target-min:** 30
|
|
9
|
-
|
|
10
|
-
## Design
|
|
11
|
-
|
|
12
|
-
What the user said: adopt Roc's error model (https://www.roc-lang.org/docs/main/langref/errors/) in full. Name the type `Try[T, E]`, replace `?:` with `??`, AI orchestrator.
|
|
13
|
-
|
|
14
|
-
1. **Try replaces Option and Result.** `enum Try[T, E] = | Ok(T) | Err(E)` lives in `plum-std/Try.plum`. `Option.plum`, `Result.plum` and `Err.plum` (an unused trait whose name clashes with the `Err` variant) are deleted. A missing value is an error tag, using Roc's names: `List.get`→`[OutOfBounds]`, `List.first`/`last`/`pop`→`[ListWasEmpty]`, `Map.get`→`[KeyNotFound]`, `parseInt`/`parseFloat`→`[BadNumStr]`, `Bool.parse`→`[BadBoolStr]`, Base64/Json decoding→`[InvalidBase64]`/`[InvalidJson(Str)]`.
|
|
15
|
-
2. **Tag union types.** The type syntax is `[A, B(Str)]`. Each tag carries zero or one payload, and the payload type must be non-generic. Ruling: tags have at most one payload and no generic payload types (because tag-union types travel through the monomorphizer as a single canonical name string, which keeps the change contained). The parser canonicalizes the union: tags sorted, no spaces, `[A,B(Str)]`, stored as `ast::Type{name, generics: []}`. The checker has `PlumType::TTags(BTreeMap<String, Option<Box<PlumType>>>)`, and its `Display` gives exactly that canonical string, so mangling and `plumTypeFromName` round-trip.
|
|
16
|
-
3. **Tag values.** An uppercase name that is not a declared type, variant or fn is a tag literal: `NotFound` has type `[NotFound]`, and `BadNum(s)` has type `[BadNum(Str)]`. Ruling: tags are never declared, as in Roc. Typos are caught by the declared return union.
|
|
17
|
-
4. **Subtyping.** `unify(expected, actual)` accepts a tag union `actual ⊆ expected` when every payload unifies. Two specializations of the same generic enum whose mangled names differ only in their tag-union segments (`Try$Int$[A]` and `Try$Int$[A,B]`) unify by comparing those segments pairwise in order. Ruling: E is covariant everywhere (because the runtime representation is identical, nothing can crash; aliasing a mutable List of Try values can only defeat exhaustiveness, which is an accepted gap).
|
|
18
|
-
5. **`?`.** `checkFn` binds the reserved env key `"$return"` to the declared return type, and closures remove it. `expr?` requires `expr: Try$..`. If `$return` is a Try, the inner E must unify into the outer E (covariantly), otherwise it's an error: `'?' error type [X] is not part of the function's error type [A,B]`. In codegen, the failure branch takes the payload and builds the **outer** function's `Err` variant, so Ok types may differ.
|
|
19
|
-
6. **`expr ? |e| T`** is parser sugar for `Expr::Try(expr.mapErr(|e| T))`. **`a ?? b`** replaces `?:` and keeps `Expr::Elvis`. **`crash expr`** is `ast::Stmt::Crash(Expr)` and checks that `expr` is `Str`. In codegen it calls the host import `plum::crash(Str)`, emitted only when used, followed by `unreachable`. `todo` stays as it is.
|
|
20
|
-
7. **Runtime representation.** There is one GC struct `$Tag {id: i32, payload: anyref (nullable)}`. Tag ids are interned in sorted order over every tag name in the program. Int, Float and Byte payloads are boxed in `$BoxI64`/`$BoxF64`/`$BoxI32`, and reference payloads are stored as-is. Codegen erases tag segments (`[...]`→`Tags`) when registering enum GC types, so specializations that differ only in their tags share wasm types.
|
|
21
|
-
8. **Matching.** On a tag-union subject, an arm `A =>` tests `id`, and `B(s) =>` tests `id`, then casts/unboxes the payload to the union's payload type. These arms also work nested (`Err(NotFound) =>`). A `match` on a tag union with no `_` must cover every tag (checker error `non-exhaustive match on [A,B]: missing B`).
|
|
22
|
-
9. **`==`** on tag unions compares `id`. It is only allowed when neither side's union has a payload tag.
|
|
23
|
-
10. **Out of scope** (YAGNI): inferring the error union of a function without an annotation, multi-payload tags, printing a tag (`toStr`), and `Try[Unit, E]` if it doesn't already work.
|
|
24
|
-
|
|
25
|
-
Testing: each compiler crate gets Rust unit tests; end-to-end behavior goes in `test` blocks in `plum-std/Try.plum` and `plum-examples/error_propagation.plum`, run with `cargo run -q -p plum-cli -- test plum-std plum-examples`.
|
|
26
|
-
|
|
27
|
-
## Global constraints
|
|
28
|
-
|
|
29
|
-
- Rust test command: `cargo test --workspace`. Plum tests: `cargo run -q -p plum-cli -- test plum-std plum-examples`.
|
|
30
|
-
- `touch` edited `.rs` files before trusting a build (stale cargo builds happen in this sandbox).
|
|
31
|
-
- Never run `plum format` on real files to "check" something: it rewrites them in place.
|
|
32
|
-
- After regenerating the grammar, rebuild the embedded grammar `.so` if the plum-cli build requires it (see Makefile.toml `grammar-so`).
|
|
33
|
-
- Canonical tag-union string: `[` + tags sorted by name, joined with `,`, each `Name` or `Name(PayloadType)` + `]`.
|
|
34
|
-
- Host import: module `plum`, name `crash`, signature identical to `plum::printLn`.
|
|
35
|
-
|
|
36
|
-
## Main diagram
|
|
37
|
-
|
|
38
|
-
```mermaid
|
|
39
|
-
flowchart LR
|
|
40
|
-
G["grammar.js + parser.rs"] -->|"ast::Type '[A,B(Str)]', Stmt::Crash, Elvis(??)"| C["checker: TTags, subset unify, ? vs $return"]
|
|
41
|
-
C -->|"monomorphized AST + types"| W["codegen: $Tag, boxing, ? rewrap, crash import"]
|
|
42
|
-
W -->|"wasm with plum::crash"| H["hosts: plum-cli, plum-runtime"]
|
|
43
|
-
W --> S["std: Try.plum migration"]
|
|
44
|
-
S --> E["examples, LSP, docs"]
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## File structure
|
|
48
|
-
|
|
49
|
-
- `plum-tooling/tree-sitter-plum/{grammar.js,src/*,test/corpus/*,queries/*}`: syntax.
|
|
50
|
-
- `plum-core/src/{ast.rs,parser.rs,formatter.rs}`: AST, canonical tag type names, desugaring.
|
|
51
|
-
- `plum-checker/src/{types.rs,lib.rs,monomorphize.rs}`: TTags, unify, `?`, matching, crash.
|
|
52
|
-
- `plum-wasm-codegen/src/lib.rs`: runtime representation.
|
|
53
|
-
- `plum-cli/src/main.rs`, `plum-runtime/src/main.rs`, `plum-*/tests/examples_test.rs`: crash host import.
|
|
54
|
-
- `plum-std/*.plum`: library migration. `plum-examples/*.plum`, `plum-lsp/src/*`, `README.md`, `website/`: users and docs.
|
|
55
|
-
|
|
56
|
-
Ruling: sequential [all] - each compiler stage's tests parse and compile real Plum source through the earlier stages, so stubs can't stand in for them - cost: about 6 sequential tasks.
|
|
57
|
-
|
|
58
|
-
```mermaid
|
|
59
|
-
flowchart LR
|
|
60
|
-
T1[syntax]
|
|
61
|
-
T2[checker]
|
|
62
|
-
T3[codegen]
|
|
63
|
-
T4[hosts]
|
|
64
|
-
T5[std migration]
|
|
65
|
-
T6[examples docs lsp]
|
|
66
|
-
%% taskgraph
|
|
67
|
-
T1 --> T2
|
|
68
|
-
T2 --> T3
|
|
69
|
-
T3 --> T4
|
|
70
|
-
T4 --> T5
|
|
71
|
-
T5 --> T6
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
```mermaid
|
|
75
|
-
%% schedule
|
|
76
|
-
gantt
|
|
77
|
-
title Predicted schedule (mm:ss)
|
|
78
|
-
dateFormat X
|
|
79
|
-
axisFormat %M:%S
|
|
80
|
-
section lane 1
|
|
81
|
-
T1 syntax :t1, 0, 142s
|
|
82
|
-
T2 checker :t2, 142, 157s
|
|
83
|
-
T3 codegen :t3, 299, 160s
|
|
84
|
-
T4 hosts :t4, 459, 93s
|
|
85
|
-
T5 std migration :t5, 553, 189s
|
|
86
|
-
T6 examples LSP docs :t6, 742, 176s
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
critical path: T1 → T2 → T3 → T4 → T5 → T6 (15.3 min) · peak width 1 · predicted build 15.3 min · predicted total ≈ 18.8 min (target ✓)
|
|
90
|
-
|
|
91
|
-
## Package: compiler
|
|
92
|
-
|
|
93
|
-
**App type:** library / parser / evaluator. **Categories:** deep, numeric, encoding, state.
|
|
94
|
-
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
95
|
-
|
|
96
|
-
### T1 syntax · Complexity 5 · Est. context 60k
|
|
97
|
-
**Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/, plum-tooling/tree-sitter-plum/test/, plum-tooling/tree-sitter-plum/queries/, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/src/formatter.rs, plum-core/tests/
|
|
98
|
-
**Components:** 2
|
|
99
|
-
**LOC owned:** 220
|
|
100
|
-
**Tests:** fast
|
|
101
|
-
**Model:** sonnet
|
|
102
|
-
**Est. time:** 4 min
|
|
103
|
-
**Consumes:** none
|
|
104
|
-
**Produces:** ast::Stmt::Crash(Expr) · parser canonical tag type name e.g. `x: [B, A(Str)]` -> ast::Type{name:"[A(Str),B]", generics:[]} · `a ?? b` -> Expr::Elvis · `f(x) ? |e| Bad` -> Expr::Try(MethodCall(f(x), "mapErr", [closure]))
|
|
105
|
-
**Review Focus:** `test_tag_type_canonical_sorted`: `fun f() -> Try[Int, [Zed, Abc(Str)]]` returns the Type with generics [Int, "[Abc(Str),Zed]"]; `test_slice_type_still_parses`: `[]Byte` still gives "[]Byte"; `test_elvis_token_removed`: `a ?: b` is a parse error; `test_ternary_unaffected`: `c ? 1 : 2` is still Ternary; `test_try_with_closure_desugars`.
|
|
106
|
-
**Test code:** Rust parser tests in plum-core/tests (follow existing file naming), with the assertions above plus `crash "boom"` → Stmt::Crash(String). Update the tree-sitter corpus fixtures for `?:`→`??`.
|
|
107
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 grammar edit + `tree-sitter generate --disable-optimizations` + `tree-sitter test`, parser.rs, 4 PASS
|
|
108
|
-
**Notes:** `try_expression` becomes `seq(primary, "?", optional(closure))`. Resolve the conflict with ternary using a declared conflict, not precedence alone. Add highlight captures for `crash` and `??`, and a formatter rule for `??` and tag-union types. Do not migrate any .plum files.
|
|
109
|
-
|
|
110
|
-
### T2 checker · Complexity 8 · Est. context 75k
|
|
111
|
-
**Owns:** plum-checker/src/types.rs, plum-checker/src/lib.rs, plum-checker/src/monomorphize.rs, plum-checker/tests/
|
|
112
|
-
**Components:** 2
|
|
113
|
-
**LOC owned:** 330
|
|
114
|
-
**Tests:** fast
|
|
115
|
-
**Model:** sonnet
|
|
116
|
-
**Est. time:** 5.5 min
|
|
117
|
-
**Consumes:** ast::Stmt::Crash(Expr) (T1)
|
|
118
|
-
**Produces:** PlumType::TTags(BTreeMap<String, Option<Box<PlumType>>>) · plum_checker::tagUnionSegmentsCompatible(expected: &str, actual: &str, ctx: &CheckCtx) -> bool · plum_checker::erasedEnumName(name: &str) -> String e.g. erasedEnumName("Try$Int$[A,B(Str)]") -> "Try$Int$Tags" · tryOperatorVariants(enum_name, ctx) now matches only Ok/Err
|
|
119
|
-
**Review Focus:** (deep) `nested_union_in_payload`: `[A(Try$Int$[X])]` segments split with brackets counted by depth; (state) `closure_question_mark_no_return_check`: `?` inside a closure is not checked against the outer `$return`; `question_mark_rejects_unlisted_tag`: `fun f() -> Try[Int,[A]] = { x := g()?; Ok(x) }` with g: `Try[Int,[B]]` reports an error naming `[B]`; `question_mark_widens`: g: `[A]` into `[A,B]` passes; `match_exhaustive_tags`; `eq_rejects_payload_tags`; `unknown_lowercase_still_error`: an unknown lowercase var is still an error, not a tag.
|
|
120
|
-
**Test code:** Rust tests in plum-checker/tests/checker_tests.rs. Embedded test sources must import `std/Try` (not Option/Result) and assert that parsing produced no errors. Migrate the existing tests in this crate that use Option/Result to Try plus tags.
|
|
121
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
122
|
-
**Notes:** The monomorphizer must walk `Stmt::Crash` everywhere it walks statements. A tag literal is anything not found in global_env, enum_variants or the class table. Treat `Display` for TTags as the canonical form (design item 2).
|
|
123
|
-
|
|
124
|
-
### T3 codegen · Complexity 8 · Est. context 78k
|
|
125
|
-
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs
|
|
126
|
-
**Components:** 2
|
|
127
|
-
**LOC owned:** 360
|
|
128
|
-
**Tests:** fast
|
|
129
|
-
**Model:** sonnet
|
|
130
|
-
**Est. time:** 5.8 min
|
|
131
|
-
**Consumes:** PlumType::TTags (T2) · plum_checker::erasedEnumName(name: &str) -> String (T2)
|
|
132
|
-
**Produces:** wasm import ("plum","crash") of printLn's type, emitted only when `crash` is used · $Tag GC struct
|
|
133
|
-
**Review Focus:** (numeric) `int_payload_roundtrip`: `Err(Big(9007199254740993))` matched back gives exactly 9007199254740993; `float_payload_roundtrip` with 0.1; (deep) `nested_err_tag_pattern`: `match r | Ok(v) => v | Err(NotFound) => 0 | Err(Bad(n)) => n`; (state) `question_mark_different_ok_types`: inner `Try[Int,[A]]` inside `fun f() -> Try[Str,[A,B]]` validates and returns Err(A); `tag_eq`: `A == A` is True, `A == B` is False; `specializations_share_gc_type`: `Try$Int$[A]` and `Try$Int$[A,B]` register one enum GC type.
|
|
134
|
-
**Test code:** Rust tests in codegen_tests.rs that compile and run through wasmtime the way existing tests do. Migrate the existing Option/Result tests in this file.
|
|
135
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS
|
|
136
|
-
**Notes:** The `?` failure branch is struct.get the inner Err payload, then struct.new the outer function's Err variant, then return. Work out the outer variant from the current fn's return type in LocalCtx (add it if it's missing). The tag-id table is built from every tag literal plus every tag inside a TTags appearing in any signature.
|
|
137
|
-
|
|
138
|
-
### T4 hosts · Complexity 2 · Est. context 30k
|
|
139
|
-
**Owns:** plum-cli/src/main.rs, plum-runtime/src/main.rs, plum-wasm-codegen/tests/examples_test.rs, plum-checker/tests/examples_test.rs
|
|
140
|
-
**Components:** 1
|
|
141
|
-
**LOC owned:** 60
|
|
142
|
-
**Tests:** fast
|
|
143
|
-
**Kind:** mechanical
|
|
144
|
-
**Model:** haiku
|
|
145
|
-
**Est. time:** 1.5 min
|
|
146
|
-
**Consumes:** wasm import ("plum","crash") (T3)
|
|
147
|
-
**Produces:** each host links `plum::crash(Str)`: it prints `crash: <msg>` to stderr, then returns, and the wasm's `unreachable` traps
|
|
148
|
-
**Review Focus:** `crash_prints_message`: running `fun main() = crash "boom"` through plum-cli exits non-zero, and stderr contains `crash: boom`.
|
|
149
|
-
**Test code:** one Rust integration test in plum-cli that runs the binary on a temp file and asserts the two outcomes above.
|
|
150
|
-
**Steps:** 1 failing test, 2 FAIL, 3 copy the printLn linker arm to a crash arm in each host, 4 PASS
|
|
151
|
-
|
|
152
|
-
## Package: library + users
|
|
153
|
-
|
|
154
|
-
**App type:** library. **Categories:** deep, state.
|
|
155
|
-
**Probe entry:** run=cargo run -q -p plum-cli -- test plum-std plum-examples
|
|
156
|
-
|
|
157
|
-
### T5 std migration · Complexity 6 · Est. context 70k
|
|
158
|
-
**Owns:** plum-std/
|
|
159
|
-
**Components:** 2
|
|
160
|
-
**LOC owned:** 300
|
|
161
|
-
**Tests:** slow
|
|
162
|
-
**Model:** sonnet
|
|
163
|
-
**Est. time:** 5 min
|
|
164
|
-
**Consumes:** compiler from T1-T4
|
|
165
|
-
**Produces:** plum-std/Try.plum with `enum Try[T, E]` and methods isOk, isErr, unwrap (crash "unwrap called on Err"), unwrapErr, unwrapOr, unwrapOrElse, map, mapErr, andThen, toTryStr · Option.plum, Result.plum, Err.plum deleted · List/Map/Json/Base64/Testing/Str/Array/Number/Bool return Try with the tag names in Design item 1
|
|
166
|
-
**Review Focus:** (state) `list_get_out_of_bounds`: `List(1).get(5)` is `Err(OutOfBounds)`; `map_get_missing`: `Err(KeyNotFound)`; `parse_int_bad`: `parseInt("x")` is `Err(BadNumStr)`; (deep) `question_mark_merges_two_unions` in Try.plum: a fn with `-> Try[Int, [BadNumStr, OutOfBounds]]` uses `?` on parseInt and on List.get; `try_closure_maps_error`: `parseInt("x") ? |_| InvalidFirst` inside `-> Try[Int,[InvalidFirst]]`; `default_operator`: `List[Int]().first() ?? 7 == 7`.
|
|
167
|
-
**Test code:** `test` blocks in plum-std/Try.plum for each Review Focus name above, plus each migrated test from the old Option.plum/Result.plum. Testing.plum: drop expectSome/expectNone and keep expectOk/expectErr.
|
|
168
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 migrate, 4 PASS
|
|
169
|
-
**Notes:** List's internal `Option[Node[T]]` links become a private `enum Link[T] = | Next(Node[T]) | End`. Ruling: they are not error tags, so a domain enum fits better than Try. Update every `import std/Option`/`std/Result` across plum-std.
|
|
170
|
-
|
|
171
|
-
### T6 examples, LSP, docs · Complexity 3 · Est. context 50k
|
|
172
|
-
**Owns:** plum-examples/, plum-lsp/src/, README.md, website/src/, plum-tooling/vscode-plum/
|
|
173
|
-
**Components:** 2
|
|
174
|
-
**LOC owned:** 200
|
|
175
|
-
**Tests:** slow
|
|
176
|
-
**Model:** sonnet
|
|
177
|
-
**Est. time:** 3 min
|
|
178
|
-
**Consumes:** Try std API (T5)
|
|
179
|
-
**Produces:** examples use Try plus tags, `??`, `? |e|`, `crash`. error_propagation.plum shows two different unions merging. safe_navigation.plum is migrated to Try. LSP keyword completion gets `crash` and `??` and loses `?:`. README and website docs describe the new model.
|
|
180
|
-
**Review Focus:** `examples_all_pass`: the full `plum test plum-examples` passes; `no_option_left`: `git grep -nw "Option\[\|Some(\|Result\[" -- '*.plum' '*.md'` is empty apart from changelog/history.
|
|
181
|
-
**Test code:** `test` blocks in plum-examples/error_propagation.plum: `merges_unions`, `maps_error_inline`, `default_operator`.
|
|
182
|
-
**Steps:** 1 failing tests, 2 FAIL, 3 migrate, 4 PASS
|
|
183
|
-
|
|
184
|
-
## Review Focus
|
|
185
|
-
|
|
186
|
-
Compiler: deep (nested tag patterns, nested brackets in mangled names), numeric (boxed i64/f64 payload precision), state (`?` widening across different Ok types, closure `$return` scoping). Library: Roc tag names returned exactly as in Design item 1.
|
|
187
|
-
|
|
188
|
-
## Self-review
|
|
189
|
-
|
|
190
|
-
All 10 design items map to tasks (1→T5, 2-4→T1/T2, 5→T2/T3, 6→T1/T3/T4, 7-9→T2/T3, 10 out of scope). There are no placeholders. The Consumes lines match T1-T3's Produces lines. The chain is sequential by ruling.
|
docs/plans/2026-10-25-roc-errors.progress.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
- start a473b02882bd4862c80377ffd7aa3a62e47a7224
|
|
2
|
-
- unblocked: dispatched T0 T1 at 15:51:57; waiting on fixes: none
|
|
3
|
-
- Ruling: intermediate tasks T1-T4 run only their own crate's unit tests; plum-std/examples suites (and examples_test.rs) are expected red until T5/T6 migrate .plum sources - cost: regressions in those suites are only caught at T5
|
|
4
|
-
- tests full passed=164 failed=0 skipped=0 seconds=88.0
|
|
5
|
-
- T1 dfe35c7 (manual commit: commit_task blocked on ruled compile-fix arms outside Owns)
|
|
6
|
-
- Ruling: T1 commit includes 10 lines of compile-only Stmt::Crash arms in plum-checker/plum-wasm-codegen/builtin_usage.rs - keeps every commit building - cost: none
|
|
7
|
-
- Ruling: single-letter tags alias generic->type_identifier in tag positions; generic tag payloads not rejected (render Name[Args]) - cost: generic payloads unsupported downstream, may miscompile
|
|
8
|
-
- T2 9c5f64a (manual commit incl. 2 compile-only codegen arms)
|
|
9
|
-
- review T1+T2: 1 fix f9467e7; open: generic tag payloads not rejected by parser (ruled out of scope, design item 2)
|
|
10
|
-
- T3 29ad747
|
|
11
|
-
- review T3: 1 fix 8336a15; residual: ? inside closure w/ differing Try types fails wasm validation (accepted)
|
|
12
|
-
- T4 0f2502c
|
|
13
|
-
- Ruling: T5/T6 may fix compiler bugs they hit (plum-core/checker/codegen) with regression tests, since nothing else runs concurrently - cost: larger T5 diff to review
|
|
14
|
-
- T5 45dada4
|
|
15
|
-
- Ruling: ?? binds tighter than comparison operators (Swift/Kotlin elvis), so a ?? 7 == 7 is (a ?? 7) == 7 - cost: differs from C#
|
|
16
|
-
- Ruling: Os/Http/Uuid keep Str error payloads (I/O messages) - cost: less Roc-like
|
|
17
|
-
- fix dc3b104: ?? precedence + |_| closures
|
|
18
|
-
- fix a3ab768: andThen bare-Ok closure; residual: |n| Err(Bad) with no typed context leaves U unresolved (same as Rust)
|
|
19
|
-
- T6 82b552d
|
|
20
|
-
- tests fast passed=1 failed=0 skipped=0 seconds=10.6
|
|
21
|
-
- tests full passed=101 failed=2 skipped=0 seconds=0.2
|
|
22
|
-
- tests full passed=218 failed=0 skipped=0 seconds=25.1
|
|
23
|
-
- fix f0eddab: Ok/Err direct args resolve from expected type; tag beats same-named variant when expected type is a tag union (#Name marker); README braces. residual: unpinned Ok in program w/o concrete Try reads as tag; keyword args matched positionally (pre-existing)
|
|
24
|
-
- tests full passed=218 failed=0 skipped=0 seconds=20.0
|
|
25
|
-
- final-review fixes d94bce0; residual: constructor sub-patterns inside a tag payload (generic payload) unsupported, ignored test documents it (design item 2 scope)
|
|
26
|
-
- tests full passed=226 failed=0 skipped=0 seconds=20.3
|
docs/plans/2026-10-26-game-engine.md
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
# Plum for games: browser-first macroquad host, port of a simplified KaM
|
|
2
|
-
|
|
3
|
-
> The engine is generic (textures, files, shapes, input); the KaM port lives in `../kam_plum`, not in this repository.
|
|
4
|
-
|
|
5
|
-
**Status:** draft. Planned only and not run; the user has more ideas to fold in first.
|
|
6
|
-
**Depends on:** `2026-10-26-zig-generics-and-numbers.md`, which supplies duck-typed per-type-copy generics, no traits (enums plus duck-typed operator methods), native typed arrays, and `Int`/`Float` kept as i64/f64 with `Int` narrowed to wasm i32 at `extern fun` boundaries (no JS BigInt in the browser bridge). That plan runs first.
|
|
7
|
-
**Goal:** Port `../kam_macroquad`, a 98k-line Rust/macroquad remake of Knights and Merchants that already runs in the browser via wasm, to Plum as a **simplified** game. There is no mission scripting (rhai) and the rules are simpler, but animations and sounds stay identical. It runs in the browser first.
|
|
8
|
-
**Architecture:** the Rust/macroquad host keeps asset decoding, audio and the window, compiled to wasm32 for the web or native. The Plum wasm-gc module owns game state, rules, pathfinding, AI, animation timing and per-frame draw calls. On the web the two modules talk through JS (miniquad plugin), and natively through wasmtime.
|
|
9
|
-
**Detail:** tests
|
|
10
|
-
|
|
11
|
-
## Design
|
|
12
|
-
|
|
13
|
-
What the user decided:
|
|
14
|
-
- macroquad is the host, and Plum handles game logic plus rendering.
|
|
15
|
-
- Value types (user: a `value enum` with a single constructor, not a new `struct` keyword) and simd128 are wanted.
|
|
16
|
-
- Port KaM, with no scripting engine and simpler rules but the same animations and sounds.
|
|
17
|
-
- It must work in the browser, as the Rust version does today.
|
|
18
|
-
|
|
19
|
-
Survey of `../kam_macroquad`:
|
|
20
|
-
- 98k LOC, 356 structs, 180 enums, 864 `match`.
|
|
21
|
-
- ~1.9k `&mut self` methods, 967 tests, ~350 macroquad calls.
|
|
22
|
-
- Collections: `Vec` ×997, `Option` ×814, `HashMap`/`Set`/`BTree`/`VecDeque`/`BinaryHeap` ×145.
|
|
23
|
-
- Closures and iterator chains ×~3.7k each.
|
|
24
|
-
- Numbers: `Int` ×5.2k, `Float` ×1.8k, `u8`/`u16`/`u32`/`usize` ×5.7k (mostly binary asset parsing).
|
|
25
|
-
- Traits are small: 7 traits, 63 impls, 40 `dyn`.
|
|
26
|
-
- The asset side uses flate2, postcard, RON and serde.
|
|
27
|
-
|
|
28
|
-
1. **Split of responsibilities.** Ruling: the host keeps asset decoding (`.rx` sprites/atlases, `kamdat`, flate2, map files), the serde/postcard/RON formats and audio, because that removes almost all the unsigned/binary-parsing and serialization code from the port. The cost is that the host stays Rust and is game-specific.
|
|
29
|
-
- The host exposes **handles**: a sprite is `(atlas: Int, rect index: Int)`, and sounds and maps are `Int` ids. Animations stay identical because Plum gets the same frame tables, read through host imports.
|
|
30
|
-
2. **Browser host first.**
|
|
31
|
-
- `plum-game` is a Rust crate on macroquad. For the web it compiles to wasm32 with macroquad's JS loader.
|
|
32
|
-
- A `plum_bridge.js` miniquad plugin instantiates Plum's wasm-gc module, wires Plum's `extern fun` imports to functions exported from the macroquad wasm, and has macroquad's loop call Plum's `init`, `update(g, dt: Float)` and `draw(g)`.
|
|
33
|
-
- The native build runs the same Plum module under wasmtime (GC, multi-value and SIMD on), with the same extern API.
|
|
34
|
-
- Ruling: game state lives in the Plum `Game` ref, held by the host and passed to every call, so no mutable globals are needed.
|
|
35
|
-
3. **Extern API (`plum-std/Game.plum`).**
|
|
36
|
-
- Window: `screenSize() -> Vec2` and `frameTime() -> Float`.
|
|
37
|
-
- Drawing: `clear(c)`, `drawSprite(atlas, rect, x, y: Float, flipX: Bool, tint: Color)`, `drawRect`, `drawLine`, `drawText(s: Str, x, y, size: Float, c)`, `setCamera2D(target: Vec2, zoom: Float)` and `resetCamera()`.
|
|
38
|
-
- Input: `isKeyDown(k: Key)`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown(b: MouseButton)` and `mouseWheel() -> Float`.
|
|
39
|
-
- Audio: `playSound(id: Int, x, y: Float)` (positional, as in KaM), `playMusic(id)` and `stopMusic()`.
|
|
40
|
-
- Assets, as host lookups by name: `atlasId`, `animFrames(unitKind, action, dir) -> List[Int]` (frame indices, copied once at load) and `animStep`, `loadMap(name) -> MapData`, which copies terrain into Plum `Array[Int]`s once.
|
|
41
|
-
- `Color` and `Vec2` are value enums flattened across the boundary. `Key` and `MouseButton` are enums matching macroquad's codes.
|
|
42
|
-
4. **Language additions this port needs** (beyond the Zig-generics plan and items 5–7 below):
|
|
43
|
-
- **Unboxed typed arrays.** With copies per type, `Array[Int]` and `Array[Float]` lower to native `array (mut i64)` and `array (mut f64)`. These are for tile grids, fog of war, passability and path costs. Add `Array.filled(n, v)` and a 2D index helper.
|
|
44
|
-
- **Collections in std:** `Set[T]`, `Deque[T]` (ring buffer) and `Heap[T]` (binary min-heap with a key fn, for A*), all Plum code over `Array`.
|
|
45
|
-
- **Constants:** large top-level constant tables must parse and be usable in expressions. A probe file with `LIMIT = 10` followed by a `type` failed to parse (cause not yet confirmed), and the notes record a SCREAMING_CASE constant gotcha. The fix needs tests with 1k-entry tables.
|
|
46
|
-
- **Seeded RNG** (`Random(seed: Int)` with xorshift/PCG) for deterministic AI and simulation.
|
|
47
|
-
- Lazy iteration is deferred. Hot loops use `for … range`, and it's revisited only if profiling shows `map`/`filter` allocation in `update`.
|
|
48
|
-
5. **Value enums** (`value enum Vec2 = | Vec2(x: Float, y: Float)`, `Color`, `TilePos`). Exactly one constructor named like the type, fields only `Int`/`Float`/`Bool` or other value enums (no `Str`/`List`, no recursion). Lowering: flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/error unions/optionals, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
|
|
49
|
-
6. **Duck-typed operators** on value enums (`add`/`sub`/`mul`/`neg`/`eq`): `pos + dir * speed`.
|
|
50
|
-
7. **SIMD (late):** `f32x4` (wasm v128) and `Mat4`/`Vec4` in `plum-std/Math.plum`. KaM is 2D, so this is low priority for the port and kept for future 3D work.
|
|
51
|
-
8. **Port scope (simplified KaM):**
|
|
52
|
-
- Kept:
|
|
53
|
-
- map and terrain rendering with camera and zoom;
|
|
54
|
-
- units with directional walk/work/fight animations and their sounds;
|
|
55
|
-
- A* pathfinding on the tile grid;
|
|
56
|
-
- buildings placed and constructed with their animations;
|
|
57
|
-
- a simplified economy: woodcutter → sawmill → storehouse, serfs carrying wares;
|
|
58
|
-
- simple melee combat;
|
|
59
|
-
- a basic AI that builds and attacks;
|
|
60
|
-
- selection and commands with the mouse;
|
|
61
|
-
- minimap;
|
|
62
|
-
- win and lose conditions.
|
|
63
|
-
- Dropped: rhai mission scripts, campaign/mission scripting, multiplayer/results screens, the map editor, the full KaM economy tree and saves (maybe Json later).
|
|
64
|
-
9. **Out of scope:** WebGPU, Plum→shader compilation, porting the asset decoders, and threads.
|
|
65
|
-
|
|
66
|
-
Success criteria:
|
|
67
|
-
- The simplified game runs in Chrome/Firefox via `plum game --web`, served as a static folder, at 60 fps on a 128×128 map with 300 units.
|
|
68
|
-
- Animations and sounds match the Rust version's for the kept unit and building types (side-by-side capture).
|
|
69
|
-
- Zero GC allocation in `update` steady-state outside unit spawn and death, measured with a host GC counter in the native build.
|
|
70
|
-
|
|
71
|
-
## Global constraints
|
|
72
|
-
|
|
73
|
-
- Both suites (`cargo test --workspace`, `plum test plum-std plum-examples`) are green after every task.
|
|
74
|
-
- The `plum-game` crate builds for `wasm32-unknown-unknown` (web) and native. CI runs the native headless tests.
|
|
75
|
-
- The asset files are read from `../kam_macroquad/assets` through the host. They are never copied into this repo, because of licensing.
|
|
76
|
-
- Plum code uses `Int` (i64) and `Float` (f64). The extern API declares `Int`/`Float`, the compiler narrows `Int` to wasm i32 at the boundary, and the host converts `Float` to macroquad's f32.
|
|
77
|
-
|
|
78
|
-
## Tasks
|
|
79
|
-
|
|
80
|
-
```mermaid
|
|
81
|
-
flowchart LR
|
|
82
|
-
L1[typed arrays + collections + consts + RNG]
|
|
83
|
-
L2[value enums]
|
|
84
|
-
L3[duck-typed operators]
|
|
85
|
-
H1[plum-game host native + Game.plum]
|
|
86
|
-
H2[browser bridge]
|
|
87
|
-
P1[map + camera]
|
|
88
|
-
P2[units + anims + sounds]
|
|
89
|
-
P3[pathfinding]
|
|
90
|
-
P4[buildings + economy]
|
|
91
|
-
P5[combat + AI + win/lose]
|
|
92
|
-
S1["f32x4 + Math.plum (later)"]
|
|
93
|
-
%% taskgraph
|
|
94
|
-
L1 --> L2
|
|
95
|
-
L2 --> L3
|
|
96
|
-
L2 --> H1
|
|
97
|
-
H1 --> H2
|
|
98
|
-
H2 --> P1
|
|
99
|
-
L3 --> P1
|
|
100
|
-
P1 --> P2
|
|
101
|
-
P2 --> P3
|
|
102
|
-
P3 --> P4
|
|
103
|
-
P4 --> P5
|
|
104
|
-
L3 --> S1
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### L1 typed arrays, collections, constants, RNG
|
|
108
|
-
- Native `Array[Int]`/`Array[Float]` (wasm-inspected), `Array.filled` and the 2D helper.
|
|
109
|
-
- `Set`, `Deque` and `Heap` in std.
|
|
110
|
-
- Fix top-level constant parsing and use.
|
|
111
|
-
- `Random(seed)`.
|
|
112
|
-
|
|
113
|
-
Tests:
|
|
114
|
-
- `array_int_native` (the wasm declares `array (mut i64)`);
|
|
115
|
-
- `heap_pop_order` on 1000 random keys;
|
|
116
|
-
- `deque_wraparound`;
|
|
117
|
-
- `set_dedup`;
|
|
118
|
-
- `const_table_1000_entries`;
|
|
119
|
-
- `random_seed_deterministic` (same seed gives the same first 100 values).
|
|
120
|
-
|
|
121
|
-
### L2 value enums (`value enum`, single constructor)
|
|
122
|
-
Grammar: a `value` modifier on `enum`, with checker errors for more than one constructor (`value enum X must have exactly one constructor`) and for non-value fields (`value enum field 'name' must be Int, Float, Bool or a value enum`). Flattened lowering, multi-value returns, copy semantics, field assignment, recursion rejected. Tests:
|
|
123
|
-
- `value_copy_semantics`;
|
|
124
|
-
- `value_multi_value_return`;
|
|
125
|
-
- `value_in_list_roundtrip`;
|
|
126
|
-
- `value_eq_fieldwise`;
|
|
127
|
-
- `value_enum_two_constructors_rejected`; `value_enum_str_field_rejected`.
|
|
128
|
-
|
|
129
|
-
### L3 duck-typed operators
|
|
130
|
-
`a + b` → `a.add(b)` and so on, resolved per instantiation. Tests:
|
|
131
|
-
- `vec2_add_mul_scalar`;
|
|
132
|
-
- `neg`;
|
|
133
|
-
- `missing_operator_method_error` with the use-site chain.
|
|
134
|
-
|
|
135
|
-
### H1 `plum-game` native host + Game.plum
|
|
136
|
-
- A macroquad + wasmtime crate with the `init`/`update`/`draw` loop.
|
|
137
|
-
- Game.plum externs as in Design item 3.
|
|
138
|
-
- Asset handles backed by kam_macroquad's existing loaders (reused as a path dependency on its asset modules).
|
|
139
|
-
- `plum game <file>`.
|
|
140
|
-
|
|
141
|
-
Tests: a headless mock recorder for call order and args; `color_flattened`; `mousePos_multi_value`; `anim_frames_match_rust` for 3 unit kinds.
|
|
142
|
-
|
|
143
|
-
### H2 browser bridge
|
|
144
|
-
- `plum_bridge.js` miniquad plugin.
|
|
145
|
-
- `plum game --web <file>` writes `dist/` (index.html, the macroquad wasm, the plum wasm, the bridge).
|
|
146
|
-
|
|
147
|
-
Check: a demo scene (sprites, sound, input) runs in Chrome and Firefox. Playwright headless-Chrome smoke test: canvas pixels change across frames and there are no console errors.
|
|
148
|
-
|
|
149
|
-
### P1 map + camera
|
|
150
|
-
Load a KaM map via the host, terrain into `Array[Int]`, tile rendering, camera pan/zoom, minimap. Check: renders the same as the Rust version on 2 maps (screenshot diff tolerance).
|
|
151
|
-
|
|
152
|
-
### P2 units + animations + sounds
|
|
153
|
-
Unit types, directional animation state machine, walk interpolation, selection, positional sounds. Tests: the animation frame sequence for walk dir 3 equals the Rust table, and the sound fires on the step frames.
|
|
154
|
-
|
|
155
|
-
### P3 pathfinding
|
|
156
|
-
A* over passability with `Heap`, path smoothing as in `src/pathfinding.rs` (simplified), unit move orders. Tests: shortest-path length on 5 fixture grids matches the Rust implementation, and 300 units repathing stays under 2 ms per frame (native).
|
|
157
|
-
|
|
158
|
-
### P4 buildings + economy
|
|
159
|
-
Placement, construction stages with animations, woodcutter → sawmill → storehouse, serfs carrying wares. Tests: a deterministic 2-minute sim produces N planks (fixed seed); building stages advance at the Rust timings.
|
|
160
|
-
|
|
161
|
-
### P5 combat + AI + win/lose
|
|
162
|
-
Melee combat with animations and sounds, a basic AI that builds and attacks, and victory/defeat. Check: an AI-vs-AI game ends deterministically for a fixed seed. The full game plays in the browser at the success-criteria scale.
|
|
163
|
-
|
|
164
|
-
### S1 f32x4 + Math.plum (later)
|
|
165
|
-
v128, lane ops, `Vec4`/`Quat`/`Mat4`. Tests: `mat4_inverse_roundtrip`, and `simd_ops_emitted`. Not needed for the 2D port.
|
|
166
|
-
|
|
167
|
-
## Open questions
|
|
168
|
-
|
|
169
|
-
- Will the user's further ideas change scope or order? This is still a draft.
|
|
170
|
-
- Which unit and building types make the "kept" list? The default above is the wood chain plus militia/axe fighters.
|
|
171
|
-
- Is reusing kam_macroquad's asset modules as a path dependency acceptable, or should the host vendor a minimal decoder?
|
docs/plans/2026-11-01-struct-value-types.review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `struct`, `record`, and enums as unions of types (review plan)
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** done (built as 2026-11-01-struct-record and 2026-11-01-union-enums).
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
```plum
|
docs/plans/2026-11-02-render-experiments.md
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# Render experiments: own batcher, command buffer, Plum scene graph
|
|
2
|
-
|
|
3
|
-
**Branch:** `superlite/render-experiments` (based on `superlite/pixi-spike`, which has `--renderer`, `plum-game/web-test --measure/--headed/--uncapped` and `fixtures/stress.plum`).
|
|
4
|
-
**Question:** what is the fastest way to get Plum draw data to the GPU, and what does a scene graph written in Plum cost? User preference: a libGDX-like scene graph; an own host/renderer if it is faster.
|
|
5
|
-
|
|
6
|
-
## Variants (all measured on the same stress scene)
|
|
7
|
-
|
|
8
|
-
| id | renderer | draw data path |
|
|
9
|
-
|---|---|---|
|
|
10
|
-
| A | macroquad (web wasm32, and native) | one extern call per draw (current) |
|
|
11
|
-
| B | Pixi (spike) | one extern call per draw |
|
|
12
|
-
| C | `gpu`: own JS batcher, WebGPU with WebGL2 fallback | one extern call per draw |
|
|
13
|
-
| D | `gpu` (web) and macroquad (native) | command buffer in wasm linear memory, read once per frame |
|
|
14
|
-
| E | C and D | stress scene built with the Plum scene graph `std/Scene` |
|
|
15
|
-
|
|
16
|
-
## Linear memory primitive (compiler)
|
|
17
|
-
|
|
18
|
-
`plum-std/Mem.plum`: `primitive Mem` with compiler intrinsics. Addresses are byte offsets (`Int`, wrapped to i32). A module that uses `Mem` gets one exported wasm memory named `memory` (initial 16 pages, growable); modules that don't use it are unchanged.
|
|
19
|
-
|
|
20
|
-
- `Mem.size() -> Int` (bytes), `Mem.grow(pages: Int) -> Int` (old page count, -1 on failure)
|
|
21
|
-
- `Mem.storeF32(addr: Int, v: Float)`, `Mem.storeI32(addr: Int, v: Int)`, `Mem.storeU8(addr: Int, v: Int)`
|
|
22
|
-
- `Mem.loadF32(addr: Int) -> Float`, `Mem.loadI32(addr: Int) -> Int`, `Mem.loadU8(addr: Int) -> Int`
|
|
23
|
-
|
|
24
|
-
## Command buffer (`plum-std/GameCmd.plum`)
|
|
25
|
-
|
|
26
|
-
Same API as `std/Game`, same names and signatures, so a game switches by changing one import. Input, assets, sound and screen queries stay `extern`. The draw functions (`clear`, `drawRect`, `drawLine`, `drawCircle`, `drawText`, `drawTexture`, `drawTextureEx`, `setCamera`, `resetCamera`) instead append commands to a buffer in linear memory, growing it as needed. GameCmd also exports:
|
|
27
|
-
|
|
28
|
-
- `export fun cmdReset()`: the host calls it before `draw`;
|
|
29
|
-
- `export fun cmdPtr() -> Int` and `export fun cmdLen() -> Int`: byte offset and length of this frame's commands, which the host reads after `draw` from the `memory` export.
|
|
30
|
-
|
|
31
|
-
Encoding: little-endian. Every field is 4 bytes: f32 for Float, i32 for Int and Bool. Each command is a u32 opcode followed by its fields:
|
|
32
|
-
|
|
33
|
-
| op | command | fields |
|
|
34
|
-
|---|---|---|
|
|
35
|
-
| 1 | clear | r g b a |
|
|
36
|
-
| 2 | rect | x y w h r g b a |
|
|
37
|
-
| 3 | line | x1 y1 x2 y2 thickness r g b a |
|
|
38
|
-
| 4 | circle | x y radius r g b a |
|
|
39
|
-
| 5 | text | x y size r g b a byteLen(i32), then UTF-8 bytes zero-padded to a multiple of 4 |
|
|
40
|
-
| 6 | texture | tex(i32) x y r g b a |
|
|
41
|
-
| 7 | textureEx | tex(i32) sx sy sw sh x y w h rotation flipX(i32) r g b a |
|
|
42
|
-
| 8 | setCamera | x y scale |
|
|
43
|
-
| 9 | resetCamera | (no fields) |
|
|
44
|
-
|
|
45
|
-
Draw order is command order. Semantics are exactly those of `std/Game` (see the README Games section).
|
|
46
|
-
|
|
47
|
-
## Scene graph (`plum-std/Scene.plum`)
|
|
48
|
-
|
|
49
|
-
libGDX Scene2D-like, written in Plum, drawing through whichever Game module the program imports. Since a std module can't be generic over the import, it targets `std/Game`; the fixtures provide a GameCmd copy, `Scene` over `GameCmd`, as a fixture file for variant E-D.
|
|
50
|
-
|
|
51
|
-
- A node has: x, y, width, height, originX, originY, scaleX, scaleY, rotation (radians), color, visible, children, and an optional `act: fn(Node, Float)`.
|
|
52
|
-
- Kinds: group, sprite (texture plus source rect), rect, circle, text.
|
|
53
|
-
- `Stage`: the root with a camera, plus `act(dt)` and `draw()`.
|
|
54
|
-
- World transforms compose parent→child (translate, rotate about origin, scale). Drawing uses `drawTextureEx` with the composed position, rotation and size; skew from non-uniform scale under rotation isn't supported, as with libGDX actors without group transforms.
|
|
55
|
-
- Hit testing: `stage.hit(x, y) -> Node?`.
|
|
56
|
-
- Pure-logic `test` blocks.
|
|
57
|
-
|
|
58
|
-
## Ownership
|
|
59
|
-
|
|
60
|
-
| agent | owns |
|
|
61
|
-
|---|---|
|
|
62
|
-
| compiler | plum-checker, plum-wasm-codegen, plum-core (if needed), `plum-std/Mem.plum`, `plum-std/GameCmd.plum`, their tests (in its own worktree) |
|
|
63
|
-
| gpu-web | `plum-game/web/plum_bridge_gpu.js`, `plum-game/web/index_gpu.html`, `plum-cli/src/game.rs` (renderer flag), plum-cli game tests, the plum-game/web-test harness (src/, tests/) |
|
|
64
|
-
| native-cmd | `plum-game/src/*` (macroquad native reading the command buffer; per-frame timing for native via `PLUM_GAME_MEASURE=secs`), plum-game/tests |
|
|
65
|
-
| scene | `plum-std/Scene.plum`, `plum-game/web-test/fixtures/*` (stress variants), `plum-examples/scene.plum` (a small scene-graph demo) |
|
|
66
|
-
|
|
67
|
-
The results go in `plum-game/web-test/BENCHMARK.md`, written by the coordinator's benchmark run.
|
|
68
|
-
|
|
69
|
-
## User rulings (2026-11-02, mid-run)
|
|
70
|
-
- **Browser-only target.** Own renderer on WebGPU directly, with no WebGL2 fallback. There's no native window: macroquad (native and wasm32) and Pixi are removed after the benchmark.
|
|
71
|
-
- **The headless wasmtime test host stays:** `plum-game` shrinks to wasmtime plus Recorder for `cargo test` draw-call checks. The browser check (playwright-rs) covers rendering.
|
|
72
|
-
- **"Pixi in Plum":** the scene graph (`std/Scene`) is the primary game API direction, written in Plum and rendered through the thin WebGPU batcher.
|
docs/plans/2026-11-03-inline-variants-match-body.review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Inline record variants and `= match` bodies: review plan
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** done, merged to master (38ccb88).
|
|
4
4
|
|
|
5
5
|
## Summary
|
|
6
6
|
Two readability changes so recursive data reads like Gleam or Haskell. First, an enum variant may declare its fields inline again: `enum Link[T] = Nil | Cons(head: T, tail: Link[T])`. As in Haskell and Gleam, `Link[T]` is the only type: `Cons` is a constructor and a pattern, never a type of its own. Under the hood the loader still generates a hidden record and lists it as a member, so code generation is unchanged; the checker only makes sure `Cons` never appears as a type and that building one gives a `Link`. Second, a function or method whose whole body is one `match` can start it right after the `=`, with the arms indented below, instead of indenting the `match` on its own line.
|