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
# Variadic Parameters Implementation Plan

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

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

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

**Tech Stack:** Rust, wasm-encoder/wasmparser, wasmtime (test execution). No grammar/tree-sitter changes — `...Type` already parses.

## Global Constraints

- Spec: `docs/superpowers/specs/2026-07-23-variadic-params-design.md`
- 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.
- 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.
- **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.
- Run `cargo test --workspace` after every task that touches Rust code — all pre-existing tests must keep passing throughout.

---

### Task 1: `PlumType::TVariadic` representation

**Files:**
- Modify: `plum-checker/src/types.rs` (the `PlumType` enum and its `Display` impl)
- Modify: `plum-checker/src/lib.rs:117,187` (two `ast::ParamType::Variadic(t) => plum_type_from_ast(t)` sites)
- 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)
- 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)

**Interfaces:**
- Consumes: nothing new.
- Produces: `pub enum PlumType { ..., TVariadic(Box<PlumType>) }`. Every later task matches on `PlumType::TVariadic(elem)`.

- [ ] **Step 1: Add the type and its `Display` arm**

In `plum-checker/src/types.rs`, change:

```rust
pub enum PlumType {
    TInt,
    TFloat,
    TBool,
    TStr,
    TUnit,
    TVar(String),
    TFun(Vec<PlumType>, Box<PlumType>),
    TNamed(String),
}
```

to:

```rust
pub enum PlumType {
    TInt,
    TFloat,
    TBool,
    TStr,
    TUnit,
    TVar(String),
    TFun(Vec<PlumType>, Box<PlumType>),
    TNamed(String),
    /// The type of a variadic parameter, e.g. `...Int` -> `TVariadic(TInt)`.
    /// Appears in exactly two places: as the trailing entry of a `TFun`'s
    /// param-types list (call-site arity/type checking), and as the type bound
    /// to the param's name inside the function body. Its only legal use inside
    /// a body is as a `for` loop's iterable — no other `unify`/`infer_expr` arm
    /// handles it, so any other use is a type error by construction.
    TVariadic(Box<PlumType>),
}
```

And in the `Display` impl, add (before the closing `}` of the `match`):

```rust
            PlumType::TVariadic(inner) => write!(f, "...{}", inner),
```

- [ ] **Step 2: Build the workspace to find every exhaustive match that needs a new arm**

Run: `cargo build --workspace 2>&1 | tail -80`
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.)

- [ ] **Step 3: Fix `plum_type_to_ast_type`'s exhaustive match**

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:

```rust
        PlumType::TVar(_) | PlumType::TFun(_, _) => t.to_string(),
```

to:

```rust
        PlumType::TVar(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => t.to_string(),
```

- [ ] **Step 4: Fix `plum_type_to_valtype`'s exhaustive match**

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:

```rust
fn plum_type_to_valtype(t: &PlumType) -> ValType {
    match t {
        PlumType::TInt => ValType::I64,
        PlumType::TFloat => ValType::F64,
        PlumType::TBool | PlumType::TStr | PlumType::TNamed(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::I32,
        PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
    }
}
```

- [ ] **Step 5: Wrap the five `ast::ParamType::Variadic` conversion sites in `plum-checker`**

In `plum-checker/src/lib.rs`, both occurrences of:

```rust
                        ast::ParamType::Variadic(t) => plum_type_from_ast(t),
```

(at line 117, inside `build_global_tables`'s function-signature loop, and line 187, inside `check_fn`'s param-binding loop) become:

```rust
                        ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_type_from_ast(t))),
```

In `plum-checker/src/monomorphize.rs`, all three occurrences of:

```rust
                ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
```

(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:

```rust
                ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plum_type_from_ast(t))),
```

- [ ] **Step 6: Wrap the two `ast::ParamType::Variadic` conversion sites in `plum-wasm-codegen`**

In `plum-wasm-codegen/src/lib.rs`'s `param_plum_type` (~line 889):

```rust
        ast::ParamType::Variadic(t) => plum_checker::plum_type_from_ast(t),
```

becomes:

```rust
        ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plum_type_from_ast(t))),
```

In `compile_fn_body`'s `base_env` construction (~line 1749), the identical line becomes the identical fix:

```rust
            ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plum_type_from_ast(t))),
```

(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.)

- [ ] **Step 7: Build and run the full workspace test suite**

Run: `cargo build --workspace 2>&1 | tail -40` — expect a clean build.
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.

- [ ] **Step 8: Commit**

```bash
git add plum-checker/src/types.rs plum-checker/src/lib.rs plum-checker/src/monomorphize.rs plum-wasm-codegen/src/lib.rs
git commit -m "feat(plum-checker,plum-wasm-codegen): add PlumType::TVariadic representation"
```

---

### Task 2: Checker — call-site arity, declaration validation, `for` typing

**Files:**
- 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)
- Test: `plum-checker/tests/checker_tests.rs`

