plum

#treesitter#compiler#wasm

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

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


42452f8 — Peter John 2026-11-04T15:48:01+05:30
docs: compiler cleanup phase 1 plan; explicit function generics queued after phase 2
docs/backlog.md CHANGED
@@ -4,7 +4,8 @@ Open work, in the order the user queued it, plus smaller items found during buil
4
4
 
5
5
  ## Planned (in order)
6
6
 
7
- 1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Needs an approve.
7
+ 1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Approved 2026-11-04; running on branch `superlite/compiler-cleanup`, phase 1 first.
8
+ - **Explicit function generics** (user 2026-11-04: right after cleanup phase 2) — functions declare their generics (`fun map[B](...)`) instead of every single capital letter being a generic; allows longer generic names and one-letter concrete types/tags. Grammar change + about 140 signatures in std/examples. Phase 2's "generics by scope" makes this mostly a grammar + signature rewrite.
8
9
  2. **Native inline variants, then decide union enums** — compile inline variants directly as struct subtypes of their enum and delete the hidden-record lowering (`hoistInlineVariants`, the `List__Cons(Cons(..))` wrappers, union forwarders). Then decide whether user-facing union enums (`enum X = A | B` of records) stay as a thin feature or fold into traits; if they go, `JsonParse` in `plum-std/Json.plum` becomes inline variants and the union examples (`unions.plum`, `struct_union_members.plum`) are rewritten. `unboxed_variants.plum` is really a compiler probe: move it to a codegen test. (Decision record §8b.)
9
10
  3. **Benchmark** before FBIP (user: FBIP waits for it).
10
11
  4. **FBIP: no heap mutation** — persistent std collections updated in place when unique, Koka-style `fip` functions as a checked in-place guarantee, a reference count used only as a uniqueness check if static proof falls short. Worklist = the `mut` fields left today (see below). (Decision record §9.)
