plum

#treesitter#compiler#wasm

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

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


ea9b8c1Peter 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 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](items: List[Pair[Str, Json]](head: None, tail: None, size: 0))
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
- # A Map is an association list O(n) `get`/`set`/`remove` via a linear scan
36
+ # Number of buckets every `Map` allocates at construction fixed for the
22
- # over `items`, not a real hash table (there's no hashing primitive to build
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
- # No `Map`-level `init` constructor here (unlike `List`'s own variadic
38
+ # would grow this as `size` outpaces it (keeping average bucket length, and
26
- # `init`, or `Buffer`'s no-arg one) — a generic method's own type params can
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
- # only be inferred from a param whose DECLARED type is directly a bare
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
- # `resolveFnInstantiation` in `plum-checker/src/monomorphize.rs`), so a
45
+ # `get`/`set`/`remove` (versus the single, whole-map linear scan a plain
31
- # variadic-pairs constructor can't be written. Construct a `Map` directly
46
+ # association list costs), degrading to O(n) only if many keys collide into
32
- # with explicit generics instead:
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
- items: List[Pair[K, V]]
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. NOT written with `for p :=
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.items.head
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 `items`
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`. Destructures only
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 := self.items.head
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
- self.items.add(Pair(key: k, val: v))
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. Keeps `node` itself bound (rather than
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 := self.items.head
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
- self.items.unlink(node)
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.items.length()
128
+ return self.size
99
129
 
100
130
  fun isEmpty(self) -> Bool =
101
- return self.items.length() == 0
131
+ return self.size == 0
102
132
 
103
133
  fun clear(self) -> Unit =
134
+ i := 0
135
+ while i < self.buckets.length()
104
- self.items.clear()
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, in insertion order.
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
- current := self.items.head
146
+ current := self.buckets.get(i).head
109
- while current != None
147
+ while current != None
110
- match current
148
+ match current
111
- Some(Node(Pair(key, val), _, next)) =>
149
+ Some(Node(Pair(key, val), _, next)) =>
112
- cb(key, val)
150
+ cb(key, val)
113
- current = next
151
+ current = next
114
- None =>
152
+ None =>
115
- break
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
- result.add(key)
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
- result.add(val)
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[X, Y](items: List[Pair[X, Y]](head: None, tail: None, size: 0))
172
+ result := Map(buckets: b, size: 0)
173
+ j := 0
174
+ while j < self.buckets.length()
149
- current := self.items.head
175
+ current := self.buckets.get(j).head
150
- while current != None
176
+ while current != None
151
- match current
177
+ match current
152
- Some(Node(Pair(key, val), _, next)) =>
178
+ Some(Node(Pair(key, val), _, next)) =>
179
+ pair := cb(key, val)
153
- result.items.add(cb(key, val))
180
+ result.set(pair.key, pair.val)
154
- current = next
181
+ current = next
155
- None =>
182
+ None =>
156
- break
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 := makeStrIntMapForTest()
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 := makeStrIntMapForTest()
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 := makeStrIntMapForTest()
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 in insertion order"
214
+ test "keys/values list every entry's key/value"
190
- m := makeStrIntMapForTest()
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().join(",") == "a,b"
219
+ assert m.keys().contains("a")
194
- assert m.values().join(",") == "1,2"
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 := makeStrIntMapForTest()
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.join(",") == "1,2"
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 := makeStrIntMapForTest()
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
- class_type_idx.insert(c.name.clone(), slots.len() as u32);
725
+ class_type_idx.insert(c.name.clone(), slots.len() as u32);
698
- slots.push(Slot::Class(c.name.clone()));
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 {