**Interfaces:**
- Consumes: `PlumType::TVariadic(Box<PlumType>)` from Task 1.
- 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.

- [ ] **Step 1: Write the failing checker tests**

Add to `plum-checker/tests/checker_tests.rs`:

```rust
#[test]
fn variadic_call_with_zero_trailing_args_passes() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  0

useSumAll() -> Int =
  sumAll()
";
    let source = parse(src);
    assert!(check_source(&source).is_ok(), "expected Ok");
}

#[test]
fn variadic_call_with_several_trailing_args_passes() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  0

useSumAll() -> Int =
  sumAll(1, 2, 3)
";
    let source = parse(src);
    assert!(check_source(&source).is_ok(), "expected Ok");
}

#[test]
fn variadic_call_with_mismatched_trailing_arg_type_is_error() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  0

useSumAll() -> Int =
  sumAll(1, \"two\")
";
    let source = parse(src);
    assert!(check_source(&source).is_err());
}

#[test]
fn variadic_call_with_fixed_prefix_passes() {
    let src = "\
combine(prefix: Int, rest: ...Int) -> Int =
  prefix

useCombine() -> Int =
  combine(1, 2, 3)
";
    let source = parse(src);
    assert!(check_source(&source).is_ok(), "expected Ok");
}

#[test]
fn two_variadic_params_is_error() {
    let src = "\
bad(a: ...Int, b: ...Int) -> Int =
  0
";
    let source = parse(src);
    assert!(check_source(&source).is_err());
}

#[test]
fn variadic_param_not_last_is_error() {
    let src = "\
bad(a: ...Int, b: Int) -> Int =
  0
";
    let source = parse(src);
    assert!(check_source(&source).is_err());
}

#[test]
fn for_loop_over_variadic_binds_element_type() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  total = 0
  for v in nums
    total = total + v
  total
";
    let source = parse(src);
    assert!(check_source(&source).is_ok(), "expected Ok");
}

#[test]
fn for_loop_over_variadic_with_two_vars_is_error() {
    let src = "\
bad(nums: ...Int) -> Int =
  for v, i in nums
    v
  0
";
    let source = parse(src);
    assert!(check_source(&source).is_err());
}
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `cargo test -p plum-checker variadic_call for_loop_over_variadic two_variadic variadic_param_not_last 2>&1 | tail -60`
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).

- [ ] **Step 3: Fix `Expr::FnCall`'s arity/unify in `infer_expr`**

In `plum-checker/src/lib.rs` (~line 541-559), replace:

```rust
            match lookup(env, &call.name) {
                Ok(PlumType::TFun(param_types, ret)) => {
                    if call.args.len() != param_types.len() {
                        return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
                    }
                    for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
                        let arg_expr = match arg {
                            ast::Arg::Positional(e) => e,
                            ast::Arg::Keyword { value, .. } => value,
                            ast::Arg::Pair { value, .. } => value,
                        };
                        let actual = infer_expr(arg_expr, env, ctx)?;
                        unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
                    }
                    Ok(*ret)
                }
                Ok(_) => Err(format!("'{}' is not a function", call.name)),
                Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
            }
```

with:

```rust
            match lookup(env, &call.name) {
                Ok(PlumType::TFun(param_types, ret)) => {
                    match param_types.last() {
                        Some(PlumType::TVariadic(elem)) => {
                            let fixed = &param_types[..param_types.len() - 1];
                            if call.args.len() < fixed.len() {
                                return Err(format!("call '{}': expected at least {} arg(s), got {}", call.name, fixed.len(), call.args.len()));
                            }
                            for (i, (arg, expected)) in call.args.iter().zip(fixed.iter()).enumerate() {
                                let arg_expr = match arg {
                                    ast::Arg::Positional(e) => e,
                                    ast::Arg::Keyword { value, .. } => value,
                                    ast::Arg::Pair { value, .. } => value,
                                };
                                let actual = infer_expr(arg_expr, env, ctx)?;
                                unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
                            }
                            for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
                                let arg_expr = match arg {
                                    ast::Arg::Positional(e) => e,
                                    ast::Arg::Keyword { value, .. } => value,
                                    ast::Arg::Pair { value, .. } => value,
                                };
                                let actual = infer_expr(arg_expr, env, ctx)?;
                                unify(elem, &actual).map_err(|e| format!("call '{}' variadic arg {}: {}", call.name, i, e))?;
                            }
                            Ok(*ret)
                        }
                        _ => {
                            if call.args.len() != param_types.len() {
                                return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len()));
                            }
                            for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() {
                                let arg_expr = match arg {
                                    ast::Arg::Positional(e) => e,
                                    ast::Arg::Keyword { value, .. } => value,
                                    ast::Arg::Pair { value, .. } => value,
                                };
                                let actual = infer_expr(arg_expr, env, ctx)?;
                                unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
                            }
                            Ok(*ret)
                        }
                    }
                }
                Ok(_) => Err(format!("'{}' is not a function", call.name)),
                Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
            }
