plum
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 +24 -15
- plum-std/List.plum +9 -0
- plum-wasm-codegen/tests/examples_test.rs +33 -0
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<
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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.
|