plum

#treesitter#compiler#wasm

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

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


c881a9f — Peter John 2026-11-03T15:39:59+05:30
docs(plans): functional-style gaps review plan
docs/plans/2026-11-03-functional-gaps.review.md ADDED
@@ -0,0 +1,98 @@
1
+ # Functional-style gaps: review plan
2
+
3
+ **Status:** written, waiting for approval (2026-11-03). Orchestrator: human.
4
+
5
+ ## Summary
6
+ Writing `plum-examples/functional.plum` hit eight compiler and formatter gaps that force workarounds in ordinary functional code. Each was reproduced on master today (repro files in `.superlite/repros/`). This plan fixes all eight, then removes the workarounds from `functional.plum` so the example shows the natural form.
7
+
8
+ | # | Gap (repro) | Today |
9
+ |---|---|---|
10
+ | 1 | Typed closure params `\|b: Box\| b.get()` (g1) | parse error |
11
+ | 2 | Untyped closure stored then called: `get := \|b\| b.get()` then `get(Box(v: 2))` (g2) | invalid wasm ("expected i64 but nothing on stack"), crashes at load |
12
+ | 3 | A function returning a function: `fun adder(n: Int) -> fn(Int) -> Int = \|x\| x + n` (g5) | parse error |
13
+ | 4 | Inline `if` mixing a value and `null`: `fun pick(c: Bool) -> Int? = if c then 1 else null` (g6) | type error "expected Int, found error{null}" |
14
+ | 5 | Generic call with a bare tag: `len(Nil)` where `len(xs: Link[T])` (g7) | codegen error "unknown function 'len'" |
15
+ | 6 | Generic `fun*` building a `Link[T]` in its body (g3) | type error "expected Link[Int], found Link[T]" |
16
+ | 7 | Formatter drops the blank line before a comment that follows a multi-line method in an enum body (g4b) | file rewritten |
17
+ | 8 | Deep recursion: `count(1000000, 0)` with a self tail call (g8) | stack overflow |
18
+
19
+ ## What you will get
20
+ - `|b: Box| b.get()` and `|a: Int, b: Int| a + b` parse and check; untyped params keep working.
21
+ - A closure stored in a local and called later gets its param types from the first call (`get := |b| b.get()` then `get(Box(v: 2))`), or a clear error "cannot infer the type of b; write |b: Box|" — never invalid wasm.
22
+ - `-> fn(Int) -> Int` as a return type (and in fields, params and locals as today); `adder(1)(2)` and `add1 := adder(1)` both work. `fn(A) -> fn(B) -> C` nests to the right.
23
+ - `if c then 1 else null` types as `Int?` (in inline `if` and `match` arms whose other branches are `T`).
24
+ - `len(Nil)`: Rust-style "cannot infer T for len(Nil); annotate, e.g. empty: Link[Int] := Nil" from the checker instead of a codegen crash (default; see decisions).
25
+ - A generic `fun*` may use its own `T` in its body (`out: Link[T] := Nil`), substituted per expansion.
26
+ - The formatter keeps blank lines before comments inside enum/record/trait bodies; idempotent on plum-std and plum-examples.
27
+ - Tail calls: a call in tail position (`return f(..)`, the last expression, an `if`/`match` arm in tail position) compiles to wasm `return_call` / `return_call_ref`, so `count(1000000, 0)` and the recursive `Link` methods on long lists run in constant stack.
28
+ - `functional.plum` updated: `Fn` record replaced by real `fn` returns, typed closures, generic `collect`, `pick`-style inline `if`, `len(Nil)` example shows the annotation; README gains the new forms (typed closure params, `fn` return types, tail calls).
29
+
30
+ ## What will be built
31
+ | Part | What it does | Size | Built by |
32
+ |---|---|---|---|
33
+ | Closures: typed params | Grammar + parser for `\|name: Type\|`; checker uses the declared types | S | a careful helper |
34
+ | Closures: call-site inference | A closure local's param types come from its first call, else a clear error; fixes the invalid-wasm case | M | a careful helper |
35
+ | `fn` return types | Grammar for `-> fn(..) -> R` (right-nested), checker and codegen for returning closures | M | a careful helper |
36
+ | Optional from inline `if` | Branch unification `T` + `null` -> `T?` in inline `if` and `match` | S | a careful helper |
37
+ | Unpinned generics | Checker detects a generic call whose type params stay unknown and reports it before codegen | S | a careful helper |
38
+ | Generic `fun*` bodies | Substitute the form's own generics in its body at each expansion (forms.rs) | M | a careful helper |
39
+ | Formatter blank lines | format.scm keeps blank lines before comments in type bodies | S | a careful helper |
40
+ | Tail calls | Codegen emits `return_call` / `return_call_ref` in tail position; wasmtime and browser config enable the tail-call proposal | M | a careful helper |
41
+ | Example and docs | Remove the workarounds from functional.plum; README sections; tests | S | a fast helper |
42
+
43
+ ## How it fits together
44
+ ```mermaid
45
+ flowchart LR
46
+ G["grammar: |x: T|, -> fn(..) -> R"] --> P[parser]
47
+ P --> F["forms.rs: generic fun* substitution"]
48
+ F --> C["checker: closure types, T + null -> T?, unpinned generics"]
49
+ C --> W["codegen: closures returned, return_call"]
50
+ G --> FM["formatter (format.scm)"]
51
+ W --> X["functional.plum + README"]
52
+ ```
53
+
54
+ ## How it gets built
55
+ ```mermaid
56
+ flowchart TB
57
+ subgraph W1["Wave 1, in parallel"]
58
+ A["grammar: typed closure params + fn return types"]
59
+ B["optional from inline if"]
60
+ C["unpinned generics error"]
61
+ D["generic fun* bodies"]
62
+ E["formatter blank lines"]
63
+ T["tail calls (codegen)"]
64
+ end
65
+ A --> A2["closure call-site inference + returning closures (checker/codegen)"]
66
+ A2 --> X["example + README"]
67
+ B --> X
68
+ C --> X
69
+ D --> X
70
+ E --> X
71
+ T --> X
72
+ ```
73
+ Several wave-1 parts touch `plum-checker/src/lib.rs`; the machine plan splits ownership by function or orders them where they can't be split.
74
+
75
+ ## Decisions you can change
76
+ - `len(Nil)` with nothing fixing `T`: clear checker error asking for an annotation, as Rust does (default) | silently instantiate `T` with a placeholder so it just works (Haskell-style defaulting).
77
+ - Untyped closure locals: infer from the first call (default) | always require typed params when a closure is stored in a local.
78
+ - Tail calls: emit `return_call` everywhere (default; supported by wasmtime, Chrome and Firefox; Safari support is checked in the build with the web-test fixtures) | only when targeting native (`plum run/test/build`), not the browser game build.
79
+ - Tail-call scope: every call in tail position (default) | only self-recursive calls.
80
+
81
+ ## Risks
82
+ - Grammar: `|x: T|` and `-> fn(A) -> B` add new shapes near closures and return types; regenerate with `--disable-optimizations`, and every corpus case plus all std and examples must parse unchanged.
83
+ - Tail calls change every function's codegen in tail position; the whole wasm suite, the browser web-test fixtures and the game benchmark must stay green. Mixed-type tail positions (a value converted to a union or boxed into `T?` on return) are not true tail calls and must keep the normal call.
84
+ - Safari support for the tail-call proposal: if a supported browser lacks it, the game build falls back via the decision above.
85
+ - `checker/lib.rs` is shared by four parts: ownership is split by function in the machine plan, or those parts run one after another.
86
+
87
+ ## Out of scope
88
+ - Exhaustiveness changes, a std `Link[T]`/`Tree[T]` library, persistent data structure performance work, and the scene engine itself.
89
+ - Closure param defaults or named closure args.
90
+
91
+ ## How we will know it works
92
+ - Each of the eight repro files in `.superlite/repros/` passes as a `plum test` (g7 gives the annotation error, as decided).
93
+ - `count(1000000, 0) == 1000000`; a 200k-element `Link` `length()` runs without a stack overflow.
94
+ - `plum-examples/functional.plum` has no `Fn` record, uses `|x: T|` where it helps, a generic `collect`, and still passes all its tests; `plum format` leaves it unchanged.
95
+ - `plum test plum-std plum-examples` and `cargo test --workspace` stay green; the browser web-test fixtures pass.
96
+
97
+ ## Rough time and cost
98
+ Rough, until the machine plan is linted: 8-9 tasks, about 35-50 minutes of build time, around $45-70.