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