plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
docs/superpowers/plans/2026-07-24-list-methods.md
# List Methods Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Wire up `libs/std/list.plum`'s remaining `todo` methods (`add`, `set`, `removeAt`, `remove`, `clear`, `reverse`) plus a rewritten `join`, using the mutable `Node`/`head`/`tail`/`size` design already in place.
**Architecture:** Each method is implemented directly against the existing `Node(a)`/`List(a)` shapes using field assignment (`self.head = ...`, `node.next = ...`) and, for `add`, variadic iteration (`for v in values`). No type/grammar changes. Tests use small, self-contained inline fixtures (a minimal `Option`/`Node`/`List` trio plus whichever methods a test needs) — matching this project's established codegen-test convention — rather than depending on cross-file loading of the real `libs/std` files, which is a separate concern.
**Tech Stack:** Rust, the existing `plum-checker`/`plum-wasm-codegen` pipeline. No new dependencies.
## Global Constraints
- Spec: `docs/superpowers/specs/2026-07-24-list-methods-design.md`
- `add` appends to the tail (fixing the stale "to the start" doc comment); `reverse<List>(self) -> List` drops its predicate parameter.
- Every fresh-empty-list construction (including the PRE-EXISTING `init<List>`/`map<List>` methods' bare `List()` calls) must use explicit fields: `List(head: None, tail: None, size: 0)` — bare `List()` leaves `head`/`tail` as zero-filled memory, which likely decodes as a garbage `Some(...)` rather than `None` (see spec's "Additional fix" section).
- Default parameter values (`sep: Str = ","`) are parsed but NOT consulted at call sites for arity checking or substitution anywhere in the checker/codegen today (confirmed: no code path reads `Param.default` except monomorphization's structural clone) — this is a separate, un-scoped gap. Every test in this plan that calls a method with a defaulted param passes the argument explicitly; do not rely on omitting it.
- Out of scope: `sort`, `find`, `contains`, `flatMap`, `retain`, `reject`, `any`, `every`, `reduce`, `sublist`, `take`, `skip`, `drop`, `sample`, `shuffle`, `partition`, `chunk`, `groupBy`, and anything in `Map`.
- Run `cargo test --workspace` after every task — all pre-existing tests must keep passing throughout.
---
### Task 1: `add<List>` (+ fix `init`/`map`'s bare `List()`)
**Files:**
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
- Modify: `libs/std/list.plum` (`init`, `add`, `map`)
**Interfaces:**
- Consumes: field assignment (`self.field = ...`, `obj.field = ...`), variadic `for v in values` iteration — both already implemented.
- Produces: `add<List>(self, values: ...a)` — appends each value to the tail, in call order, updating `self.head`/`self.tail`/`self.size`. Later tasks' tests build lists via `add` (or via `List(head: None, tail: None, size: 0)` directly, as needed).
This is the riskiest task in the plan: `add` constructs a `Node(a)` instance (a class literal for one generic class) *inside* a method of a *different* generic class (`List`'s own `add<List>`), with `List`'s type parameter needing to flow into `Node`'s specialization. No existing test covers this exact shape (existing generic tests cover a class's method reading its own field, or a generic function call chain, not nested generic-class construction across two classes). If the RED step below fails with something other than a straightforward "method not implemented" trap — e.g. a monomorphization error about `Node`'s type parameter, or a codegen panic — **stop and report BLOCKED** rather than trying to work around it; that would mean this plan's risk assessment was right and the controller needs to decide how to proceed (fix a compiler gap first, or reshape the approach), not something to paper over inside this task.
- [ ] **Step 1: Write the failing test**
Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
```rust
#[test]
fn list_add_appends_values_in_order_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
a = unwrapOr(l.get(0), -1)
b = unwrapOr(l.get(1), -1)
c = unwrapOr(l.get(2), -1)
d = l.length()
a * 1000 + b * 100 + c * 10 + d
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 1233);
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `cargo test -p plum-wasm-codegen list_add_appends_values_in_order 2>&1 | tail -60`
Expected: FAIL — `add`'s body in this inline test source already has the real implementation (Step 1 writes the test with the real `add` body directly in the test source, since this test doesn't depend on `libs/std/list.plum` at all), so this run is really the first real exercise of the nested-generic-construction risk described above. If it fails with a clear codegen/checker error about `Node`'s type resolution, that confirms the risk; if it fails only because you haven't run it yet (trivial), re-check you actually ran the command.
- [ ] **Step 3: If it passes (or fails only for a benign reason), apply the same `add` implementation to `libs/std/list.plum`**
In `libs/std/list.plum`, replace:
```
init<List>(self, values: ...a) -> List =
List().add(values)
```
with:
```
init<List>(self, values: ...a) -> List =
List(head: None, tail: None, size: 0).add(values)
```
Replace:
```
# adds the specified elements to the start of the list
add<List>(self, values: ...a) =
todo
```
with:
```
# adds the specified elements to the end of the list
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
```
And in `map<List>`, replace:
```
map<List>(self, cb: fn(a) -> b) -> List(b) =
nl = List()
```
with:
```
map<List>(self, cb: fn(a) -> b) -> List(b) =
nl = List(head: None, tail: None, size: 0)
```
(Leave the rest of `map`'s body unchanged.)
- [ ] **Step 4: Run the test to verify it passes**
Run: `cargo test -p plum-wasm-codegen list_add_appends_values_in_order 2>&1 | tail -60`
Expected: PASS (`1233`).
- [ ] **Step 5: Run the full workspace test suite**
Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS (no existing test compiles the real `libs/std/list.plum`, so the `init`/`add`/`map` edits there have no effect on the Rust test suite — this step exists to catch the unexpected case where something does reference it).
- [ ] **Step 6: Commit**
```bash
git add plum-wasm-codegen/tests/codegen_tests.rs libs/std/list.plum
git commit -m "feat(libs/std): implement List.add, fix List() zero-field construction in init/map"
```
---
### Task 2: `set<List>`
**Files:**
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
- Modify: `libs/std/list.plum` (`set`)
**Interfaces:**
- Consumes: `add<List>` (Task 1, for building test fixtures), field assignment on a match-bound local (`node.value = v`).
- Produces: `set<List>(self, i: Int, v: a) -> Option(a)` — replaces the value at index `i`, returning the old value wrapped in `Some`, or `None` if `i` is out of range.
- [ ] **Step 1: Write the failing tests**
Add to `plum-wasm-codegen/tests/codegen_tests.rs` (reusing the same `Option`/`Node`/`List`/`get`/`length`/`add`/`unwrapOr` preamble as Task 1's test — repeated here in full since each test source is self-contained):
```rust
#[test]
fn list_set_in_range_replaces_value_and_returns_old_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
set<List>(self, i: Int, v: a) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
oldValue = node.value
node.value = v
return Some(oldValue)
current = node.next
index = index + 1
None =>
break
None
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
old = unwrapOr(l.set(1, 99), -1)
new = unwrapOr(l.get(1), -1)
old * 1000 + new
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 2099);
}
#[test]
fn list_set_out_of_range_returns_none_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
set<List>(self, i: Int, v: a) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
oldValue = node.value
node.value = v
return Some(oldValue)
current = node.next
index = index + 1
None =>
break
None
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
result = l.set(10, 99)
match result
Some(v) =>
1
None =>
0
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 0);
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `cargo test -p plum-wasm-codegen list_set_ 2>&1 | tail -80`
Expected: FAIL to compile (crate-internal `set`'s body is already written directly in the test source in Step 1, so this exercises the real implementation immediately — same reasoning as Task 1). If it fails for a reason other than "these tests don't exist yet before you add them," re-check.
- [ ] **Step 3: Apply the same `set` implementation to `libs/std/list.plum`**
Replace:
```
# sets the element at i'th index of the list
set<List>(self, i: Int, v: a) -> Option(a) =
todo
```
with:
```
# sets the element at i'th index of the list
set<List>(self, i: Int, v: a) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
oldValue = node.value
node.value = v
return Some(oldValue)
current = node.next
index = index + 1
None =>
break
None
```
- [ ] **Step 4: Run the tests to verify they pass**
Run: `cargo test -p plum-wasm-codegen list_set_ 2>&1 | tail -80`
Expected: both PASS (`2099`, `0`).
- [ ] **Step 5: Run the full workspace test suite**
Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS.
- [ ] **Step 6: Commit**
```bash
git add plum-wasm-codegen/tests/codegen_tests.rs libs/std/list.plum
git commit -m "feat(libs/std): implement List.set"
```
---
### Task 3: `removeAt<List>` and `remove<List>`
**Files:**
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
- Modify: `libs/std/list.plum` (`removeAt`, `remove`)
**Interfaces:**
- Consumes: `add<List>` (Task 1, for building test fixtures).
- Produces: `removeAt<List>(self, i: Int)` and `remove<List>(self, v: a)` — both unlink the target node (by index / by value) from the doubly-linked chain, fixing up `self.head`/`self.tail`/neighboring `prev`/`next`, and decrement `self.size`. Both are a no-op if no matching node is found (no error), matching `get`'s existing out-of-range convention.
- [ ] **Step 1: Write the failing tests**
Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
```rust
#[test]
fn list_remove_at_head_updates_list_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
removeAt<List>(self, i: Int) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
match node.prev
Some(p) =>
p.next = node.next
None =>
self.head = node.next
match node.next
Some(n) =>
n.prev = node.prev
None =>
self.tail = node.prev
self.size = self.size - 1
return
current = node.next
index = index + 1
None =>
break
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
l.removeAt(0)
a = unwrapOr(l.get(0), -1)
b = unwrapOr(l.get(1), -1)
c = l.length()
a * 100 + b * 10 + c
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 232);
}
#[test]
fn list_remove_at_tail_relinks_tail_pointer_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
removeAt<List>(self, i: Int) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
match node.prev
Some(p) =>
p.next = node.next
None =>
self.head = node.next
match node.next
Some(n) =>
n.prev = node.prev
None =>
self.tail = node.prev
self.size = self.size - 1
return
current = node.next
index = index + 1
None =>
break
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
l.removeAt(2)
l.add(4)
a = unwrapOr(l.get(0), -1)
b = unwrapOr(l.get(1), -1)
c = unwrapOr(l.get(2), -1)
d = l.length()
a * 1000 + b * 100 + c * 10 + d
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 1243);
}
#[test]
fn list_remove_by_value_removes_middle_element_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
remove<List>(self, v: a) =
current = self.head
while current != None
match current
Some(node) =>
if node.value == v
match node.prev
Some(p) =>
p.next = node.next
None =>
self.head = node.next
match node.next
Some(n) =>
n.prev = node.prev
None =>
self.tail = node.prev
self.size = self.size - 1
return
current = node.next
None =>
break
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
l.remove(2)
a = unwrapOr(l.get(0), -1)
b = unwrapOr(l.get(1), -1)
c = l.length()
a * 100 + b * 10 + c
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 132);
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `cargo test -p plum-wasm-codegen list_remove 2>&1 | tail -100`
Expected: FAIL to compile (the tests don't exist before you add them; their bodies already contain the real implementation, matching Task 1/2's pattern).
- [ ] **Step 3: Apply the same implementations to `libs/std/list.plum`**
Replace:
```
# removes the element at i'th index of the list
removeAt<List>(self, i: Int) =
todo
```
with:
```
# removes the element at i'th index of the list
removeAt<List>(self, i: Int) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
match node.prev
Some(p) =>
p.next = node.next
None =>
self.head = node.next
match node.next
Some(n) =>
n.prev = node.prev
None =>
self.tail = node.prev
self.size = self.size - 1
return
current = node.next
index = index + 1
None =>
break
```
Replace:
```
# removes the element v from list
remove<List>(self, v: a) =
todo
```
with:
```
# removes the element v from list
remove<List>(self, v: a) =
current = self.head
while current != None
match current
Some(node) =>
if node.value == v
match node.prev
Some(p) =>
p.next = node.next
None =>
self.head = node.next
match node.next
Some(n) =>
n.prev = node.prev
None =>
self.tail = node.prev
self.size = self.size - 1
return
current = node.next
None =>
break
```
- [ ] **Step 4: Run the tests to verify they pass**
Run: `cargo test -p plum-wasm-codegen list_remove 2>&1 | tail -100`
Expected: all 3 PASS (`232`, `1243`, `132`).
- [ ] **Step 5: Run the full workspace test suite**
Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS.
- [ ] **Step 6: Commit**
```bash
git add plum-wasm-codegen/tests/codegen_tests.rs libs/std/list.plum
git commit -m "feat(libs/std): implement List.removeAt and List.remove"
```
---
### Task 4: `clear<List>` and `reverse<List>`
**Files:**
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
- Modify: `libs/std/list.plum` (`clear`, `reverse`)
**Interfaces:**
- Consumes: `add<List>` (Task 1).
- Produces: `clear<List>(self)` — resets `head`/`tail` to `None`, `size` to `0`. `reverse<List>(self) -> List` (predicate parameter dropped, per the spec) — returns a NEW list with elements in reverse order, built via `add`, leaving `self` unmodified.
- [ ] **Step 1: Write the failing tests**
Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
```rust
#[test]
fn list_clear_resets_list_to_empty_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
clear<List>(self) =
self.head = None
self.tail = None
self.size = 0
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
l.clear()
a = l.length()
b = unwrapOr(l.get(0), -1)
a * 100 + b
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), -1);
}
#[test]
fn list_reverse_returns_new_reversed_list_and_leaves_original_unchanged() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
get<List>(self, i: Int) -> Option(a) =
current = self.head
index = 0
while current != None
match current
Some(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
None =>
break
None
length<List>(self) -> Int =
self.size
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
reverse<List>(self) -> List =
nl = List(head: None, tail: None, size: 0)
current = self.tail
while current != None
match current
Some(node) =>
nl.add(node.value)
current = node.prev
None =>
break
nl
unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) => v
None => default
main() -> Int =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
r = l.reverse()
ra = unwrapOr(r.get(0), -1)
rb = unwrapOr(r.get(1), -1)
rc = unwrapOr(r.get(2), -1)
rlen = r.length()
oa = unwrapOr(l.get(0), -1)
olen = l.length()
ra * 100000 + rb * 10000 + rc * 1000 + rlen * 100 + oa * 10 + olen
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main(&bytes), 321313);
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `cargo test -p plum-wasm-codegen list_clear list_reverse 2>&1 | tail -80`
Expected: FAIL to compile (tests don't exist yet).
- [ ] **Step 3: Apply the same implementations to `libs/std/list.plum`**
Replace:
```
# removes all objects from this list
clear<List>(self) =
todo
```
with:
```
# removes all objects from this list
clear<List>(self) =
self.head = None
self.tail = None
self.size = 0
```
Replace:
```
# returns a new list with the elements in reverse order.
reverse<List>(self, v: fn(a) -> Bool) -> List =
todo
```
with:
```
# returns a new list with the elements in reverse order.
reverse<List>(self) -> List =
nl = List(head: None, tail: None, size: 0)
current = self.tail
while current != None
match current
Some(node) =>
nl.add(node.value)
current = node.prev
None =>
break
nl
```
- [ ] **Step 4: Run the tests to verify they pass**
Run: `cargo test -p plum-wasm-codegen list_clear list_reverse 2>&1 | tail -80`
Expected: both PASS (`-1`, `321313`).
- [ ] **Step 5: Run the full workspace test suite**
Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS.
- [ ] **Step 6: Commit**
```bash
git add plum-wasm-codegen/tests/codegen_tests.rs libs/std/list.plum
git commit -m "feat(libs/std): implement List.clear and List.reverse"
```
---
### Task 5: `join<List>` (rewritten, no `Buffer`)
**Files:**
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
- Modify: `libs/std/list.plum` (`join`)
**Interfaces:**
- Consumes: `add<List>` (Task 1); the existing `run_main_str` test helper (already defined in `plum-wasm-codegen/tests/codegen_tests.rs`, used elsewhere in this file to read a `Str`-returning `main`'s result).
- Produces: `join<List>(self, sep: Str = ",") -> Str` — concatenates each element's `Str` interpolation with `sep` after it (including after the last element — this matches the ORIGINAL `Buffer`-based version's existing behavior of always appending a trailing separator, not a regression).
- [ ] **Step 1: Write the failing test**
Add to `plum-wasm-codegen/tests/codegen_tests.rs`:
```rust
#[test]
fn list_join_concatenates_elements_with_separator_runs_correctly() {
let src = "\
enum Option =
| Some(a)
| None
type Node(a) =
value: a
prev: Option[Node]
next: Option[Node]
type List(a) =
head: Option[Node]
tail: Option[Node]
size: Int
add<List>(self, values: ...a) =
for v in values
newNode = Node(value: v, prev: self.tail, next: None)
match self.tail
Some(oldTail) =>
oldTail.next = Some(newNode)
None =>
self.head = Some(newNode)
self.tail = Some(newNode)
self.size = self.size + 1
join<List>(self, sep: Str) -> Str =
result = \"\"
current = self.head
while current != None
match current
Some(node) =>
result = \"{result}{node.value}{sep}\"
current = node.next
None =>
break
result
main() -> Str =
l = List(head: None, tail: None, size: 0)
l.add(1, 2, 3)
l.join(\",\")
";
let source = parse(src);
let bytes = compile_source(&source).expect("compile failed");
assert_eq!(run_main_str(&bytes), "1,2,3,");
}
```
(Note: this test declares `join<List>(self, sep: Str) -> Str` — no default value on `sep` — and always passes the separator explicitly at the call site, per this plan's Global Constraints: default parameter values aren't consulted at call sites anywhere in the checker/codegen today, so a test relying on omitting `sep` would fail for that unrelated, out-of-scope reason. `libs/std/list.plum`'s own declaration keeps its existing `sep: Str = ","` default in the signature — the default annotation is harmless to leave in place since nothing reads it, it's just never *usable* at a call site yet, which is not new or introduced by this task.)
- [ ] **Step 2: Run the test to verify it fails**
Run: `cargo test -p plum-wasm-codegen list_join_concatenates 2>&1 | tail -60`
Expected: FAIL to compile (test doesn't exist yet).
- [ ] **Step 3: Apply the same implementation to `libs/std/list.plum`**
Replace:
```
join<List>(self, sep: Str = ",") -> Str =
res = Buffer()
self.each(|v|
res.write(v.toStr())
res.write(sep)
)
res.toStr()
```
with:
```
join<List>(self, sep: Str = ",") -> Str =
result = ""
current = self.head
while current != None
match current
Some(node) =>
result = "{result}{node.value}{sep}"
current = node.next
None =>
break
result
```
- [ ] **Step 4: Run the test to verify it passes**
Run: `cargo test -p plum-wasm-codegen list_join_concatenates 2>&1 | tail -60`
Expected: PASS (`"1,2,3,"`).
- [ ] **Step 5: Run the full workspace test suite**
Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS.
- [ ] **Step 6: Commit**
```bash
git add plum-wasm-codegen/tests/codegen_tests.rs libs/std/list.plum
git commit -m "feat(libs/std): rewrite List.join using string interpolation instead of Buffer"
```
---
### Task 6: README — close the gap
**Files:**
- Modify: `README.md` (the "Known gaps" section)
**Interfaces:**
- Consumes: nothing.
- Produces: nothing (docs only).
- [ ] **Step 1: Update the Known gaps bullet**
Run: `grep -n "List" README.md` to find the current bullet, which reads along the lines of:
```
- `libs/std`'s actual `List`/`Map` still don't fully compile — cross-file `import` resolution now works (`import <path>` resolves against `--lib-path`, defaulting to `./libs`), and variadic parameters work, but `List`'s methods beyond `get`/`length` (`add`, `set`, `removeAt`, `remove`, `clear`, `reverse`) are still `todo`; separately, `List`'s own `join` method (and `Map`) reference a `Buffer` type and trait-bounded dispatch (`Stringable`) that don't exist yet — `plum-checker` doesn't process trait declarations at all currently
```
Replace it with:
```
- `libs/std/map.plum`'s `Map` still doesn't fully compile — it references a `Buffer` type that doesn't exist and relies on trait-bounded dispatch (`Stringable`) that `plum-checker` doesn't process at all currently. `List` (`libs/std/list.plum`) is otherwise fully wired up: `get`, `length`, `each`, `map`, `first`, `last`, `add`, `set`, `removeAt`, `remove`, `clear`, `reverse`, and `join` all compile and run (`join` was rewritten to use string interpolation instead of `Buffer`). `List`'s remaining extras (`sort`, `find`, `flatMap`, `retain`, and similar) are still `todo` — a much longer tail that was never part of this gap's original scope.
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: List's core methods are wired up; Map remains blocked on Buffer/traits"
```