```

- [ ] **Step 4: Add the "at most one variadic, must be last" validation to `check_fn`**

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:

```rust
    let variadic_positions: Vec<usize> = f.params.iter().enumerate()
        .filter(|(_, p)| matches!(p.ty, ast::ParamType::Variadic(_)))
        .map(|(i, _)| i)
        .collect();
    if variadic_positions.len() > 1 {
        errors.push(CheckError { message: format!("fn '{}': at most one variadic parameter is allowed", f.name) });
    } else if let Some(&pos) = variadic_positions.first() {
        if pos != f.params.len() - 1 {
            errors.push(CheckError { message: format!("fn '{}': a variadic parameter must be last", f.name) });
        }
    }
```

- [ ] **Step 5: Fix `check_stmt`'s `Stmt::For` arm**

In `plum-checker/src/lib.rs` (~line 344-354), replace:

```rust
        ast::Stmt::For(f_stmt) => {
            match infer_expr(&f_stmt.iter, env, ctx) {
                Ok(_) => {}
                Err(msg) => errors.push(CheckError { message: format!("fn '{}': for iter: {}", fn_name, msg) }),
            }
            let mut inner_env = env.clone();
            for var in &f_stmt.vars {
                inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
            }
            errors.append(&mut check_block(&f_stmt.body, &mut inner_env, declared_ret, fn_name, ctx));
        }
```

with:

```rust
        ast::Stmt::For(f_stmt) => {
            let iter_ty = infer_expr(&f_stmt.iter, env, ctx);
            let mut inner_env = env.clone();
            match &iter_ty {
                Ok(PlumType::TVariadic(elem)) => {
                    if f_stmt.vars.len() != 1 {
                        errors.push(CheckError { message: format!("fn '{}': for-loop over a variadic param must bind exactly one variable", fn_name) });
                    }
                    for var in &f_stmt.vars {
                        inner_env.insert(var.clone(), TypeScheme::mono((**elem).clone()));
                    }
                }
                Ok(_) => {
                    for var in &f_stmt.vars {
                        inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
                    }
                }
                Err(msg) => {
                    errors.push(CheckError { message: format!("fn '{}': for iter: {}", fn_name, msg) });
                    for var in &f_stmt.vars {
                        inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt));
                    }
                }
            }
            errors.append(&mut check_block(&f_stmt.body, &mut inner_env, declared_ret, fn_name, ctx));
        }
```

- [ ] **Step 6: Run the new tests**

Run: `cargo test -p plum-checker variadic_call for_loop_over_variadic two_variadic variadic_param_not_last 2>&1 | tail -60`
Expected: all 8 new tests PASS.

- [ ] **Step 7: Run the full checker test suite**

Run: `cargo test -p plum-checker 2>&1 | tail -60`
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).

- [ ] **Step 8: Commit**

```bash
git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs
git commit -m "feat(plum-checker): type-check variadic call arity and for-in-variadic iteration"
```

---

### Task 3: Codegen — wasm signature + call-site buffer construction

**Files:**
- Modify: `plum-wasm-codegen/src/lib.rs`:
  - the wasm function-signature registration loop (~line 443-444)
  - `Collector`'s `Expr::FnCall` arm in `walk_expr` (~line 1691-1703)
  - the real `Expr::FnCall` arm in `compile_expr` (~line 2740-2764)
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`

**Interfaces:**
- 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).
- 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".

- [ ] **Step 1: Write the failing codegen test**

Add to `plum-wasm-codegen/tests/codegen_tests.rs`:

```rust
#[test]
fn variadic_call_with_varying_trailing_arg_counts_runs_correctly() {
    let src = "\
combine(prefix: Int, rest: ...Int) -> Int =
  prefix

main() -> Int =
  a = combine(10)
  b = combine(20, 1)
  c = combine(30, 1, 2, 3)
  a + b + c
";
    let source = parse(src);
    let bytes = compile_source(&source).expect("compile failed");
    assert_eq!(run_main(&bytes), 60);
}
```

