plum

#treesitter#compiler#wasm

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

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


378ab1a — Peter John 2026-10-26T20:31:02+05:30
refactor(std): drop closure iteration methods from List and Map
README.md CHANGED
@@ -246,7 +246,7 @@ A parameter typed `src: *T` is a sequence: the caller passes a list or another `
246
246
 
247
247
  #### Chains without closures
248
248
 
249
- A closure chain such as `units.filter(|u| u.team == me.team).map(|u| u.hp).sum()` is written as a single `for`:
249
+ `List` and `Map` have no closure-taking iteration methods (no `each`/`map`/`filter`/`reduce`/`find`/`any`/`partition`): transformation is written with `for` forms from `std/Iter`. A chain such as "keep my team's units, take their hp, sum it" is a single `for`:
250
250
 
251
251
  - per-element stages are header clauses: filter is `, if cond`, map is `, name := expr`;
252
252
  - the terminal step is the outer form plus its body (`fold`, `map`, `find`, `count`, `any`, `all`);
@@ -271,7 +271,7 @@ lines := for i, u := map(indexed(take(5, units))), if u.hp > 0.0
271
271
  "{i}: {u.name}"
272
272
  ```
273
273
 
274
- The closure methods (`units.map(|u| ...)`) stay for passing behavior around as values. See [`plum-examples/chains.plum`](plum-examples/chains.plum), [`plum-examples/forms.plum`](plum-examples/forms.plum) and [`plum-std/Iter.plum`](plum-std/Iter.plum).
274
+ Closures remain in the language for passing behavior around as values (`Num` below, `Try.map`). Walk a `Map` with `for k, v := entries(m)` (`keys(m)`, `values(m)` for one side; all from `std/Iter`). See [`plum-examples/chains.plum`](plum-examples/chains.plum), [`plum-examples/forms.plum`](plum-examples/forms.plum) and [`plum-std/Iter.plum`](plum-std/Iter.plum).
275
275
 
276
276
  ## Testing
277
277
 
@@ -355,7 +355,10 @@ enum Num[T] =
355
355
  | Num(add: fn(T, T) -> T, zero: T)
356
356
 
357
357
  fun sumWith(xs: List[T], num: Num[T]) -> T =
358
+ acc := num.zero
359
+ for x := xs
358
- xs.reduce(num.zero, |acc, x| num.add(acc, x))
360
+ acc = num.add(acc, x)
361
+ acc
359
362
 
360
363
  intNum := Num(add: |a, b| a + b, zero: 0)
361
364
  floatNum := Num(add: |a, b| a + b, zero: 0.0)
@@ -597,6 +600,6 @@ It's sugar for `obj.andThen(|v| v.field)` (the plain field value is auto-wrapped
597
600
  - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
598
601
  - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic generic array that `List` and `Map` are written on
599
602
  - **`Map[K, V]`** (`import std/Map`) — a real bucketed hash table
