plum

#treesitter#compiler#wasm

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

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


25dce5c — Peter John 2026-11-01T22:39:21+05:30
docs/test: Array[T] value arrays are unboxed (i64/f64/i32); stale anyref comment fixed
plum-std/Array.plum CHANGED
@@ -4,16 +4,17 @@ import std/Float
4
4
  import std/Str
5
5
 
6
6
  # A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
7
+ # array. `T` is NOT erased: codegen allocates one wasm array type per distinct
8
+ # element representation (see `plum-wasm-codegen`'s native-arrays loop):
9
+ # `Array[Int]` is `array<mut i64>`, `Array[Float]` is `array<mut f64>`,
10
+ # `Array[Bool]`/`Array[Byte]` are `array<mut i32>`, and a reference `T` is
7
- # `array<anyref>` — `Array[T]` is erased, so every `T` shares the SAME
11
+ # `array<mut ref null T>` (all reference types of the same wasm type share one
8
- # underlying wasm array type (see
9
- # `plum-wasm-codegen::GcTypeRegistry::array_type_idx`), exactly like `[]Byte`
10
- # reuses `Str`'s own array type. `init`/`get`/`set`/`length`/`push`/`resize` are compiler
12
+ # array type). `get`/`set`/`push`/`resize`/`length`/`filled` on the value
13
+ # arrays are plain `array.get`/`array.set`/`array.copy` on the raw elements —
11
- # intrinsics (see `compileIntrinsicFnBody`) — there's no way to express raw
14
+ # nothing is boxed or allocated per element. `init`/`get`/`set`/`length`/
15
+ # `push`/`resize` are compiler intrinsics (see `compileIntrinsicFnBody`) —
12
- # array allocation/indexing in Plum source itself.
16
+ # there's no way to express raw array allocation/indexing in Plum source itself.
13
- #
14
- # Elements are stored as `anyref`; `T` is erased, so a primitive `T` (`Int`,
15
- # `Float`, ...) is boxed on `set`/`push` and unboxed on `get` at the call
16
- # site, like any other generic edge. `List[T]` is built on this.
17
+ # `List[T]` is built on this.
17
18
  #
18
19
  # `init()` is deliberately zero-arg (no `Array[T](n)` constructor exists) —
19
20
  # `class_call`'s grammar (`Type[Generics](...)`) only ever accepts `name:
@@ -26,16 +27,14 @@ import std/Str
26
27
  # fixed-length allocation once made), so it allocates a new, one-longer array
27
28
  # and copies, same amortized-growth idea as `Buffer`'s own `write`.
28
29
  #
29
- # New elements start out null-shaped — really a null `anyref` under the
30
+ # New slots are the wasm default: null for a reference `T` (so `get` on a
30
- # hood — so `get` on an index that was never `set` traps rather than
31
- # returning some default `T` value (there is no way to conjure a default `T`
31
+ # never-`set` index traps), zero for a value `T` (`Int`/`Float`/`Bool`/`Byte`).
32
- # generically).
33
32
  primitive Array[T] =
34
33
  # A new, empty array.
35
34
  fun init() -> Array[T] =
36
35
  todo
37
36
 
38
- # The element at index `i`. Traps if `i` is out of range or was never `set`.
37
+ # The element at index `i`. Traps if `i` is out of range (or, for a reference `T`, was never `set`).
39
38
  fun get(self, i: Int) -> T =
40
39
  todo
41
40
 
@@ -82,3 +81,13 @@ test "Array is a primitive with working generic methods"
82
81
  assert a.get(1) == "y"
83
82
  b := Array.filled(3, 7)
84
83
  assert b.at(2) == 7
84
+
85
+ test "Array[Int] holds a million elements unboxed"
86
+ a := Array.filled(1000000, 0)
87
+ i := 0
88
+ while i < 1000000
89
+ a.set(i, i * 2)
90
+ i = i + 1
91
+ assert a.length() == 1000000
92
+ assert a.get(999999) == 1999998
93
+ assert a.get(0) == 0
plum-std/List.plum CHANGED
@@ -373,3 +373,12 @@ test "list of strings keeps its elements through growth"
373
373
  i = i + 1
374
374
  assert l.get(39)! == "s39"
375
375
  assert l.get(16)! == "s16"
376
+
377
+ test "List[Float] push/get round-trips"
378
+ l := List[Float]()
379
+ i := 0
380
+ while i < 1000
381
+ l.push(Float(i) + 0.5)
382
+ i = i + 1
383
+ assert l.length() == 1000
384
+ assert l.get(999)! == 999.5
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -231,6 +231,39 @@ fn array_int_native() {
231
231
  assert!(elems.contains(&ValType::I32), "no (mut i32) array in {:?}", elems);
232
232
  }
233
233
 
234
+ /// Game-perf 1: no mutable array is an erased abstract-ref array, and
235
+ /// `Array[Int]`/`Array[Float]` element storage is exactly i64/f64.
236
+ #[test]
237
+ fn array_elements_are_never_erased_anyref() {
238
+ use wasmparser::{CompositeInnerType, HeapType, Parser, Payload, StorageType, ValType};
239
+ let bytes = assertCompiles("typed_arrays.plum");
240
+ let mut mut_elems: Vec<StorageType> = Vec::new();
241
+ for payload in Parser::new(0).parse_all(&bytes) {
242
+ if let Payload::TypeSection(reader) = payload.unwrap() {
243
+ for rec in reader {
244
+ for sub in rec.unwrap().into_types() {
245
+ if let CompositeInnerType::Array(a) = &sub.composite_type.inner {
246
+ if a.0.mutable {
247
+ mut_elems.push(a.0.element_type);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ for e in &mut_elems {
255
+ if let StorageType::Val(ValType::Ref(r)) = e {
256
+ assert!(
257
+ !matches!(r.heap_type(), HeapType::Abstract { .. }),
258
+ "mutable array of erased abstract ref: {:?}",
259
+ mut_elems
260
+ );
261
+ }
262
+ }
263
+ assert!(mut_elems.contains(&StorageType::Val(ValType::I64)));
264
+ assert!(mut_elems.contains(&StorageType::Val(ValType::F64)));
265
+ }
266
+
234
267
  /// L1 `const_table_1000_entries`: 1000 associated consts (spread over two
235
268
  /// bodies, before and between other decls) plus a 1000-element list literal
236
269
  /// parse, type-check and are usable inside methods.