plum

#treesitter#compiler#wasm

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

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


02b3582Peter John 2026-08-10T13:20:39+05:30
feat(libs/std): implement List's add/set/removeAt/remove/clear/reverse on wasm-gc
docs/superpowers/plans/2026-07-25-wasm-gc-migration.md CHANGED
@@ -196,15 +196,25 @@ git commit -m "feat(plum-wasm-codegen): migrate structs, enums, strings, and clo
196
196
  - Consumes: `struct.new`/`struct.set`/`ref null` field semantics from Task 2 — specifically, that unlinking a `Node` (overwriting a `next`/`prev` field with `None`) makes it unreachable and GC-reclaimed with no explicit free.
197
197
  - Produces: `add`, `set`, `removeAt`, `remove`, `clear`, `reverse` on `List[T]` (currently `todo`) have real implementations; `Map` (built on `List`) works transitively.
198
198
 
199
- - [ ] **Step 1: Read `libs/std/list.plum`'s current `Node`/`List` field declarations and the already-implemented `get`/`length`/`each`/`map` methods** for the established style (`Option[Node]` traversal via `match current`) to match.
200
- - [ ] **Step 2: Implement `add`** — append (or prepend, per the existing doc comment on `add` — re-read it; the comment currently says "adds the specified elements to the start of the list" — confirm this is the intended semantic, since "add" more commonly means append, and fix the comment or the implementation, whichever is actually wrong, rather than silently implementing whichever is more convenient) by splicing a new `Node` in via `self.tail`/`self.head` and updating `size`.
201
- - [ ] **Step 3: Implement `set`** — traverse to index `i` (mirroring `get`'s traversal), overwrite the found node's `value` field, return `Some(oldValue)` or `None` if out of bounds.
202
- - [ ] **Step 4: Implement `removeAt`/`remove`** — traverse to the target node, splice it out by rewriting its neighbors' `prev`/`next` to point at each other (or to `None` at the list's ends, updating `self.head`/`self.tail` accordingly), decrement `size`. The removed node becomes unreachable once nothing points to it — no manual free needed.
203
- - [ ] **Step 5: Implement `clear`** — reset `self.head`/`self.tail` to `None` and `size` to `0`; every node becomes unreachable transitively.
204
- - [ ] **Step 6: Implement `reverse`** — build (or in-place relink) a new list with `prev`/`next` swapped at every node.
205
- - [ ] **Step 7: Add codegen tests** in `plum-wasm-codegen/tests/codegen_tests.rs` for each newly-implemented method, plus one test specifically proving a `Node.next: Option[Node]` field correctly becomes unreachable after `removeAt`/`clear` (per the spec's Testing section) — this can only be an indirect proof (e.g. removing many nodes in a loop and confirming the program still runs correctly and the list's `length`/traversal reflects the removal), since there's no direct "assert this was garbage collected" hook available from a compiled program's own execution.
206
- - [ ] **Step 8: Run `cargo test --workspace`.** Expected: 100% pass, including new tests.
207
- - [ ] **Step 9: Commit**
199
+ - [x] **Step 1: Read `libs/std/list.plum`'s current `Node`/`List` field declarations and the already-implemented `get`/`length`/`each`/`map` methods** for the established style (`Option[Node]` traversal via `match current`) to match.
200
+ - [x] **Step 2: Implement `add`** — append (or prepend, per the existing doc comment on `add` — re-read it; the comment currently says "adds the specified elements to the start of the list" — confirm this is the intended semantic, since "add" more commonly means append, and fix the comment or the implementation, whichever is actually wrong, rather than silently implementing whichever is more convenient) by splicing a new `Node` in via `self.tail`/`self.head` and updating `size`. **Was prepend in the doc comment, append in spirit (`makeList` builds in call order) — fixed the comment; implemented as append.**
201
+ - [x] **Step 3: Implement `set`** — traverse to index `i` (mirroring `get`'s traversal), overwrite the found node's `value` field, return `Some(oldValue)` or `None` if out of bounds.
202
+ - [x] **Step 4: Implement `removeAt`/`remove`** — traverse to the target node, splice it out by rewriting its neighbors' `prev`/`next` to point at each other (or to `None` at the list's ends, updating `self.head`/`self.tail` accordingly), decrement `size`. The removed node becomes unreachable once nothing points to it — no manual free needed. **Added a shared `unlink` helper for the splice logic, used by both.**
203
+ - [x] **Step 5: Implement `clear`** — reset `self.head`/`self.tail` to `None` and `size` to `0`; every node becomes unreachable transitively.
204
+ - [x] **Step 6: Implement `reverse`** — build (or in-place relink) a new list with `prev`/`next` swapped at every node. **The pre-existing declared signature was `reverse(self, v: fn(T) -> Bool) -> List` (a leftover, unrelated predicate param — looks copy-pasted from `sort`); fixed to `reverse(self) -> List`.**
205
+ - [x] **Step 7: Add codegen tests** with an important caveat discovered while writing them (see "Discovered pre-existing gaps" below): `libs/std/list.plum` itself **cannot currently compile** through `plum-wasm-codegen`, for reasons unrelated to Task 3's new method bodies this was already true of the pre-existing `get`/`each`/`map` before this task touched the file. The tests therefore exercise a field-for-field/statement-for-statement port of the same six methods against a deliberately **non-generic** `Node`/`List`/`NodeLink` (swapping the generic `Option[Node]` for a monomorphic `NodeLink` enum) — this validates the actual wasm-gc mechanics (self-referential nullable-via-enum struct fields, `struct.set` mutation through an aliased reference, `ref.test` dispatch, GC reachability after `removeAt`/`clear`) without depending on the separate, deeper checker gap. Two tests added: `listAddSetRemoveAtRemoveClearReverseAllWorkCorrectly` and `removingEveryNodeInALoopLeavesAnEmptyCorrectlyFunctioningList` (the latter is this step's required unreachability proof).
206
+ - [x] **Step 8: Run `cargo test --workspace`.** 100% pass (211 tests across the workspace), including the new tests.
207
+ - [x] **Step 9: Commit**
208
+
209
+ #### Discovered pre-existing gaps (found while implementing this task, NOT fixed — out of scope for "implement List's methods")
210
+
211
+ While testing against the REAL generic `List[T]`/`Node[T]`/`Option[Node]` shape, compilation failed with errors like `unknown type 'Option' in GC type registry` and `type name 'None' is not yet supported as a value`. Root cause, confirmed by direct inspection of the monomorphized AST: **`plum_checker::plumTypeFromAst` drops generic type arguments entirely** (`Option[Int]` and bare `Option` both become `PlumType::TNamed("Option")`) — so a class or enum-variant field declared with a concrete instantiation of a generic type (`Node.next: Option[Node]`) keeps referencing the generic type's bare name. Once monomorphization actually specializes that generic type for a concrete argument (renaming it to e.g. `Option$Int` and REMOVING the unspecialized original — confirmed via direct inspection), the field's stored type annotation is left pointing at a name that no longer exists. This is a real, pre-existing gap in `plum-checker`'s generics/monomorphization support (not something Task 2's wasm-gc work introduced — it would misbehave identically under the old bump-allocator representation, just never crash as loudly), and it blocks `libs/std/list.plum`'s `get`/`each`/`map` from compiling too, not just this task's new methods. Properly fixing it needs `PlumType` to represent type applications (not just bare names) and the monomorphizer to rewrite field-type annotations consistently with however it renames the types they reference — a separate, sizable piece of work.
212
+
213
+ Two smaller, genuinely wasm-gc-migration-scoped bugs were found and fixed along the way (both covered by the existing/new test suite):
214
+ - `Expr::Compare`'s codegen unconditionally emitted `i64.eq`/`i64.ne` for any non-`Float` comparison — correct when `Bool`/enums were `i32`, but wrong now that they're wasm-gc refs. Fixed to emit `ref.eq` for `==`/`!=` between reference-typed operands (`plum-wasm-codegen/src/lib.rs`).
215
+ - Method calls (`self.method(...)`/`obj.method(...)`) never packed trailing arguments into a GC array for a method with a variadic (`...T`) parameter — only plain function calls did. `add(self, values: ...T)` was the first variadic *method* anywhere in the codebase, which is how this surfaced. Fixed by porting the same variadic-packing logic to the method-call codegen path.
216
+
217
+ Also found (not fixed, not wasm-gc-related): plum's grammar's `class_call` rule requires named (`field: value`) arguments — there is no positional class-construction syntax — so the pre-existing `makeList`'s `List(None, None, 0)` (positional) has never actually compiled either. Left as-is since fixing it also requires resolving a second, separate issue (passing an already-packed variadic array through to another variadic parameter in `List(...).add(values)`).
208
218
 
