plum

#treesitter#compiler#wasm

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

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


0ec0f63Peter 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.