600
- - **`List[T]`** (`import std/List`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`each`/`map`/`reduce`/`sort`/`join`/`chunk`/`partition`
603
+ - **`List[T]`** (`import std/List`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`)
601
604
 
602
605
  See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.
plum-examples/chains.plum CHANGED
@@ -6,7 +6,7 @@ import std/Iter
6
6
 
7
7
  # Closure-free chains. A method chain such as
8
8
  #
9
- # units.filter(|u| u.team == me.team).map(|u| u.hp).sum()
9
+ # keep my team's units, take their hp, sum it
10
10
  #
11
11
  # becomes ONE `for` over the sequence:
12
12
  #
plum-examples/generics.plum CHANGED
@@ -75,7 +75,10 @@ enum Num[T] =
75
75
  | Num(add: fn(T, T) -> T, zero: T)
76
76
 
77
77
  fun sumWith(xs: List[T], num: Num[T]) -> T =
78
+ acc := num.zero
79
+ for x := xs
78
- xs.reduce(num.zero, |acc, x| num.add(acc, x))
80
+ acc = num.add(acc, x)
81
+ acc
79
82
 
80
83
  test "Int extremes survive a generic fn"
81
84
  big := 9223372036854775807
@@ -170,14 +173,16 @@ test "List[List[Map[Str, Try[Int, [A]]]]] builds and reads back"
170
173
  assert {readDeep(outer, 1, 0, "big") ?? 0} == 9223372036854775807
171
174
  assert {readDeep(outer, 2, 0, "big") ?? 97} == 97
172
175
 
173
- test "a method's own generic is chosen per call on a generic receiver"
176
+ test "a generic receiver's methods are chosen per element type"
174
177
  ints := List(1, 22, 333)
178
+ strs := List[Str]()
179
+ for n := ints
175
- strs := ints.map(|n| "n{n}")
180
+ strs.push("n{n}")
176
181
  assert strs.join(",") == "n1,n22,n333"
182
+ lens := List[Int]()
183
+ for s := strs
177
- lens := strs.map(|s| s.length())
184
+ lens.push(s.length())
178
185
  assert lens.join(",") == "2,3,4"
179
- back := lens.map(|n| n * 10)
180
- assert back.reduce(0, |a, b| a + b) == 90
181
186
 
182
187
  test "closures nested three deep capture Int and Str locals through generic params"
183
188
  k := 5
@@ -198,8 +203,13 @@ test "a duck-typed generic fn dispatches statically per instantiation"
198
203
  shapes := List[Circle]()
199
204
  shapes.push(Circle(r: 2.0))
200
205
  shapes.push(Circle(r: 3.0))
206
+ names := List[Str]()
207
+ total := 0.0
208
+ for s := shapes
209
+ names.push(s.name())
210
+ total = total + s.area()
201
- assert shapes.map(|s| s.name()).join(",") == "circle,circle"
211
+ assert names.join(",") == "circle,circle"
202
- assert shapes.reduce(0.0, |acc, s| acc + s.area()) == 39.0
212
+ assert total == 39.0
203
213
 
204
214
  test "one generic fn serves Int, Str and a class in the same program"
205
215
  assert id(3) == 3
plum-std/Http.plum CHANGED
@@ -46,12 +46,21 @@ extern fun rawHttpRequest(method: Str, url: Str, headers: Str, body: Str) -> Str
46
46
  # expects for its own `headers` argument.
47
47
  fun encodeHeaders(headers: Map[Str, Str]) -> Str =
48
48
  buf := Buffer()
49
+ i := 0
50
+ while i < headers.buckets.length()
49
- headers.each(|k, v|
51
+ bucket := headers.buckets.get(i)
52
+ j := 0
53
+ while j < bucket.size
54
+ pair := bucket.data.get(j)
55
+ k := pair.key
56
+ v := pair.val
50
- if buf.length() > 0
57
+ if buf.length() > 0
51
- buf.write("\n")
58
+ buf.write("\n")
52
- buf.write(k)
59
+ buf.write(k)
53
- buf.write(": ")
60
+ buf.write(": ")
54
- buf.write(v))
61
+ buf.write(v)
62
+ j = j + 1
63
+ i = i + 1
55
64
  return buf.toStr()
56
65
 
57
66
  # The reverse of `encodeHeaders`, for the response headers `rawHttpRequest`
@@ -60,12 +69,12 @@ fun decodeHeaders(raw: Str) -> Map[Str, Str] =
60
69
  result := emptyHeaders()
61
70
  if raw == ""
62
71
  return result
63
- raw.split("\n", 0).each(|line|
72
+ for line := raw.split("\n", 0)
64
73
  idx := line.indexOf(": ")
65
74
  if idx >= 0
66
75
  key := line.sub(0, idx)
67
76
  val := line.sub(idx + 2, line.length())
68
- result.set(key, val))
77
+ result.set(key, val)
69
78
  return result
70
79
 
71
80
  # Performs a single HTTP request and waits for the full response. Fails
plum-std/Iter.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
 
3
3
  import std/Number
4
4
  import std/List
5
+ import std/Map
5
6
  import std/Try
6
7
 
7
8
  # Iterators: `for u := filter(units)` expands the `fun*`'s body at the call
@@ -161,3 +162,27 @@ fun* chunks(size: Int, xs: List[T]) *List[T] =
161
162
  piece := xs.sublist(start, stop)
162
163
  start = stop
163
164
  yield piece
165
+
166
+ # ---- Map: bucket order, then insertion order within a bucket ---------------
167
+
168
+ # `for k, v := entries(m)`: every key with its value.
169
+ fun* entries(m: Map[K, V]) *(K, V) =
170
+ i := 0
171
+ while i < m.buckets.length()
172
+ bucket := m.buckets.at(i)
173
+ j := 0
174
+ while j < bucket.size
175
+ pair := bucket.data.get(j)
176
+ j = j + 1
177
+ yield pair.key, pair.val
178
+ i = i + 1
179
+
180
+ # `for k := keys(m)`.
181
+ fun* keys(m: Map[K, V]) *K =
182
+ for k, v := entries(m)
183
+ yield k
184
+
185
+ # `for v := values(m)`.
186
+ fun* values(m: Map[K, V]) *V =
187
+ for k, v := entries(m)
188
+ yield v
plum-std/Json.plum CHANGED
@@ -368,7 +368,15 @@ fun jsonStrValue(j: Json) -> Str =
368
368
  # reference (as `appendJsonPair` does to `buf`) does.
369
369
  fun jsonMapToStr(pairs: Map[Str, Json]) -> Str =
370
370
  buf := Buffer()
371
+ i := 0
372
+ while i < pairs.buckets.length()
373
+ bucket := pairs.buckets.get(i)
374
+ j := 0
375
+ while j < bucket.size
376
+ pair := bucket.data.get(j)
371
- pairs.each(|k, v| appendJsonPair(buf, k, v))
377
+ appendJsonPair(buf, pair.key, pair.val)
378
+ j = j + 1
379
+ i = i + 1
372
380
  return buf.toStr()
373
381
 
374
382
  fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit =
plum-std/List.plum CHANGED
@@ -146,76 +146,6 @@ enum List[T] =
146
146
  fun contains(self, v: T) -> Bool =
147
147
  return self.find(v).isOk()
148
148
 
149
- # calls f for each elem in the list
150
- fun each(self, cb: fn(T)) -> Unit =
151
- i := 0
152
- while i < self.size
153
- cb(self.data.get(i))
154
- i = i + 1
155
-
156
- # Returns a new list with each element transformed by `cb`. `U` is a
157
- # generic param of `map` itself, separate from `List[T]`'s own `T`.
158
- fun map(self, cb: fn(T) -> U) -> List[U] =
159
- result := List[U]()
160
- i := 0
161
- while i < self.size
162
- result.push(cb(self.data.get(i)))
163
- i = i + 1
164
- return result
165
-
166
- # returns a new list with each element flat-mapped
167
- fun flatMap(self) =
168
- todo
169
-
170
- # Returns a new list with only the elements `predicate` holds for.
171
- fun retain(self, predicate: fn(T) -> Bool) -> List[T] =
172
- result := List[T]()
173
- i := 0
174
- while i < self.size
175
- value := self.data.get(i)
176
- if predicate(value)
177
- result.push(value)
178
- i = i + 1
179
- return result
180
-
181
- # Returns a new list with the elements `predicate` holds for removed.
182
- fun reject(self, predicate: fn(T) -> Bool) -> List[T] =
183
- result := List[T]()
184
- i := 0
185
- while i < self.size
186
- value := self.data.get(i)
187
- if !predicate(value)
188
- result.push(value)
189
- i = i + 1
190
- return result
191
-
192
- fun any(self, predicate: fn(T) -> Bool) -> Bool =
193
- i := 0
194
- while i < self.size
195
- if predicate(self.data.get(i))
196
- return True
197
- i = i + 1
198
- return False
199
-
200
- fun every(self, predicate: fn(T) -> Bool) -> Bool =
201
- i := 0
202
- while i < self.size
203
- if !predicate(self.data.get(i))
204
- return False
205
- i = i + 1
206
- return True
207
-
208
- # Folds `self`'s elements into a single value, starting from `acc` and
209
- # combining each element in with `cb(accumulatedSoFar, element)`. `U` is
210
- # `reduce`'s own generic param (the accumulator's type).
211
- fun reduce(self, acc: U, cb: fn(U, T) -> U) -> U =
212
- result := acc
213
- i := 0
214
- while i < self.size
215
- result = cb(result, self.data.get(i))
216
- i = i + 1
217
- return result
218
-
219
149
  # returns the first element in the list
220
150
  fun first(self) -> T ! [ListWasEmpty] =
221
151
  if self.size == 0
@@ -287,15 +217,6 @@ enum List[T] =
287
217
  i = i + size
288
218
  return result
289
219
 
290
- # Splits into two lists: elements `predicate` holds for, then the rest —
291
- # in that order. Returns them as a 2-element `List[List[T]]` (`[matched,
292
- # unmatched]`) since this language has no tuple type.
293
- fun partition(self, predicate: fn(T) -> Bool) -> List[List[T]] =
294
- result := List[List[T]]()
295
- result.push(self.retain(predicate))
296
- result.push(self.reject(predicate))
297
- return result
298
-
299
220
  # Grouping by an arbitrary key type would need `Map[K, List[T]]`, but
300
221
  # `Map.plum` depends on this file. Not implemented.
301
222
  fun groupBy(self) =
@@ -382,12 +303,6 @@ test "List[Int]() with no args runs init() and produces a working empty list"
382
303
  test "List(values) with positional args runs init(values) directly, infers T from args"
383
304
  assert exerciseListInitWithValues() == 43
384
305
 
385
- test "list map and reduce work correctly"
386
- l = List(1, 2, 3, 4)
387
- doubled = l.map(|v| v * 2)
388
- total = doubled.reduce(0, |acc, v| acc + v)
389
- assert total == 20
390
-
391
306
  test "list equals compares elementwise"
392
307
  a = List(1, 2, 3)
393
308
  b = List(1, 2, 3)
@@ -420,13 +335,6 @@ test "chunk splits into sublists of at most size elements each"
420
335
  assert chunks.get(2).unwrap().join(",") == "5"
421
336
  assert l.chunk(0).length() == 0
422
337
 
423
- test "partition splits into elements matching a predicate, then the rest"
424
- l := List(1, 2, 3, 4, 5, 6)
425
- parts := l.partition(|v| v % 2 == 0)
426
- assert parts.length() == 2
427
- assert parts.get(0).unwrap().join(",") == "2,4,6"
428
- assert parts.get(1).unwrap().join(",") == "1,3,5"
429
-
430
338
  test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is Err"
431
339
  l := List[Int]()
432
340
  i := 0
plum-std/Map.plum CHANGED
@@ -109,44 +109,6 @@ enum Map[K, V] =
109
109
  i = i + 1
110
110
  self.size = 0
111
111
 
112
- # Calls `cb` with each key and value in the map. Order is bucket order,
113
- # then insertion order within a bucket — NOT overall insertion order (that
114
- # would need a separate side list to track, which nothing here needs today).
115
- fun each(self, cb: fn(K, V)) -> Unit =
116
- i := 0
117
- while i < self.buckets.length()
118
- bucket := self.buckets.get(i)
119
- j := 0
120
- while j < bucket.size
121
- pair := bucket.data.get(j)
122
- cb(pair.key, pair.val)
123
- j = j + 1
124
- i = i + 1
125
-
126
- fun keys(self) -> List[K] =
127
- result := List[K]()
128
- self.each(|k, v| result.add(k))
129
- return result
130
-
131
- fun values(self) -> List[V] =
132
- result := List[V]()
133
- self.each(|k, v| result.add(v))
134
- return result
135
-
136
- fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
137
- result := Map[X, Y]()
138
- i := 0
139
- while i < self.buckets.length()
140
- bucket := self.buckets.get(i)
141
- j := 0
142
- while j < bucket.size
143
- pair := bucket.data.get(j)
144
- mapped := cb(pair.key, pair.val)
145
- result.set(mapped.key, mapped.val)
146
- j = j + 1
147
- i = i + 1
148
- return result
149
-
150
112
  test "get/set/has round-trip and set replaces an existing key in place"
151
113
  m := Map[Str, Int]()
152
114
  assert m.has("a") == False
@@ -174,37 +136,15 @@ test "remove deletes an entry and clear empties the map"
174
136
  m.clear()
175
137
  assert m.isEmpty() == True
176
138
 
177
- test "keys/values list every entry's key/value"
139
+ test "removing then re-adding keys keeps lookups correct"
178
140
  m := Map[Str, Int]()
179
141
  m.set("a", 1)
180
142
  m.set("b", 2)
143
+ m.remove("a")
144
+ m.set("a", 3)
145
+ assert m.get("a").unwrap() == 3
146
+ assert m.get("b").unwrap() == 2
181
147
  assert m.length() == 2
182
- assert m.keys().contains("a")
183
- assert m.keys().contains("b")
184
- assert m.values().contains(1)
185
- assert m.values().contains(2)
186
-
187
- test "each calls the callback with every key and value"
188
- m := Map[Str, Int]()
189
- m.set("a", 1)
190
- m.set("b", 2)
191
- # A closure captures its outer variables BY VALUE at creation time, so
192
- # reassigning a captured `Int` inside the callback wouldn't be visible out
193
- # here — mutate a captured `List` (a class, reference semantics) instead.
194
- seen := List[Int]()
195
- m.each(|k, v| seen.add(v))
196
- assert seen.length() == 2
197
- assert seen.contains(1)
198
- assert seen.contains(2)
199
-
200
- test "map transforms every key/value pair into a new map, changing the value type"
201
- m := Map[Str, Int]()
202
- m.set("a", 1)
203
- m.set("b", 2)
204
- stringified := m.map(|k, v| Pair(key: k, val: v.toStr()))
205
- assert stringified.get("a").unwrap() == "1"
206
- assert stringified.get("b").unwrap() == "2"
207
- assert stringified.length() == 2
208
148
 
209
149
  test "many keys landing in the same bucket still resolve correctly (collision chaining)"
210
150
  m := Map[Int, Int]()
plum-std/Os.plum CHANGED
@@ -175,11 +175,15 @@ fun opendir(path: Str) =
175
175
  fun readdir(path: Str) -> Try[List[Str], Str] =
176
176
  if !exists(path)
177
177
  return Err("no such directory: '{path}'")
178
- # `reject`ing empty names handles both a genuinely empty directory (whose
178
+ # skipping empty names handles both a genuinely empty directory (whose
179
179
  # joined `""` would otherwise split into one bogus `""` entry) and
180
180
  # `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
181
181
  # check above) the same way — no directory entry is ever named `""`.
182
+ names := List[Str]()
182
- return Ok(rawReadDir(path).split("\n", 0).reject(|n| n == ""))
183
+ for n := rawReadDir(path).split("\n", 0)
184
+ if n != ""
185
+ names.push(n)
186
+ return Ok(names)
183
187
 
184
188
  fun readlink(path: Str) -> Try[Str, Str] =
185
189
  if !exists(path)
plum-std/Set.plum CHANGED
@@ -33,7 +33,16 @@ enum Set[T] =
33
33
 
34
34
  # Every member, in no particular order.
35
35
  fun toList(self) -> List[T] =
36
+ result := List[T]()
37
+ i := 0
38
+ while i < self.map.buckets.length()
36
- return self.map.keys()
39
+ bucket := self.map.buckets.get(i)
40
+ j := 0
41
+ while j < bucket.size
42
+ result.push(bucket.data.get(j).key)
43
+ j = j + 1
44
+ i = i + 1
45
+ return result
37
46
 
38
47
  test "set_dedup: adding duplicates keeps one copy"
39
48
  s := Set[Int]()
plum-std/Str.plum CHANGED
@@ -589,8 +589,8 @@ test "string interpolation supports a boolean expression, not just a bare variab
589
589
 
590
590
  test "string interpolation supports a method call taking a closure literal"
591
591
  l := List(1, 2, 3, 4)
592
- assert "{l.any(|x| x % 2 == 0)}" == "True"
592
+ assert "{l.find(3).unwrap()}" == "3"
593
- assert "{l.any(|x| x > 100)}" == "False"
593
+ assert "{l.contains(100)}" == "False"
594
594
 
595
595
  test "string interpolation supports a ternary expression"
596
596
  n := 5
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1613,7 +1613,10 @@ enum Shape =
1613
1613
  Square(s) => s * s
1614
1614
 
1615
1615
  fun total(xs: List[Shape]) -> Float =
1616
+ sum := 0.0
1617
+ for x := xs
1616
- xs.reduce(0.0, |acc, x| acc + x.area())
1618
+ sum = sum + x.area()
1619
+ sum
1617
1620
 
1618
1621
  fun main() -> Float =
1619
1622
  xs := List[Shape]()
@@ -1659,10 +1662,11 @@ import std/Try
1659
1662
  import std/List
1660
1663
 
1661
1664
  fun main() -> Int =
1662
- xs := List(1, 2, 3).map(|n| n * 2)
1665
+ xs := mapList(List(1, 2, 3), |n| n * 2)
1663
- xs.reduce(0, |acc, x| acc + x)
1666
+ reduceList(xs, 0, |acc, x| acc + x)
1664
1667
  ";
1668
+ let src = format!("{src}\n{LIST_FNS}");
1665
- assert_eq!(runMerged::<i64>("thunk", src), 12);
1669
+ assert_eq!(runMerged::<i64>("thunk", &src), 12);
1666
1670
  }
1667
1671
 
1668
1672
  #[test]
@@ -1707,6 +1711,28 @@ fn compiledKeys(name: &str, src: &str) -> (Vec<u8>, Vec<String>) {
1707
1711
  r
1708
1712
  }
1709
1713
 
1714
+ const LIST_FNS: &str = "\
1715
+ fun mapList(xs: List[T], cb: fn(T) -> U) -> List[U] =
1716
+ result := List[U]()
1717
+ for x := xs
1718
+ result.push(cb(x))
1719
+ result
1720
+
1721
+ fun reduceList(xs: List[T], acc: U, cb: fn(U, T) -> U) -> U =
1722
+ result := acc
1723
+ for x := xs
1724
+ result = cb(result, x)
1725
+ result
1726
+
1727
+ fun retainList(xs: List[T], keep: fn(T) -> Bool) -> List[T] =
1728
+ result := List[T]()
1729
+ for x := xs
1730
+ if keep(x)
1731
+ result.push(x)
1732
+ result
1733
+
1734
+ ";
1735
+
1710
1736
  const STD_HEAD: &str = "\
1711
1737
  import std/Str
1712
1738
  import std/Number
@@ -1716,27 +1742,27 @@ import std/List
1716
1742
 
1717
1743
  #[test]
1718
1744
  fn one_copy_per_distinct_type() {
1719
- let src = format!("{}\
1745
+ let src = format!("{}{}\
1720
1746
  fun main() -> Int =
1721
- a := List(1, 2).map(|n| n + 1)
1747
+ a := mapList(List(1, 2), |n| n + 1)
1722
- b := List(\"x\", \"y\").map(|s| s + \"!\")
1748
+ b := mapList(List(\"x\", \"y\"), |s| s + \"!\")
1723
1749
  a.length() + b.length()
1724
- ", STD_HEAD);
1750
+ ", STD_HEAD, LIST_FNS);
1725
1751
  let (_, keys) = compiledKeys("copy2", &src);
1726
- let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("List[") && k.contains("].map")).collect();
1752
+ let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("mapList")).collect();
1727
1753
  assert_eq!(maps.len(), 2, "got {:?}", maps);
1728
- assert!(keys.iter().any(|k| k.starts_with("List[Int].map")), "{:?}", maps);
1754
+ assert!(keys.iter().any(|k| k.starts_with("mapList") && k.contains("Int")), "{:?}", maps);
1729
- assert!(keys.iter().any(|k| k.starts_with("List[Str].map")), "{:?}", maps);
1755
+ assert!(keys.iter().any(|k| k.starts_with("mapList") && k.contains("Str")), "{:?}", maps);
1730
1756
  assert!(!keys.iter().any(|k| k.contains('$')), "mangled names remain");
1731
1757
 
1732
- let src = format!("{}\
1758
+ let src = format!("{}{}\
1733
1759
  fun main() -> Int =
1734
- a := List(1, 2).map(|n| n + 1)
1760
+ a := mapList(List(1, 2), |n| n + 1)
1735
- b := List(3).map(|n| n * 2)
1761
+ b := mapList(List(3), |n| n * 2)
1736
1762
  a.length() + b.length()
1737
- ", STD_HEAD);
1763
+ ", STD_HEAD, LIST_FNS);
1738
1764
  let (_, keys) = compiledKeys("copy1", &src);
1739
- let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("List[") && k.contains("].map")).collect();
1765
+ let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("mapList")).collect();
1740
1766
  assert_eq!(maps.len(), 1, "got {:?}", maps);
1741
1767
  }
1742
1768
 
@@ -1795,16 +1821,16 @@ fn namedBodies(bytes: &[u8]) -> Vec<(String, Vec<String>)> {
1795
1821
 
1796
1822
  #[test]
1797
1823
  fn no_boxes_in_list_int_map() {
1798
- let src = format!("{}\
1824
+ let src = format!("{}{}\
1799
1825
  fun main() -> Int =
1800
- List(1, 2).map(|n| n + 1).length()
1826
+ mapList(List(1, 2), |n| n + 1).length()
1801
- ", STD_HEAD);
1827
+ ", STD_HEAD, LIST_FNS);
1802
1828
  let (bytes, _) = compiledKeys("nobox", &src);
1803
1829
  let bodies = namedBodies(&bytes);
1804
1830
  let (name, ops) = bodies
1805
1831
  .iter()
1806
- .find(|(n, _)| n.starts_with("List[Int].map"))
1832
+ .find(|(n, _)| n.starts_with("mapList"))
1807
- .expect("List[Int].map compiled");
1833
+ .expect("mapList compiled");
1808
1834
  assert!(!ops.is_empty(), "{name}");
1809
1835
  let box_idx: Vec<u32> = {
1810
1836
  // function indices of the box helpers
@@ -2125,24 +2151,24 @@ fun main() -> Int =
2125
2151
 
2126
2152
  #[test]
2127
2153
  fn closure_calls_fn_param() {
2128
- let src = format!("{}\
2154
+ let src = format!("{}{}\
2129
2155
  fun apply(xs: List[Int], cb: fn(Int) -> Int) -> List[Int] =
2130
- xs.map(|x| cb(x))
2156
+ mapList(xs, |x| cb(x))
2131
2157
 
2132
2158
  fun main() -> Int =
2133
- apply(List(1, 2, 3), |n| n * 10).reduce(0, |a, b| a + b)
2159
+ reduceList(apply(List(1, 2, 3), |n| n * 10), 0, |a, b| a + b)
2134
- ", STD_HEAD);
2160
+ ", STD_HEAD, LIST_FNS);
2135
2161
  assert_eq!(runMerged::<i64>("closurefnparam", &src), 60);
2136
2162
  }
2137
2163
 
2138
2164
  #[test]
2139
2165
  fn record_of_fns_passed_explicitly() {
2140
- let src = format!("{}\
2166
+ let src = format!("{}{}\
2141
2167
  enum Num[T] =
2142
2168
  | Num(add: fn(T, T) -> T, zero: T)
2143
2169
 
2144
2170
  fun sumWith(xs: List[T], num: Num[T]) -> T =
2145
- xs.reduce(num.zero, |a, b| num.add(a, b))
2171
+ reduceList(xs, num.zero, |a, b| num.add(a, b))
2146
2172
 
2147
2173
  fun main() -> Int =
2148
2174
  intNum := Num(add: |a, b| a + b, zero: 0)
@@ -2150,7 +2176,7 @@ fun main() -> Int =
2150
2176
  if sumWith(List(1, 2, 3), intNum) == 6 && sumWith(List(1.5, 2.5), floatNum) == 4.0
2151
2177
  return 1
2152
2178
  return 0
2153
- ", STD_HEAD);
2179
+ ", STD_HEAD, LIST_FNS);
2154
2180
  assert_eq!(runMerged::<i64>("recfns", &src), 1);
2155
2181
  let (_, keys) = compiledKeys("recfnskeys", &src);
2156
2182
  let inst: Vec<&String> = keys.iter().filter(|k| k.starts_with("sumWith")).collect();
@@ -2405,16 +2431,16 @@ fun main() -> Int =
2405
2431
 
2406
2432
  #[test]
2407
2433
  fn const_list_table() {
2408
- let src = format!("{}\
2434
+ let src = format!("{}{}\
2409
2435
  TABLE = List(10, 20, 30)
2410
2436
 
2411
2437
  fun total() -> Int =
2412
- TABLE.reduce(0, |a, b| a + b)
2438
+ reduceList(TABLE, 0, |a, b| a + b)
2413
2439
 
2414
2440
  fun main() -> Int =
2415
2441
  TABLE.push(4)
2416
2442
  TABLE.get(1).unwrap() + TABLE.length() + total() - 78 + 4
2417
- ", STD_HEAD);
2443
+ ", STD_HEAD, LIST_FNS);
2418
2444
  // TABLE is a single shared object, initialised once: push(4) persists, so
2419
2445
  // total() sees 64 (60 + 4), length 4, get(1) 20 -> 20 + 4 + 64 - 78 + 4 = 14
2420
2446
  assert_eq!(runMerged::<i64>("constlist", &src), 14);
@@ -2487,14 +2513,14 @@ fun main() -> Int =
2487
2513
  let closure = format!("{STD_HEAD}\
2488
2514
  fun main() -> Int =
2489
2515
  units := List(1, 2, 3, 4)
2490
- alive := units.retain(|u| u > 2)
2516
+ alive := retainList(units, |u| u > 2)
2491
2517
  alive.length()
2492
- ");
2518
+ {LIST_FNS}");
2493
2519
  let (cbytes, _) = compiledKeys("closurefilter", &closure);
2494
2520
  let cops = opsOf(&cbytes, "main");
2495
2521
  assert_eq!(countOps(&ops, "StructNew"), 0, "form main allocates nothing itself: {ops:?}");
2496
2522
  assert!(countOps(&cops, "StructNew") >= 1, "closure main builds a closure struct: {cops:?}");
2497
- let retain = opsOf(&cbytes, "List[Int].retain");
2523
+ let retain = opsOf(&cbytes, "retainList[Int]");
2498
2524
  assert!(countOps(&retain, "CallRef") + countOps(&retain, "CallIndirect") >= 1, "{retain:?}");
2499
2525
  }
2500
2526