plum

#treesitter#compiler#wasm

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 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
- # `Array[Bool]`/`Array[Byte]` are `array<mut i32>`, and a reference `T` is
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: `data` is a backing `Array[T]` whose length is the list's
9
+ # A growable list over a backing `Array[T]`: element `i` is `data[start + i]`,
10
- # capacity, `size` the number of slots in use. Capacity doubles when `push`
10
+ # `size` slots are in use and `cap` slots (from `start`) may be written. Capacity
11
- # runs out of room (via `Array.resize`), so `push` is O(1) amortized and
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
- # `get`/`set` are O(1). The core operations (`init`, `length`, `get`, `set`,
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.data.length()
69
+ if self.size == self.cap
66
- cap := if self.size == 0 then 16 else self.size * 2
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(i, self.data.get(j))
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(i, result.data.get(j))
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], &reg);
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
- Instruction::ArrayGet(array_type_idx).encode(&mut body);
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)