plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
64b1d3c
— Peter 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.
|