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