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
0ec0f63 1
# Design: wire up List's remaining methods
0ec0f63 2
0ec0f63 3
## Problem
0ec0f63 4
0ec0f63 5
`libs/std/list.plum`'s `List` (a generic, mutable doubly-linked list backed by
0ec0f63 6
`Node(a)` with `head`/`tail`/`size` fields) has `get`, `length`, `each`, `map`,
0ec0f63 7
`first`, and `last` already implemented and working. `add`, `set`, `removeAt`,
0ec0f63 8
`remove`, `clear`, and `reverse` are still `todo` stubs. This was blocked on
0ec0f63 9
two prerequisites that are now both done: field/attribute assignment
0ec0f63 10
(`self.head = ...`, needed to mutate the linked structure) and variadic
0ec0f63 11
parameters (`values: ...a`, needed by `add`'s signature). `join` is not one of
0ec0f63 12
the six originally-scoped methods, but it separately calls `Buffer()` (an
0ec0f63 13
undeclared type) and `v.toStr()` on a generic-typed value (which would need
0ec0f63 14
trait-bounded dispatch — `plum-checker` doesn't process trait declarations at
0ec0f63 15
all) — both unrelated to this cycle's actual blockers, so it's included here
0ec0f63 16
with a simpler fix instead of being deferred again.
0ec0f63 17
0ec0f63 18
## Scope
0ec0f63 19
0ec0f63 20
In scope: `add`, `set`, `removeAt`, `remove`, `clear`, `reverse`, `join`.
0ec0f63 21
0ec0f63 22
Out of scope (untouched, still `todo`, not part of README's original gap and
0ec0f63 23
not addressed here): `sort`, `find`, `contains`, `flatMap`, `retain`, `reject`,
0ec0f63 24
`any`, `every`, `reduce`, `sublist`, `take`, `skip`, `drop`, `sample`,
0ec0f63 25
`shuffle`, `partition`, `chunk`, `groupBy`. `Map`'s own methods (a separate
0ec0f63 26
class in `libs/std/map.plum`) are untouched.
0ec0f63 27
0ec0f63 28
Two corrections to the existing (buggy/inconsistent) doc comments and one
0ec0f63 29
signature change, confirmed with the project owner:
0ec0f63 30
- `add`'s doc comment currently says "to the start" but `init<List>` calls
0ec0f63 31
  `List().add(values)` expecting insertion order to be preserved, which only
0ec0f63 32
  works if `add` appends to the *tail*. The comment is stale; `add` appends.
0ec0f63 33
- `reverse`'s current signature, `reverse<List>(self, v: fn(a) -> Bool) ->
0ec0f63 34
  List`, has a predicate parameter that doesn't match its own doc comment
0ec0f63 35
  ("returns a new list with the elements in reverse order") — a copy-paste
0ec0f63 36
  artifact from a filter-style method. Signature becomes `reverse<List>(self)
