plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/List.plum
module std
import std/Option
import std/Buffer
import std/Number
import std/Bool
import std/Str
# A node stores the data in a list and contains pointers to the previous and next sibling nodes
enum Node[T] =
| Node(value: T, prev: Option[Node[T]], next: Option[Node[T]])
# A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
# It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
enum List[T: ToStr](ToStr) =
| List(head: Option[Node[T]], tail: Option[Node[T]], size: Int)
# `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
# generics required either way, since nothing else in a call this shape
# pins down `T`) constructs a list via this variadic `init` — see
# `plum-checker`/`plum-wasm-codegen`'s `ClassCall`/`FnCall` handling, which
# desugars both call shapes to `List.init(...)`.
fun init(values: ...T) -> List[T] =
l := List(head: None, tail: None, size: 0)
for v := range values
l.add(v)
return l
# gets the element at i'th index of the list
fun get(self, i: Int) -> Option[T] =
current := self.head
index := 0
while current != None
match current
Some(Node(value, _, next)) =>
if index == i
return Some(value)
current = next
index = index + 1
None =>
break
None
# sets the element at i'th index of the list, returning the old value (or None if i is out of bounds)
fun set(self, i: Int, v: T) -> Option[T] =
current := self.head
index := 0
while current != None
match current
Some(node) =>
if index == i
old := node.value
node.value = v
return Some(old)
current = node.next
index = index + 1
None =>
break
None
# returns the no of elements in the list
fun length(self) -> Int =
self.size
# Elementwise comparison via `==` — see the caveat on `expectEq` in
# `libs/std/testing.plum` for what that means when `T` is a class/enum
# without its own `equals` (reference identity, not structural equality).
fun equals(self, other: List[T]) -> Bool =
if self.length() != other.length()
return False
for i := range self.length()
if self.get(i).unwrap() != other.get(i).unwrap()
return False
return True
# adds the specified elements to the end of the list
fun add(self, values: ...T) =
for v := range values
node := Node(value: v, prev: self.tail, next: None)
match self.tail
Some(t) =>
t.next = Some(node)
None =>
self.head = Some(node)
self.tail = Some(node)
self.size = self.size + 1
# unlinks node from the list, patching its neighbors (or head/tail) to close the gap
fun unlink(self, node: Node[T]) =
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
# removes the element at i'th index of the list
fun removeAt(self, i: Int) =
current := self.head
index := 0
while current != None
match current
Some(node) =>
if index == i
self.unlink(node)
return
current = node.next
index = index + 1
None =>
break
# removes the first element equal to v from list
fun remove(self, v: T) =
current := self.head
while current != None
match current
Some(node) =>
if node.value == v
self.unlink(node)
return
current = node.next
None =>
break
# removes all objects from this list
fun clear(self) =
self.head = None
self.tail = None
self.size = 0
# returns the list with the elements relinked in reverse order.
fun reverse(self) -> List =
current := self.head
while current != None
match current
Some(node) =>
next := node.next
node.next = node.prev
node.prev = next
current = next
None =>
break
old_head := self.head
self.head = self.tail
self.tail = old_head
self
# Inserts `value` immediately before `node` (which must belong to `self`),
# patching neighbors (or `head`) to make room — the insertion-point half of
# `sort`'s pointer surgery, split out since `unlink`'s counterpart already
# gets its own method.
fun insertBefore(self, node: Node[T], value: T) -> Unit =
new_node := Node(value: value, prev: node.prev, next: Some(node))
match node.prev
Some(p) =>
p.next = Some(new_node)
None =>
self.head = Some(new_node)
node.prev = Some(new_node)
self.size = self.size + 1
# Inserts `value` into `self` (assumed already sorted by `before`) at the
# position that keeps it sorted — the first position whose existing element
# `before(value, existing)` holds for, or the end if none does.
fun insertSorted(self, value: T, before: fn(T, T) -> Bool) -> Unit =
current := self.head
while current != None
match current
Some(node) =>
if before(value, node.value)
self.insertBefore(node, value)
return
current = node.next
None =>
break
self.add(value)
# Returns a NEW list with `self`'s elements sorted, using `before(a, b)` as
# the "does a belong before b" comparator. Insertion sort (O(n^2)) — simple
# and adequate for the list sizes a linked list is already suited to.
fun sort(self, before: fn(T, T) -> Bool) -> List[T] =
result := List(head: None, tail: None, size: 0)
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
result.insertSorted(value, before)
current = next
None =>
break
return result
# Returns the first element equal to `search`, or `None`.
fun find(self, search: T) -> Option[T] =
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
if value == search
return Some(value)
current = next
None =>
break
return None
fun contains(self, v: T) -> Bool =
return self.find(v) != None
# calls f for each elem in the list
fun each(self, cb: fn(T)) -> Unit =
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
cb(value)
current = next
None =>
break
# Returns a new list with each element transformed by `cb`.
#
# `U` here is a generic param of `map` ITSELF, separate from `List[T]`'s own
# `T` — resolved per CALL SITE (from `cb`'s own inferred return type), not
# when `List[T]` itself is specialized, via `resolveMethodOwnGenerics` in
# `plum-checker/src/monomorphize.rs`.
fun map(self, cb: fn(T) -> U) -> List[U] =
result := List(head: None, tail: None, size: 0)
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
result.add(cb(value))
current = next
None =>
break
return result
# returns a new list with each element flat-mapped
fun flatMap(self) =
todo
# Returns a new list with only the elements `predicate` holds for.
fun retain(self, predicate: fn(T) -> Bool) -> List[T] =
result := List(head: None, tail: None, size: 0)
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
if predicate(value)
result.add(value)
current = next
None =>
break
return result
# Returns a new list with the elements `predicate` holds for removed.
fun reject(self, predicate: fn(T) -> Bool) -> List[T] =
result := List(head: None, tail: None, size: 0)
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
if !predicate(value)
result.add(value)
current = next
None =>
break
return result
fun any(self, predicate: fn(T) -> Bool) -> Bool =
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
if predicate(value)
return True
current = next
None =>
break
return False
fun every(self, predicate: fn(T) -> Bool) -> Bool =
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
if !predicate(value)
return False
current = next
None =>
break
return True
# Folds `self`'s elements into a single value, starting from `acc` and
# combining each element in with `cb(accumulatedSoFar, element)`.
#
# `U` is `reduce`'s OWN generic param (the accumulator's type), separate
# from `List[T]`'s own `T` — same per-CALL-SITE resolution as `map`'s `U`.
fun reduce(self, acc: U, cb: fn(U, T) -> U) -> U =
result := acc
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
result = cb(result, value)
current = next
None =>
break
return result
# returns the first element in the list
fun first(self) -> Option[T] =
match self.head
Some(Node(value, _, _)) => Some(value)
None => None
# returns the last element in the list
fun last(self) -> Option[T] =
match self.tail
Some(Node(value, _, _)) => Some(value)
None => None
# Returns a new list of the elements at index `[start, end)`, clamped into
# range (a `start`/`end` outside `[0, length]` is clamped, not an error).
fun sublist(self, start: Int, end: Int) -> List[T] =
result := List(head: None, tail: None, size: 0)
s := start < 0 ? 0 : start
e := end > self.size ? self.size : end
current := self.head
i := 0
while current != None && i < e
match current
Some(Node(value, _, next)) =>
if i >= s
result.add(value)
current = next
i = i + 1
None =>
break
return result
# The first `n` elements.
fun take(self, n: Int) -> List[T] =
return self.sublist(0, n)
# Every element AFTER the first `n`.
fun skip(self, n: Int) -> List[T] =
return self.sublist(n, self.size)
fun drop(self, n: Int) -> List[T] =
return self.skip(n)
# Returns one uniformly random element, or `None` if `self` is empty.
fun sample(self) -> Option[T] =
if self.size == 0
return None
return self.get(randomInt(self.size))
# Returns a new list with `self`'s elements in random order (Fisher-Yates).
fun shuffle(self) -> List[T] =
result := self.sublist(0, self.size)
i := result.size - 1
while i > 0
j := randomInt(i + 1)
vi := result.get(i).unwrap()
vj := result.get(j).unwrap()
result.set(i, vj)
result.set(j, vi)
i = i - 1
return result
# `chunk`/`partition` both return `List[List[T]]` — the SAME generic class
# (`List`) nested inside itself as a method's own return type. This used to
# be an unconditional stack-overflow landmine for the WHOLE COMPILER (see
# `plum-checker/src/monomorphize.rs`'s `isSelfNested`/`methodMentionsTemplate`
# doc comments for the full fix) — that danger is now FIXED (2026-09-02).
#
# A second, more contained bug surfaced once past that: two SEPARATE bare
# `List(head: None, tail: None, size: 0)` constructions in the SAME method
# body — one for the OUTER `result: List[List[T]]` (matching this
# method's own declared return type) and one for an INNER per-chunk
# `piece: List[T]` — are ambiguous to `resolveClassInstantiation`'s
# `current_return_type` fallback: since `List[T]` and `List[List[T]]` both
# have exactly ONE generic argument, the fallback (which only ever
# compares ARITY, not identity) can't tell "this bare construction IS the
# function's own return value" apart from "this is some OTHER, unrelated
# nested construction of the same template that happens to need the same
# arity" — so `piece` was silently resolved to the SAME type as `result`
# (`List$List$Int` instead of `List$Int`), corrupting its own `add` calls.
# Worked around by building `piece` via `self.sublist(...)` instead of a
# second bare construction — `sublist` is an ordinary method already
# unambiguously resolved through the RECEIVER's own (already-correct)
# binding, no `current_return_type` guessing involved at all.
fun chunk(self, size: Int) -> List[List[T]] =
result := List(head: None, tail: None, size: 0)
if size <= 0
return result
i := 0
n := self.length()
while i < n
result.add(self.sublist(i, i + size))
i = i + size
return result
# Splits into two lists: elements `predicate` holds for, then the rest —
# in that order. Returns them as a 2-element `List[List[T]]` (`[matched,
# unmatched]`) since this language has no tuple type. Deliberately built
# from `self.retain(predicate)`/`self.reject(predicate)` rather than two
# local `matched`/`unmatched := List(head: None, ...)` bare constructions
# — see `chunk`'s doc comment above for exactly why that would be
# ambiguous (`matched`/`unmatched` need `List[T]`, but a bare
# construction here would resolve via `current_return_type` fallback
# against THIS method's own `List[List[T]]` return instead, same arity
# coincidence). `retain`/`reject` are each already unambiguous (single
# bare construction per method body, matching their own single-level
# return type).
fun partition(self, predicate: fn(T) -> Bool) -> List[List[T]] =
result := List(head: None, tail: None, size: 0)
result.add(self.retain(predicate))
result.add(self.reject(predicate))
return result
# Grouping by an arbitrary key type would need its own generic param
# (`groupBy<K>(keyFn: fn(T) -> K) -> Map[K, List[T]]`) resolved from a
# closure's return type — the same method-level-generics gap documented on
# `List.map` above, PLUS `Map[K, List[T]]` would make `list.plum` depend on
# `map.plum`, which already depends on `list.plum`. Not implemented.
fun groupBy(self) =
todo
# Note: NOT written with `each` + a closure-captured "is this the first
# element" flag — a closure's captured variables are snapshotted at
# CREATION time (see `libs/std/list.plum`'s own `add`/`each` usage
# elsewhere), so an assignment to a captured var INSIDE the closure body
# does not persist across that closure's own repeated invocations from one
# `each` call. A plain `while`/`match` traversal (same idiom as `unlink`)
# sidesteps that entirely.
fun join(self, sep: Str = ",") -> Str =
res := Buffer()
current := self.head
while current != None
match current
Some(Node(value, _, next)) =>
res.write(value.toStr())
if next != None
res.write(sep)
current = next
None =>
break
res.toStr()
# `List[T: ToStr](ToStr)` declared implementing `ToStr`
# from the start but never actually defined `toStr` — harmless for a
# PLAIN `List[Int]`/`List[Str]`/etc (nothing needs to call `.toStr()` on
# the LIST ITSELF, just on its elements, which `join` above already
# does), but a genuine gap the moment a `List` is used AS another
# `List`'s own ELEMENT type (`List[List[T]]`) — the OUTER list's eagerly-
# compiled `join`/`toStr` calls `.toStr()` on each element, and an inner
# `List[T]` element had no such method to call, failing to compile with
# "unknown method 'List$T.toStr'" even for a program that never actually
# calls `.toStr()`/`.join()` on the outer list (per the "eager compile
# every method for every specialization" architecture noted elsewhere in
# this codebase's history).
fun toStr(self) -> Str =
return "[" + self.join(",") + "]"
# ---- regression tests ----
fun optSumForListTest(o: Option[Int]) -> Int =
match o
Some(v) => v
None => -1000
fun exerciseList() -> Int =
l = List(1, 2, 3, 4, 5)
a = l.length()
b = optSumForListTest(l.get(0))
c = optSumForListTest(l.get(4))
oldVal = optSumForListTest(l.set(2, 30))
d = optSumForListTest(l.get(2))
l.removeAt(0)
e = l.length()
f = optSumForListTest(l.get(0))
l.remove(30)
g = l.length()
l.reverse()
h = optSumForListTest(l.get(0))
l.clear()
i = l.length()
a + b + c + oldVal + d + e + f + g + h + i
fun removeAllNodesOneAtATime() -> Int =
l = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
i = 0
while i < 10
l.removeAt(0)
i = i + 1
afterLoopLength = l.length()
isEmpty = optSumForListTest(l.get(0))
l.add(42)
afterReAdd = optSumForListTest(l.get(0))
afterLoopLength + isEmpty + afterReAdd
fun exerciseListInit() -> Int =
l := List[Int]()
before := l.length()
l.add(7)
before + l.length() + optSumForListTest(l.get(0))
fun exerciseListInitWithValues() -> Int =
l := List(10, 20, 30)
l.length() + optSumForListTest(l.get(0)) + optSumForListTest(l.get(2))
test "list add set remove at remove clear reverse all work correctly"
# add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
# set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
# removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
# remove(30): g=length=3 -> list [2,4,5]
# reverse(): h=get(0)=5 -> list [5,4,2]
# clear(): i=length=0
# 5+1+5+3+30+4+2+3+5+0 = 58
assert exerciseList() == 58
test "removing every node in a loop leaves an empty correctly functioning list"
# afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
# 0 + -1000 + 42 = -958
assert removeAllNodesOneAtATime() == -958
test "List[Int]() with no args runs init() and produces a working empty list"
assert exerciseListInit() == 8
test "List(values) with positional args runs init(values) directly, infers T from args"
assert exerciseListInitWithValues() == 43
test "list map and reduce work correctly"
l = List(1, 2, 3, 4)
doubled = l.map(|v| v * 2)
total = doubled.reduce(0, |acc, v| acc + v)
assert total == 20
test "list equals compares elementwise"
a = List(1, 2, 3)
b = List(1, 2, 3)
c = List(1, 2)
assert a.equals(b)
assert !a.equals(c)
test "list sort orders elements correctly"
l = List(3, 1, 4, 1, 5)
sorted = l.sort(|a, b| a < b)
assert optSumForListTest(sorted.get(0)) == 1
assert optSumForListTest(sorted.get(4)) == 5
test "list join renders elements correctly with separator"
l = List(1, 2, 3)
assert l.join(",") == "1,2,3"
test "list toStr formats a plain list and a nested list of lists"
l := List(1, 2, 3)
assert l.toStr() == "[1,2,3]"
chunks := l.chunk(2)
assert chunks.toStr() == "[[1,2],[3]]"
test "chunk splits into sublists of at most size elements each"
l := List(1, 2, 3, 4, 5)
chunks := l.chunk(2)
assert chunks.length() == 3
assert chunks.get(0).unwrap().join(",") == "1,2"
assert chunks.get(1).unwrap().join(",") == "3,4"
assert chunks.get(2).unwrap().join(",") == "5"
assert l.chunk(0).length() == 0
test "partition splits into elements matching a predicate, then the rest"
l := List(1, 2, 3, 4, 5, 6)
parts := l.partition(|v| v % 2 == 0)
assert parts.length() == 2
assert parts.get(0).unwrap().join(",") == "2,4,6"
assert parts.get(1).unwrap().join(",") == "1,3,5"