plum

#treesitter#compiler#wasm

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

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


64b1d3cPeter John 2026-07-23T19:33:45+05:30
docs: add design spec for variadic parameters
docs/superpowers/specs/2026-07-23-variadic-params-design.md ADDED
@@ -0,0 +1,178 @@
1
+ # Design: variadic parameters (`values: ...a`)
2
+
3
+ ## Problem
4
+
5
+ `fn(..., values: ...a)` parses today (`ast::ParamType::Variadic(Type)`), but every
6
+ downstream stage — checker, `monomorphize.rs`, codegen — treats a `Variadic(t)`
7
+ param identically to a plain fixed-arity `Type(t)` param: arity checks require
8
+ `args.len() == params.len()` exactly, and the param is bound as a single `t`-typed
9
+ value, not a collection. There is no way to actually call a variadic function with
10
+ 0, 1, or many arguments, or to do anything with the collected values inside the
11
+ body. `examples/functions.plum`'s own `sumAll(nums: ...Int) -> Int = todo` has never
12
+ been implemented because there's nothing to implement it with.
13
+
14
+ This is the second of the two remaining "Known gaps" follow-ups (the first,
15
+ field/attribute assignment, is already done); it's a prerequisite for wiring up
16
+ `libs/std/list.plum`'s `add`/`init` methods, which is explicitly **out of scope**
17
+ for this cycle — this spec covers only making variadic parameters real as a
18
+ language feature, with `examples/functions.plum`'s `sumAll` as the target sanity
19
+ check.
20
+
21
+ ## Scope
22
+
23
+ In scope:
24
+ - Call-site arity: a variadic param accepts 0 or more trailing arguments.
25
+ - Type-checking: each trailing argument unifies against the variadic's declared
26
+ element type.
27
+ - The one supported operation on a variadic param inside the function body:
28
+ `for v in nums` (direct iteration, binding `v` to each element in call order).
29
+ - `sumAll(nums: ...Int) -> Int` (from `examples/functions.plum`) implemented and
30
+ tested end-to-end (parse → typecheck → compile → run).
31
+
32
+ Out of scope (tracked separately):
33
+ - Indexing syntax (`values[i]`) — no grammar for `[...]` expressions exists at all
34
+ today; not needed since direct iteration covers the target use case.
35
+ - A `.length()` builtin or any other method on a variadic param.
36
+ - `libs/std/list.plum`'s `add`/`init`/etc. — those need this feature plus separate
37
+ work once it lands.
38
+ - Passing an existing collection where variadic args are expected (i.e. no
39
+ "spread" call syntax); every call site must list its trailing args literally.
40
+ - Variadic params anywhere but the last position (grammar already allows only one
41
+ `variadic_type` per param list positionally by convention; the checker will
42
+ enforce it's declared last and that there's at most one).
43
+
44
+ ## Type representation
45
+
46
+ Add one variant to `plum-checker/src/types.rs`'s `PlumType`:
47
+
48
+ ```rust
49
+ pub enum PlumType {
50
+ // ...existing variants...
51
+ /// The type of a variadic parameter, e.g. `...Int` -> `TVariadic(TInt)`.
52
+ /// Appears in exactly two places: as the trailing entry of a `TFun`'s
53
+ /// param-types list (for call-site arity/type checking), and as the type
54
+ /// bound to the param's name inside the function body. Its only legal use
55
+ /// inside a body is as a `for` loop's iterable; using it any other way
56
+ /// (returning it, passing it to another call, unifying it against a
57
+ /// concrete type) is a type error by construction — no other match arm
58
+ /// in `unify` or `infer_expr` handles it.
59
+ TVariadic(Box<PlumType>),
60
+ }
61
+ ```
62
+
63
+ `Display` renders it as `...{inner}` (e.g. `...Int`), matching the source syntax,
64
+ for error messages.
65
+
66
+ Every place that currently converts `ast::ParamType::Variadic(t)` to a `PlumType`
67
+ by unwrapping straight to `plum_type_from_ast(t)` (there are several — in
68
+ `plum-checker/src/lib.rs`'s method/function signature building, `monomorphize.rs`,
69
+ and `plum-wasm-codegen/src/lib.rs`'s param-type resolution helpers) instead wraps it:
70
+ `PlumType::TVariadic(Box::new(plum_type_from_ast(t)))`.
71
+
72
+ ## Checker changes
73
+
74
+ **Call-site arity + unification** (`plum-checker/src/lib.rs`'s `infer_expr` for
75
+ `Expr::FnCall`, and the analogous `ClassCall`/method-call arms if they can ever
76
+ target a variadic-param method — in practice today only free functions and
77
+ `name<Receiver>(...)` methods can declare `...a`, both go through the same
78
+ `TFun(param_types, ret)` shape): if `param_types.last()` is `TVariadic(elem)`,
79
+ require `args.len() >= param_types.len() - 1`; unify the fixed prefix positionally
80
+ as today; unify every remaining (trailing) arg against `elem`. If `param_types` is
81
+ empty this can't happen (a variadic-only function has `param_types.len() == 1`,
82
+ the variadic entry itself, so `args.len() >= 0` always holds — any number of args
83
+ including zero is valid).
84
+
85
+ **Declaring a variadic param**: when building a function/method's `TFun` signature
86
+ from its `ast::Param` list, error if more than one param is `ParamType::Variadic`,
87
+ or if a `Variadic` param isn't the last one in the list. This is a new validation,
88
+ not currently enforced anywhere (today it's moot since variadic isn't handled
89
+ specially at all).
90
+
91
+ **`for` loop typing** (`check_stmt`'s `Stmt::For` arm, `plum-checker/src/lib.rs`):
92
+ today it unconditionally binds every loop var to `TInt` without even checking the
93
+ iterable's shape. Change it to: infer the iterable's type; if it's
94
+ `TVariadic(elem)`, require exactly one loop var (`for v, i in nums` over a variadic
95
+ is a checker error — multi-var iteration isn't defined for this type) and bind
96
+ that var to `elem`; otherwise (the existing range case, `a..b`) keep today's
97
+ behavior of binding every loop var to `TInt` unconditionally, unchanged.
98
+
99
+ ## Codegen changes
100
+
101
+ **Callee side**: a variadic param compiles to a single `i32` local — a pointer,
102
+ exactly like a class-instance param — carrying the calling convention used for all
103
+ of "unmodeled/pointer" types today (`ast_type_to_wasm`'s catch-all `Some(ValType::I32)`
104
+ arm). No new callee-side representation needed beyond wherever param `PlumType`s are
105
+ resolved for local typing (those sites already need the `TVariadic` unwrap from the
106
+ Type representation section above; the *wasm width* of a `TVariadic`-typed local is
107
+ always `ValType::I32`, same as any other pointer).
108
+
109
+ **Caller side** (the call-compiling code path used for `Expr::FnCall`, e.g. around
110
+ `plum-wasm-codegen/src/lib.rs`'s call-compilation logic): when the callee's last
111
+ param is variadic, compile the fixed-prefix args normally (pushed as ordinary wasm
112
+ call args), then build a length-prefixed buffer for the trailing args:
113
+
114
+ - `count = trailing_args.len()` (known at compile time — every call site lists its
115
+ variadic args literally per this spec's scope)
116
+ - bump-allocate `8 * (count + 1)` bytes: `result = bump_global; bump_global += 8 * (count + 1)`
117
+ (same inline bump-and-increment pattern already used for `ClassCall`/class-literal
118
+ construction — no new allocation helper function needed, this is a handful of
119
+ instructions inlined at the call site, matching how class construction already
120
+ works)
121
+ - store `count` as an `i64` at offset `0` (`emit_store`, matching the class-field
122
+ 8-byte-stride convention used everywhere else in this file)
123
+ - for each trailing arg (compile-time-known position `i`), `compile_expr` it and
124
+ `emit_store` it at the *static* offset `8 * (i + 1)` — this part needs no dynamic
125
+ address arithmetic, since `i` is a Rust-level loop variable over the AST at
126
+ codegen time, not a wasm runtime value
127
+ - push the resulting pointer as the final actual wasm call argument
128
+
129
+ **`for v in nums` codegen** (new branch in `compile_stmt`'s `Stmt::For` handling,
130
+ alongside the existing `matches!(b.op, ast::BinOp::Range)` branch — selected by the
131
+ checker-computed type of `f.iter` being `TVariadic`, not by the iterable's AST
132
+ shape, since the iterable is just a `Var` reference to the param, not a literal
133
+ range expression):
134
+
135
+ - load the variadic pointer into a scratch local (`ptr`)
136
+ - load `count` from `[ptr + 0]` (`I64Load`) into a scratch local
137
+ - loop an index `i` from `0` to `count` (same loop-structure codegen already used
138
+ for range `for`, just with a runtime bound instead of a constant/expression one —
139
+ range `for` already supports a non-constant upper bound today, e.g. `for i in 0..n`
140
+ where `n` is a variable, so this reuses that existing loop-bound machinery, not new
141
+ loop-control codegen)
142
+ - each iteration, compute the element's runtime address: `addr = ptr + 8 + i * 8`
143
+ (`i32.add`/`i32.mul` on the loop index, converted from the `i64` loop-counter
144
+ convention used by range `for` to `i32` for pointer arithmetic — check how range
145
+ `for`'s existing loop counter width interacts with this and keep it consistent,
146
+ adjusting with `i32.wrap_i64` if the existing loop counter is `i64`), then load the
147
+ element from that computed address (offset `0` in the `MemArg`, since the offset
148
+ is now baked into `addr` itself rather than being a `MemArg` immediate — this is
149
+ the one genuinely new pattern in this feature, since every other load/store in
150
+ this file uses a compile-time-constant `MemArg.offset`)
151
+ - bind the loop var to the loaded element value for the body's execution, exactly
152
+ like the existing range `for`'s loop var binding
153
+
154
+ ## Testing
155
+
156
+ - Checker tests (`plum-checker/tests/checker_tests.rs`): a variadic call with 0
157
+ args passes; with 3 args of the right type passes; with a wrong-typed trailing
158
+ arg is an error; declaring two variadic params is an error; declaring a variadic
159
+ param not last is an error; `for v in nums` (variadic) type-checks and binds `v`
160
+ to the element type; `for v, i in nums` (variadic, two loop vars) is an error.
161
+ - Codegen tests (`plum-wasm-codegen/tests/codegen_tests.rs`), each compiled and run
162
+ via `run_main`:
163
+ - `sumAll(nums: ...Int) -> Int` (the target example) called with 0 args returns 0
164
+ - called with 3 args returns their sum
165
+ - a variadic function with one fixed prefix param plus variadic
166
+ (`combine(prefix: Int, rest: ...Int) -> Int`) correctly separates the two
167
+ - `examples/functions.plum`'s `sumAll` is updated from `todo` to a real
168
+ implementation and covered by `plum-checker/tests/examples_test.rs` /
169
+ `plum-wasm-codegen/tests/examples_test.rs`'s existing per-example compile
170
+ (and, since it now has real behavior, a runtime assertion) checks.
171
+
172
+ ## README
173
+
174
+ Once this lands, update the "Known gaps" bullet that currently mentions `List`'s
175
+ methods being blocked on variadic support — `List`'s methods themselves stay
176
+ `todo` (out of scope here), but the bullet's phrasing changes from "blocked on
177
+ variadic-parameter support" to reflect that variadic parameters now exist as a
178
+ language feature and `List`'s own methods are the remaining, separate work.