plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ea9b8c1
— Peter John
2026-09-04T17:05:20+05:30
feat(plum): add generic Array[T] primitive, rewrite Map as a real hash table
- libs/std/array.plum +56 -0
- libs/std/int.plum +5 -0
- libs/std/json.plum +1 -1
- libs/std/map.plum +137 -90
- libs/std/str.plum +13 -0
- plum-wasm-codegen/src/lib.rs +153 -2
libs/std/array.plum
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module std
|
|
2
|
+
|
|
3
|
+
# A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
|
|
4
|
+
# `array<anyref>` — every `Array[T]` specialization (`Array$Int`, `Array$Str`,
|
|
5
|
+
# ...) shares the SAME underlying wasm array type (see
|
|
6
|
+
# `plum-wasm-codegen::GcTypeRegistry::array_type_idx`), exactly like `[]Byte`
|
|
7
|
+
# reuses `Str`'s own array type. `init`/`get`/`set`/`length` are compiler
|
|
8
|
+
# intrinsics (see `compileIntrinsicFnBody`) — there's no way to express raw
|
|
9
|
+
# array allocation/indexing in Plum source itself.
|
|
10
|
+
#
|
|
11
|
+
# LIMITATION: `T` must be a reference type (a `type`/`enum` value, `Str`,
|
|
12
|
+
# `List`, etc.) — every element is stored as a boxed `anyref` and `get`
|
|
13
|
+
# narrows it back to `T` via `ref.cast`. A primitive `T` (`Int`/`Float`/`Bool`/
|
|
14
|
+
# `Byte`) is NOT supported (no boxing/unboxing exists yet) and will not
|
|
15
|
+
# codegen correctly. Nothing in `libs/std` instantiates `Array` with a
|
|
16
|
+
# primitive `T` today — the only user is `Map[K, V]`'s bucket array,
|
|
17
|
+
# `Array[List[Pair[K, V]]]`.
|
|
18
|
+
#
|
|
19
|
+
# `init()` is deliberately zero-arg (no `Array[T](n)` constructor exists) —
|
|
20
|
+
# `class_call`'s grammar (`Type[Generics](...)`) only ever accepts `name:
|
|
21
|
+
# value` field arguments or an empty arg list, never bare positional ones
|
|
22
|
+
# (see `tooling/tree-sitter-plum/grammar.js`'s `class_argument_list`), and
|
|
23
|
+
# `Array` has no real fields for a keyword arg to bind to. Building a
|
|
24
|
+
# specific-length array is `push`ing onto an empty one `n` times instead
|
|
25
|
+
# (`Map`'s bucket array does exactly this at construction) — `push` itself
|
|
26
|
+
# has no in-place "grow" to fall back on either (a wasm-gc `array` is a
|
|
27
|
+
# fixed-length allocation once made), so it allocates a new, one-longer array
|
|
28
|
+
# and copies, same amortized-growth idea as `Buffer`'s own `write`.
|
|
29
|
+
#
|
|
30
|
+
# New elements start out `None`-shaped — really a null `anyref` under the
|
|
31
|
+
# hood — so `get` on an index that was never `set` traps rather than
|
|
32
|
+
# returning some default `T` value (there is no way to conjure a default `T`
|
|
33
|
+
# generically).
|
|
34
|
+
type Array[T] =
|
|
35
|
+
# A new, empty array.
|
|
36
|
+
fun init() -> Array[T] =
|
|
37
|
+
todo
|
|
38
|
+
|
|
39
|
+
# The element at index `i`. Traps if `i` is out of range or was never `set`.
|
|
40
|
+
fun get(self, i: Int) -> T =
|
|
41
|
+
todo
|
|
42
|
+
|
|
43
|
+
# Overwrites the element at index `i` with `v`. Traps if `i` is out of range.
|
|
44
|
+
fun set(self, i: Int, v: T) -> Unit =
|
|
45
|
+
todo
|
|
46
|
+
|
|
47
|
+
# Number of slots in the array.
|
|
48
|
+
fun length(self) -> Int =
|
|
49
|
+
todo
|
|
50
|
+
|
|
51
|
+
# Returns a NEW array, one longer than `self`, with `v` appended at the end
|
|
52
|
+
# — `self` itself is left untouched (this allocates and copies, it does not
|
|
53
|
+
# mutate in place). Callers grow in a loop by reassigning: `arr =
|
|
54
|
+
# arr.push(v)`.
|
|
55
|
+
fun push(self, v: T) -> Array[T] =
|
|
56
|
+
todo
|
libs/std/int.plum
CHANGED
|
@@ -12,6 +12,11 @@ type Int =
|
|
|
12
12
|
fun abs(self) -> Int =
|
|
13
13
|
self < 0 ? -self : self
|
|
14
14
|
|
|
15
|
+
# An `Int` already IS its own well-distributed bit pattern — used by
|
|
16
|
+
# `Map[K: Hashable, V]` (see `map.plum`) to bucket `Int` keys.
|
|
17
|
+
fun hash(self) -> Int =
|
|
18
|
+
self
|
|
19
|
+
|
|
15
20
|
# An Int is always already whole, so ceil/floor/round/trunc are all just
|
|
16
21
|
# the value itself widened to Float.
|
|
17
22
|
fun ceil(self) -> Float =
|
libs/std/json.plum
CHANGED
|
@@ -333,7 +333,7 @@ type JsonParser =
|
|
|
333
333
|
fun parseObject(self) -> Result[Json, JsonParseError] =
|
|
334
334
|
self.advance()
|
|
335
335
|
self.skipSpace()
|
|
336
|
-
pairs := Map[Str, Json](
|
|
336
|
+
pairs := Map[Str, Json]()
|
|
337
337
|
if self.peek() == 125
|
|
338
338
|
self.advance()
|
|
339
339
|
return Ok(JsonMap(pairs))
|
libs/std/map.plum
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/list
|
|
3
3
|
import std/option
|
|
4
|
+
import std/array
|
|
5
|
+
|
|
6
|
+
# Any type usable as a `Map` key needs a `hash`, so keys landing in different
|
|
7
|
+
# buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full
|
|
8
|
+
# linear scan of the whole map — see `Str.hash`/`Int.hash` in `str.plum`/
|
|
9
|
+
# `int.plum` for the two implementations `libs/std` provides today. Declared
|
|
10
|
+
# here (rather than alongside `ToStr`/`Comparable` in `str.plum`) since `Map`
|
|
11
|
+
# is its only real consumer; like `Comparable`/`Readable`/`Writable`
|
|
12
|
+
# elsewhere in `libs/std`, `Str`/`Int` don't formally list it in their own
|
|
13
|
+
# `implements` clause (that would need them to import `map.plum`, an
|
|
14
|
+
# unwanted dependency direction for such foundational types) — this is
|
|
15
|
+
# checked the same "unenforced, trust the method exists" way those already
|
|
16
|
+
# are (see the README's note on `checkTraitConformance`).
|
|
17
|
+
trait Hashable =
|
|
18
|
+
hash() -> Int
|
|
4
19
|
|
|
5
20
|
# A Pair is a grouping of a key with a value.
|
|
6
21
|
#
|
|
@@ -18,29 +33,45 @@ type Pair[K, V](ToStr) =
|
|
|
18
33
|
fun toStr(self) -> Str =
|
|
19
34
|
return "{self.key.toStr()}: {self.val.toStr()}"
|
|
20
35
|
|
|
21
|
-
#
|
|
36
|
+
# Number of buckets every `Map` allocates at construction — fixed for the
|
|
22
|
-
#
|
|
37
|
+
# map's whole lifetime, no rehashing/resizing as it grows. A real hash table
|
|
23
|
-
# one on). Fine for small maps; a real hash-based `Map` is future work.
|
|
24
|
-
#
|
|
25
|
-
#
|
|
38
|
+
# would grow this as `size` outpaces it (keeping average bucket length, and
|
|
26
|
-
#
|
|
39
|
+
# so average `get`/`set` cost, roughly constant); that's future work; for now
|
|
40
|
+
# a big-enough fixed count keeps small-to-medium maps fast in practice.
|
|
41
|
+
BUCKET_COUNT = 16
|
|
42
|
+
|
|
43
|
+
# A Map is a hash table: `BUCKET_COUNT` buckets, each an association-list
|
|
27
|
-
#
|
|
44
|
+
# chain (`List[Pair[K, V]]`) of every key hashing into it — O(1) average
|
|
28
|
-
# letter (`k: K`), not one nested
|
|
29
|
-
# inside another generic type like `...Pair[K, V]` (see
|
|
30
|
-
# `
|
|
45
|
+
# `get`/`set`/`remove` (versus the single, whole-map linear scan a plain
|
|
31
|
-
#
|
|
46
|
+
# association list costs), degrading to O(n) only if many keys collide into
|
|
32
|
-
#
|
|
47
|
+
# the same bucket.
|
|
33
|
-
# m := Map[Str, Int](items: List[Pair[Str, Int]](head: None, tail: None, size: 0))
|
|
34
|
-
type Map[K, V] =
|
|
48
|
+
type Map[K: Hashable, V] =
|
|
35
|
-
|
|
49
|
+
buckets: Array[List[Pair[K, V]]]
|
|
36
|
-
|
|
50
|
+
size: Int
|
|
51
|
+
|
|
52
|
+
# `Map[Str, Int]()` — explicit generics required (nothing in a no-arg call
|
|
53
|
+
# pins down `K`/`V`), builds `BUCKET_COUNT` empty bucket lists up front via
|
|
54
|
+
# `Array`'s own `push`-to-grow (see `libs/std/array.plum`'s header comment
|
|
55
|
+
# on why `Array` itself has no direct "make me length N" constructor).
|
|
56
|
+
fun init() -> Map[K, V] =
|
|
57
|
+
b := Array[List[Pair[K, V]]]()
|
|
58
|
+
i := 0
|
|
59
|
+
while i < BUCKET_COUNT
|
|
60
|
+
b = b.push(List[Pair[K, V]]())
|
|
61
|
+
i = i + 1
|
|
62
|
+
return Map(buckets: b, size: 0)
|
|
63
|
+
|
|
64
|
+
# `k`'s bucket index — `hash`'s sign is unconstrained (a plain `%` on a
|
|
65
|
+
# negative `Int` stays negative in Plum, matching wasm's signed
|
|
66
|
+
# `i64.rem_s`), so this folds a negative result back into `[0,
|
|
67
|
+
# BUCKET_COUNT)` rather than indexing `buckets` out of range.
|
|
68
|
+
fun bucketIndex(self, k: K) -> Int =
|
|
69
|
+
m := k.hash() % BUCKET_COUNT
|
|
70
|
+
return m < 0 ? m + BUCKET_COUNT : m
|
|
71
|
+
|
|
37
|
-
# Gets the value for `k`, or `None` if absent.
|
|
72
|
+
# Gets the value for `k`, or `None` if absent.
|
|
38
|
-
# range self.items` (a `for`/`range` loop only supports ranging over an
|
|
39
|
-
# `Int` count or a `...T` variadic pack — `items` is neither, it's a
|
|
40
|
-
# `List`) — a plain `while`/`match` traversal over the list's own nodes
|
|
41
|
-
# instead, matching `List`'s own internal methods' idiom.
|
|
42
73
|
fun get(self, k: K) -> Option[V] =
|
|
43
|
-
current := self.
|
|
74
|
+
current := self.buckets.get(self.bucketIndex(k)).head
|
|
44
75
|
while current != None
|
|
45
76
|
match current
|
|
46
77
|
Some(Node(Pair(key, val), _, next)) =>
|
|
@@ -56,13 +87,11 @@ type Map[K, V] =
|
|
|
56
87
|
|
|
57
88
|
# Sets `k`'s value to `v` — replacing an existing entry for `k` in place
|
|
58
89
|
# (mutating the stored `Pair`'s `val` field) rather than appending a
|
|
59
|
-
# duplicate, so a repeated `set` on the same key doesn't grow
|
|
90
|
+
# duplicate, so a repeated `set` on the same key doesn't grow its bucket
|
|
60
|
-
# unboundedly or leave a stale value reachable by `get`.
|
|
91
|
+
# unboundedly or leave a stale value reachable by `get`.
|
|
61
|
-
# as far as the `Pair` itself (not its own `key`/`val` fields) — `val` is
|
|
62
|
-
# MUTATED below, which needs the `Pair` reference kept, not a copy of its
|
|
63
|
-
# field pulled out by value.
|
|
64
92
|
fun set(self, k: K, v: V) -> Unit =
|
|
93
|
+
bucket := self.buckets.get(self.bucketIndex(k))
|
|
65
|
-
current :=
|
|
94
|
+
current := bucket.head
|
|
66
95
|
while current != None
|
|
67
96
|
match current
|
|
68
97
|
Some(Node(pair, _, next)) =>
|
|
@@ -72,95 +101,91 @@ type Map[K, V] =
|
|
|
72
101
|
current = next
|
|
73
102
|
None =>
|
|
74
103
|
break
|
|
75
|
-
|
|
104
|
+
bucket.add(Pair(key: k, val: v))
|
|
105
|
+
self.size = self.size + 1
|
|
76
106
|
|
|
77
107
|
# Sets `k`'s value to `v` only if `k` isn't already present.
|
|
78
108
|
fun putIfAbsent(self, k: K, v: V) -> Unit =
|
|
79
109
|
if !self.has(k)
|
|
80
110
|
self.set(k, v)
|
|
81
111
|
|
|
82
|
-
# Removes `k`'s entry, if present.
|
|
112
|
+
# Removes `k`'s entry, if present.
|
|
83
|
-
# destructuring further) — `unlink` needs the actual `Node` reference, not
|
|
84
|
-
# just a copy of its `value` field.
|
|
85
113
|
fun remove(self, k: K) -> Unit =
|
|
114
|
+
bucket := self.buckets.get(self.bucketIndex(k))
|
|
86
|
-
current :=
|
|
115
|
+
current := bucket.head
|
|
87
116
|
while current != None
|
|
88
117
|
match current
|
|
89
118
|
Some(node) =>
|
|
90
119
|
if node.value.key == k
|
|
91
|
-
|
|
120
|
+
bucket.unlink(node)
|
|
121
|
+
self.size = self.size - 1
|
|
92
122
|
return
|
|
93
123
|
current = node.next
|
|
94
124
|
None =>
|
|
95
125
|
break
|
|
96
126
|
|
|
97
127
|
fun length(self) -> Int =
|
|
98
|
-
return self.
|
|
128
|
+
return self.size
|
|
99
129
|
|
|
100
130
|
fun isEmpty(self) -> Bool =
|
|
101
|
-
return self.
|
|
131
|
+
return self.size == 0
|
|
102
132
|
|
|
103
133
|
fun clear(self) -> Unit =
|
|
134
|
+
i := 0
|
|
135
|
+
while i < self.buckets.length()
|
|
104
|
-
|
|
136
|
+
self.buckets.get(i).clear()
|
|
137
|
+
i = i + 1
|
|
138
|
+
self.size = 0
|
|
105
139
|
|
|
106
|
-
# Calls `cb` with each key and value in the map
|
|
140
|
+
# Calls `cb` with each key and value in the map. Order is bucket order,
|
|
141
|
+
# then insertion order within a bucket — NOT overall insertion order (that
|
|
142
|
+
# would need a separate side list to track, which nothing here needs today).
|
|
107
143
|
fun each(self, cb: fn(K, V)) -> Unit =
|
|
144
|
+
i := 0
|
|
145
|
+
while i < self.buckets.length()
|
|
108
|
-
|
|
146
|
+
current := self.buckets.get(i).head
|
|
109
|
-
|
|
147
|
+
while current != None
|
|
110
|
-
|
|
148
|
+
match current
|
|
111
|
-
|
|
149
|
+
Some(Node(Pair(key, val), _, next)) =>
|
|
112
|
-
|
|
150
|
+
cb(key, val)
|
|
113
|
-
|
|
151
|
+
current = next
|
|
114
|
-
|
|
152
|
+
None =>
|
|
115
|
-
|
|
153
|
+
break
|
|
154
|
+
i = i + 1
|
|
116
155
|
|
|
117
|
-
# `keys`/`values` build a `List` of `K`/`V` alone — no NEW generic param
|
|
118
|
-
# beyond `Map[K, V]`'s own, so (unlike `map` below) these don't need
|
|
119
|
-
# `resolveMethodOwnGenerics` at all; the class's own already-known `K`/`V`
|
|
120
|
-
# is enough for the bare `List(...)` construction to resolve (via the
|
|
121
|
-
# ordinary `current_return_type` fallback every other bare-`List(...)`-
|
|
122
|
-
# returning method already uses).
|
|
123
156
|
fun keys(self) -> List[K] =
|
|
124
157
|
result := List(head: None, tail: None, size: 0)
|
|
125
|
-
current := self.items.head
|
|
126
|
-
while current != None
|
|
127
|
-
match current
|
|
128
|
-
Some(Node(Pair(key, _), _, next)) =>
|
|
129
|
-
|
|
158
|
+
self.each(|k, v| result.add(k))
|
|
130
|
-
current = next
|
|
131
|
-
None =>
|
|
132
|
-
break
|
|
133
159
|
return result
|
|
134
160
|
|
|
135
161
|
fun values(self) -> List[V] =
|
|
136
162
|
result := List(head: None, tail: None, size: 0)
|
|
137
|
-
current := self.items.head
|
|
138
|
-
while current != None
|
|
139
|
-
match current
|
|
140
|
-
Some(Node(Pair(_, val), _, next)) =>
|
|
141
|
-
|
|
163
|
+
self.each(|k, v| result.add(v))
|
|
142
|
-
current = next
|
|
143
|
-
None =>
|
|
144
|
-
break
|
|
145
164
|
return result
|
|
146
165
|
|
|
147
166
|
fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
|
|
167
|
+
b := Array[List[Pair[X, Y]]]()
|
|
168
|
+
i := 0
|
|
169
|
+
while i < BUCKET_COUNT
|
|
170
|
+
b = b.push(List[Pair[X, Y]]())
|
|
171
|
+
i = i + 1
|
|
148
|
-
result := Map
|
|
172
|
+
result := Map(buckets: b, size: 0)
|
|
173
|
+
j := 0
|
|
174
|
+
while j < self.buckets.length()
|
|
149
|
-
|
|
175
|
+
current := self.buckets.get(j).head
|
|
150
|
-
|
|
176
|
+
while current != None
|
|
151
|
-
|
|
177
|
+
match current
|
|
152
|
-
|
|
178
|
+
Some(Node(Pair(key, val), _, next)) =>
|
|
179
|
+
pair := cb(key, val)
|
|
153
|
-
|
|
180
|
+
result.set(pair.key, pair.val)
|
|
154
|
-
|
|
181
|
+
current = next
|
|
155
|
-
|
|
182
|
+
None =>
|
|
156
|
-
|
|
183
|
+
break
|
|
184
|
+
j = j + 1
|
|
157
185
|
return result
|
|
158
186
|
|
|
159
|
-
fun makeStrIntMapForTest() -> Map[Str, Int] =
|
|
160
|
-
return Map[Str, Int](items: List[Pair[Str, Int]](head: None, tail: None, size: 0))
|
|
161
|
-
|
|
162
187
|
test "get/set/has round-trip and set replaces an existing key in place"
|
|
163
|
-
m :=
|
|
188
|
+
m := Map[Str, Int]()
|
|
164
189
|
assert m.has("a") == False
|
|
165
190
|
m.set("a", 1)
|
|
166
191
|
assert m.has("a") == True
|
|
@@ -171,13 +196,13 @@ test "get/set/has round-trip and set replaces an existing key in place"
|
|
|
171
196
|
assert m.length() == 1
|
|
172
197
|
|
|
173
198
|
test "putIfAbsent only sets a key that isn't already present"
|
|
174
|
-
m :=
|
|
199
|
+
m := Map[Str, Int]()
|
|
175
200
|
m.putIfAbsent("a", 1)
|
|
176
201
|
m.putIfAbsent("a", 2)
|
|
177
202
|
assert m.get("a").unwrap() == 1
|
|
178
203
|
|
|
179
204
|
test "remove deletes an entry and clear empties the map"
|
|
180
|
-
m :=
|
|
205
|
+
m := Map[Str, Int]()
|
|
181
206
|
m.set("a", 1)
|
|
182
207
|
m.set("b", 2)
|
|
183
208
|
m.remove("a")
|
|
@@ -186,15 +211,18 @@ test "remove deletes an entry and clear empties the map"
|
|
|
186
211
|
m.clear()
|
|
187
212
|
assert m.isEmpty() == True
|
|
188
213
|
|
|
189
|
-
test "keys/values list every entry's key/value
|
|
214
|
+
test "keys/values list every entry's key/value"
|
|
190
|
-
m :=
|
|
215
|
+
m := Map[Str, Int]()
|
|
191
216
|
m.set("a", 1)
|
|
192
217
|
m.set("b", 2)
|
|
218
|
+
assert m.length() == 2
|
|
193
|
-
assert m.keys().
|
|
219
|
+
assert m.keys().contains("a")
|
|
194
|
-
assert m.
|
|
220
|
+
assert m.keys().contains("b")
|
|
221
|
+
assert m.values().contains(1)
|
|
222
|
+
assert m.values().contains(2)
|
|
195
223
|
|
|
196
224
|
test "each calls the callback with every key and value"
|
|
197
|
-
m :=
|
|
225
|
+
m := Map[Str, Int]()
|
|
198
226
|
m.set("a", 1)
|
|
199
227
|
m.set("b", 2)
|
|
200
228
|
# A closure captures its outer variables BY VALUE at creation time, so
|
|
@@ -202,13 +230,32 @@ test "each calls the callback with every key and value"
|
|
|
202
230
|
# here — mutate a captured `List` (a class, reference semantics) instead.
|
|
203
231
|
seen := List[Int]()
|
|
204
232
|
m.each(|k, v| seen.add(v))
|
|
205
|
-
assert seen.
|
|
233
|
+
assert seen.length() == 2
|
|
234
|
+
assert seen.contains(1)
|
|
235
|
+
assert seen.contains(2)
|
|
206
236
|
|
|
207
237
|
test "map transforms every key/value pair into a new map, changing the value type"
|
|
208
|
-
m :=
|
|
238
|
+
m := Map[Str, Int]()
|
|
209
239
|
m.set("a", 1)
|
|
210
240
|
m.set("b", 2)
|
|
211
241
|
stringified := m.map(|k, v| Pair(key: k, val: v.toStr()))
|
|
212
242
|
assert stringified.get("a").unwrap() == "1"
|
|
213
243
|
assert stringified.get("b").unwrap() == "2"
|
|
214
244
|
assert stringified.length() == 2
|
|
245
|
+
|
|
246
|
+
test "many keys landing in the same bucket still resolve correctly (collision chaining)"
|
|
247
|
+
m := Map[Int, Int]()
|
|
248
|
+
i := 0
|
|
249
|
+
while i < 40
|
|
250
|
+
m.set(i, i * i)
|
|
251
|
+
i = i + 1
|
|
252
|
+
assert m.length() == 40
|
|
253
|
+
j := 0
|
|
254
|
+
while j < 40
|
|
255
|
+
assert m.get(j).unwrap() == j * j
|
|
256
|
+
j = j + 1
|
|
257
|
+
m.remove(5)
|
|
258
|
+
assert m.has(5) == False
|
|
259
|
+
assert m.get(4).unwrap() == 16
|
|
260
|
+
assert m.get(21).unwrap() == 441
|
|
261
|
+
assert m.length() == 39
|
libs/std/str.plum
CHANGED
|
@@ -22,6 +22,19 @@ type Str(Comparable, ToStr, Readable, Writable) =
|
|
|
22
22
|
fun byteAt(self, i: Int) -> Int =
|
|
23
23
|
todo
|
|
24
24
|
|
|
25
|
+
# FNV-1a over the raw UTF-8 bytes — used by `Map[K: Hashable, V]` (see
|
|
26
|
+
# `map.plum`) to bucket `Str` keys. Not cryptographic, just a well-known,
|
|
27
|
+
# decently-distributed general-purpose hash; wraps silently on overflow
|
|
28
|
+
# (ordinary 64-bit multiplication), which is fine — only the bit pattern
|
|
29
|
+
# matters, not the numeric value itself.
|
|
30
|
+
fun hash(self) -> Int =
|
|
31
|
+
h := 0xcbf29ce484222325
|
|
32
|
+
i := 0
|
|
33
|
+
while i < self.length()
|
|
34
|
+
h = {h ^ self.byteAt(i)} * 0x100000001b3
|
|
35
|
+
i = i + 1
|
|
36
|
+
return h
|
|
37
|
+
|
|
25
38
|
# The single-character substring at index `i`. (Was declared to return a
|
|
26
39
|
# `Char` type, but `Char` was never actually defined anywhere in `libs/std` —
|
|
27
40
|
# returning a length-1 `Str` instead makes it immediately usable with every
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -623,6 +623,22 @@ pub struct GcTypeRegistry {
|
|
|
623
623
|
/// `[count][elem...]` blob — `array.len` replaces the explicit count). Populated
|
|
624
624
|
/// alongside `closure_type_idx`, from every `ParamType::Variadic` in the program.
|
|
625
625
|
pub variadic_array_type_idx: HashMap<ValType, u32>,
|
|
626
|
+
/// The single shared `array<anyref>` type index every `Array[T]`
|
|
627
|
+
/// specialization (`Array$Int`, `Array$Str`, ...) uses regardless of its own
|
|
628
|
+
/// `T` — see `libs/std/array.plum`'s header comment. `0` (the `Str` slot,
|
|
629
|
+
/// harmlessly never read as such) until/unless the program actually
|
|
630
|
+
/// instantiates `Array` at least once.
|
|
631
|
+
pub array_type_idx: u32,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/// Whether `name` is `Array` itself or one of its monomorphized specializations
|
|
635
|
+
/// (`Array$Int`, `Array$List$Pair$Str$Int`, ...) — every such class shares one
|
|
636
|
+
/// physical wasm-gc array type (`GcTypeRegistry::array_type_idx`) instead of
|
|
637
|
+
/// getting its own struct type, and its `init`/`get`/`set`/`length` methods are
|
|
638
|
+
/// compiler intrinsics rather than compiled `todo` bodies (see
|
|
639
|
+
/// `compileIntrinsicFnBody`).
|
|
640
|
+
fn isArraySpecialization(name: &str) -> bool {
|
|
641
|
+
name == "Array" || name.starts_with("Array$")
|
|
626
642
|
}
|
|
627
643
|
|
|
628
644
|
/// Resolves a plum type to the wasm-gc `ValType` its values are represented as, given
|
|
@@ -683,6 +699,9 @@ fn buildGcTypeRegistry(
|
|
|
683
699
|
Class(String),
|
|
684
700
|
EnumSuper(String),
|
|
685
701
|
Variant(String),
|
|
702
|
+
/// The single shared `array<anyref>` type every `Array[T]`
|
|
703
|
+
/// specialization aliases — see `isArraySpecialization`.
|
|
704
|
+
ArrayRef,
|
|
686
705
|
}
|
|
687
706
|
|
|
688
707
|
// Pass 1: assign every entry a slot (and therefore a type index) up front, before
|
|
@@ -691,11 +710,21 @@ fn buildGcTypeRegistry(
|
|
|
691
710
|
let mut class_type_idx: HashMap<String, u32> = HashMap::new();
|
|
692
711
|
let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
|
|
693
712
|
let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
|
|
713
|
+
let mut array_ref_slot: Option<u32> = None;
|
|
694
714
|
|
|
695
715
|
for item in &source.items {
|
|
696
716
|
if let ast::Item::Class(c) = item {
|
|
717
|
+
if isArraySpecialization(&c.name) {
|
|
718
|
+
let idx = *array_ref_slot.get_or_insert_with(|| {
|
|
719
|
+
let idx = slots.len() as u32;
|
|
720
|
+
slots.push(Slot::ArrayRef);
|
|
721
|
+
idx
|
|
722
|
+
});
|
|
723
|
+
class_type_idx.insert(c.name.clone(), idx);
|
|
724
|
+
} else {
|
|
697
|
-
|
|
725
|
+
class_type_idx.insert(c.name.clone(), slots.len() as u32);
|
|
698
|
-
|
|
726
|
+
slots.push(Slot::Class(c.name.clone()));
|
|
727
|
+
}
|
|
699
728
|
}
|
|
700
729
|
}
|
|
701
730
|
|
|
@@ -743,6 +772,7 @@ fn buildGcTypeRegistry(
|
|
|
743
772
|
str_type_idx: 0,
|
|
744
773
|
closure_type_idx: 0,
|
|
745
774
|
variadic_array_type_idx: HashMap::new(),
|
|
775
|
+
array_type_idx: array_ref_slot.unwrap_or(0),
|
|
746
776
|
};
|
|
747
777
|
|
|
748
778
|
// Pass 2: build the real SubType for every slot, now that every cross-reference
|
|
@@ -808,6 +838,17 @@ fn buildGcTypeRegistry(
|
|
|
808
838
|
composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
|
|
809
839
|
}
|
|
810
840
|
}
|
|
841
|
+
Slot::ArrayRef => SubType {
|
|
842
|
+
is_final: true,
|
|
843
|
+
supertype_idx: None,
|
|
844
|
+
composite_type: CompositeType {
|
|
845
|
+
inner: CompositeInnerType::Array(ArrayType(FieldType {
|
|
846
|
+
element_type: StorageType::Val(ValType::Ref(RefType::ANYREF)),
|
|
847
|
+
mutable: true,
|
|
848
|
+
})),
|
|
849
|
+
shared: false,
|
|
850
|
+
},
|
|
851
|
+
},
|
|
811
852
|
}).collect();
|
|
812
853
|
|
|
813
854
|
module.addGcTypes(subtypes);
|
|
@@ -1708,6 +1749,11 @@ fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32
|
|
|
1708
1749
|
/// here — by `fnKey` — instead of compiling their `todo` body to `unreachable`.
|
|
1709
1750
|
/// Returns `None` for any other function, meaning "compile it normally."
|
|
1710
1751
|
fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
|
|
1752
|
+
if let Some(recv) = f.type_param.as_deref() {
|
|
1753
|
+
if isArraySpecialization(recv) {
|
|
1754
|
+
return compileArrayIntrinsicFnBody(f);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1711
1757
|
let str_type_idx = withGcTypes(|r| r.str_type_idx);
|
|
1712
1758
|
match (f.type_param.as_deref(), f.name.as_str()) {
|
|
1713
1759
|
// Str.length(self) -> Int
|
|
@@ -1838,6 +1884,111 @@ fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
|
|
|
1838
1884
|
}
|
|
1839
1885
|
}
|
|
1840
1886
|
|
|
1887
|
+
/// `Array[T]`'s `init`/`get`/`set`/`length` — every specialization (`Array$Int`,
|
|
1888
|
+
/// `Array$List$Pair$Str$Int`, ...) shares ONE wasm-gc `array<anyref>` type
|
|
1889
|
+
/// (`GcTypeRegistry::array_type_idx`, see `isArraySpecialization`), so these
|
|
1890
|
+
/// compile identically regardless of which concrete `T` this specialization is
|
|
1891
|
+
/// for. Every `type_param`-bearing `Fn` — methods AND non-`self` nested
|
|
1892
|
+
/// "static" constructors like `init` (see `fnWasmParamTypes`) — gets an
|
|
1893
|
+
/// implicit receiver as wasm param 0, so a declared plum param at position `k`
|
|
1894
|
+
/// is always wasm local `k + 1`. `get` is the one case that also needs a
|
|
1895
|
+
/// `ref.cast` back from the raw stored `anyref` down to `T`'s own concrete
|
|
1896
|
+
/// type; `set`/`init` need no such cast the other way, since any concrete ref
|
|
1897
|
+
/// is already a valid subtype wherever `anyref` is expected.
|
|
1898
|
+
fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
|
|
1899
|
+
let array_type_idx = withGcTypes(|r| r.array_type_idx);
|
|
1900
|
+
match f.name.as_str() {
|
|
1901
|
+
// init() -> Array[T] — always starts empty; see `push` for how a
|
|
1902
|
+
// specific length is actually built up.
|
|
1903
|
+
"init" => {
|
|
1904
|
+
let mut body = vec![0u8];
|
|
1905
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
1906
|
+
Instruction::ArrayNewDefault(array_type_idx).encode(&mut body);
|
|
1907
|
+
Instruction::End.encode(&mut body);
|
|
1908
|
+
Some(body)
|
|
1909
|
+
}
|
|
1910
|
+
// push(self, v: T) -> Array[T] — a new array one longer than `self`,
|
|
1911
|
+
// `self`'s elements copied in, `v` appended at the end. `self` itself
|
|
1912
|
+
// is left untouched (wasm-gc arrays are fixed-length once allocated).
|
|
1913
|
+
"push" => {
|
|
1914
|
+
const SELF: u32 = 0;
|
|
1915
|
+
const V: u32 = 1;
|
|
1916
|
+
const OLD_LEN: u32 = 2;
|
|
1917
|
+
const NEW_ARR: u32 = 3;
|
|
1918
|
+
let array_ref = gcRef(array_type_idx);
|
|
1919
|
+
let mut body = Vec::new();
|
|
1920
|
+
body.extend(encodeLeb128U32(2)); // two locals groups
|
|
1921
|
+
body.extend(encodeLeb128U32(1)); // old_len: i32
|
|
1922
|
+
ValType::I32.encode(&mut body);
|
|
1923
|
+
body.extend(encodeLeb128U32(1)); // new_arr: ref
|
|
1924
|
+
array_ref.encode(&mut body);
|
|
1925
|
+
|
|
1926
|
+
Instruction::LocalGet(SELF).encode(&mut body);
|
|
1927
|
+
Instruction::ArrayLen.encode(&mut body);
|
|
1928
|
+
Instruction::LocalSet(OLD_LEN).encode(&mut body);
|
|
1929
|
+
|
|
1930
|
+
Instruction::LocalGet(OLD_LEN).encode(&mut body);
|
|
1931
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
1932
|
+
Instruction::I32Add.encode(&mut body);
|
|
1933
|
+
Instruction::ArrayNewDefault(array_type_idx).encode(&mut body);
|
|
1934
|
+
Instruction::LocalSet(NEW_ARR).encode(&mut body);
|
|
1935
|
+
|
|
1936
|
+
// array.copy(dst=new_arr, dst_offset=0, src=self, src_offset=0, len=old_len)
|
|
1937
|
+
Instruction::LocalGet(NEW_ARR).encode(&mut body);
|
|
1938
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
1939
|
+
Instruction::LocalGet(SELF).encode(&mut body);
|
|
1940
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
1941
|
+
Instruction::LocalGet(OLD_LEN).encode(&mut body);
|
|
1942
|
+
Instruction::ArrayCopy { array_type_index_dst: array_type_idx, array_type_index_src: array_type_idx }.encode(&mut body);
|
|
1943
|
+
|
|
1944
|
+
// new_arr[old_len] = v
|
|
1945
|
+
Instruction::LocalGet(NEW_ARR).encode(&mut body);
|
|
1946
|
+
Instruction::LocalGet(OLD_LEN).encode(&mut body);
|
|
1947
|
+
Instruction::LocalGet(V).encode(&mut body);
|
|
1948
|
+
Instruction::ArraySet(array_type_idx).encode(&mut body);
|
|
1949
|
+
|
|
1950
|
+
Instruction::LocalGet(NEW_ARR).encode(&mut body);
|
|
1951
|
+
Instruction::End.encode(&mut body);
|
|
1952
|
+
Some(body)
|
|
1953
|
+
}
|
|
1954
|
+
"get" => {
|
|
1955
|
+
let mut body = vec![0u8];
|
|
1956
|
+
Instruction::LocalGet(0).encode(&mut body); // self
|
|
1957
|
+
Instruction::LocalGet(1).encode(&mut body); // i
|
|
1958
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
1959
|
+
Instruction::ArrayGet(array_type_idx).encode(&mut body);
|
|
1960
|
+
// Narrow the raw `anyref` element back to `T`'s own concrete type —
|
|
1961
|
+
// skipped when `T` itself resolves to a bare `anyref` (nothing to cast to).
|
|
1962
|
+
if let Some(ValType::Ref(RefType { heap_type: HeapType::Concrete(idx), .. })) =
|
|
1963
|
+
f.returns.as_ref().and_then(|t| astTypeToWasm(&t.name))
|
|
1964
|
+
{
|
|
1965
|
+
Instruction::RefCastNullable(HeapType::Concrete(idx)).encode(&mut body);
|
|
1966
|
+
}
|
|
1967
|
+
Instruction::End.encode(&mut body);
|
|
1968
|
+
Some(body)
|
|
1969
|
+
}
|
|
1970
|
+
"set" => {
|
|
1971
|
+
let mut body = vec![0u8];
|
|
1972
|
+
Instruction::LocalGet(0).encode(&mut body); // self
|
|
1973
|
+
Instruction::LocalGet(1).encode(&mut body); // i
|
|
1974
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
1975
|
+
Instruction::LocalGet(2).encode(&mut body); // v
|
|
1976
|
+
Instruction::ArraySet(array_type_idx).encode(&mut body);
|
|
1977
|
+
Instruction::End.encode(&mut body);
|
|
1978
|
+
Some(body)
|
|
1979
|
+
}
|
|
1980
|
+
"length" => {
|
|
1981
|
+
let mut body = vec![0u8];
|
|
1982
|
+
Instruction::LocalGet(0).encode(&mut body); // self
|
|
1983
|
+
Instruction::ArrayLen.encode(&mut body);
|
|
1984
|
+
Instruction::I64ExtendI32U.encode(&mut body);
|
|
1985
|
+
Instruction::End.encode(&mut body);
|
|
1986
|
+
Some(body)
|
|
1987
|
+
}
|
|
1988
|
+
_ => None,
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1841
1992
|
/// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`.
|
|
1842
1993
|
fn paramPlumType(pt: &ast::ParamType) -> PlumType {
|
|
1843
1994
|
match pt {
|