0ec0f63 37
  -> List`, no predicate.
0ec0f63 38
- The mutable, doubly-linked `Node`/`head`/`tail`/`size` design is kept as-is
0ec0f63 39
  (not rewritten as an immutable `Cons`/`Empty` enum) — it's already
0ec0f63 40
  half-built and tested (`get`/`length`/`each`/`map`/`first`/`last` all depend
0ec0f63 41
  on it), and it gives O(1) append/length, which a cons-list can't for `add`'s
0ec0f63 42
  agreed append-in-order semantics.
0ec0f63 43
0ec0f63 44
## Method designs
0ec0f63 45
0ec0f63 46
All methods below operate on the existing `type Node(a) = value: a, prev:
0ec0f63 47
Option[Node], next: Option[Node]` and `type List(Stringable)(a: Stringable) =
0ec0f63 48
head: Option[Node], tail: Option[Node], size: Int` — no type changes.
0ec0f63 49
0ec0f63 50
**`add<List>(self, values: ...a)`**: for each value (in call order, via `for v
0ec0f63 51
in values`), construct `newNode = Node(value: v, prev: self.tail, next: None)`,
0ec0f63 52
link it after the current tail (`self.tail`'s node gets `.next = Some(newNode)`
0ec0f63 53
if a tail exists, otherwise this is the first node so `self.head =
0ec0f63 54
Some(newNode)` too), then `self.tail = Some(newNode)` and `self.size = self.size
0ec0f63 55
+ 1`.
0ec0f63 56
0ec0f63 57
**`set<List>(self, i: Int, v: a) -> Option(a)`**: traverse from `self.head`
0ec0f63 58
exactly like `get` does; at the matching index, capture the node's current
0ec0f63 59
`value`, assign `node.value = v`, and return `Some(oldValue)`; if the traversal
0ec0f63 60
runs off the end without finding index `i`, return `None`.
0ec0f63 61
0ec0f63 62
**`removeAt<List>(self, i: Int)`** / **`remove<List>(self, v: a)`**: both
0ec0f63 63
locate a target node (`removeAt` by traversing to index `i`; `remove` by
0ec0f63 64
traversing and comparing `node.value == v`) and, if found, unlink it: its
0ec0f63 65
`prev` node's `next` becomes the target's `next` (or, if the target had no
0ec0f63 66
`prev`, `self.head` becomes the target's `next` — the target was the head);
0ec0f63 67
symmetrically for `next`/`self.tail`. Both decrement `self.size`. Neither
0ec0f63 68
returns a value (matching their current `todo` signatures, which declare no
0ec0f63 69
`-> T`). If the target isn't found, both are a no-op (no error) — this
0ec0f63 70
matches `get`'s existing behavior of returning `None` rather than erroring on
0ec0f63 71
an out-of-range index, so `removeAt`/`remove` follow the same "graceful
0ec0f63 72
no-match" convention already established in this file.
0ec0f63 73
0ec0f63 74
**`clear<List>(self)`**: `self.head = None`, `self.tail = None`, `self.size =
0ec0f63 75
0`.
0ec0f63 76
0ec0f63 77
**`reverse<List>(self) -> List`**: `nl = List()`, then walk `self` from
0ec0f63 78
`self.tail` backward via each node's `.prev` (this naturally visits values in
0ec0f63 79
reverse order), calling `nl.add(value)` for each — reuses `add` rather than
0ec0f63 80
reimplementing low-level linking for the new list.
0ec0f63 81
0ec0f63 82
**`join<List>(self, sep: Str = ",") -> Str`**: rewritten to avoid `Buffer`,
0ec0f63 83
`.toStr()`, and any trait dispatch — builds the result via string
0ec0f63 84
interpolation, which already compiles for `Str`/`Int`/`Bool` element types
0ec0f63 85
(interpolating a `Float` element remains a separate, pre-existing, unrelated
0ec0f63 86
gap — same as everywhere else in the language):
0ec0f63 87
0ec0f63 88
```
0ec0f63 89
join<List>(self, sep: Str = ",") -> Str =
0ec0f63 90
  result = ""
0ec0f63 91
  current = self.head
0ec0f63 92
  while current != None
0ec0f63 93
    match current
0ec0f63 94
      Some(node) =>
0ec0f63 95
        result = "{result}{node.value}{sep}"
0ec0f63 96
        current = node.next
0ec0f63 97
      None =>
0ec0f63 98
        break
0ec0f63 99
  result