(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.)

- [ ] **Step 2: Run the test to verify it fails**

Run: `cargo test -p plum-wasm-codegen variadic_call_with_varying_trailing_arg_counts 2>&1 | tail -60`
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).

- [ ] **Step 3: Fix the wasm function-signature registration loop**

In `plum-wasm-codegen/src/lib.rs` (~line 443-445), replace:

```rust
            for p in &f.params {
                param_types.push(ast_type_to_wasm(param_type_name(&p.ty)).unwrap_or(ValType::I32));
            }
```

with:

```rust
            for p in &f.params {
                let vt = match &p.ty {
                    ast::ParamType::Variadic(_) => ValType::I32,
                    other => ast_type_to_wasm(param_type_name(other)).unwrap_or(ValType::I32),
                };
                param_types.push(vt);
            }
```

- [ ] **Step 4: Reserve a `classcall_scratch` slot for a variadic call site**

`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:

```rust
            ast::Expr::FnCall(call) => {
                let carries_payload = self.cctx.enum_variants.get(&call.name)
                    .map(|info| !info.field_types.is_empty())
                    .unwrap_or(false);
                if carries_payload {
                    let idx = self.next_classcall_slot;
                    self.next_classcall_slot += 1;
                    self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
                }
                for arg in &call.args {
                    self.walk_arg(arg);
                }
            }
```

with:

```rust
            ast::Expr::FnCall(call) => {
                let carries_payload = self.cctx.enum_variants.get(&call.name)
                    .map(|info| !info.field_types.is_empty())
                    .unwrap_or(false);
                let is_variadic_call = matches!(
                    plum_checker::lookup(&self.env, &call.name),
                    Ok(PlumType::TFun(params, _)) if matches!(params.last(), Some(PlumType::TVariadic(_)))
                );
                if carries_payload || is_variadic_call {
                    let idx = self.next_classcall_slot;
                    self.next_classcall_slot += 1;
                    self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
                }
                for arg in &call.args {
                    self.walk_arg(arg);
                }
            }
