plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
02b3582
— Peter 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
|
-
- [
|
|
200
|
-
- [
|
|
201
|
-
- [
|
|
202
|
-
- [
|
|
203
|
-
- [
|
|
204
|
-
- [
|
|
205
|
-
- [
|
|
206
|
-
- [
|
|
207
|
-
- [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
self.head = None
|
|
111
|
+
self.tail = None
|
|
112
|
+
self.size = 0
|
|
56
113
|
|
|
57
|
-
# returns
|
|
114
|
+
# returns the list with the elements relinked in reverse order.
|
|
58
|
-
fun reverse(self
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3181
|
+
match left_ty {
|
|
3182
|
+
PlumType::TFloat => {
|
|
3175
|
-
|
|
3183
|
+
match c.op {
|
|
3176
|
-
|
|
3184
|
+
ast::CmpOp::Lt => Instruction::F64Lt,
|
|
3177
|
-
|
|
3185
|
+
ast::CmpOp::Lte => Instruction::F64Le,
|
|
3178
|
-
|
|
3186
|
+
ast::CmpOp::Eq => Instruction::F64Eq,
|
|
3179
|
-
|
|
3187
|
+
ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::F64Ne,
|
|
3180
|
-
|
|
3188
|
+
ast::CmpOp::Gte => Instruction::F64Ge,
|
|
3181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3394
|
+
fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
|
|
3366
|
-
|
|
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
|
-
|
|
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
|
+
|