plum

#treesitter#compiler#wasm

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

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


515004dPeter John 2026-07-23T19:45:10+05:30
docs: add implementation plan for variadic parameters
docs/superpowers/plans/2026-07-23-variadic-params.md ADDED
@@ -0,0 +1,1169 @@
1
+ # Variadic Parameters Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make `fn(..., values: ...a)` a real variadic parameter — call-site arity of 0+ trailing args, and `for v in values` iteration inside the body — using `examples/functions.plum`'s `sumAll(nums: ...Int) -> Int` as the end-to-end target.
6
+
7
+ **Architecture:** Add `PlumType::TVariadic(Box<PlumType>)`, used both as the trailing entry of a `TFun`'s param-types (call-site arity/unification) and as the type bound to the param's name inside the body (only legal use: a `for` loop's iterable). At the call site, codegen builds a length-prefixed buffer (`[count: i64][elem0]...`) in bump memory, reusing the existing `ClassCall` scratch-local/bump-alloc pattern verbatim. The callee receives one `i32` pointer, exactly like a class instance. `for v in nums` is a new codegen branch (selected by the iterable's checker type, not its AST shape) alongside the existing range-only `for` codegen, computing a *dynamic* per-index address — the one genuinely new pattern here, since every other load/store in this codegen is a static field offset.
8
+
9
+ **Tech Stack:** Rust, wasm-encoder/wasmparser, wasmtime (test execution). No grammar/tree-sitter changes — `...Type` already parses.
10
+
11
+ ## Global Constraints
12
+
13
+ - Spec: `docs/superpowers/specs/2026-07-23-variadic-params-design.md`
14
+ - In scope: call-site arity (0+ trailing args), type-checking each trailing arg against the element type, and `for v in nums` iteration. `sumAll` from `examples/functions.plum` is the target end-to-end example.
15
+ - Out of scope: indexing syntax (`values[i]`), a `.length()` builtin, wiring up `libs/std/list.plum`'s methods, spread-call syntax, variadic params anywhere but last position.
16
+ - **Known, deliberately accepted limitation**: codegen's call-site buffer construction is implemented only for free-function calls (`Expr::FnCall`), not method calls (`Attribute`/`AttrKind::Method`). No test or example in this plan calls a method with a variadic param (that's `libs/std`'s `List`/`Map`, explicitly out of scope), so this is safe today, but is a real gap if a future method-call site ever declares `...a` — flag it, don't silently "fix" it by expanding scope.
17
+ - Run `cargo test --workspace` after every task that touches Rust code — all pre-existing tests must keep passing throughout.
18
+
19
+ ---
20
+
21
+ ### Task 1: `PlumType::TVariadic` representation
22
+
23
+ **Files:**
24
+ - Modify: `plum-checker/src/types.rs` (the `PlumType` enum and its `Display` impl)
25
+ - Modify: `plum-checker/src/lib.rs:117,187` (two `ast::ParamType::Variadic(t) => plum_type_from_ast(t)` sites)
26
+ - Modify: `plum-checker/src/monomorphize.rs:78-88` (`plum_type_to_ast_type`'s exhaustive match), `:253`, `:837`, `:860` (three more `ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t)` sites)
27
+ - Modify: `plum-wasm-codegen/src/lib.rs:357-366` (`plum_type_to_valtype`'s exhaustive match), `:889` (`param_plum_type`), `:1749` (`compile_fn_body`'s `base_env` construction)
28
+
29
+ **Interfaces:**
30
+ - Consumes: nothing new.
31
+ - Produces: `pub enum PlumType { ..., TVariadic(Box<PlumType>) }`. Every later task matches on `PlumType::TVariadic(elem)`.
32
+
33
+ - [ ] **Step 1: Add the type and its `Display` arm**
34
+
35
+ In `plum-checker/src/types.rs`, change:
36
+
37
+ ```rust
38
+ pub enum PlumType {
39
+ TInt,
40
+ TFloat,
41
+ TBool,
42
+ TStr,
43
+ TUnit,
44
+ TVar(String),
45
+ TFun(Vec<PlumType>, Box<PlumType>),
46
+ TNamed(String),
47
+ }
48
+ ```
49
+
50
+ to:
51
+
52
+ ```rust
53
+ pub enum PlumType {
54
+ TInt,
55
+ TFloat,
56
+ TBool,
57
+ TStr,
58
+ TUnit,
59
+ TVar(String),
60
+ TFun(Vec<PlumType>, Box<PlumType>),
61
+ TNamed(String),
62
+ /// The type of a variadic parameter, e.g. `...Int` -> `TVariadic(TInt)`.
63
+ /// Appears in exactly two places: as the trailing entry of a `TFun`'s
64
+ /// param-types list (call-site arity/type checking), and as the type bound
65
+ /// to the param's name inside the function body. Its only legal use inside
66
+ /// a body is as a `for` loop's iterable — no other `unify`/`infer_expr` arm
67
+ /// handles it, so any other use is a type error by construction.
68
+ TVariadic(Box<PlumType>),
69
+ }
70
+ ```
71
+
72
+ And in the `Display` impl, add (before the closing `}` of the `match`):
73
+
74
+ ```rust
75
+ PlumType::TVariadic(inner) => write!(f, "...{}", inner),
76
+ ```
77
+
78
+ - [ ] **Step 2: Build the workspace to find every exhaustive match that needs a new arm**
79
+
80
+ Run: `cargo build --workspace 2>&1 | tail -80`
81
+ Expected: compile errors for non-exhaustive `match` on `PlumType` at exactly these locations (matching the Files list above) — `plum-checker/src/monomorphize.rs`'s `plum_type_to_ast_type` and `plum-wasm-codegen/src/lib.rs`'s `plum_type_to_valtype`. (`unify` in `plum-checker/src/lib.rs` has a wildcard `_ => Err(...)` arm already and needs no change.)
82
+
83
+ - [ ] **Step 3: Fix `plum_type_to_ast_type`'s exhaustive match**
84
+
85
+ In `plum-checker/src/monomorphize.rs`, this function's doc comment already explains `TVar`/`TFun` are "an internal-error case rather than something this needs to model" because they never arise from a concrete call-site argument's inferred type — `TVariadic` is the same kind of case (it only ever appears as a *declared parameter's* type, never as an argument's own inferred type). Change:
86
+
87
+ ```rust
88
+ PlumType::TVar(_) | PlumType::TFun(_, _) => t.to_string(),
89
+ ```
90
+
91
+ to:
92
+
93
+ ```rust
94
+ PlumType::TVar(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => t.to_string(),
95
+ ```
96
+
97
+ - [ ] **Step 4: Fix `plum_type_to_valtype`'s exhaustive match**
98
+
99
+ In `plum-wasm-codegen/src/lib.rs`, a `TVariadic` value is always a pointer to a bump-allocated buffer, exactly like a class instance — add it to the existing pointer-typed arm:
100
+
101
+ ```rust
102
+ fn plum_type_to_valtype(t: &PlumType) -> ValType {
103
+ match t {
104
+ PlumType::TInt => ValType::I64,
105
+ PlumType::TFloat => ValType::F64,
106
+ PlumType::TBool | PlumType::TStr | PlumType::TNamed(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::I32,
107
+ PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
108
+ }
109
+ }
110
+ ```
111
+
112
+ - [ ] **Step 5: Wrap the five `ast::ParamType::Variadic` conversion sites in `plum-checker`**
113
+
114
+ In `plum-checker/src/lib.rs`, both occurrences of:
115
+
116
+ ```rust
117
+ ast::ParamType::Variadic(t) => plum_type_from_ast(t),
118
+ ```
119
+
120
+ (at line 117, inside `build_global_tables`'s function-signature loop, and line 187, inside `check_fn`'s param-binding loop) become:
121
+
122
+ ```rust
123
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_type_from_ast(t))),
124
+ ```
125
+
126
+ In `plum-checker/src/monomorphize.rs`, all three occurrences of:
127
+
128
+ ```rust
129
+ ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
130
+ ```
131
+
132
+ (at line 253 inside `rewrite_fn_body`, line 837 inside the `Class` specialization arm's method-signature rebuild, and line 860 inside the `Fn` specialization arm's signature rebuild) become:
133
+
134
+ ```rust
135
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plum_type_from_ast(t))),
136
+ ```
137
+
138
+ - [ ] **Step 6: Wrap the two `ast::ParamType::Variadic` conversion sites in `plum-wasm-codegen`**
139
+
140
+ In `plum-wasm-codegen/src/lib.rs`'s `param_plum_type` (~line 889):
141
+
142
+ ```rust
143
+ ast::ParamType::Variadic(t) => plum_checker::plum_type_from_ast(t),
144
+ ```
145
+
146
+ becomes:
147
+
148
+ ```rust
149
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plum_type_from_ast(t))),
150
+ ```
151
+
152
+ In `compile_fn_body`'s `base_env` construction (~line 1749), the identical line becomes the identical fix:
153
+
154
+ ```rust
155
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plum_type_from_ast(t))),
156
+ ```
157
+
158
+ (Leave `param_type_name`, at line ~338, and the wasm function-*signature* registration loop at line ~444 untouched in this task — those are fixed in Task 3, which handles the actual wasm-level calling convention. This task is purely about the `PlumType` representation.)
159
+
160
+ - [ ] **Step 7: Build and run the full workspace test suite**
161
+
162
+ Run: `cargo build --workspace 2>&1 | tail -40` — expect a clean build.
163
+ Run: `cargo test --workspace 2>&1 | tail -100` — expect every pre-existing test to still pass. No test today declares or calls a variadic-param function (confirmed by grepping `plum-checker/tests/checker_tests.rs` and `plum-wasm-codegen/tests/codegen_tests.rs` for `\.\.\.` before writing this plan), so this representational change should have zero effect on any existing test's outcome.
164
+
165
+ - [ ] **Step 8: Commit**
166
+
167
+ ```bash
168
+ git add plum-checker/src/types.rs plum-checker/src/lib.rs plum-checker/src/monomorphize.rs plum-wasm-codegen/src/lib.rs
169
+ git commit -m "feat(plum-checker,plum-wasm-codegen): add PlumType::TVariadic representation"
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Task 2: Checker — call-site arity, declaration validation, `for` typing
175
+
176
+ **Files:**
177
+ - Modify: `plum-checker/src/lib.rs` (`Expr::FnCall`'s arity/unify in `infer_expr`, ~line 542-556; `check_fn`, ~line 173; `check_stmt`'s `Stmt::For` arm, ~line 344-354)
178
+ - Test: `plum-checker/tests/checker_tests.rs`
179
+
180
+ **Interfaces:**
181
+ - Consumes: `PlumType::TVariadic(Box<PlumType>)` from Task 1.
182
+ - Produces: correct arity/unify for a variadic call; a `check_fn`-level validation rejecting more than one variadic param or a non-last variadic param; `for v in nums` binds `v` to the variadic's element type. No new public functions.
183
+
184
+ - [ ] **Step 1: Write the failing checker tests**
185
+
186
+ Add to `plum-checker/tests/checker_tests.rs`:
187
+
188
+ ```rust
189
+ #[test]
190
+ fn variadic_call_with_zero_trailing_args_passes() {
191
+ let src = "\
192
+ sumAll(nums: ...Int) -> Int =
193
+ 0
194
+
195
+ useSumAll() -> Int =
196
+ sumAll()
197
+ ";
198
+ let source = parse(src);
199
+ assert!(check_source(&source).is_ok(), "expected Ok");
200
+ }
201
+
202
+ #[test]
203
+ fn variadic_call_with_several_trailing_args_passes() {
204
+ let src = "\
205
+ sumAll(nums: ...Int) -> Int =
206
+ 0
207
+
208
+ useSumAll() -> Int =
209
+ sumAll(1, 2, 3)
210
+ ";
211
+ let source = parse(src);
212
+ assert!(check_source(&source).is_ok(), "expected Ok");
213
+ }
214
+
215
+ #[test]
216
+ fn variadic_call_with_mismatched_trailing_arg_type_is_error() {
217
+ let src = "\
218
+ sumAll(nums: ...Int) -> Int =
219
+ 0
220
+
221
+ useSumAll() -> Int =
222
+ sumAll(1, \"two\")
223
+ ";
224
+ let source = parse(src);
225
+ assert!(check_source(&source).is_err());
226
+ }
227
+
228
+ #[test]
229
+ fn variadic_call_with_fixed_prefix_passes() {
230
+ let src = "\
231
+ combine(prefix: Int, rest: ...Int) -> Int =
232
+ prefix
233
+
234
+ useCombine() -> Int =
235
+ combine(1, 2, 3)
236
+ ";
237
+ let source = parse(src);
238
+ assert!(check_source(&source).is_ok(), "expected Ok");
239
+ }
240
+
241
+ #[test]
242
+ fn two_variadic_params_is_error() {
243
+ let src = "\
244
+ bad(a: ...Int, b: ...Int) -> Int =
245
+ 0
246
+ ";
247
+ let source = parse(src);
248
+ assert!(check_source(&source).is_err());
249
+ }
250
+
251
+ #[test]
252
+ fn variadic_param_not_last_is_error() {
253
+ let src = "\
254
+ bad(a: ...Int, b: Int) -> Int =
255
+ 0
256
+ ";
257
+ let source = parse(src);
258
+ assert!(check_source(&source).is_err());
259
+ }
260
+
261
+ #[test]
262
+ fn for_loop_over_variadic_binds_element_type() {
263
+ let src = "\
264
+ sumAll(nums: ...Int) -> Int =
265
+ total = 0
266
+ for v in nums
267
+ total = total + v
268
+ total
269
+ ";
270
+ let source = parse(src);
271
+ assert!(check_source(&source).is_ok(), "expected Ok");
272
+ }
273
+
274
+ #[test]
275
+ fn for_loop_over_variadic_with_two_vars_is_error() {
276
+ let src = "\
277
+ bad(nums: ...Int) -> Int =
278
+ for v, i in nums
279
+ v
280
+ 0
281
+ ";
282
+ let source = parse(src);
283
+ assert!(check_source(&source).is_err());
284
+ }
285
+ ```
286
+
287
+ - [ ] **Step 2: Run the tests to verify they fail**
288
+
289
+ Run: `cargo test -p plum-checker variadic_call for_loop_over_variadic two_variadic variadic_param_not_last 2>&1 | tail -60`
290
+ Expected: some pass by accident (e.g. `variadic_call_with_mismatched_trailing_arg_type_is_error` may already fail-to-typecheck under the OLD passthrough semantics too, for the wrong reason), but `variadic_call_with_zero_trailing_args_passes`, `variadic_call_with_fixed_prefix_passes`, and `for_loop_over_variadic_binds_element_type` FAIL — today's arity check requires exact `args.len() == param_types.len()`, and `for` unconditionally binds `TInt` without checking `nums`'s actual iterated-element semantics (this one happens to also "pass" today only because both sides are already `Int` — the real test of intent is that it must be a *deliberate* codepath, not an accident; the next steps make it one).
291
+
292
+ - [ ] **Step 3: Fix `Expr::FnCall`'s arity/unify in `infer_expr`**
293
+
294
+ In `plum-checker/src/lib.rs` (~line 541-559), replace:
295
+
296
+ ```rust
297
+ match lookup(env, &call.name) {
298
+ Ok(PlumType::TFun(param_types, ret)) => {
299
+ if call.args.len() != param_types.len() {
300
+ return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
301
+ }
302
+ for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
303
+ let arg_expr = match arg {
304
+ ast::Arg::Positional(e) => e,
305
+ ast::Arg::Keyword { value, .. } => value,
306
+ ast::Arg::Pair { value, .. } => value,
307
+ };
308
+ let actual = infer_expr(arg_expr, env, ctx)?;
309
+ unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
310
+ }
311
+ Ok(*ret)
312
+ }
313
+ Ok(_) => Err(format!("'{}' is not a function", call.name)),
314
+ Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
315
+ }
316
+ ```
317
+
318
+ with:
319
+
320
+ ```rust
321
+ match lookup(env, &call.name) {
322
+ Ok(PlumType::TFun(param_types, ret)) => {
323
+ match param_types.last() {
324
+ Some(PlumType::TVariadic(elem)) => {
325
+ let fixed = &param_types[..param_types.len() - 1];
326
+ if call.args.len() < fixed.len() {
327
+ return Err(format!("call '{}': expected at least {} arg(s), got {}", call.name, fixed.len(), call.args.len()));
328
+ }
329
+ for (i, (arg, expected)) in call.args.iter().zip(fixed.iter()).enumerate() {
330
+ let arg_expr = match arg {
331
+ ast::Arg::Positional(e) => e,
332
+ ast::Arg::Keyword { value, .. } => value,
333
+ ast::Arg::Pair { value, .. } => value,
334
+ };
335
+ let actual = infer_expr(arg_expr, env, ctx)?;
336
+ unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
337
+ }
338
+ for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
339
+ let arg_expr = match arg {
340
+ ast::Arg::Positional(e) => e,
341
+ ast::Arg::Keyword { value, .. } => value,
342
+ ast::Arg::Pair { value, .. } => value,
343
+ };
344
+ let actual = infer_expr(arg_expr, env, ctx)?;
345
+ unify(elem, &actual).map_err(|e| format!("call '{}' variadic arg {}: {}", call.name, i, e))?;
346
+ }
347
+ Ok(*ret)
348
+ }
349
+ _ => {
350
+ if call.args.len() != param_types.len() {
351
+ return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
352
+ }
353
+ for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
354
+ let arg_expr = match arg {
355
+ ast::Arg::Positional(e) => e,
356
+ ast::Arg::Keyword { value, .. } => value,
357
+ ast::Arg::Pair { value, .. } => value,
358
+ };
359
+ let actual = infer_expr(arg_expr, env, ctx)?;
360
+ unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
361
+ }
362
+ Ok(*ret)
363
+ }
364
+ }
365
+ }
366
+ Ok(_) => Err(format!("'{}' is not a function", call.name)),
367
+ Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
368
+ }
369
+ ```
370
+
371
+ - [ ] **Step 4: Add the "at most one variadic, must be last" validation to `check_fn`**
372
+
373
+ In `plum-checker/src/lib.rs`'s `check_fn` (~line 173), after the `errors` vector is created and before the existing param-binding loop, add:
374
+
375
+ ```rust
376
+ let variadic_positions: Vec<usize> = f.params.iter().enumerate()
377
+ .filter(|(_, p)| matches!(p.ty, ast::ParamType::Variadic(_)))
378
+ .map(|(i, _)| i)
379
+ .collect();
380
+ if variadic_positions.len() > 1 {
381
+ errors.push(CheckError { message: format!("fn '{}': at most one variadic parameter is allowed", f.name) });
382
+ } else if let Some(&pos) = variadic_positions.first() {
383
+ if pos != f.params.len() - 1 {
384
+ errors.push(CheckError { message: format!("fn '{}': a variadic parameter must be last", f.name) });
385
+ }
386
+ }
387
+ ```
388
+
389
+ - [ ] **Step 5: Fix `check_stmt`'s `Stmt::For` arm**
390
+
391
+ In `plum-checker/src/lib.rs` (~line 344-354), replace:
392
+
393
+ ```rust
394
+ ast::Stmt::For(f_stmt) => {
395
+ match infer_expr(&f_stmt.iter, env, ctx) {
396
+ Ok(_) => {}
397
+ Err(msg) => errors.push(CheckError { message: format!("fn '{}': for iter: {}", fn_name, msg) }),
398
+ }
399
+ let mut inner_env = env.clone();
400
+ for var in &f_stmt.vars {
401
+ inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
402
+ }
403
+ errors.append(&mut check_block(&f_stmt.body, &mut inner_env, declared_ret, fn_name, ctx));
404
+ }
405
+ ```
406
+
407
+ with:
408
+
409
+ ```rust
410
+ ast::Stmt::For(f_stmt) => {
411
+ let iter_ty = infer_expr(&f_stmt.iter, env, ctx);
412
+ let mut inner_env = env.clone();
413
+ match &iter_ty {
414
+ Ok(PlumType::TVariadic(elem)) => {
415
+ if f_stmt.vars.len() != 1 {
416
+ errors.push(CheckError { message: format!("fn '{}': for-loop over a variadic param must bind exactly one variable", fn_name) });
417
+ }
418
+ for var in &f_stmt.vars {
419
+ inner_env.insert(var.clone(), TypeScheme::mono((**elem).clone()));
420
+ }
421
+ }
422
+ Ok(_) => {
423
+ for var in &f_stmt.vars {
424
+ inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
425
+ }
426
+ }
427
+ Err(msg) => {
428
+ errors.push(CheckError { message: format!("fn '{}': for iter: {}", fn_name, msg) });
429
+ for var in &f_stmt.vars {
430
+ inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
431
+ }
432
+ }
433
+ }
434
+ errors.append(&mut check_block(&f_stmt.body, &mut inner_env, declared_ret, fn_name, ctx));
435
+ }
436
+ ```
437
+
438
+ - [ ] **Step 6: Run the new tests**
439
+
440
+ Run: `cargo test -p plum-checker variadic_call for_loop_over_variadic two_variadic variadic_param_not_last 2>&1 | tail -60`
441
+ Expected: all 8 new tests PASS.
442
+
443
+ - [ ] **Step 7: Run the full checker test suite**
444
+
445
+ Run: `cargo test -p plum-checker 2>&1 | tail -60`
446
+ Expected: all tests PASS (pre-existing tests unaffected — the `for`-loop default-TInt behavior for a non-variadic iterable, i.e. every range `for` in the existing test suite, is unchanged, since it still takes the `Ok(_) =>` branch and binds `TInt` exactly as before).
447
+
448
+ - [ ] **Step 8: Commit**
449
+
450
+ ```bash
451
+ git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs
452
+ git commit -m "feat(plum-checker): type-check variadic call arity and for-in-variadic iteration"
453
+ ```
454
+
455
+ ---
456
+
457
+ ### Task 3: Codegen — wasm signature + call-site buffer construction
458
+
459
+ **Files:**
460
+ - Modify: `plum-wasm-codegen/src/lib.rs`:
461
+ - the wasm function-signature registration loop (~line 443-444)
462
+ - `Collector`'s `Expr::FnCall` arm in `walk_expr` (~line 1691-1703)
463
+ - the real `Expr::FnCall` arm in `compile_expr` (~line 2740-2764)
464
+ - Test: `plum-wasm-codegen/tests/codegen_tests.rs`
465
+
466
+ **Interfaces:**
467
+ - Consumes: `PlumType::TVariadic(Box<PlumType>)` from Task 1; checker's already-correct arity/typing from Task 2 (codegen assumes checker-accepted input, per this file's existing convention — see `infer_local_type`'s doc comment).
468
+ - Produces: a variadic-declaring function's wasm signature ends in one `i32` (pointer) param; a call site to such a function builds the length-prefixed buffer and passes the pointer. No new public functions. This task deliberately does NOT implement `for v in nums` iteration (Task 4) — its test calls a variadic function whose body ignores the variadic param entirely, to isolate "does the call-site buffer construction produce valid, correctly-shaped wasm" from "can the body read the buffer back".
469
+
470
+ - [ ] **Step 1: Write the failing codegen test**
471
+
472
+ Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
473
+
474
+ ```rust
475
+ #[test]
476
+ fn variadic_call_with_varying_trailing_arg_counts_runs_correctly() {
477
+ let src = "\
478
+ combine(prefix: Int, rest: ...Int) -> Int =
479
+ prefix
480
+
481
+ main() -> Int =
482
+ a = combine(10)
483
+ b = combine(20, 1)
484
+ c = combine(30, 1, 2, 3)
485
+ a + b + c
486
+ ";
487
+ let source = parse(src);
488
+ let bytes = compile_source(&source).expect("compile failed");
489
+ assert_eq!(run_main(&bytes), 60);
490
+ }
491
+ ```
492
+
493
+ (This test only proves the buffer is built without corrupting the fixed `prefix` param or trapping/failing wasm validation across 0, 1, and 3 trailing args — it does not read `rest`'s contents, since `for v in nums` isn't implemented until Task 4.)
494
+
495
+ - [ ] **Step 2: Run the test to verify it fails**
496
+
497
+ Run: `cargo test -p plum-wasm-codegen variadic_call_with_varying_trailing_arg_counts 2>&1 | tail -60`
498
+ Expected: FAIL — today's call-compiling code pushes `call.args` positionally 1:1 against the callee's wasm signature; since the signature isn't fixed yet either, this currently produces a wasm arity mismatch (`combine`'s wasm signature today has 2 `I64` params from the old `Variadic` passthrough, but `combine(20, 1)` pushes 2 args and `combine(30, 1, 2, 3)` pushes 4 — a real mismatch: invalid wasm, module fails to validate/instantiate).
499
+
500
+ - [ ] **Step 3: Fix the wasm function-signature registration loop**
501
+
502
+ In `plum-wasm-codegen/src/lib.rs` (~line 443-445), replace:
503
+
504
+ ```rust
505
+ for p in &f.params {
506
+ param_types.push(ast_type_to_wasm(param_type_name(&p.ty)).unwrap_or(ValType::I32));
507
+ }
508
+ ```
509
+
510
+ with:
511
+
512
+ ```rust
513
+ for p in &f.params {
514
+ let vt = match &p.ty {
515
+ ast::ParamType::Variadic(_) => ValType::I32,
516
+ other => ast_type_to_wasm(param_type_name(other)).unwrap_or(ValType::I32),
517
+ };
518
+ param_types.push(vt);
519
+ }
520
+ ```
521
+
522
+ - [ ] **Step 4: Reserve a `classcall_scratch` slot for a variadic call site**
523
+
524
+ `Collector` already reuses its `classcall_scratch`/`next_classcall_slot` pool for enum-variant payload construction (`Expr::FnCall`'s `carries_payload` branch, ~line 1691-1699) — a variadic call site needs the exact same "multi-step construction, need a scratch pointer across several stores, then a final value" shape, so it reuses the same pool rather than introducing a new one. In `plum-wasm-codegen/src/lib.rs`'s `Collector::walk_expr`, replace:
525
+
526
+ ```rust
527
+ ast::Expr::FnCall(call) => {
528
+ let carries_payload = self.cctx.enum_variants.get(&call.name)
529
+ .map(|info| !info.field_types.is_empty())
530
+ .unwrap_or(false);
531
+ if carries_payload {
532
+ let idx = self.next_classcall_slot;
533
+ self.next_classcall_slot += 1;
534
+ self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
535
+ }
536
+ for arg in &call.args {
537
+ self.walk_arg(arg);
538
+ }
539
+ }
540
+ ```
541
+
542
+ with:
543
+
544
+ ```rust
545
+ ast::Expr::FnCall(call) => {
546
+ let carries_payload = self.cctx.enum_variants.get(&call.name)
547
+ .map(|info| !info.field_types.is_empty())
548
+ .unwrap_or(false);
549
+ let is_variadic_call = matches!(
550
+ plum_checker::lookup(&self.env, &call.name),
551
+ Ok(PlumType::TFun(params, _)) if matches!(params.last(), Some(PlumType::TVariadic(_)))
552
+ );
553
+ if carries_payload || is_variadic_call {
554
+ let idx = self.next_classcall_slot;
555
+ self.next_classcall_slot += 1;
556
+ self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
557
+ }
558
+ for arg in &call.args {
559
+ self.walk_arg(arg);
560
+ }
561
+ }
562
+ ```
563
+
564
+ `plum_checker::lookup`'s signature is `pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String>` (defined in `plum-checker/src/lib.rs`) — already used elsewhere in this codebase the same way, so it's already in scope via the `plum_checker::` path used throughout this file.
565
+
566
+ - [ ] **Step 5: Build the buffer in `compile_expr`'s real `Expr::FnCall` arm**
567
+
568
+ In `plum-wasm-codegen/src/lib.rs` (~line 2740-2764), replace:
569
+
570
+ ```rust
571
+ ast::Expr::FnCall(call) => {
572
+ // A call whose callee name is a *local* of function type is a closure call,
573
+ // dispatched via `call_indirect` — not a direct `Call` to a named function.
574
+ let is_closure_call = ctx.locals.contains_key(&call.name)
575
+ && matches!(infer_local_type(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
576
+ if is_closure_call {
577
+ compile_closure_call(call, body, ctx, state)?;
578
+ } else if let Some(info) = ctx.enum_variants.get(&call.name) {
579
+ compile_variant_construction(info, call, expr, body, ctx, state)?;
580
+ } else {
581
+ for arg in &call.args {
582
+ let arg_expr = match arg {
583
+ ast::Arg::Positional(e) => e,
584
+ ast::Arg::Keyword { value, .. } => value,
585
+ ast::Arg::Pair { value, .. } => value,
586
+ };
587
+ compile_expr(arg_expr, body, ctx, state)?;
588
+ }
589
+ let func_idx = ctx
590
+ .func_ids
591
+ .get(&call.name)
592
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
593
+ Instruction::Call(*func_idx).encode(body);
594
+ }
595
+ }
596
+ ```
597
+
598
+ with:
599
+
600
+ ```rust
601
+ ast::Expr::FnCall(call) => {
602
+ // A call whose callee name is a *local* of function type is a closure call,
603
+ // dispatched via `call_indirect` — not a direct `Call` to a named function.
604
+ let is_closure_call = ctx.locals.contains_key(&call.name)
605
+ && matches!(infer_local_type(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
606
+ if is_closure_call {
607
+ compile_closure_call(call, body, ctx, state)?;
608
+ } else if let Some(info) = ctx.enum_variants.get(&call.name) {
609
+ compile_variant_construction(info, call, expr, body, ctx, state)?;
610
+ } else {
611
+ let arg_expr_of = |arg: &ast::Arg| -> &ast::Expr {
612
+ match arg {
613
+ ast::Arg::Positional(e) => e,
614
+ ast::Arg::Keyword { value, .. } => value,
615
+ ast::Arg::Pair { value, .. } => value,
616
+ }
617
+ };
618
+ let callee_sig = infer_local_type(&ast::Expr::Var(call.name.clone()), ctx);
619
+ let variadic_split = match &callee_sig {
620
+ PlumType::TFun(params, _) => match params.last() {
621
+ Some(PlumType::TVariadic(elem)) => Some(((**elem).clone(), params.len() - 1)),
622
+ _ => None,
623
+ },
624
+ _ => None,
625
+ };
626
+ match variadic_split {
627
+ Some((elem_ty, fixed_count)) => {
628
+ for arg in call.args.iter().take(fixed_count) {
629
+ compile_expr(arg_expr_of(arg), body, ctx, state)?;
630
+ }
631
+ let trailing: Vec<&ast::Expr> = call.args.iter().skip(fixed_count).map(arg_expr_of).collect();
632
+ let count = trailing.len() as i32;
633
+ let size = 8 * (count + 1);
634
+
635
+ let scratch_key = expr as *const ast::Expr as usize;
636
+ let scratch_idx = *ctx
637
+ .classcall_scratch
638
+ .get(&scratch_key)
639
+ .ok_or_else(|| "internal codegen error: missing variadic-call scratch slot".to_string())?;
640
+ let scratch_local = ctx.classcall_scratch_base + scratch_idx;
641
+
642
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
643
+ Instruction::LocalSet(scratch_local).encode(body);
644
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
645
+ Instruction::I32Const(size).encode(body);
646
+ Instruction::I32Add.encode(body);
647
+ Instruction::GlobalSet(ctx.bump_global).encode(body);
648
+
649
+ Instruction::LocalGet(scratch_local).encode(body);
650
+ Instruction::I64Const(count as i64).encode(body);
651
+ emit_store(ValType::I64, 0, body);
652
+
653
+ let elem_vt = plum_type_to_valtype(&elem_ty);
654
+ for (i, arg_expr) in trailing.iter().enumerate() {
655
+ Instruction::LocalGet(scratch_local).encode(body);
656
+ compile_expr(arg_expr, body, ctx, state)?;
657
+ emit_store(elem_vt, 8 * (i as u64 + 1), body);
658
+ }
659
+ Instruction::LocalGet(scratch_local).encode(body);
660
+
661
+ let func_idx = ctx
662
+ .func_ids
663
+ .get(&call.name)
664
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
665
+ Instruction::Call(*func_idx).encode(body);
666
+ }
667
+ None => {
668
+ for arg in &call.args {
669
+ compile_expr(arg_expr_of(arg), body, ctx, state)?;
670
+ }
671
+ let func_idx = ctx
672
+ .func_ids
673
+ .get(&call.name)
674
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
675
+ Instruction::Call(*func_idx).encode(body);
676
+ }
677
+ }
678
+ }
679
+ }
680
+ ```
681
+
682
+ - [ ] **Step 6: Run the new test**
683
+
684
+ Run: `cargo test -p plum-wasm-codegen variadic_call_with_varying_trailing_arg_counts 2>&1 | tail -60`
685
+ Expected: PASS (`60`). If it traps or fails wasm validation, check the store order first: `LocalGet(scratch_local)` (address) must be pushed *before* the value being stored (`I64Const(count)` or `compile_expr(arg_expr, ...)`) — `emit_store` pops value-then-address.
686
+
687
+ - [ ] **Step 7: Run the full workspace test suite**
688
+
689
+ Run: `cargo test --workspace 2>&1 | tail -100`
690
+ Expected: all tests PASS, including every pre-existing test (`ClassCall` construction, enum-variant payload construction, and every other call site are untouched in shape — only the `Expr::FnCall` arm's `None` branch is new code, behaviorally identical to the old unconditional loop).
691
+
692
+ - [ ] **Step 8: Commit**
693
+
694
+ ```bash
695
+ git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
696
+ git commit -m "feat(plum-wasm-codegen): compile variadic call sites into a length-prefixed buffer"
697
+ ```
698
+
699
+ ---
700
+
701
+ ### Task 4: Codegen — `for v in nums` iteration
702
+
703
+ **Files:**
704
+ - Modify: `plum-wasm-codegen/src/lib.rs`:
705
+ - `LocalCtx` struct (~line 281-329) and `compile_fn_body`'s local-index-assignment block (~line 1780-1855)
706
+ - `ClosureWalker::walk_stmt`'s `Stmt::For` arm (~line 972-978)
707
+ - `Collector::walk_stmt`'s `Stmt::For` arm (~line 1633-1639)
708
+ - `compile_stmt`'s `Stmt::For` arm (~line 2084-2115)
709
+ - Modify: `examples/functions.plum` (`sumAll`, from `todo` to a real implementation)
710
+ - Test: `plum-wasm-codegen/tests/codegen_tests.rs`, `plum-wasm-codegen/tests/examples_test.rs`
711
+
712
+ **Interfaces:**
713
+ - Consumes: `PlumType::TVariadic(Box<PlumType>)` (Task 1); the call-site buffer layout from Task 3 (`[count: i64 @ offset 0][elem0 @ offset 8]...`, one `i32` pointer param per variadic).
714
+ - Produces: `for v in nums` (where `nums`'s checker type is `TVariadic`) iterates every element in call order, binding `v` to each. No new public functions.
715
+
716
+ - [ ] **Step 1: Write the failing codegen test (the plan's target example) and update `examples/functions.plum`**
717
+
718
+ Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
719
+
720
+ ```rust
721
+ #[test]
722
+ fn sum_all_variadic_int_runs_correctly() {
723
+ let src = "\
724
+ sumAll(nums: ...Int) -> Int =
725
+ total = 0
726
+ for v in nums
727
+ total = total + v
728
+ total
729
+
730
+ main() -> Int =
731
+ sumAll(1, 2, 3, 4)
732
+ ";
733
+ let source = parse(src);
734
+ let bytes = compile_source(&source).expect("compile failed");
735
+ assert_eq!(run_main(&bytes), 10);
736
+ }
737
+
738
+ #[test]
739
+ fn sum_all_variadic_int_with_zero_args_runs_correctly() {
740
+ let src = "\
741
+ sumAll(nums: ...Int) -> Int =
742
+ total = 0
743
+ for v in nums
744
+ total = total + v
745
+ total
746
+
747
+ main() -> Int =
748
+ sumAll()
749
+ ";
750
+ let source = parse(src);
751
+ let bytes = compile_source(&source).expect("compile failed");
752
+ assert_eq!(run_main(&bytes), 0);
753
+ }
754
+ ```
755
+
756
+ In `examples/functions.plum`, replace:
757
+
758
+ ```
759
+ sumAll(nums: ...Int) -> Int =
760
+ todo
761
+ ```
762
+
763
+ with:
764
+
765
+ ```
766
+ sumAll(nums: ...Int) -> Int =
767
+ total = 0
768
+ for v in nums
769
+ total = total + v
770
+ total
771
+ ```
772
+
773
+ - [ ] **Step 2: Run the tests to verify they fail**
774
+
775
+ Run: `cargo test -p plum-wasm-codegen sum_all_variadic 2>&1 | tail -60`
776
+ Expected: FAIL — `compile_stmt`'s `Stmt::For` arm falls through to its final `else` branch (`compile_expr(&f.iter, ...); Drop;`) for any iterable that isn't a literal `a..b` range expression, so the loop body never runs at all; `total` stays `0` for the 4-arg case too (wrong — expected `10`), or the module may fail wasm validation depending on how the surrounding block is structured.
777
+
778
+ - [ ] **Step 3: Add the `variadic_for_scratch` pool to `LocalCtx` and `Collector`**
779
+
780
+ In `plum-wasm-codegen/src/lib.rs`'s `Collector` struct (~line 1506-1524), add a field (alongside the existing `nested_class_scratch`/`next_nested_class_slot` pair):
781
+
782
+ ```rust
783
+ /// `For` stmt identity (pointer address) -> a slot number; each slot reserves 2
784
+ /// consecutive `i32` scratch locals for variadic iteration (`for v in nums`):
785
+ /// [count, loop index]. Only `for` statements whose iterable is a `TVariadic`
786
+ /// use this — an ordinary range `for` reuses its own loop var as the counter
787
+ /// and needs no extra scratch locals.
788
+ variadic_for_scratch: HashMap<usize, u32>,
789
+ next_variadic_for_slot: u32,
790
+ ```
791
+
792
+ `compile_fn_body`'s `Collector { ... }` construction (~line 1759-1771) currently reads:
793
+
794
+ ```rust
795
+ let mut collector = Collector {
796
+ env: base_env.clone(),
797
+ cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
798
+ named: Vec::new(),
799
+ named_set: Default::default(),
800
+ classcall_scratch: HashMap::new(),
801
+ match_scratch: HashMap::new(),
802
+ next_classcall_slot: 0,
803
+ closure_scratch: HashMap::new(),
804
+ next_closure_slot: 0,
805
+ nested_class_scratch: HashMap::new(),
806
+ next_nested_class_slot: 0,
807
+ };
808
+ ```
809
+
810
+ Add the two new fields (alongside `nested_class_scratch`/`next_nested_class_slot`):
811
+
812
+ ```rust
813
+ let mut collector = Collector {
814
+ env: base_env.clone(),
815
+ cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
816
+ named: Vec::new(),
817
+ named_set: Default::default(),
818
+ classcall_scratch: HashMap::new(),
819
+ match_scratch: HashMap::new(),
820
+ next_classcall_slot: 0,
821
+ closure_scratch: HashMap::new(),
822
+ next_closure_slot: 0,
823
+ nested_class_scratch: HashMap::new(),
824
+ next_nested_class_slot: 0,
825
+ variadic_for_scratch: HashMap::new(),
826
+ next_variadic_for_slot: 0,
827
+ };
828
+ ```
829
+
830
+ In `LocalCtx` (~line 281-311), add (alongside `nested_class_scratch_base`/`nested_class_scratch`):
831
+
832
+ ```rust
833
+ /// First local index reserved for variadic-`for` scratch temporaries (2 `i32`
834
+ /// slots per `for` statement that iterates a `TVariadic`: count, loop index).
835
+ variadic_for_scratch_base: u32,
836
+ /// `For` stmt identity (pointer address) -> slot number (multiply by 2 and add
837
+ /// `variadic_for_scratch_base` for the count local; +1 more for the index local).
838
+ variadic_for_scratch: HashMap<usize, u32>,
839
+ ```
840
+
841
+ In `compile_fn_body`'s local-index-assignment block (~line 1816-1822), insert a new section *between* `nested_class_scratch` and `closure_scratch` (so `idx` stays correctly threaded — `closure_scratch`'s own block doesn't increment `idx` further since it's already last, so anything added after it would get a wrong base):
842
+
843
+ ```rust
844
+ let variadic_for_scratch_base = idx;
845
+ let variadic_for_scratch_count = collector.variadic_for_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
846
+ for _ in 0..variadic_for_scratch_count {
847
+ groups.push(ValType::I32); // count
848
+ groups.push(ValType::I32); // loop index
849
+ idx += 2;
850
+ }
851
+ ```
852
+
853
+ The `LocalCtx { ... }` construction that follows (~line 1839-1862) currently reads:
854
+
855
+ ```rust
856
+ let local_ctx = LocalCtx {
857
+ locals,
858
+ classcall_scratch_base,
859
+ classcall_scratch: collector.classcall_scratch,
860
+ match_scratch_base,
861
+ match_scratch_index,
862
+ closure_scratch_base,
863
+ closure_scratch: collector.closure_scratch,
864
+ nested_class_scratch_base,
865
+ nested_class_scratch: collector.nested_class_scratch,
866
+ func_ids: &ctx.func_ids,
867
+ func_sigs: &ctx.func_sigs,
868
+ closures: &ctx.closures,
869
+ closure_call_types: &ctx.closure_call_types,
870
+ named_fn_values: &ctx.named_fn_values,
871
+ string_concat_func: ctx.string_concat_func,
872
+ int_to_string_func: ctx.int_to_string_func,
873
+ classes: &ctx.classes,
874
+ methods: &ctx.methods,
875
+ enum_variants: &ctx.enum_variants,
876
+ type_env: RefCell::new(base_env),
877
+ closure_local_sigs: RefCell::new(HashMap::new()),
878
+ bump_global: ctx.bump_global,
879
+ };
880
+ ```
881
+
882
+ Add the two new fields (alongside `nested_class_scratch_base`/`nested_class_scratch`):
883
+
884
+ ```rust
885
+ let local_ctx = LocalCtx {
886
+ locals,
887
+ classcall_scratch_base,
888
+ classcall_scratch: collector.classcall_scratch,
889
+ match_scratch_base,
890
+ match_scratch_index,
891
+ closure_scratch_base,
892
+ closure_scratch: collector.closure_scratch,
893
+ nested_class_scratch_base,
894
+ nested_class_scratch: collector.nested_class_scratch,
895
+ variadic_for_scratch_base,
896
+ variadic_for_scratch: collector.variadic_for_scratch,
897
+ func_ids: &ctx.func_ids,
898
+ func_sigs: &ctx.func_sigs,
899
+ closures: &ctx.closures,
900
+ closure_call_types: &ctx.closure_call_types,
901
+ named_fn_values: &ctx.named_fn_values,
902
+ string_concat_func: ctx.string_concat_func,
903
+ int_to_string_func: ctx.int_to_string_func,
904
+ classes: &ctx.classes,
905
+ methods: &ctx.methods,
906
+ enum_variants: &ctx.enum_variants,
907
+ type_env: RefCell::new(base_env),
908
+ closure_local_sigs: RefCell::new(HashMap::new()),
909
+ bump_global: ctx.bump_global,
910
+ };
911
+ ```
912
+
913
+ - [ ] **Step 4: Fix `ClosureWalker::walk_stmt`'s `Stmt::For` arm**
914
+
915
+ In `plum-wasm-codegen/src/lib.rs` (~line 972-978), replace:
916
+
917
+ ```rust
918
+ ast::Stmt::For(f) => {
919
+ self.walk_expr(&f.iter, None);
920
+ for v in &f.vars {
921
+ self.env.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
922
+ self.locals.insert(v.clone());
923
+ }
924
+ self.walk_block(&f.body);
925
+ }
926
+ ```
927
+
928
+ with:
929
+
930
+ ```rust
931
+ ast::Stmt::For(f) => {
932
+ self.walk_expr(&f.iter, None);
933
+ let elem_ty = match plum_checker::infer_expr(&f.iter, &self.env, &self.cctx) {
934
+ Ok(PlumType::TVariadic(elem)) => *elem,
935
+ _ => PlumType::TInt,
936
+ };
937
+ for v in &f.vars {
938
+ self.env.insert(v.clone(), TypeScheme::mono(elem_ty.clone()));
939
+ self.locals.insert(v.clone());
940
+ }
941
+ self.walk_block(&f.body);
942
+ }
943
+ ```
944
+
945
+ - [ ] **Step 5: Fix `Collector::walk_stmt`'s `Stmt::For` arm**
946
+
947
+ In `plum-wasm-codegen/src/lib.rs` (~line 1633-1639), replace:
948
+
949
+ ```rust
950
+ ast::Stmt::For(f) => {
951
+ self.walk_expr(&f.iter);
952
+ for v in &f.vars {
953
+ self.bind(v, PlumType::TInt);
954
+ }
955
+ self.walk_block(&f.body);
956
+ }
957
+ ```
958
+
959
+ with:
960
+
961
+ ```rust
962
+ ast::Stmt::For(f) => {
963
+ self.walk_expr(&f.iter);
964
+ let iter_ty = plum_checker::infer_expr(&f.iter, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
965
+ if let PlumType::TVariadic(elem) = &iter_ty {
966
+ let idx = self.next_variadic_for_slot;
967
+ self.next_variadic_for_slot += 1;
968
+ self.variadic_for_scratch.insert(f as *const ast::For as usize, idx);
969
+ for v in &f.vars {
970
+ self.bind(v, (**elem).clone());
971
+ }
972
+ } else {
973
+ for v in &f.vars {
974
+ self.bind(v, PlumType::TInt);
975
+ }
976
+ }
977
+ self.walk_block(&f.body);
978
+ }
979
+ ```
980
+
981
+ - [ ] **Step 6: Add the new branch to `compile_stmt`'s `Stmt::For` arm**
982
+
983
+ In `plum-wasm-codegen/src/lib.rs` (~line 2084-2115), replace:
984
+
985
+ ```rust
986
+ ast::Stmt::For(f) => {
987
+ if let ast::Expr::Binary(b) = &f.iter {
988
+ if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
989
+ let var_name = &f.vars[0];
990
+ let var_idx = ctx
991
+ .locals
992
+ .get(var_name)
993
+ .copied()
994
+ .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
995
+ ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
996
+ compile_expr(&b.left, body, ctx, state)?;
997
+ Instruction::LocalSet(var_idx).encode(body);
998
+ Instruction::Block(BlockType::Empty).encode(body);
999
+ Instruction::Loop(BlockType::Empty).encode(body);
1000
+ Instruction::LocalGet(var_idx).encode(body);
1001
+ compile_expr(&b.right, body, ctx, state)?;
1002
+ Instruction::I64GeS.encode(body);
1003
+ Instruction::BrIf(1).encode(body);
1004
+ compile_block(&f.body, body, ctx, state)?;
1005
+ Instruction::LocalGet(var_idx).encode(body);
1006
+ Instruction::I64Const(1).encode(body);
1007
+ Instruction::I64Add.encode(body);
1008
+ Instruction::LocalSet(var_idx).encode(body);
1009
+ Instruction::Br(0).encode(body);
1010
+ Instruction::End.encode(body);
1011
+ Instruction::End.encode(body);
1012
+ return Ok(());
1013
+ }
1014
+ }
1015
+ compile_expr(&f.iter, body, ctx, state)?;
1016
+ Instruction::Drop.encode(body);
1017
+ }
1018
+ ```
1019
+
1020
+ with:
1021
+
1022
+ ```rust
1023
+ ast::Stmt::For(f) => {
1024
+ if let ast::Expr::Binary(b) = &f.iter {
1025
+ if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
1026
+ let var_name = &f.vars[0];
1027
+ let var_idx = ctx
1028
+ .locals
1029
+ .get(var_name)
1030
+ .copied()
1031
+ .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
1032
+ ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
1033
+ compile_expr(&b.left, body, ctx, state)?;
1034
+ Instruction::LocalSet(var_idx).encode(body);
1035
+ Instruction::Block(BlockType::Empty).encode(body);
1036
+ Instruction::Loop(BlockType::Empty).encode(body);
1037
+ Instruction::LocalGet(var_idx).encode(body);
1038
+ compile_expr(&b.right, body, ctx, state)?;
1039
+ Instruction::I64GeS.encode(body);
1040
+ Instruction::BrIf(1).encode(body);
1041
+ compile_block(&f.body, body, ctx, state)?;
1042
+ Instruction::LocalGet(var_idx).encode(body);
1043
+ Instruction::I64Const(1).encode(body);
1044
+ Instruction::I64Add.encode(body);
1045
+ Instruction::LocalSet(var_idx).encode(body);
1046
+ Instruction::Br(0).encode(body);
1047
+ Instruction::End.encode(body);
1048
+ Instruction::End.encode(body);
1049
+ return Ok(());
1050
+ }
1051
+ }
1052
+ if let PlumType::TVariadic(elem_ty) = infer_local_type(&f.iter, ctx) {
1053
+ if f.vars.len() != 1 {
1054
+ return Err("codegen: for-loop over a variadic param must bind exactly one variable".to_string());
1055
+ }
1056
+ let var_name = &f.vars[0];
1057
+ let var_idx = ctx
1058
+ .locals
1059
+ .get(var_name)
1060
+ .copied()
1061
+ .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
1062
+ ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono((*elem_ty).clone()));
1063
+
1064
+ let scratch_key = f as *const ast::For as usize;
1065
+ let slot = *ctx
1066
+ .variadic_for_scratch
1067
+ .get(&scratch_key)
1068
+ .ok_or_else(|| "internal codegen error: missing variadic-for scratch slot".to_string())?;
1069
+ let count_local = ctx.variadic_for_scratch_base + slot * 2;
1070
+ let index_local = count_local + 1;
1071
+ let elem_vt = plum_type_to_valtype(&elem_ty);
1072
+
1073
+ // count_local = i32.wrap_i64(load_i64([nums + 0]))
1074
+ compile_expr(&f.iter, body, ctx, state)?;
1075
+ Instruction::I64Load(MemArg { offset: 0, align: 3, memory_index: 0 }).encode(body);
1076
+ Instruction::I32WrapI64.encode(body);
1077
+ Instruction::LocalSet(count_local).encode(body);
1078
+
1079
+ // index_local = 0
1080
+ Instruction::I32Const(0).encode(body);
1081
+ Instruction::LocalSet(index_local).encode(body);
1082
+
1083
+ Instruction::Block(BlockType::Empty).encode(body);
1084
+ Instruction::Loop(BlockType::Empty).encode(body);
1085
+ Instruction::LocalGet(index_local).encode(body);
1086
+ Instruction::LocalGet(count_local).encode(body);
1087
+ Instruction::I32GeS.encode(body);
1088
+ Instruction::BrIf(1).encode(body);
1089
+
1090
+ // var = load_elem([nums + 8 + index * 8])
1091
+ compile_expr(&f.iter, body, ctx, state)?;
1092
+ Instruction::I32Const(8).encode(body);
1093
+ Instruction::I32Add.encode(body);
1094
+ Instruction::LocalGet(index_local).encode(body);
1095
+ Instruction::I32Const(8).encode(body);
1096
+ Instruction::I32Mul.encode(body);
1097
+ Instruction::I32Add.encode(body);
1098
+ emit_load(elem_vt, 0, body);
1099
+ Instruction::LocalSet(var_idx).encode(body);
1100
+
1101
+ compile_block(&f.body, body, ctx, state)?;
1102
+
1103
+ Instruction::LocalGet(index_local).encode(body);
1104
+ Instruction::I32Const(1).encode(body);
1105
+ Instruction::I32Add.encode(body);
1106
+ Instruction::LocalSet(index_local).encode(body);
1107
+ Instruction::Br(0).encode(body);
1108
+ Instruction::End.encode(body);
1109
+ Instruction::End.encode(body);
1110
+ return Ok(());
1111
+ }
1112
+ compile_expr(&f.iter, body, ctx, state)?;
1113
+ Instruction::Drop.encode(body);
1114
+ }
1115
+ ```
1116
+
1117
+ - [ ] **Step 7: Run the new tests**
1118
+
1119
+ Run: `cargo test -p plum-wasm-codegen sum_all_variadic 2>&1 | tail -60`
1120
+ Expected: PASS (`10` and `0`). If the sum is wrong, double-check the element address computation order: `compile_expr(&f.iter)` pushes the pointer, `+8` skips the count slot, then `+ index*8` reaches the right element — verify against Task 3's store layout (`count @ offset 0`, `elem[i] @ offset 8*(i+1)`, i.e. `elem[0]` at byte 8, matching `8 + 0*8 = 8`).
1121
+
1122
+ - [ ] **Step 8: Update and run the examples test**
1123
+
1124
+ Check `plum-wasm-codegen/tests/examples_test.rs` for its existing `functions_compiles`-style test (confirmed present per the codebase's existing per-example compile check) — since `sumAll` now has a real, non-`todo` body, if that test only checks `compile_source(...).is_ok()`, no change is needed there beyond the source file update from Step 1 already making it compile. Run:
1125
+
1126
+ Run: `cargo test -p plum-wasm-codegen functions_compiles 2>&1 | tail -30`
1127
+ Expected: PASS.
1128
+
1129
+ - [ ] **Step 9: Run the full workspace test suite**
1130
+
1131
+ Run: `cargo test --workspace 2>&1 | tail -100`
1132
+ Expected: all tests PASS, including every pre-existing range-`for` test (unchanged branch, still taken first) and every Task 1-3 test.
1133
+
1134
+ - [ ] **Step 10: Commit**
1135
+
1136
+ ```bash
1137
+ git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs examples/functions.plum
1138
+ git commit -m "feat(plum-wasm-codegen): compile for-v-in-variadic-param iteration"
1139
+ ```
1140
+
1141
+ ---
1142
+
1143
+ ### Task 5: README — close the gap
1144
+
1145
+ **Files:**
1146
+ - Modify: `README.md` (the "Known gaps" section)
1147
+
1148
+ **Interfaces:**
1149
+ - Consumes: nothing.
1150
+ - Produces: nothing (docs only).
1151
+
1152
+ - [ ] **Step 1: Update the Known gaps bullet**
1153
+
1154
+ Run: `grep -n "variadic" README.md` to find the current bullet (added by the previous field-assignment cycle), which currently reads along the lines of "`List`'s methods beyond `get`/`length` are still `todo` pending variadic-parameter support (`values: ...a`)". Replace it to reflect that variadic parameters are now a real language feature (call-site arity, `for v in nums` iteration), and that `List`'s own methods remain a separate, still-open gap (not blocked on variadic support anymore, just not yet implemented):
1155
+
1156
+ ```
1157
+ - `libs/std`'s actual `List`/`Map` still don't fully compile — there's no cross-file import resolution yet, so a file that references a type/enum declared in a different `libs/std` file won't type-check standalone; separately, `List`'s methods beyond `get`/`length` (`add`, `set`, `removeAt`, `remove`, `clear`, `reverse`) are still `todo` — variadic parameters (`values: ...a`) now work as a language feature, but wiring these methods up is separate, unstarted work
1158
+ ```
1159
+
1160
+ Also check for a spot earlier in the README (wherever variadic parameters or `fn(...)` types are first introduced, if such a section exists) that might describe variadic params as unsupported, and update it if so — search first:
1161
+
1162
+ Run: `grep -n "variadic\|\.\.\.a\|\.\.\.Int" README.md`
1163
+
1164
+ - [ ] **Step 2: Commit**
1165
+
1166
+ ```bash
1167
+ git add README.md
1168
+ git commit -m "docs: variadic parameters are no longer a known gap"
1169
+ ```