```

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

- [ ] **Step 5: Build the buffer in `compile_expr`'s real `Expr::FnCall` arm**

In `plum-wasm-codegen/src/lib.rs` (~line 2740-2764), replace:

```rust
        ast::Expr::FnCall(call) => {
            // A call whose callee name is a *local* of function type is a closure call,
            // dispatched via `call_indirect` — not a direct `Call` to a named function.
            let is_closure_call = ctx.locals.contains_key(&call.name)
                && matches!(infer_local_type(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
            if is_closure_call {
                compile_closure_call(call, body, ctx, state)?;
            } else if let Some(info) = ctx.enum_variants.get(&call.name) {
                compile_variant_construction(info, call, expr, body, ctx, state)?;
            } else {
                for arg in &call.args {
                    let arg_expr = match arg {
                        ast::Arg::Positional(e) => e,
                        ast::Arg::Keyword { value, .. } => value,
                        ast::Arg::Pair { value, .. } => value,
                    };
                    compile_expr(arg_expr, body, ctx, state)?;
                }
                let func_idx = ctx
                    .func_ids
                    .get(&call.name)
                    .ok_or_else(|| format!("unknown function '{}'", call.name))?;
                Instruction::Call(*func_idx).encode(body);
            }
        }
```

with:

```rust
        ast::Expr::FnCall(call) => {
            // A call whose callee name is a *local* of function type is a closure call,
            // dispatched via `call_indirect` — not a direct `Call` to a named function.
            let is_closure_call = ctx.locals.contains_key(&call.name)
                && matches!(infer_local_type(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
            if is_closure_call {
                compile_closure_call(call, body, ctx, state)?;
            } else if let Some(info) = ctx.enum_variants.get(&call.name) {
                compile_variant_construction(info, call, expr, body, ctx, state)?;
            } else {
                let arg_expr_of = |arg: &ast::Arg| -> &ast::Expr {
                    match arg {
                        ast::Arg::Positional(e) => e,
                        ast::Arg::Keyword { value, .. } => value,
                        ast::Arg::Pair { value, .. } => value,
                    }
                };
                let callee_sig = infer_local_type(&ast::Expr::Var(call.name.clone()), ctx);
                let variadic_split = match &callee_sig {
                    PlumType::TFun(params, _) => match params.last() {
                        Some(PlumType::TVariadic(elem)) => Some(((**elem).clone(), params.len() - 1)),
                        _ => None,
                    },
                    _ => None,
                };
                match variadic_split {
                    Some((elem_ty, fixed_count)) => {
                        for arg in call.args.iter().take(fixed_count) {
                            compile_expr(arg_expr_of(arg), body, ctx, state)?;
                        }
                        let trailing: Vec<&ast::Expr> = call.args.iter().skip(fixed_count).map(arg_expr_of).collect();
                        let count = trailing.len() as i32;
                        let size = 8 * (count + 1);

                        let scratch_key = expr as *const ast::Expr as usize;
                        let scratch_idx = *ctx
                            .classcall_scratch
                            .get(&scratch_key)
                            .ok_or_else(|| "internal codegen error: missing variadic-call scratch slot".to_string())?;
                        let scratch_local = ctx.classcall_scratch_base + scratch_idx;

                        Instruction::GlobalGet(ctx.bump_global).encode(body);
                        Instruction::LocalSet(scratch_local).encode(body);
                        Instruction::GlobalGet(ctx.bump_global).encode(body);
                        Instruction::I32Const(size).encode(body);
                        Instruction::I32Add.encode(body);
                        Instruction::GlobalSet(ctx.bump_global).encode(body);

                        Instruction::LocalGet(scratch_local).encode(body);
                        Instruction::I64Const(count as i64).encode(body);
                        emit_store(ValType::I64, 0, body);

                        let elem_vt = plum_type_to_valtype(&elem_ty);
                        for (i, arg_expr) in trailing.iter().enumerate() {
                            Instruction::LocalGet(scratch_local).encode(body);
                            compile_expr(arg_expr, body, ctx, state)?;
                            emit_store(elem_vt, 8 * (i as u64 + 1), body);
                        }
                        Instruction::LocalGet(scratch_local).encode(body);

                        let func_idx = ctx
                            .func_ids
                            .get(&call.name)
                            .ok_or_else(|| format!("unknown function '{}'", call.name))?;
                        Instruction::Call(*func_idx).encode(body);
                    }
                    None => {
                        for arg in &call.args {
                            compile_expr(arg_expr_of(arg), body, ctx, state)?;
                        }
                        let func_idx = ctx
                            .func_ids
                            .get(&call.name)
                            .ok_or_else(|| format!("unknown function '{}'", call.name))?;
                        Instruction::Call(*func_idx).encode(body);
                    }
                }
            }
        }
```

- [ ] **Step 6: Run the new test**

Run: `cargo test -p plum-wasm-codegen variadic_call_with_varying_trailing_arg_counts 2>&1 | tail -60`
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.

- [ ] **Step 7: Run the full workspace test suite**

Run: `cargo test --workspace 2>&1 | tail -100`
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).

- [ ] **Step 8: Commit**

```bash
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs
git commit -m "feat(plum-wasm-codegen): compile variadic call sites into a length-prefixed buffer"
```

---

### Task 4: Codegen — `for v in nums` iteration

**Files:**
- Modify: `plum-wasm-codegen/src/lib.rs`:
  - `LocalCtx` struct (~line 281-329) and `compile_fn_body`'s local-index-assignment block (~line 1780-1855)
  - `ClosureWalker::walk_stmt`'s `Stmt::For` arm (~line 972-978)
  - `Collector::walk_stmt`'s `Stmt::For` arm (~line 1633-1639)
  - `compile_stmt`'s `Stmt::For` arm (~line 2084-2115)
- Modify: `examples/functions.plum` (`sumAll`, from `todo` to a real implementation)
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`, `plum-wasm-codegen/tests/examples_test.rs`

**Interfaces:**
- 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).
- 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.

- [ ] **Step 1: Write the failing codegen test (the plan's target example) and update `examples/functions.plum`**

Add to `plum-wasm-codegen/tests/codegen_tests.rs`:

```rust
#[test]
fn sum_all_variadic_int_runs_correctly() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  total = 0
  for v in nums
    total = total + v
  total

main() -> Int =
  sumAll(1, 2, 3, 4)
";
    let source = parse(src);
    let bytes = compile_source(&source).expect("compile failed");
    assert_eq!(run_main(&bytes), 10);
}

#[test]
fn sum_all_variadic_int_with_zero_args_runs_correctly() {
    let src = "\
sumAll(nums: ...Int) -> Int =
  total = 0
  for v in nums
    total = total + v
  total

main() -> Int =
  sumAll()
";
    let source = parse(src);
    let bytes = compile_source(&source).expect("compile failed");
    assert_eq!(run_main(&bytes), 0);
}
```

In `examples/functions.plum`, replace:

```
sumAll(nums: ...Int) -> Int =
  todo
```

with:

```
sumAll(nums: ...Int) -> Int =
  total = 0
  for v in nums
    total = total + v
  total
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `cargo test -p plum-wasm-codegen sum_all_variadic 2>&1 | tail -60`
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.

- [ ] **Step 3: Add the `variadic_for_scratch` pool to `LocalCtx` and `Collector`**

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):

```rust
    /// `For` stmt identity (pointer address) -> a slot number; each slot reserves 2
    /// consecutive `i32` scratch locals for variadic iteration (`for v in nums`):
    /// [count, loop index]. Only `for` statements whose iterable is a `TVariadic`
    /// use this — an ordinary range `for` reuses its own loop var as the counter
    /// and needs no extra scratch locals.
    variadic_for_scratch: HashMap<usize, u32>,
    next_variadic_for_slot: u32,
```

`compile_fn_body`'s `Collector { ... }` construction (~line 1759-1771) currently reads:

```rust
    let mut collector = Collector {
        env: base_env.clone(),
        cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
        named: Vec::new(),
        named_set: Default::default(),
        classcall_scratch: HashMap::new(),
        match_scratch: HashMap::new(),
        next_classcall_slot: 0,
        closure_scratch: HashMap::new(),
        next_closure_slot: 0,
        nested_class_scratch: HashMap::new(),
        next_nested_class_slot: 0,
    };
```

Add the two new fields (alongside `nested_class_scratch`/`next_nested_class_slot`):

```rust
    let mut collector = Collector {
        env: base_env.clone(),
        cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
        named: Vec::new(),
        named_set: Default::default(),
        classcall_scratch: HashMap::new(),
        match_scratch: HashMap::new(),
        next_classcall_slot: 0,
        closure_scratch: HashMap::new(),
        next_closure_slot: 0,
        nested_class_scratch: HashMap::new(),
        next_nested_class_slot: 0,
        variadic_for_scratch: HashMap::new(),
        next_variadic_for_slot: 0,
    };
```

In `LocalCtx` (~line 281-311), add (alongside `nested_class_scratch_base`/`nested_class_scratch`):

```rust
    /// First local index reserved for variadic-`for` scratch temporaries (2 `i32`
    /// slots per `for` statement that iterates a `TVariadic`: count, loop index).
    variadic_for_scratch_base: u32,
    /// `For` stmt identity (pointer address) -> slot number (multiply by 2 and add
    /// `variadic_for_scratch_base` for the count local; +1 more for the index local).
    variadic_for_scratch: HashMap<usize, u32>,
```

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):

```rust
    let variadic_for_scratch_base = idx;
    let variadic_for_scratch_count = collector.variadic_for_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
    for _ in 0..variadic_for_scratch_count {
        groups.push(ValType::I32); // count
        groups.push(ValType::I32); // loop index
        idx += 2;
    }
```

The `LocalCtx { ... }` construction that follows (~line 1839-1862) currently reads:

```rust
    let local_ctx = LocalCtx {
        locals,
        classcall_scratch_base,
        classcall_scratch: collector.classcall_scratch,
        match_scratch_base,
        match_scratch_index,
        closure_scratch_base,
        closure_scratch: collector.closure_scratch,
        nested_class_scratch_base,
        nested_class_scratch: collector.nested_class_scratch,
        func_ids: &ctx.func_ids,
        func_sigs: &ctx.func_sigs,
        closures: &ctx.closures,
        closure_call_types: &ctx.closure_call_types,
        named_fn_values: &ctx.named_fn_values,
        string_concat_func: ctx.string_concat_func,
        int_to_string_func: ctx.int_to_string_func,
        classes: &ctx.classes,
        methods: &ctx.methods,
        enum_variants: &ctx.enum_variants,
        type_env: RefCell::new(base_env),
        closure_local_sigs: RefCell::new(HashMap::new()),
        bump_global: ctx.bump_global,
    };
```

Add the two new fields (alongside `nested_class_scratch_base`/`nested_class_scratch`):

```rust
    let local_ctx = LocalCtx {
        locals,
        classcall_scratch_base,
        classcall_scratch: collector.classcall_scratch,
        match_scratch_base,
        match_scratch_index,
        closure_scratch_base,
        closure_scratch: collector.closure_scratch,
        nested_class_scratch_base,
        nested_class_scratch: collector.nested_class_scratch,
        variadic_for_scratch_base,
        variadic_for_scratch: collector.variadic_for_scratch,
        func_ids: &ctx.func_ids,
        func_sigs: &ctx.func_sigs,
        closures: &ctx.closures,
        closure_call_types: &ctx.closure_call_types,
        named_fn_values: &ctx.named_fn_values,
        string_concat_func: ctx.string_concat_func,
        int_to_string_func: ctx.int_to_string_func,
        classes: &ctx.classes,
        methods: &ctx.methods,
        enum_variants: &ctx.enum_variants,
        type_env: RefCell::new(base_env),
        closure_local_sigs: RefCell::new(HashMap::new()),
        bump_global: ctx.bump_global,
    };
```

- [ ] **Step 4: Fix `ClosureWalker::walk_stmt`'s `Stmt::For` arm**

In `plum-wasm-codegen/src/lib.rs` (~line 972-978), replace:

```rust
            ast::Stmt::For(f) => {
                self.walk_expr(&f.iter, None);
                for v in &f.vars {
                    self.env.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
                    self.locals.insert(v.clone());
                }
                self.walk_block(&f.body);
            }
```

with:

```rust
            ast::Stmt::For(f) => {
                self.walk_expr(&f.iter, None);
                let elem_ty = match plum_checker::infer_expr(&f.iter, &self.env, &self.cctx) {
                    Ok(PlumType::TVariadic(elem)) => *elem,
                    _ => PlumType::TInt,
                };
                for v in &f.vars {
                    self.env.insert(v.clone(), TypeScheme::mono(elem_ty.clone()));
                    self.locals.insert(v.clone());
                }
                self.walk_block(&f.body);
            }
```

- [ ] **Step 5: Fix `Collector::walk_stmt`'s `Stmt::For` arm**

In `plum-wasm-codegen/src/lib.rs` (~line 1633-1639), replace:

```rust
            ast::Stmt::For(f) => {
                self.walk_expr(&f.iter);
                for v in &f.vars {
                    self.bind(v, PlumType::TInt);
                }
                self.walk_block(&f.body);
            }
```

with:

```rust
            ast::Stmt::For(f) => {
                self.walk_expr(&f.iter);
                let iter_ty = plum_checker::infer_expr(&f.iter, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
                if let PlumType::TVariadic(elem) = &iter_ty {
                    let idx = self.next_variadic_for_slot;
                    self.next_variadic_for_slot += 1;
                    self.variadic_for_scratch.insert(f as *const ast::For as usize, idx);
                    for v in &f.vars {
                        self.bind(v, (**elem).clone());
                    }
                } else {
                    for v in &f.vars {
                        self.bind(v, PlumType::TInt);
                    }
                }
                self.walk_block(&f.body);
            }
```

- [ ] **Step 6: Add the new branch to `compile_stmt`'s `Stmt::For` arm**

In `plum-wasm-codegen/src/lib.rs` (~line 2084-2115), replace:

```rust
        ast::Stmt::For(f) => {
            if let ast::Expr::Binary(b) = &f.iter {
                if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
                    let var_name = &f.vars[0];
                    let var_idx = ctx
                        .locals
                        .get(var_name)
                        .copied()
                        .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
                    ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
                    compile_expr(&b.left, body, ctx, state)?;
                    Instruction::LocalSet(var_idx).encode(body);
                    Instruction::Block(BlockType::Empty).encode(body);
                    Instruction::Loop(BlockType::Empty).encode(body);
                    Instruction::LocalGet(var_idx).encode(body);
                    compile_expr(&b.right, body, ctx, state)?;
                    Instruction::I64GeS.encode(body);
                    Instruction::BrIf(1).encode(body);
                    compile_block(&f.body, body, ctx, state)?;
                    Instruction::LocalGet(var_idx).encode(body);
                    Instruction::I64Const(1).encode(body);
                    Instruction::I64Add.encode(body);
                    Instruction::LocalSet(var_idx).encode(body);
                    Instruction::Br(0).encode(body);
                    Instruction::End.encode(body);
                    Instruction::End.encode(body);
                    return Ok(());
                }
            }
            compile_expr(&f.iter, body, ctx, state)?;
            Instruction::Drop.encode(body);
        }
```

with:

```rust
        ast::Stmt::For(f) => {
            if let ast::Expr::Binary(b) = &f.iter {
                if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
                    let var_name = &f.vars[0];
                    let var_idx = ctx
                        .locals
                        .get(var_name)
                        .copied()
                        .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
                    ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
                    compile_expr(&b.left, body, ctx, state)?;
                    Instruction::LocalSet(var_idx).encode(body);
                    Instruction::Block(BlockType::Empty).encode(body);
                    Instruction::Loop(BlockType::Empty).encode(body);
                    Instruction::LocalGet(var_idx).encode(body);
                    compile_expr(&b.right, body, ctx, state)?;
                    Instruction::I64GeS.encode(body);
                    Instruction::BrIf(1).encode(body);
                    compile_block(&f.body, body, ctx, state)?;
                    Instruction::LocalGet(var_idx).encode(body);
                    Instruction::I64Const(1).encode(body);
                    Instruction::I64Add.encode(body);
                    Instruction::LocalSet(var_idx).encode(body);
                    Instruction::Br(0).encode(body);
                    Instruction::End.encode(body);
                    Instruction::End.encode(body);
                    return Ok(());
                }
            }
            if let PlumType::TVariadic(elem_ty) = infer_local_type(&f.iter, ctx) {
                if f.vars.len() != 1 {
                    return Err("codegen: for-loop over a variadic param must bind exactly one variable".to_string());
                }
                let var_name = &f.vars[0];
                let var_idx = ctx
                    .locals
                    .get(var_name)
                    .copied()
                    .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
                ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono((*elem_ty).clone()));

                let scratch_key = f as *const ast::For as usize;
                let slot = *ctx
                    .variadic_for_scratch
                    .get(&scratch_key)
                    .ok_or_else(|| "internal codegen error: missing variadic-for scratch slot".to_string())?;
                let count_local = ctx.variadic_for_scratch_base + slot * 2;
                let index_local = count_local + 1;
                let elem_vt = plum_type_to_valtype(&elem_ty);

                // count_local = i32.wrap_i64(load_i64([nums + 0]))
                compile_expr(&f.iter, body, ctx, state)?;
                Instruction::I64Load(MemArg { offset: 0, align: 3, memory_index: 0 }).encode(body);
                Instruction::I32WrapI64.encode(body);
                Instruction::LocalSet(count_local).encode(body);

                // index_local = 0
                Instruction::I32Const(0).encode(body);
                Instruction::LocalSet(index_local).encode(body);

                Instruction::Block(BlockType::Empty).encode(body);
                Instruction::Loop(BlockType::Empty).encode(body);
                Instruction::LocalGet(index_local).encode(body);
                Instruction::LocalGet(count_local).encode(body);
                Instruction::I32GeS.encode(body);
                Instruction::BrIf(1).encode(body);

                // var = load_elem([nums + 8 + index * 8])
                compile_expr(&f.iter, body, ctx, state)?;
                Instruction::I32Const(8).encode(body);
                Instruction::I32Add.encode(body);
                Instruction::LocalGet(index_local).encode(body);
                Instruction::I32Const(8).encode(body);
                Instruction::I32Mul.encode(body);
                Instruction::I32Add.encode(body);
                emit_load(elem_vt, 0, body);
                Instruction::LocalSet(var_idx).encode(body);

                compile_block(&f.body, body, ctx, state)?;

                Instruction::LocalGet(index_local).encode(body);
                Instruction::I32Const(1).encode(body);
                Instruction::I32Add.encode(body);
                Instruction::LocalSet(index_local).encode(body);
                Instruction::Br(0).encode(body);
                Instruction::End.encode(body);
                Instruction::End.encode(body);
                return Ok(());
            }
            compile_expr(&f.iter, body, ctx, state)?;
            Instruction::Drop.encode(body);
        }
```

- [ ] **Step 7: Run the new tests**

Run: `cargo test -p plum-wasm-codegen sum_all_variadic 2>&1 | tail -60`
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`).

- [ ] **Step 8: Update and run the examples test**

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:

Run: `cargo test -p plum-wasm-codegen functions_compiles 2>&1 | tail -30`
Expected: PASS.

- [ ] **Step 9: Run the full workspace test suite**

Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS, including every pre-existing range-`for` test (unchanged branch, still taken first) and every Task 1-3 test.

- [ ] **Step 10: Commit**

```bash
git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs examples/functions.plum
git commit -m "feat(plum-wasm-codegen): compile for-v-in-variadic-param iteration"
```

---

### Task 5: README — close the gap

**Files:**
- Modify: `README.md` (the "Known gaps" section)

**Interfaces:**
- Consumes: nothing.
- Produces: nothing (docs only).

- [ ] **Step 1: Update the Known gaps bullet**

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):

```
- `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
```

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:

Run: `grep -n "variadic\|\.\.\.a\|\.\.\.Int" README.md`

- [ ] **Step 2: Commit**

```bash
git add README.md
git commit -m "docs: variadic parameters are no longer a known gap"
```