0ec0f63 100
```
0ec0f63 101
0ec0f63 102
(This preserves the original `Buffer`-based version's existing quirk of
0ec0f63 103
always appending a trailing separator after the last element — not a
0ec0f63 104
regression, matching prior behavior.)
0ec0f63 105
abb8959 106
## Additional fix: `List()` must always give explicit fields
abb8959 107
abb8959 108
`plum-checker`'s `ClassCall` type-checking only validates whatever fields ARE
abb8959 109
given, not that all of a class's fields are supplied — so `List()` (zero
abb8959 110
fields) type-checks today. Codegen never initializes an unmentioned field, so
abb8959 111
it's left as whatever the WASM linear memory already held there — which, for
abb8959 112
memory a bump allocator has never written to before, is zero (WASM memory
abb8959 113
starts zero-filled). For `size: Int` that's correctly `0`, but `head`/`tail:
abb8959 114
Option[Node]` are enum-tagged, and `Option`'s declaration order (`Some` listed
abb8959 115
before `None`) means `None`'s tag is very likely not `0` — so a zero-filled
abb8959 116
`head`/`tail` probably decodes as a garbage `Some(...)`, not `None`. This is a
abb8959 117
latent bug in the *existing* `init<List>`/`map<List>` methods (both call bare
abb8959 118
`List()`), not something newly introduced. Fix: every fresh-empty-list
abb8959 119
construction in this cycle — including `init`/`map`'s pre-existing bare
abb8959 120
`List()` calls — becomes explicit: `List(head: None, tail: None, size: 0)`.
abb8959 121
0ec0f63 122
## Risk: generic class construction inside a generic method of another generic class
0ec0f63 123
0ec0f63 124
`add`'s `Node(value: v, prev: ..., next: ...)` construction is a class
0ec0f63 125
literal for one generic class (`Node(a)`) built inside a method of a
0ec0f63 126
*different* generic class (`List`'s `add<List>`), with `List`'s own type
0ec0f63 127
parameter `a` needing to flow into `Node`'s specialization. Existing
0ec0f63 128
generic-codegen tests cover a generic class's method reading its own field,
0ec0f63 129
and a generic *function* call chain — neither is this exact shape. It should
0ec0f63 130
work given the current monomorphization machinery (both classes are already
0ec0f63 131
generic and specialized elsewhere independently), but it's genuinely
0ec0f63 132
untested. `add` is deliberately the first implementation task so if this
0ec0f63 133
reveals a real compiler gap, it surfaces immediately rather than after
0ec0f63 134
building `set`/`removeAt`/`remove`/`reverse` on top of an unverified
0ec0f63 135
assumption.
0ec0f63 136
0ec0f63 137
## Testing
0ec0f63 138
0ec0f63 139
Each method gets `plum-checker` (where relevant — e.g. `set`'s `Option(a)`
0ec0f63 140
return) and `plum-wasm-codegen` tests exercising it via `run_main`, using
0ec0f63 141
small `Node`/`List`/`Option` fixtures inline in the test source (matching this
0ec0f63 142
project's established testing convention — see e.g.
0ec0f63 143
`generic_method_on_generic_class_runs_correctly` in `codegen_tests.rs`) rather
0ec0f63 144
than depending on cross-file loading of the real `libs/std` files (a separate
0ec0f63 145
concern from this spec). Specifically:
0ec0f63 146
- `add`: append 3 values, confirm `length()` is 3 and traversal order matches
0ec0f63 147
  insertion order (via `get(0)`/`get(1)`/`get(2)`).
0ec0f63 148
- `set`: set an in-range index, confirm the returned `Option` holds the old
0ec0f63 149
  value and `get` reflects the new one; set an out-of-range index, confirm
0ec0f63 150
  `None`.
0ec0f63 151
- `removeAt`/`remove`: remove the head, the tail, and a middle element in
0ec0f63 152
  separate cases; confirm `length()` decrements and neighboring links are
0ec0f63 153
  correct (traverse the remaining list end-to-end).
0ec0f63 154
- `clear`: add elements, clear, confirm `length()` is 0 and `get(0)` is
0ec0f63 155
  `None`.
0ec0f63 156
- `reverse`: build a list, reverse it, confirm element order via `get`.
0ec0f63 157
- `join`: build a list of `Int`s, confirm the joined `Str` matches the
0ec0f63 158
  expected (including the trailing-separator quirk).
0ec0f63 159
0ec0f63 160
## README
0ec0f63 161
0ec0f63 162
Once this lands, remove the `List`-methods-still-`todo` clause from README's
0ec0f63 163
"Known gaps" bullet entirely (all six originally-listed methods, plus `join`,
0ec0f63 164
are done) — the much longer tail of still-`todo` extras (`sort`, `find`,
0ec0f63 165
`flatMap`, etc.) was never part of that bullet's scope and stays unmentioned,
0ec0f63 166
consistent with how the bullet was already scoped before this cycle.