plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
f60e57a
— Peter John
2026-11-02T11:30:15+05:30
feat: packed Array[Byte]; List views via slice(i, j)
- plum-std/Array.plum +14 -1
- plum-std/List.plum +133 -36
- plum-wasm-codegen/src/lib.rs +13 -1
plum-std/Array.plum
CHANGED
|
@@ -2,12 +2,14 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
+
import std/Byte
|
|
5
6
|
|
|
6
7
|
# A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
|
|
7
8
|
# array. `T` is NOT erased: codegen allocates one wasm array type per distinct
|
|
8
9
|
# element representation (see `plum-wasm-codegen`'s native-arrays loop):
|
|
9
10
|
# `Array[Int]` is `array<mut i64>`, `Array[Float]` is `array<mut f64>`,
|
|
11
|
+
# `Array[Bool]` is `array<mut i32>`, `Array[Byte]` is packed `array<mut i8>`
|
|
10
|
-
#
|
|
12
|
+
# (one byte per element, the storage `Str` uses), and a reference `T` is
|
|
11
13
|
# `array<mut ref null T>` (all reference types of the same wasm type share one
|
|
12
14
|
# array type). `get`/`set`/`push`/`resize`/`length`/`filled` on the value
|
|
13
15
|
# arrays are plain `array.get`/`array.set`/`array.copy` on the raw elements —
|
|
@@ -98,3 +100,14 @@ test "Array[Int] holds a million elements unboxed"
|
|
|
98
100
|
assert a.length() == 1000000
|
|
99
101
|
assert a.get(999999) == 1999998
|
|
100
102
|
assert a.get(0) == 0
|
|
103
|
+
|
|
104
|
+
test "Array[Byte] is packed bytes: get, set, push, resize, filled"
|
|
105
|
+
a := Array.filled(3, Byte(7))
|
|
106
|
+
assert a.length() == 3
|
|
107
|
+
a.set(1, Byte(200))
|
|
108
|
+
assert a.get(1).toInt() == 200
|
|
109
|
+
assert a.get(0).toInt() == 7
|
|
110
|
+
b := a.push(Byte(255)).resize(6)
|
|
111
|
+
assert b.length() == 6
|
|
112
|
+
assert b.get(3).toInt() == 255
|
|
113
|
+
assert b.get(5).toInt() == 0
|
plum-std/List.plum
CHANGED
|
@@ -6,13 +6,17 @@ import std/Int
|
|
|
6
6
|
import std/Float
|
|
7
7
|
import std/Str
|
|
8
8
|
|
|
9
|
-
# A growable list
|
|
9
|
+
# A growable list over a backing `Array[T]`: element `i` is `data[start + i]`,
|
|
10
|
-
#
|
|
10
|
+
# `size` slots are in use and `cap` slots (from `start`) may be written. Capacity
|
|
11
|
-
# runs out of room
|
|
11
|
+
# doubles when `push` runs out of room, so `push` is O(1) amortized and
|
|
12
|
+
# `get`/`set` are O(1). `slice(i, j)` returns a VIEW: a new `List` over the same
|
|
13
|
+
# array (no copy) whose `cap` equals its `size`, so `set` on a view writes the
|
|
14
|
+
# parent's storage while a `push` on a view moves it to a fresh array and never
|
|
15
|
+
# overwrites the parent. In-place reordering (`removeAt`, `reverse`) of a list
|
|
12
|
-
# `
|
|
16
|
+
# is visible through its views; `sublist` copies. The core operations (`init`, `length`, `get`, `set`,
|
|
13
17
|
# `push`, `pop`, `clear`) sit directly on the `Array` intrinsics; everything
|
|
14
18
|
# else (`map`, `retain`, `reduce`, `chunk`, ...) is ordinary Plum on top of them.
|
|
15
|
-
record List[T](data: Array[T], size: Int) =
|
|
19
|
+
record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
16
20
|
|
|
17
21
|
# `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
|
|
18
22
|
# generics required either way, since nothing else in a call this shape
|
|
@@ -20,7 +24,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
20
24
|
# `plum-checker`/`plum-wasm-codegen`'s `ClassCall`/`FnCall` handling, which
|
|
21
25
|
# desugars both call shapes to `List.init(...)`.
|
|
22
26
|
fun init(values: ...T) -> List[T] =
|
|
23
|
-
l := List(data: Array[T](), size: 0)
|
|
27
|
+
l := List(data: Array[T](), start: 0, size: 0, cap: 0)
|
|
24
28
|
for v := range values
|
|
25
29
|
l.push(v)
|
|
26
30
|
return l
|
|
@@ -29,19 +33,19 @@ record List[T](data: Array[T], size: Int) =
|
|
|
29
33
|
fun get(self, i: Int) -> error{OutOfBounds}!T =
|
|
30
34
|
if i < 0 || i >= self.size
|
|
31
35
|
return error.OutOfBounds
|
|
32
|
-
self.data.get(i)
|
|
36
|
+
self.data.get(self.start + i)
|
|
33
37
|
|
|
34
38
|
# sets the element at i'th index of the list, returning the old value (or fails with `OutOfBounds` if i is out of bounds)
|
|
35
39
|
fun set(self, i: Int, v: T) -> error{OutOfBounds}!T =
|
|
36
40
|
if i < 0 || i >= self.size
|
|
37
41
|
return error.OutOfBounds
|
|
38
|
-
old := self.data.get(i)
|
|
42
|
+
old := self.data.get(self.start + i)
|
|
39
|
-
self.data.set(i, v)
|
|
43
|
+
self.data.set(self.start + i, v)
|
|
40
44
|
old
|
|
41
45
|
|
|
42
46
|
# the element at index i, unchecked (the `for x := range list` protocol; i must be in range)
|
|
43
47
|
fun at(self, i: Int) -> T =
|
|
44
|
-
self.data.get(i)
|
|
48
|
+
self.data.get(self.start + i)
|
|
45
49
|
|
|
46
50
|
# returns the no of elements in the list
|
|
47
51
|
fun length(self) -> Int =
|
|
@@ -55,19 +59,41 @@ record List[T](data: Array[T], size: Int) =
|
|
|
55
59
|
return false
|
|
56
60
|
i := 0
|
|
57
61
|
while i < self.length()
|
|
58
|
-
if self.data.get(i) != other.data.get(i)
|
|
62
|
+
if self.data.get(self.start + i) != other.data.get(other.start + i)
|
|
59
63
|
return false
|
|
60
64
|
i = i + 1
|
|
61
65
|
return true
|
|
62
66
|
|
|
63
67
|
# appends `v` to the end of the list, doubling the capacity when full
|
|
64
68
|
fun push(self, v: T) -> Unit =
|
|
65
|
-
if self.size == self.
|
|
69
|
+
if self.size == self.cap
|
|
66
|
-
|
|
70
|
+
self.grow(if self.size == 0 then 16 else self.size * 2)
|
|
67
|
-
self.data = self.data.resize(cap)
|
|
68
|
-
self.data.set(self.size, v)
|
|
71
|
+
self.data.set(self.start + self.size, v)
|
|
69
72
|
self.size = self.size + 1
|
|
70
73
|
|
|
74
|
+
# Moves the live elements into a fresh array of capacity `n` (a list that
|
|
75
|
+
# starts at 0 keeps its array prefix via `resize`; a view copies its range).
|
|
76
|
+
fun grow(self, n: Int) =
|
|
77
|
+
if self.start == 0
|
|
78
|
+
self.data = self.data.resize(n)
|
|
79
|
+
else
|
|
80
|
+
fresh := Array[T]().resize(n)
|
|
81
|
+
i := 0
|
|
82
|
+
while i < self.size
|
|
83
|
+
fresh.set(i, self.data.get(self.start + i))
|
|
84
|
+
i = i + 1
|
|
85
|
+
self.data = fresh
|
|
86
|
+
self.start = 0
|
|
87
|
+
self.cap = n
|
|
88
|
+
|
|
89
|
+
# A view of the elements at `[i, j)`, clamped into range: shares this list's
|
|
90
|
+
# storage (no copy). `set` on the view changes this list; `push` on the view
|
|
91
|
+
# copies it out first.
|
|
92
|
+
fun slice(self, i: Int, j: Int) -> List[T] =
|
|
93
|
+
s := if i < 0 then 0 else if i > self.size then self.size else i
|
|
94
|
+
e := if j > self.size then self.size else if j < s then s else j
|
|
95
|
+
return List(data: self.data, start: self.start + s, size: e - s, cap: e - s)
|
|
96
|
+
|
|
71
97
|
# adds the specified elements to the end of the list
|
|
72
98
|
fun add(self, values: ...T) =
|
|
73
99
|
for v := range values
|
|
@@ -78,7 +104,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
78
104
|
if self.size == 0
|
|
79
105
|
return error.ListWasEmpty
|
|
80
106
|
self.size = self.size - 1
|
|
81
|
-
self.data.get(self.size)
|
|
107
|
+
self.data.get(self.start + self.size)
|
|
82
108
|
|
|
83
109
|
# removes the element at i'th index of the list (out of range is a no-op)
|
|
84
110
|
fun removeAt(self, i: Int) =
|
|
@@ -86,7 +112,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
86
112
|
return
|
|
87
113
|
j := i
|
|
88
114
|
while j < self.size - 1
|
|
89
|
-
self.data.set(j, self.data.get(j + 1))
|
|
115
|
+
self.data.set(self.start + j, self.data.get(self.start + j + 1))
|
|
90
116
|
j = j + 1
|
|
91
117
|
self.size = self.size - 1
|
|
92
118
|
|
|
@@ -94,7 +120,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
94
120
|
fun remove(self, v: T) =
|
|
95
121
|
i := 0
|
|
96
122
|
while i < self.size
|
|
97
|
-
if self.data.get(i) == v
|
|
123
|
+
if self.data.get(self.start + i) == v
|
|
98
124
|
self.removeAt(i)
|
|
99
125
|
return
|
|
100
126
|
i = i + 1
|
|
@@ -108,9 +134,9 @@ record List[T](data: Array[T], size: Int) =
|
|
|
108
134
|
i := 0
|
|
109
135
|
j := self.size - 1
|
|
110
136
|
while i < j
|
|
111
|
-
tmp := self.data.get(i)
|
|
137
|
+
tmp := self.data.get(self.start + i)
|
|
138
|
+
self.data.set(self.start + i, self.data.get(self.start + j))
|
|
112
|
-
self.data.set(
|
|
139
|
+
self.data.set(self.start + j, tmp)
|
|
113
|
-
self.data.set(j, tmp)
|
|
114
140
|
i = i + 1
|
|
115
141
|
j = j - 1
|
|
116
142
|
self
|
|
@@ -121,14 +147,14 @@ record List[T](data: Array[T], size: Int) =
|
|
|
121
147
|
result := self.sublist(0, self.size)
|
|
122
148
|
i := 1
|
|
123
149
|
while i < result.size
|
|
124
|
-
x := result.data.get(i)
|
|
150
|
+
x := result.data.get(result.start + i)
|
|
125
151
|
j := i - 1
|
|
126
152
|
while j >= 0
|
|
127
|
-
if !before(x, result.data.get(j))
|
|
153
|
+
if !before(x, result.data.get(result.start + j))
|
|
128
154
|
break
|
|
129
|
-
result.data.set(j + 1, result.data.get(j))
|
|
155
|
+
result.data.set(result.start + j + 1, result.data.get(result.start + j))
|
|
130
156
|
j = j - 1
|
|
131
|
-
result.data.set(j + 1, x)
|
|
157
|
+
result.data.set(result.start + j + 1, x)
|
|
132
158
|
i = i + 1
|
|
133
159
|
return result
|
|
134
160
|
|
|
@@ -136,7 +162,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
136
162
|
fun findValue(self, search: T) -> error{NotFound}!T =
|
|
137
163
|
i := 0
|
|
138
164
|
while i < self.size
|
|
139
|
-
value := self.data.get(i)
|
|
165
|
+
value := self.data.get(self.start + i)
|
|
140
166
|
if value == search
|
|
141
167
|
return value
|
|
142
168
|
i = i + 1
|
|
@@ -145,7 +171,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
145
171
|
fun contains(self, v: T) -> Bool =
|
|
146
172
|
i := 0
|
|
147
173
|
while i < self.size
|
|
148
|
-
if self.data.get(i) == v
|
|
174
|
+
if self.data.get(self.start + i) == v
|
|
149
175
|
return true
|
|
150
176
|
i = i + 1
|
|
151
177
|
return false
|
|
@@ -154,19 +180,19 @@ record List[T](data: Array[T], size: Int) =
|
|
|
154
180
|
fun first(self) -> error{ListWasEmpty}!T =
|
|
155
181
|
if self.size == 0
|
|
156
182
|
return error.ListWasEmpty
|
|
157
|
-
self.data.get(0)
|
|
183
|
+
self.data.get(self.start + 0)
|
|
158
184
|
|
|
159
185
|
# the first element, or null when the list is empty (the typed tail of `find`)
|
|
160
186
|
fun head(self) -> T? =
|
|
161
187
|
if self.size == 0
|
|
162
188
|
return null
|
|
163
|
-
return self.data.get(0)
|
|
189
|
+
return self.data.get(self.start + 0)
|
|
164
190
|
|
|
165
191
|
# returns the last element in the list
|
|
166
192
|
fun last(self) -> error{ListWasEmpty}!T =
|
|
167
193
|
if self.size == 0
|
|
168
194
|
return error.ListWasEmpty
|
|
169
|
-
self.data.get(self.size - 1)
|
|
195
|
+
self.data.get(self.start + self.size - 1)
|
|
170
196
|
|
|
171
197
|
# Returns a new list of the elements at index `[start, end)`, clamped into
|
|
172
198
|
# range (a `start`/`end` outside `[0, length]` is clamped, not an error).
|
|
@@ -176,7 +202,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
176
202
|
e := if end > self.size then self.size else end
|
|
177
203
|
i := s
|
|
178
204
|
while i < e
|
|
179
|
-
result.push(self.data.get(i))
|
|
205
|
+
result.push(self.data.get(self.start + i))
|
|
180
206
|
i = i + 1
|
|
181
207
|
return result
|
|
182
208
|
|
|
@@ -188,7 +214,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
188
214
|
fun sample(self) -> error{ListWasEmpty}!T =
|
|
189
215
|
if self.size == 0
|
|
190
216
|
return error.ListWasEmpty
|
|
191
|
-
return self.data.get(Random.intBelow(self.size))
|
|
217
|
+
return self.data.get(self.start + Random.intBelow(self.size))
|
|
192
218
|
|
|
193
219
|
# Returns a new list with `self`'s elements in random order (Fisher-Yates).
|
|
194
220
|
fun shuffle(self) -> List[T] =
|
|
@@ -196,9 +222,9 @@ record List[T](data: Array[T], size: Int) =
|
|
|
196
222
|
i := result.size - 1
|
|
197
223
|
while i > 0
|
|
198
224
|
j := Random.intBelow(i + 1)
|
|
199
|
-
vi := result.data.get(i)
|
|
225
|
+
vi := result.data.get(result.start + i)
|
|
226
|
+
result.data.set(result.start + i, result.data.get(result.start + j))
|
|
200
|
-
result.data.set(
|
|
227
|
+
result.data.set(result.start + j, vi)
|
|
201
|
-
result.data.set(j, vi)
|
|
202
228
|
i = i - 1
|
|
203
229
|
return result
|
|
204
230
|
|
|
@@ -225,7 +251,7 @@ record List[T](data: Array[T], size: Int) =
|
|
|
225
251
|
while i < self.size
|
|
226
252
|
if i > 0
|
|
227
253
|
res.write(sep)
|
|
228
|
-
res.write(self.data.get(i).toStr())
|
|
254
|
+
res.write(self.data.get(self.start + i).toStr())
|
|
229
255
|
i = i + 1
|
|
230
256
|
res.toStr()
|
|
231
257
|
|
|
@@ -359,3 +385,74 @@ test "List[Float] push/get round-trips"
|
|
|
359
385
|
i = i + 1
|
|
360
386
|
assert l.length() == 1000
|
|
361
387
|
assert l.get(999)! == 999.5
|
|
388
|
+
|
|
389
|
+
test "slice is a view: shares storage, no copy"
|
|
390
|
+
xs := List(1, 2, 3, 4, 5)
|
|
391
|
+
ys := xs.slice(1, 4)
|
|
392
|
+
assert ys.length() == 3
|
|
393
|
+
assert ys.get(0)! == 2
|
|
394
|
+
assert ys.get(2)! == 4
|
|
395
|
+
assert {ys.get(3) catch -1} == -1
|
|
396
|
+
ys.set(0, 20)!
|
|
397
|
+
assert xs.get(1)! == 20
|
|
398
|
+
xs.set(3, 40)!
|
|
399
|
+
assert ys.get(2)! == 40
|
|
400
|
+
|
|
401
|
+
test "push on a view copies it out and never overwrites the parent"
|
|
402
|
+
xs := List(1, 2, 3, 4, 5)
|
|
403
|
+
ys := xs.slice(0, 2)
|
|
404
|
+
ys.push(99)
|
|
405
|
+
assert ys.toStr() == "[1,2,99]"
|
|
406
|
+
assert xs.toStr() == "[1,2,3,4,5]"
|
|
407
|
+
ys.set(0, 7)!
|
|
408
|
+
assert xs.get(0)! == 1
|
|
409
|
+
|
|
410
|
+
test "a view of a view, clamping, and empty views"
|
|
411
|
+
xs := List(10, 20, 30, 40, 50, 60)
|
|
412
|
+
ys := xs.slice(1, 5).slice(1, 3)
|
|
413
|
+
assert ys.toStr() == "[30,40]"
|
|
414
|
+
assert xs.slice(-3, 2).toStr() == "[10,20]"
|
|
415
|
+
assert xs.slice(4, 100).toStr() == "[50,60]"
|
|
416
|
+
assert xs.slice(4, 2).length() == 0
|
|
417
|
+
assert xs.slice(6, 6).length() == 0
|
|
418
|
+
|
|
419
|
+
test "every method indexes from the view's start"
|
|
420
|
+
xs := List(9, 1, 5, 3, 7, 8)
|
|
421
|
+
v := xs.slice(1, 5)
|
|
422
|
+
assert v.first()! == 1
|
|
423
|
+
assert v.last()! == 7
|
|
424
|
+
assert v.head()! == 1
|
|
425
|
+
assert v.contains(5) && !v.contains(9) && !v.contains(8)
|
|
426
|
+
assert v.findValue(3)! == 3
|
|
427
|
+
assert v.sort(|a, b| a < b).toStr() == "[1,3,5,7]"
|
|
428
|
+
assert v.equals(List(1, 5, 3, 7))
|
|
429
|
+
assert v.sublist(1, 3).toStr() == "[5,3]"
|
|
430
|
+
assert v.drop(2).toStr() == "[3,7]"
|
|
431
|
+
assert v.chunk(3).length() == 2
|
|
432
|
+
assert v.join("-") == "1-5-3-7"
|
|
433
|
+
total := 0
|
|
434
|
+
for x := range v
|
|
435
|
+
total = total + x
|
|
436
|
+
assert total == 16
|
|
437
|
+
assert v.pop()! == 7
|
|
438
|
+
assert v.length() == 3
|
|
439
|
+
assert xs.length() == 6
|
|
440
|
+
|
|
441
|
+
test "in-place reordering through a view is visible in the parent"
|
|
442
|
+
xs := List(1, 2, 3, 4, 5)
|
|
443
|
+
v := xs.slice(1, 4)
|
|
444
|
+
v.reverse()
|
|
445
|
+
assert xs.toStr() == "[1,4,3,2,5]"
|
|
446
|
+
v.removeAt(0)
|
|
447
|
+
assert v.toStr() == "[3,2]"
|
|
448
|
+
assert xs.toStr() == "[1,3,2,2,5]"
|
|
449
|
+
|
|
450
|
+
test "a growing list keeps working after a view is taken"
|
|
451
|
+
xs := List[Int]()
|
|
452
|
+
for i := range 20
|
|
453
|
+
xs.push(i)
|
|
454
|
+
v := xs.slice(0, 3)
|
|
455
|
+
for i := range 20
|
|
456
|
+
xs.push(i)
|
|
457
|
+
assert xs.length() == 40
|
|
458
|
+
assert v.toStr() == "[0,1,2]"
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1180,6 +1180,13 @@ fn buildGcTypeRegistry(
|
|
|
1180
1180
|
reg.app_class.insert(a.clone(), idx);
|
|
1181
1181
|
continue;
|
|
1182
1182
|
}
|
|
1183
|
+
// `Array[Byte]` is packed: the shared `array<mut i8>` (slot 0) that
|
|
1184
|
+
// `Str` and `[]Byte` use, one byte per element.
|
|
1185
|
+
if args[0] == PlumType::TByte {
|
|
1186
|
+
reg.app_super.insert(a.clone(), reg.byte_array_type_idx);
|
|
1187
|
+
reg.app_class.insert(a.clone(), reg.byte_array_type_idx);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1183
1190
|
let vt = plumTypeToGcValtype(&args[0], ®);
|
|
1184
1191
|
let idx = *by_vt.entry(vt).or_insert_with(|| {
|
|
1185
1192
|
let idx = slots.len() as u32;
|
|
@@ -2963,7 +2970,12 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
|
|
|
2963
2970
|
Instruction::LocalGet(0).encode(&mut body); // self
|
|
2964
2971
|
Instruction::LocalGet(1).encode(&mut body); // i
|
|
2965
2972
|
Instruction::I32WrapI64.encode(&mut body);
|
|
2973
|
+
if array_elem == PlumType::TByte {
|
|
2974
|
+
// packed i8 storage: zero-extend to Byte's i32
|
|
2975
|
+
Instruction::ArrayGetU(array_type_idx).encode(&mut body);
|
|
2976
|
+
} else {
|
|
2966
|
-
|
|
2977
|
+
Instruction::ArrayGet(array_type_idx).encode(&mut body);
|
|
2978
|
+
}
|
|
2967
2979
|
unboxIfValue(&array_elem, &mut body);
|
|
2968
2980
|
Instruction::End.encode(&mut body);
|
|
2969
2981
|
Some(body)
|