plum

#treesitter#compiler#wasm

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

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


docs/superpowers/specs/2026-07-24-list-methods-design.md
# Design: wire up List's remaining methods

## Problem

`libs/std/list.plum`'s `List` (a generic, mutable doubly-linked list backed by
`Node(a)` with `head`/`tail`/`size` fields) has `get`, `length`, `each`, `map`,
`first`, and `last` already implemented and working. `add`, `set`, `removeAt`,
`remove`, `clear`, and `reverse` are still `todo` stubs. This was blocked on
two prerequisites that are now both done: field/attribute assignment
(`self.head = ...`, needed to mutate the linked structure) and variadic
parameters (`values: ...a`, needed by `add`'s signature). `join` is not one of
the six originally-scoped methods, but it separately calls `Buffer()` (an
undeclared type) and `v.toStr()` on a generic-typed value (which would need
trait-bounded dispatch — `plum-checker` doesn't process trait declarations at
all) — both unrelated to this cycle's actual blockers, so it's included here
with a simpler fix instead of being deferred again.

## Scope

In scope: `add`, `set`, `removeAt`, `remove`, `clear`, `reverse`, `join`.

Out of scope (untouched, still `todo`, not part of README's original gap and
not addressed here): `sort`, `find`, `contains`, `flatMap`, `retain`, `reject`,
`any`, `every`, `reduce`, `sublist`, `take`, `skip`, `drop`, `sample`,
`shuffle`, `partition`, `chunk`, `groupBy`. `Map`'s own methods (a separate
class in `libs/std/map.plum`) are untouched.

Two corrections to the existing (buggy/inconsistent) doc comments and one
signature change, confirmed with the project owner:
- `add`'s doc comment currently says "to the start" but `init<List>` calls
  `List().add(values)` expecting insertion order to be preserved, which only
  works if `add` appends to the *tail*. The comment is stale; `add` appends.