docs/plans/2026-11-04-compiler-cleanup-phase1.md ADDED
@@ -0,0 +1,251 @@
1
+ # Compiler cleanup, phase 1: deletions and test helpers
2
+
3
+ **Review plan:** docs/plans/2026-11-04-compiler-cleanup.review.md
4
+ **Goal:** Delete dead code and small duplicates in the compiler; one shared test-support crate; fewer, de-duplicated test binaries. No behaviour change: every example compiles to byte-identical wasm.
5
+ **Architecture:** Rust workspace (plum-core parser+passes, plum-checker, plum-wasm-codegen, plum-cli, plum-lsp). New workspace member `plum-test-support` (dev-dependency only).
6
+ **Tech stack:** Rust 2021, cargo, wasmtime (GC + function references + tail calls), tree-sitter-plum.
7
+ **Detail:** contracts
8
+ **Target-min:** 14
9
+
10
+ ## Global constraints
11
+ - Language: Rust. Test command per crate: `cargo test -p <crate>` (do NOT trust `run_tests --fast` path filters in this repo; run the crate's tests directly). Full: `cargo test --workspace`.
12
+ - `touch` an edited .rs file before trusting a cargo result (stale-build gotcha in this sandbox).
13
+ - Functions are camelCase (`#![allow(non_snake_case)]` at crate/test top), matching the repo.
14
+ - No behaviour change. No test may be deleted unless this plan names it.
15
+ - `plum-game` and `plum-runtime` are not workspace members: out of scope this phase.
16
+ - Never run `plum format` on real files (it rewrites in place).
17
+
18
+ ## Main diagram
19
+ ```mermaid
20
+ flowchart LR
21
+ TS["plum-test-support"] --> CT["checker tests"]
22
+ TS --> GT["codegen tests"]
23
+ TS --> E2E["e2e binaries"]
24
+ TS --> CO["core + cli tests"]
25
+ DC["dead code: core / checker / codegen"] --> V["verify: suite + wasm hashes"]
26
+ WH["wasm-hashes script"] --> V
27
+ CT --> V
28
+ GT --> V
29
+ E2E --> V
30
+ CO --> V
31
+ ```
32
+
33
+ ## File structure
34
+ - `scripts/wasm-hashes.sh` — compile every example with `main` at a git rev, print sha256 per file.
35
+ - `plum-test-support/{Cargo.toml,src/lib.rs}` — shared test helpers (parse, load, compile, engine, run, errors).
36
+ - `plum-wasm-codegen/tests/{e2e_records,e2e_types,e2e_tail,e2e_mem}/` — 4 e2e binaries replacing 19 files.
37
+ - `plum-checker/tests/features/` — one binary for the small checker test files.
38
+
39
+ ## Task graph
40
+ ```mermaid
41
+ flowchart LR
42
+ T1[wasm hash script]
43
+ T2[core dead code]
44
+ T3[checker duplicates]
45
+ T4[codegen dead code]
46
+ T5[test-support crate]
47
+ T6[checker tests]
48
+ T7[codegen unit tests]
49
+ T8[e2e records binary]
50
+ T12[e2e types binary]
51
+ T9[e2e tail binary]
52
+ T13[e2e memory binary]
53
+ T10[core and cli tests]
54
+ T11[verify phase 1]
55
+ %% taskgraph
56
+ T5 --> T6
57
+ T5 --> T7
58
+ T5 --> T8
59
+ T5 --> T9
60
+ T5 --> T12
61
+ T5 --> T13
62
+ T5 --> T10
63
+ T1 --> T11
64
+ T2 --> T11
65
+ T3 --> T11
66
+ T4 --> T11
67
+ T6 --> T11
68
+ T7 --> T11
69
+ T8 --> T11
70
+ T9 --> T11
71
+ T12 --> T11
72
+ T13 --> T11
73
+ T10 --> T11
74
+ ```
75
+
76
+ ## Package A: compiler deletions
77
+ **Probe entry:** run=cargo test --workspace
78
+
79
+ ### T1 wasm hash script · Complexity 3 · Est. context 25k
80
+ **Owns:** scripts/wasm-hashes.sh
81
+ **Components:** 1
82
+ **LOC owned:** 60
83
+ **Tests:** slow
84
+ **Kind:** design
85
+ **Model:** sonnet
86
+ **Produces:** scripts/wasm-hashes.sh REV OUT -> writes OUT with lines `<sha256> <relpath>` sorted by relpath
87
+ **Test code:** `bash scripts/wasm-hashes.sh HEAD /tmp/a.txt && bash scripts/wasm-hashes.sh HEAD /tmp/b.txt && diff /tmp/a.txt /tmp/b.txt` exits 0 and a.txt has ≥ 20 lines.
88
+ **Steps:** `git worktree add --detach $TMP REV`; `cargo build -p plum-cli` there (separate target dir under $TMP); for every `plum-examples/**/*.plum` containing `fun main`, run a fresh `plum compile <file> -o <tmp>.wasm` (one process per file, so global counters start at 0); sha256 each; print `sha relpath`; files that fail to compile print `FAIL relpath`; remove the worktree on exit (trap).
89
+
90
+ ### T2 plum-core dead code · Complexity 2 · Est. context 30k
91
+ **Owns:** plum-core/src/drive.rs, plum-core/src/simplify.rs, plum-core/src/closure_spec.rs, plum-core/src/parser.rs, plum-core/src/uniqueness.rs
92
+ **Components:** 1
93
+ **LOC owned:** 40
94
+ **Tests:** fast
95
+ **Kind:** mechanical
96
+ **Model:** haiku
97
+ **Produces:** none
98
+ **Test code:** `cargo test -p plum-core` passes; `cargo build -p plum-core` prints no dead_code warning.
99
+ **Steps:** delete `struct Scoper` and its impl in drive.rs (≈line 2275; simplify.rs has its own used Scoper — keep that one); delete `pub fn specialise` in closure_spec.rs:22 (keep `specialiseWith`); remove `#![allow(dead_code)]` from drive.rs:11 and simplify.rs:7, then delete every item the warnings name (expected: simplify `Scoper.ctors` field, uniqueness `selfUnit`); inline `parser::forStmts` (parser.rs:903) at its call sites. Grep all crates before deleting anything `pub`.
100
+
101
+ ### T3 checker duplicates · Complexity 4 · Est. context 45k
102
+ **Owns:** plum-checker/src/lib.rs
103
+ **Components:** 2
104
+ **LOC owned:** 90
105
+ **Tests:** fast
106
+ **Model:** sonnet
107
+ **Produces:** PlumType::primitiveName(&self) -> Option<&'static str> · PlumType::fromPrimitiveName(name: &str) -> Option<PlumType>
108
+ e.g. PlumType::TInt.primitiveName() -> Some("Int")
109
+ e.g. PlumType::fromPrimitiveName("Bool") -> Some(PlumType::TBool)
110
+ e.g. PlumType::fromPrimitiveName("Str") -> None
111
+ **Review Focus:** `trmc_recheck_uses_recheck_keep`: the TRMC block in `typedSource` (≈1294-1309) calls `recheckKeep` and existing trmc/fusion tests still pass; `empty_union_plan_single_check`: `checkUnions` with an empty recorded plan returns the lowered copy without a second `check(source)`, and unions_tests still pass.
112
+ **Test code:** unit tests in a `#[cfg(test)] mod primitive_names` inside lib.rs asserting the three e.g. lines plus round-trip for Int/Float/Bool/Byte/Unit; `cargo test -p plum-checker` passes.
113
+ **Steps:** add the two fns as an inherent `impl PlumType` block in lib.rs (not types.rs); replace the 7 hand-written name↔type matches (≈lines 655, 681, 715, 1387, 1879, 2157, 4823) with them where the mapping is identical (leave a site alone if it differs, e.g. extra names); replace `closureHints::head` (≈1384) with `methodReceiverName`; TRMC block → `recheckKeep`; `checkUnions` empty-plan shortcut; fix the stale `OP_CALLS` doc comment (≈45).
114
+
115
+ ### T4 codegen dead code · Complexity 1 · Est. context 22k
116
+ **Owns:** plum-wasm-codegen/src/lib.rs
117
+ **Components:** 1
118
+ **LOC owned:** 20
119
+ **Tests:** fast
120
+ **Kind:** mechanical
121
+ **Model:** haiku
122
+ **Produces:** none
123
+ **Test code:** `cargo test -p plum-wasm-codegen --lib` and `cargo build --workspace` pass.
124
+ **Steps:** delete `WasmModule::addDataSegment` (lib.rs:137), the `DataSegmentEntry::Active` variant and its encoding branch (≈317) if nothing else constructs it (grep); remove the stale comments "Task 1" (≈156) and "Before wasm-gc" (≈4487).
125
+
126
+ ## Package B: tests
127
+ **Probe entry:** run=cargo test --workspace
128
+
129
+ ### T5 plum-test-support crate · Complexity 4 · Est. context 40k
130
+ **Owns:** plum-test-support/Cargo.toml, plum-test-support/src/lib.rs, Cargo.toml, plum-checker/Cargo.toml, plum-wasm-codegen/Cargo.toml, plum-core/Cargo.toml, plum-cli/Cargo.toml
131
+ **Components:** 1
132
+ **LOC owned:** 140
133
+ **Tests:** fast
134
+ **Model:** sonnet
135
+ **Produces:**
136
+ - plum_test_support::repoRoot() -> PathBuf
137
+ - plum_test_support::parse(src: &str) -> plum_core::ast::Source (panics with the syntax errors if the tree has_error)
138
+ - plum_test_support::parseRaw(src: &str) -> plum_core::ast::Source (no assert)
139
+ - plum_test_support::load(name: &str, src: &str) -> Result<plum_core::ast::Source, String>
140
+ - plum_test_support::compile(name: &str, src: &str) -> Vec<u8> (load + compileSource + validate, panics on error)
141
+ - plum_test_support::engine() -> wasmtime::Engine (gc, function_references, tail_call on)
142
+ - plum_test_support::runInt(name: &str, src: &str) -> i64
143
+ - plum_test_support::runStr(name: &str, src: &str) -> String
144
+ - plum_test_support::checkErrors(name: &str, src: &str) -> Vec<String> (load + typedSource errors; load error returned as one string)
145
+ - plum_test_support::PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n"
146
+ e.g. parse("fun main() -> Int = 1").fns.len() -> 1
147
+ e.g. runInt("t1", &format!("{PRE}fun main() -> Int = 40 + 2")) -> 42
148
+ e.g. checkErrors("t2", &format!("{PRE}fun main() -> Int = True")) -> vec with one message containing "Bool"
149
+ **Test code:** `plum-test-support/src/lib.rs` `#[cfg(test)]` tests asserting each e.g. line, plus `parse("fun main( ->")` panics (`#[should_panic]`).
150
+ **Notes:** copy behaviour from `plum-wasm-codegen/tests/record_init_e2e_test.rs` (load uses a temp dir `plum_ts_<name>_<pid>` + `loadAndMerge(&path, &repoRoot())`; run uses `define_unknown_imports_as_default_values`). `runStr` reads a returned Str the way `plum-wasm-codegen/tests/examples_test.rs` does. Add the crate to workspace members, and add `plum-test-support = { path = "../plum-test-support" }` under `[dev-dependencies]` of plum-checker, plum-wasm-codegen, plum-core and plum-cli (T5 alone edits these Cargo.toml files). If the exact checker error API differs, match what `plum-checker/tests/*` already call.
151
+
152
+ ### T6 checker tests · Complexity 4 · Est. context 55k
153
+ **Owns:** plum-checker/tests/*.rs, plum-checker/tests/features/*, plum-checker/src/types.rs
154
+ **Components:** 2
155
+ **LOC owned:** 120
156
+ **Tests:** fast
157
+ **Model:** sonnet
158
+ **Consumes:** plum_test_support::parse(src: &str) -> plum_core::ast::Source (T5) · plum_test_support::load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> (T5) · plum_test_support::repoRoot() -> PathBuf (T5)
159
+ **Produces:** none
160
+ **Review Focus:** `sweep_recurses`: `typed_source_has_no_unresolved_infer_vars` (checker_tests.rs:315) walks plum-std and plum-examples recursively (incl. plum-std/game/* and plum-examples/games/**) and still passes — if a newly covered file fails, keep it listed in an explicit skip list with a comment and report it; `parse_asserts`: checker_tests' `parse()` now fails on syntax errors.
161
+ **Test code:** `cargo test -p plum-checker` passes; test count unchanged except the deleted `freshVarsAreUnique` InferState test.
162
+ **Steps:** replace local parse/load/repoRoot helpers; merge io_capability, primitive_generics, variant_pattern, types, pattern_casing, closure_types, optional_unpinned, mutability test files into `tests/features/main.rs` with one `mod` file each; delete `types::InferState` and its test.
163
+
164
+ ### T7 codegen unit tests · Complexity 3 · Est. context 50k
165
+ **Owns:** plum-wasm-codegen/tests/codegen_tests.rs, plum-wasm-codegen/tests/examples_test.rs, plum-wasm-codegen/tests/fusion_alloc_test.rs
166
+ **Components:** 1
167
+ **LOC owned:** 80
168
+ **Tests:** slow
169
+ **Model:** sonnet
170
+ **Consumes:** plum_test_support::parse(src: &str) -> plum_core::ast::Source (T5) · plum_test_support::engine() -> wasmtime::Engine (T5) · plum_test_support::load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> (T5) · plum_test_support::repoRoot() -> PathBuf (T5)
171
+ **Produces:** none
172
+ **Review Focus:** `parse_asserts`: codegen_tests' `parse()` (line 6) fails on syntax errors; any of its 16 callers that relied on a partial parse is fixed in the test source, not by `parseRaw`, and reported.
173
+ **Test code:** `cargo test -p plum-wasm-codegen --test codegen_tests --test examples_test --test fusion_alloc_test` passes, same test count.
174
+ **Steps:** replace local helpers; examples_test's examples walk recursive if it isn't already.
175
+
176
+ ### T8 e2e records binary · Complexity 3 · Est. context 35k
177
+ **Notes:** all e2e tasks (T8, T12, T9, T13): move files to `tests/<bin>/<feature>.rs` as `mod`s of `tests/<bin>/main.rs` with the shared helpers, delete the old files; temp names unique per test; `cargo test -p plum-wasm-codegen --test <bin>`.
178
+ **Owns:** plum-wasm-codegen/tests/e2e_records/*, plum-wasm-codegen/tests/{empty_record_embed,enum_init,inline_variants,record_embed,record_init,unboxed_variants}_e2e_test.rs
179
+ **Components:** 1
180
+ **LOC owned:** 60
181
+ **Tests:** slow
182
+ **Model:** sonnet
183
+ **Consumes:** plum_test_support::runInt(name: &str, src: &str) -> i64 (T5) · plum_test_support::PRE: &str (T5)
184
+ **Produces:** none
185
+ **Test code:** `--test e2e_records` passes, same count.
186
+
187
+ ### T12 e2e types binary · Complexity 3 · Est. context 35k
188
+ **Owns:** plum-wasm-codegen/tests/e2e_types/*, plum-wasm-codegen/tests/{traits,unions,io_entry,closure_types}_e2e_test.rs, plum-wasm-codegen/tests/type_param_static_test.rs
189
+ **Components:** 1
190
+ **LOC owned:** 60
191
+ **Tests:** slow
192
+ **Model:** sonnet
193
+ **Consumes:** plum_test_support::runInt(name: &str, src: &str) -> i64 (T5) · plum_test_support::compile(name: &str, src: &str) -> Vec<u8> (T5) · plum_test_support::PRE: &str (T5)
194
+ **Produces:** none
195
+ **Review Focus:** `closure_dupes`: delete closure_types `g2_never_called_is_error` and `g1_typed_param` only if their source equals checker `local_closure_never_called_error` / `typed_param_used`; else keep and report.
196
+ **Test code:** `--test e2e_types` passes, count minus the deleted ones.
197
+
198
+ ### T9 e2e tail binary · Complexity 3 · Est. context 35k
199
+ **Owns:** plum-wasm-codegen/tests/e2e_tail/*, plum-wasm-codegen/tests/{closure_tail,error_tail,loop_break,tail_call,tail_calls}_e2e_test.rs
200
+ **Components:** 1
201
+ **LOC owned:** 60
202
+ **Tests:** slow
203
+ **Model:** sonnet
204
+ **Consumes:** plum_test_support::runInt(name: &str, src: &str) -> i64 (T5) · plum_test_support::compile(name: &str, src: &str) -> Vec<u8> (T5) · plum_test_support::PRE: &str (T5)
205
+ **Produces:** none
206
+ **Review Focus:** `tail_dedupe`: tail_call + tail_calls become one `tail` mod; delete `self_tail_call_deep`, `mutual_tail_call_deep`, `closure_tail_call`; keep `self_tail_million`, `mutual_tail_million`, `closure_tail_call_runs` and the wasm-shape tests.
207
+ **Test code:** `--test e2e_tail` passes, count minus 3.
208
+
209
+ ### T13 e2e memory binary · Complexity 3 · Est. context 30k
210
+ **Owns:** plum-wasm-codegen/tests/e2e_mem/*, plum-wasm-codegen/tests/{reuse,unique_inplace,unique_spread}_e2e_test.rs
211
+ **Components:** 1
212
+ **LOC owned:** 50
213
+ **Tests:** slow
214
+ **Model:** sonnet
215
+ **Consumes:** plum_test_support::runInt(name: &str, src: &str) -> i64 (T5) · plum_test_support::compile(name: &str, src: &str) -> Vec<u8> (T5) · plum_test_support::PRE: &str (T5)
216
+ **Produces:** none
217
+ **Test code:** `--test e2e_mem` passes, same count.
218
+
219
+ ### T10 core and cli tests · Complexity 3 · Est. context 45k
220
+ **Owns:** plum-core/tests/*.rs, plum-cli/tests/*.rs
221
+ **Components:** 1
222
+ **LOC owned:** 100
223
+ **Tests:** fast
224
+ **Model:** sonnet
225
+ **Consumes:** plum_test_support::parse(src: &str) -> plum_core::ast::Source (T5) · plum_test_support::repoRoot() -> PathBuf (T5)
226
+ **Produces:** none
227
+ **Test code:** `cargo test -p plum-core` and `cargo test -p plum-cli` pass, same counts.
228
+ **Notes:** plum-core dev-depending on plum-test-support is a dev-dependency cycle. If it causes `ast::Source` type mismatches (then remove nothing from Cargo.toml; just don't use it), keep plum-core's local parse helpers but make every one assert `!tree.root_node().has_error()`, and report it. plum-cli tests: use the support crate only where they hand-roll the same helpers.
229
+
230
+ ## Package C: verify
231
+ **Probe entry:** run=bash scripts/wasm-hashes.sh
232
+
233
+ ### T11 verify phase 1 · Complexity 2 · Est. context 30k
234
+ **Owns:** docs/plans/2026-11-04-compiler-cleanup-phase1.results.md
235
+ **Components:** 1
236
+ **LOC owned:** 40
237
+ **Tests:** slow
238
+ **Model:** sonnet
239
+ **Consumes:** scripts/wasm-hashes.sh REV OUT -> writes OUT with lines `<sha256> <relpath>` sorted by relpath (T1)
240
+ **Produces:** none
241
+ **Review Focus:** `wasm_identical`: hashes at the start sha (`.superlite/progress.md`) equal hashes of the current tree (`git stash create` gives a sha; commit nothing).
242
+ **Test code:** `cargo test --workspace` green; hash diff empty.
243
+ **Steps:** results file: hash diff, workspace test totals, Rust LOC, test-binary count, `#[test]` count, each before/after.
244
+
245
+ ## Review Focus
246
+ - Package A (library/compiler): deep, state — risk: deleting something only used via another crate; the checkUnions shortcut changing which errors surface.
247
+ - Package B (test harness): file, state — risks: temp-dir name collisions between merged tests (name must stay unique per test), test count silently dropping, dev-dep cycle.
248
+ - Package C: file — wasm output must be identical.
249
+
250
+ ## Self-review
251
+ Coverage: every phase-1 row of the review plan maps to T2–T13. Scope rulings are in `.superlite/progress.md`.