plum
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 +7 -4
- plum-examples/chains.plum +1 -1
- plum-examples/generics.plum +18 -8
- plum-std/Http.plum +17 -8
- plum-std/Iter.plum +25 -0
- plum-std/Json.plum +9 -1
- plum-std/List.plum +0 -92
- plum-std/Map.plum +5 -65
- plum-std/Os.plum +6 -2
- plum-std/Set.plum +10 -1
- plum-std/Str.plum +2 -2
- plum-wasm-codegen/tests/codegen_tests.rs +60 -34
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`/`
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
180
|
+
strs.push("n{n}")
|
|
176
181
|
assert strs.join(",") == "n1,n22,n333"
|
|
182
|
+
lens := List[Int]()
|
|
183
|
+
for s := strs
|
|
177
|
-
|
|
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
|
|
211
|
+
assert names.join(",") == "circle,circle"
|
|
202
|
-
assert
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
if buf.length() > 0
|
|
51
|
-
|
|
58
|
+
buf.write("\n")
|
|
52
|
-
|
|
59
|
+
buf.write(k)
|
|
53
|
-
|
|
60
|
+
buf.write(": ")
|
|
54
|
-
|
|
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)
|
|
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
|
-
|
|
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 "
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
592
|
+
assert "{l.find(3).unwrap()}" == "3"
|
|
593
|
-
assert "{l.
|
|
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
|
-
|
|
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)
|
|
1665
|
+
xs := mapList(List(1, 2, 3), |n| n * 2)
|
|
1663
|
-
|
|
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)
|
|
1747
|
+
a := mapList(List(1, 2), |n| n + 1)
|
|
1722
|
-
b := List(\"x\", \"y\")
|
|
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("
|
|
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("
|
|
1754
|
+
assert!(keys.iter().any(|k| k.starts_with("mapList") && k.contains("Int")), "{:?}", maps);
|
|
1729
|
-
assert!(keys.iter().any(|k| k.starts_with("
|
|
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)
|
|
1760
|
+
a := mapList(List(1, 2), |n| n + 1)
|
|
1735
|
-
b := List(3)
|
|
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("
|
|
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)
|
|
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("
|
|
1832
|
+
.find(|(n, _)| n.starts_with("mapList"))
|
|
1807
|
-
.expect("
|
|
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
|
-
|
|
2156
|
+
mapList(xs, |x| cb(x))
|
|
2131
2157
|
|
|
2132
2158
|
fun main() -> Int =
|
|
2133
|
-
apply(List(1, 2, 3), |n| n * 10)
|
|
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
|
-
|
|
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
|
-
|
|
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 :=
|
|
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, "
|
|
2523
|
+
let retain = opsOf(&cbytes, "retainList[Int]");
|
|
2498
2524
|
assert!(countOps(&retain, "CallRef") + countOps(&retain, "CallIndirect") >= 1, "{retain:?}");
|
|
2499
2525
|
}
|
|
2500
2526
|
|