- `reverse`'s current signature, `reverse<List>(self, v: fn(a) -> Bool) ->
  List`, has a predicate parameter that doesn't match its own doc comment
  ("returns a new list with the elements in reverse order") — a copy-paste
  artifact from a filter-style method. Signature becomes `reverse<List>(self)
  -> List`, no predicate.
- The mutable, doubly-linked `Node`/`head`/`tail`/`size` design is kept as-is
  (not rewritten as an immutable `Cons`/`Empty` enum) — it's already
  half-built and tested (`get`/`length`/`each`/`map`/`first`/`last` all depend
  on it), and it gives O(1) append/length, which a cons-list can't for `add`'s
  agreed append-in-order semantics.

## Method designs

All methods below operate on the existing `type Node(a) = value: a, prev:
Option[Node], next: Option[Node]` and `type List(Stringable)(a: Stringable) =
head: Option[Node], tail: Option[Node], size: Int` — no type changes.

**`add<List>(self, values: ...a)`**: for each value (in call order, via `for v
in values`), construct `newNode = Node(value: v, prev: self.tail, next: None)`,
link it after the current tail (`self.tail`'s node gets `.next = Some(newNode)`
if a tail exists, otherwise this is the first node so `self.head =
Some(newNode)` too), then `self.tail = Some(newNode)` and `self.size = self.size
+ 1`.

**`set<List>(self, i: Int, v: a) -> Option(a)`**: traverse from `self.head`
exactly like `get` does; at the matching index, capture the node's current
`value`, assign `node.value = v`, and return `Some(oldValue)`; if the traversal
runs off the end without finding index `i`, return `None`.

**`removeAt<List>(self, i: Int)`** / **`remove<List>(self, v: a)`**: both
locate a target node (`removeAt` by traversing to index `i`; `remove` by
traversing and comparing `node.value == v`) and, if found, unlink it: its
`prev` node's `next` becomes the target's `next` (or, if the target had no
`prev`, `self.head` becomes the target's `next` — the target was the head);
symmetrically for `next`/`self.tail`. Both decrement `self.size`. Neither
returns a value (matching their current `todo` signatures, which declare no
`-> T`). If the target isn't found, both are a no-op (no error) — this
matches `get`'s existing behavior of returning `None` rather than erroring on
an out-of-range index, so `removeAt`/`remove` follow the same "graceful
no-match" convention already established in this file.

**`clear<List>(self)`**: `self.head = None`, `self.tail = None`, `self.size =
0`.

**`reverse<List>(self) -> List`**: `nl = List()`, then walk `self` from
`self.tail` backward via each node's `.prev` (this naturally visits values in
reverse order), calling `nl.add(value)` for each — reuses `add` rather than
reimplementing low-level linking for the new list.

**`join<List>(self, sep: Str = ",") -> Str`**: rewritten to avoid `Buffer`,
`.toStr()`, and any trait dispatch — builds the result via string
interpolation, which already compiles for `Str`/`Int`/`Bool` element types
(interpolating a `Float` element remains a separate, pre-existing, unrelated
gap — same as everywhere else in the language):

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

(This preserves the original `Buffer`-based version's existing quirk of
always appending a trailing separator after the last element — not a
regression, matching prior behavior.)

## Additional fix: `List()` must always give explicit fields

`plum-checker`'s `ClassCall` type-checking only validates whatever fields ARE
given, not that all of a class's fields are supplied — so `List()` (zero
fields) type-checks today. Codegen never initializes an unmentioned field, so
it's left as whatever the WASM linear memory already held there — which, for
memory a bump allocator has never written to before, is zero (WASM memory
starts zero-filled). For `size: Int` that's correctly `0`, but `head`/`tail:
Option[Node]` are enum-tagged, and `Option`'s declaration order (`Some` listed
before `None`) means `None`'s tag is very likely not `0` — so a zero-filled
`head`/`tail` probably decodes as a garbage `Some(...)`, not `None`. This is a
latent bug in the *existing* `init<List>`/`map<List>` methods (both call bare
`List()`), not something newly introduced. Fix: every fresh-empty-list
construction in this cycle — including `init`/`map`'s pre-existing bare
`List()` calls — becomes explicit: `List(head: None, tail: None, size: 0)`.

## Risk: generic class construction inside a generic method of another generic class

`add`'s `Node(value: v, prev: ..., next: ...)` construction is a class
literal for one generic class (`Node(a)`) built inside a method of a
*different* generic class (`List`'s `add<List>`), with `List`'s own type
parameter `a` needing to flow into `Node`'s specialization. Existing
generic-codegen tests cover a generic class's method reading its own field,
and a generic *function* call chain — neither is this exact shape. It should
work given the current monomorphization machinery (both classes are already
generic and specialized elsewhere independently), but it's genuinely
untested. `add` is deliberately the first implementation task so if this
reveals a real compiler gap, it surfaces immediately rather than after
building `set`/`removeAt`/`remove`/`reverse` on top of an unverified
assumption.

## Testing

Each method gets `plum-checker` (where relevant — e.g. `set`'s `Option(a)`
return) and `plum-wasm-codegen` tests exercising it via `run_main`, using
small `Node`/`List`/`Option` fixtures inline in the test source (matching this
project's established testing convention — see e.g.
`generic_method_on_generic_class_runs_correctly` in `codegen_tests.rs`) rather
than depending on cross-file loading of the real `libs/std` files (a separate
concern from this spec). Specifically:
- `add`: append 3 values, confirm `length()` is 3 and traversal order matches
  insertion order (via `get(0)`/`get(1)`/`get(2)`).
- `set`: set an in-range index, confirm the returned `Option` holds the old
  value and `get` reflects the new one; set an out-of-range index, confirm
  `None`.
- `removeAt`/`remove`: remove the head, the tail, and a middle element in
  separate cases; confirm `length()` decrements and neighboring links are
  correct (traverse the remaining list end-to-end).
- `clear`: add elements, clear, confirm `length()` is 0 and `get(0)` is
  `None`.
- `reverse`: build a list, reverse it, confirm element order via `get`.
- `join`: build a list of `Int`s, confirm the joined `Str` matches the
  expected (including the trailing-separator quirk).

## README

Once this lands, remove the `List`-methods-still-`todo` clause from README's
"Known gaps" bullet entirely (all six originally-listed methods, plus `join`,
are done) — the much longer tail of still-`todo` extras (`sort`, `find`,
`flatMap`, etc.) was never part of that bullet's scope and stays unmentioned,
consistent with how the bullet was already scoped before this cycle.