plum
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 = ¶m_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 | ``` |