plum

#treesitter#compiler#wasm

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"
```