plum
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.
|