209
219
  ```bash
210
220
  git add libs/std/list.plum plum-wasm-codegen/tests/codegen_tests.rs
libs/std/list.plum CHANGED
@@ -30,33 +30,103 @@ type List[T: Stringable](Stringable) =
30
30
  break
31
31
  None
32
32
 
33
- # sets the element at i'th index of the list
33
+ # sets the element at i'th index of the list, returning the old value (or None if i is out of bounds)
34
34
  fun set(self, i: Int, v: T) -> Option[T] =
35
+ current = self.head
36
+ index = 0
37
+ while current != None
38
+ match current
39
+ Some(node) =>
40
+ if index == i
41
+ old = node.value
42
+ node.value = v
43
+ return Some(old)
44
+ current = node.next
45
+ index = index + 1
46
+ None =>
47
+ break
35
- todo
48
+ None
36
49
 
37
50
  # returns the no of elements in the list
38
51
  fun length(self) -> Int =
39
52
  self.size
40
53
 
41
- # adds the specified elements to the start of the list
54
+ # adds the specified elements to the end of the list
42
55
  fun add(self, values: ...T) =
56
+ for v in values
57
+ node = Node(value: v, prev: self.tail, next: None)
58
+ match self.tail
59
+ Some(t) =>
60
+ t.next = Some(node)
61
+ None =>
62
+ self.head = Some(node)
63
+ self.tail = Some(node)
64
+ self.size = self.size + 1
65
+
66
+ # unlinks node from the list, patching its neighbors (or head/tail) to close the gap
67
+ fun unlink(self, node: Node) =
43
- todo
68
+ match node.prev
69
+ Some(p) =>
70
+ p.next = node.next
71
+ None =>
72
+ self.head = node.next
73
+ match node.next
74
+ Some(n) =>
75
+ n.prev = node.prev
76
+ None =>
77
+ self.tail = node.prev
78
+ self.size = self.size - 1
44
79
 
45
80
  # removes the element at i'th index of the list
46
81
  fun removeAt(self, i: Int) =
82
+ current = self.head
83
+ index = 0
84
+ while current != None
85
+ match current
86
+ Some(node) =>
87
+ if index == i
88
+ self.unlink(node)
47
- todo
89
+ return
90
+ current = node.next
91
+ index = index + 1
92
+ None =>
93
+ break
48
94
 
49
- # removes the element v from list
95
+ # removes the first element equal to v from list
50
96
  fun remove(self, v: T) =
97
+ current = self.head
98
+ while current != None
99
+ match current
100
+ Some(node) =>
101
+ if node.value == v
102
+ self.unlink(node)
51
- todo
103
+ return
104
+ current = node.next
105
+ None =>
106
+ break
52
107
 
53
108
  # removes all objects from this list
54
109
  fun clear(self) =
55
- todo
110
+ self.head = None
111
+ self.tail = None
112
+ self.size = 0
56
113
 
57
- # returns a new list with the elements in reverse order.
114
+ # returns the list with the elements relinked in reverse order.
58
- fun reverse(self, v: fn(T) -> Bool) -> List =
115
+ fun reverse(self) -> List =
116
+ current = self.head
117
+ while current != None
118
+ match current
119
+ Some(node) =>
120
+ next = node.next
121
+ node.next = node.prev
122
+ node.prev = next
123
+ current = next
124
+ None =>
125
+ break
126
+ oldHead = self.head
127
+ self.head = self.tail
128
+ self.tail = oldHead
59
- todo
129
+ self
60
130
 
61
131
  # returns a new list with the elements sorted by sorter
62
132
  fun sort(self, sorter: fn(T) -> Bool) -> List =
plum-wasm-codegen/src/lib.rs CHANGED
@@ -528,12 +528,19 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
528
528
  ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(idx) })
529
529
  }
530
530
  PlumType::TStr => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
531
- PlumType::TNamed(name) => {
532
- let idx = registry.class_type_idx.get(name)
533
- .or_else(|| registry.enum_super_type_idx.get(name))
531
+ PlumType::TNamed(name) => match registry.class_type_idx.get(name).or_else(|| registry.enum_super_type_idx.get(name)) {
534
- .unwrap_or_else(|| panic!("internal codegen error: unknown type '{}' in GC type registry", name));
535
- ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) })
532
+ Some(idx) => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) }),
533
+ // A field typed as a generic enum/class (e.g. `Node.next: Option[Node]`)
534
+ // resolves here to the BARE generic name — `ClassEnv`'s `plumTypeFromAst`
535
+ // has no representation for type arguments, so it can't know this means
536
+ // `Option$Int` once monomorphization specializes (and removes the
537
+ // unspecialized) `Option` — same permissive `anyref` fallback as
538
+ // `plumTypeToValtype` above, for the same "genuinely unmodeled" reason:
539
+ // `struct.get`/`ref.test`/`ref.cast` all work against `anyref` operands
540
+ // fine, so a field merely being STORED as `anyref` instead of the exact
541
+ // concrete type costs nothing but static precision.
542
+ None => ValType::Ref(RefType::ANYREF),
536
- }
543
+ },
537
544
  // TFun (closures) and TVariadic get their own concrete representation once
538
545
  // Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
539
546
  // yet-meaningful placeholder in the meantime, since nothing consumes it yet.
@@ -3168,29 +3175,50 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
3168
3175
  pushBoolRefFromI32Flag(body, ctx);
3169
3176
  }
3170
3177
  ast::Expr::Compare(c) => {
3171
- let is_float = matches!(inferLocalType(&c.left, ctx), PlumType::TFloat);
3178
+ let left_ty = inferLocalType(&c.left, ctx);
3172
3179
  compileExpr(&c.left, body, ctx, state)?;
3173
3180
  compileExpr(&c.right, body, ctx, state)?;
3174
- if is_float {
3181
+ match left_ty {
3182
+ PlumType::TFloat => {
3175
- match c.op {
3183
+ match c.op {
3176
- ast::CmpOp::Lt => Instruction::F64Lt,
3184
+ ast::CmpOp::Lt => Instruction::F64Lt,
3177
- ast::CmpOp::Lte => Instruction::F64Le,
3185
+ ast::CmpOp::Lte => Instruction::F64Le,
3178
- ast::CmpOp::Eq => Instruction::F64Eq,
3186
+ ast::CmpOp::Eq => Instruction::F64Eq,
3179
- ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::F64Ne,
3187
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::F64Ne,
3180
- ast::CmpOp::Gte => Instruction::F64Ge,
3188
+ ast::CmpOp::Gte => Instruction::F64Ge,
3181
- ast::CmpOp::Gt => Instruction::F64Gt,
3189
+ ast::CmpOp::Gt => Instruction::F64Gt,
3190
+ }
3191
+ .encode(body);
3182
3192
  }
3193
+ // `TVar`/`TUnit` share `Int`'s `i64` wasm representation (see
3194
+ // `plumTypeToValtype`) — an unresolved generic defaults the same way.
3195
+ PlumType::TInt | PlumType::TVar(_) | PlumType::TUnit => {
3196
+ match c.op {
3197
+ ast::CmpOp::Lt => Instruction::I64LtS,
3198
+ ast::CmpOp::Lte => Instruction::I64LeS,
3199
+ ast::CmpOp::Eq => Instruction::I64Eq,
3200
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne,
3201
+ ast::CmpOp::Gte => Instruction::I64GeS,
3202
+ ast::CmpOp::Gt => Instruction::I64GtS,
3203
+ }
3183
- .encode(body);
3204
+ .encode(body);
3184
- } else {
3185
- match c.op {
3186
- ast::CmpOp::Lt => Instruction::I64LtS,
3187
- ast::CmpOp::Lte => Instruction::I64LeS,
3188
- ast::CmpOp::Eq => Instruction::I64Eq,
3189
- ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne,
3190
- ast::CmpOp::Gte => Instruction::I64GeS,
3191
- ast::CmpOp::Gt => Instruction::I64GtS,
3192
3205
  }
3206
+ // Bool/Str/class/enum values are wasm-gc refs — `==`/`!=` compares
3207
+ // reference identity via `ref.eq`. That's exactly right for a
3208
+ // payload-free singleton (`None`/`True`/`False`, this migration
3209
+ // plan's Decision 2) and for class-instance identity; it's NOT a
3210
+ // deep/structural comparison (two distinct `Str` values holding
3211
+ // equal text compare unequal) — the same caveat this codegen
3212
+ // already had pre-wasm-gc, when it was an i32 POINTER comparison.
3213
+ // Ordering a ref type has no meaning and was never valid.
3214
+ _ => match &c.op {
3215
+ ast::CmpOp::Eq => Instruction::RefEq.encode(body),
3216
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => {
3193
- .encode(body);
3217
+ Instruction::RefEq.encode(body);
3218
+ Instruction::I32Eqz.encode(body);
3219
+ }
3220
+ other => return Err(format!("codegen: '{:?}' is not supported between reference-typed values", other)),
3221
+ },
3194
3222
  }
3195
3223
  pushBoolRefFromI32Flag(body, ctx);
3196
3224
  }
@@ -3362,13 +3390,47 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
3362
3390
  .get(&key)
3363
3391
  .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, call.name))?;
3364
3392
  compileExpr(&attr.object, body, ctx, state)?; // push self
3393
+
3365
- for arg in &call.args {
3394
+ fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
3366
- let arg_expr = match arg {
3395
+ match arg {
3367
3396
  ast::Arg::Positional(e) => e,
3368
3397
  ast::Arg::Keyword { value, .. } => value,
3369
3398
  ast::Arg::Pair { value, .. } => value,
3399
+ }
3400
+ }
3401
+ // A trailing `...T` param (e.g. `add(self, values: ...T)`) packs its
3402
+ // trailing args into one GC array, exactly like a plain function call
3403
+ // (see the `Expr::FnCall` variadic-call arm) — `call.args` doesn't
3404
+ // include `self`, matching `ctx.methods`' own param list.
3405
+ let variadic_split = match ctx.methods.get(&(class_name.clone(), call.name.clone())) {
3406
+ Some(PlumType::TFun(params, _)) => match params.last() {
3407
+ Some(PlumType::TVariadic(elem)) => Some(((**elem).clone(), params.len() - 1)),
3408
+ _ => None,
3409
+ },
3410
+ _ => None,
3370
- };
3411
+ };
3412
+ match variadic_split {
3413
+ Some((elem_ty, fixed_count)) => {
3414
+ for arg in call.args.iter().take(fixed_count) {
3415
+ compileExpr(argExprOf(arg), body, ctx, state)?;
3416
+ }
3417
+ let trailing: Vec<&ast::Expr> = call.args.iter().skip(fixed_count).map(argExprOf).collect();
3418
+ let elem_vt = plumTypeToValtype(&elem_ty);
3419
+ let array_type_idx = *ctx
3420
+ .gc_types
3421
+ .variadic_array_type_idx
3422
+ .get(&elem_vt)
3423
+ .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
3424
+ for arg_expr in &trailing {
3371
- compileExpr(arg_expr, body, ctx, state)?;
3425
+ compileExpr(arg_expr, body, ctx, state)?;
3426
+ }
3427
+ Instruction::ArrayNewFixed { array_type_index: array_type_idx, array_size: trailing.len() as u32 }.encode(body);
3428
+ }
3429
+ None => {
3430
+ for arg in &call.args {
3431
+ compileExpr(argExprOf(arg), body, ctx, state)?;
3432
+ }
3433
+ }
3372
3434
  }
3373
3435
  Instruction::Call(func_idx).encode(body);
3374
3436
  }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -2025,3 +2025,221 @@ fun main() -> Int =
2025
2025
  // this task is purely additive, nothing behavioral should have changed.
2026
2026
  assert_eq!(runMain(&bytes), 7);
2027
2027
  }
2028
+
2029
+ /// Task 3 of the wasm-gc migration plan: `libs/std/list.plum`'s `add`/`unlink`/`set`/
2030
+ /// `removeAt`/`remove`/`clear`/`reverse`, ported field-for-field/statement-for-statement
2031
+ /// from the real file (see that file's identical method bodies), against the
2032
+ /// struct.new/struct.get/struct.set representation.
2033
+ ///
2034
+ /// This uses `NodeLink`/`Int` in place of the real file's `Option[Node]`/generic `T`:
2035
+ /// `plum-checker`'s monomorphizer mangles a generic type's name once it's specialized
2036
+ /// (`Option` -> `Option$Int`) but does NOT rewrite `ClassEnv`/`EnumVariants`' OWN
2037
+ /// declared field types to match (`plumTypeFromAst` drops type arguments entirely,
2038
+ /// recording a class field typed `Option[Node]` as the bare, now-dangling `TNamed("Option")`)
2039
+ /// — a real, pre-existing gap in the checker's generics support, unrelated to and
2040
+ /// discovered while working on this migration, that currently blocks the REAL
2041
+ /// `libs/std/list.plum` (and its already-existing, unrelated `get`/`each`/`map`
2042
+ /// methods) from compiling at all. `NodeLink`/`Node` here are deliberately NOT
2043
+ /// generic, sidestepping that gap, so this test still exercises the exact wasm-gc
2044
+ /// struct/array mechanics (self-referential nullable-via-enum fields, `struct.set`
2045
+ /// mutation through an aliased reference, `ref.test` dispatch) Task 2 built.
2046
+ const LIST_SOURCE_PREFIX: &str = "\
2047
+ enum NodeLink =
2048
+ | HasNode[Node]
2049
+ | NoNode
2050
+
2051
+ enum Option =
2052
+ | Some[Int]
2053
+ | None
2054
+
2055
+ type Node =
2056
+ value: Int
2057
+ prev: NodeLink
2058
+ next: NodeLink
2059
+
2060
+ type List =
2061
+ head: NodeLink
2062
+ tail: NodeLink
2063
+ size: Int
2064
+
2065
+ fun get(self, i: Int) -> Option =
2066
+ current = self.head
2067
+ index = 0
2068
+ while current != NoNode
2069
+ match current
2070
+ HasNode(node) =>
2071
+ if index == i
2072
+ return Some(node.value)
2073
+ current = node.next
2074
+ index = index + 1
2075
+ NoNode =>
2076
+ break
2077
+ None
2078
+
2079
+ fun length(self) -> Int =
2080
+ self.size
2081
+
2082
+ fun add(self, values: ...Int) =
2083
+ for v in values
2084
+ node = Node(value: v, prev: self.tail, next: NoNode)
2085
+ match self.tail
2086
+ HasNode(t) =>
2087
+ t.next = HasNode(node)
2088
+ NoNode =>
2089
+ self.head = HasNode(node)
2090
+ self.tail = HasNode(node)
2091
+ self.size = self.size + 1
2092
+
2093
+ fun unlink(self, node: Node) =
2094
+ match node.prev
2095
+ HasNode(p) =>
2096
+ p.next = node.next
2097
+ NoNode =>
2098
+ self.head = node.next
2099
+ match node.next
2100
+ HasNode(n) =>
2101
+ n.prev = node.prev
2102
+ NoNode =>
2103
+ self.tail = node.prev
2104
+ self.size = self.size - 1
2105
+
2106
+ fun set(self, i: Int, v: Int) -> Option =
2107
+ current = self.head
2108
+ index = 0
2109
+ while current != NoNode
2110
+ match current
2111
+ HasNode(node) =>
2112
+ if index == i
2113
+ old = node.value
2114
+ node.value = v
2115
+ return Some(old)
2116
+ current = node.next
2117
+ index = index + 1
2118
+ NoNode =>
2119
+ break
2120
+ None
2121
+
2122
+ fun removeAt(self, i: Int) =
2123
+ current = self.head
2124
+ index = 0
2125
+ while current != NoNode
2126
+ match current
2127
+ HasNode(node) =>
2128
+ if index == i
2129
+ self.unlink(node)
2130
+ return
2131
+ current = node.next
2132
+ index = index + 1
2133
+ NoNode =>
2134
+ break
2135
+
2136
+ fun remove(self, v: Int) =
2137
+ current = self.head
2138
+ while current != NoNode
2139
+ match current
2140
+ HasNode(node) =>
2141
+ if node.value == v
2142
+ self.unlink(node)
2143
+ return
2144
+ current = node.next
2145
+ NoNode =>
2146
+ break
2147
+
2148
+ fun clear(self) =
2149
+ self.head = NoNode
2150
+ self.tail = NoNode
2151
+ self.size = 0
2152
+
2153
+ fun reverse(self) -> List =
2154
+ current = self.head
2155
+ while current != NoNode
2156
+ match current
2157
+ HasNode(node) =>
2158
+ next = node.next
2159
+ node.next = node.prev
2160
+ node.prev = next
2161
+ current = next
2162
+ NoNode =>
2163
+ break
2164
+ oldHead = self.head
2165
+ self.head = self.tail
2166
+ self.tail = oldHead
2167
+ self
2168
+
2169
+ fun optSum(o: Option) -> Int =
2170
+ match o
2171
+ Some(v) =>
2172
+ v
2173
+ None =>
2174
+ -1000
2175
+ ";
2176
+
2177
+ #[test]
2178
+ fn listAddSetRemoveAtRemoveClearReverseAllWorkCorrectly() {
2179
+ let src = format!("{LIST_SOURCE_PREFIX}\
2180
+ fun main() -> Int =
2181
+ l = List(head: NoNode, tail: NoNode, size: 0)
2182
+ l.add(1, 2, 3, 4, 5)
2183
+ a = l.length()
2184
+ b = optSum(l.get(0))
2185
+ c = optSum(l.get(4))
2186
+ oldVal = optSum(l.set(2, 30))
2187
+ d = optSum(l.get(2))
2188
+ l.removeAt(0)
2189
+ e = l.length()
2190
+ f = optSum(l.get(0))
2191
+ l.remove(30)
2192
+ g = l.length()
2193
+ l.reverse()
2194
+ h = optSum(l.get(0))
2195
+ l.clear()
2196
+ i = l.length()
2197
+ a + b + c + oldVal + d + e + f + g + h + i
2198
+ ");
2199
+ let source = parse(&src);
2200
+ let bytes = compileSource(&source).expect("compile failed");
2201
+ let result = wasmparser::validate(&bytes);
2202
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
2203
+ // add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
2204
+ // set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
2205
+ // removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
2206
+ // remove(30): g=length=3 -> list [2,4,5]
2207
+ // reverse(): h=get(0)=5 -> list [5,4,2]
2208
+ // clear(): i=length=0
2209
+ // 5+1+5+3+30+4+2+3+5+0 = 58
2210
+ assert_eq!(runMain(&bytes), 58);
2211
+ }
2212
+
2213
+ /// Proves `removeAt`/`clear` actually detach nodes from the list (not just decrement
2214
+ /// `size`) by removing every node one at a time via repeated `removeAt(0)` and
2215
+ /// confirming the list ends up correctly empty and reports zero length — the removed
2216
+ /// `Node`s (and their `NodeLink` links to each other) become unreachable and eligible
2217
+ /// for collection once nothing in the list still points to them, since there's no
2218
+ /// direct "assert this was garbage collected" hook available from a compiled
2219
+ /// program's own execution.
2220
+ #[test]
2221
+ fn removingEveryNodeInALoopLeavesAnEmptyCorrectlyFunctioningList() {
2222
+ let src = format!("{LIST_SOURCE_PREFIX}\
2223
+ fun main() -> Int =
2224
+ l = List(head: NoNode, tail: NoNode, size: 0)
2225
+ l.add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
2226
+ i = 0
2227
+ while i < 10
2228
+ l.removeAt(0)
2229
+ i = i + 1
2230
+ afterLoopLength = l.length()
2231
+ isEmpty = optSum(l.get(0))
2232
+ l.add(42)
2233
+ afterReAdd = optSum(l.get(0))
2234
+ afterLoopLength + isEmpty + afterReAdd
2235
+ ");
2236
+ let source = parse(&src);
2237
+ let bytes = compileSource(&source).expect("compile failed");
2238
+ let result = wasmparser::validate(&bytes);
2239
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
2240
+ // afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
2241
+ // 0 + -1000 + 42 = -958
2242
+ assert_eq!(runMain(&bytes), -958);
2243
+ }
2244
+
2245
+