plum

#treesitter#compiler#wasm

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

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


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