plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
609556f
— Peter John
2026-11-01T22:44:34+05:30
perf: Array[struct] stored as struct-of-arrays (one native array per leaf), no per-element boxing
- README.md +1 -1
- plum-examples/struct_arrays.plum +110 -0
- plum-std/Array.plum +8 -1
- plum-wasm-codegen/src/lib.rs +166 -0
- plum-wasm-codegen/tests/codegen_tests.rs +84 -0
README.md
CHANGED
|
@@ -418,7 +418,7 @@ u2 := u
|
|
|
418
418
|
u2.age = 4 # u.age is now 4: a record is a heap object, shared on assignment
|
|
419
419
|
```
|
|
420
420
|
|
|
421
|
-
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed inside `T?` and other generic slots.
|
|
421
|
+
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed inside `T?` and other generic slots. Arrays of structs (`Array[Vec2]`, and so `List[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. A `record` may have any field types and may be generic (`record Pair[K, V](key: K, val: V)`). Methods and `match` work as for any enum. See [`plum-examples/values.plum`](plum-examples/values.plum).
|
|
422
422
|
|
|
423
423
|
### Enums
|
|
424
424
|
|
plum-examples/struct_arrays.plum
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Arrays of structs are stored field by field (struct-of-arrays): `Array[Vec2]`
|
|
2
|
+
# is one object holding an `f64` array for `x` and one for `y`, so `get`/`set`
|
|
3
|
+
# allocate nothing per element. `List`, `Grid` and friends get this for free.
|
|
4
|
+
module struct_arrays
|
|
5
|
+
import std/Str
|
|
6
|
+
import std/Int
|
|
7
|
+
import std/Float
|
|
8
|
+
import std/Array
|
|
9
|
+
import std/List
|
|
10
|
+
import std/Grid
|
|
11
|
+
|
|
12
|
+
struct Vec2(x: Float, y: Float)
|
|
13
|
+
|
|
14
|
+
struct Rect(pos: Vec2, size: Vec2) =
|
|
15
|
+
|
|
16
|
+
fun area(self) -> Float =
|
|
17
|
+
self.size.x * self.size.y
|
|
18
|
+
|
|
19
|
+
struct Tile(id: Int, solid: Bool, weight: Float)
|
|
20
|
+
|
|
21
|
+
record World(points: List[Vec2], name: Str)
|
|
22
|
+
|
|
23
|
+
test "List[Vec2] push/get/set round-trips across growth"
|
|
24
|
+
l := List[Vec2]()
|
|
25
|
+
for i := range 100
|
|
26
|
+
l.push(Vec2(x: Float(i), y: Float(i) * 2.0))
|
|
27
|
+
assert l.length() == 100
|
|
28
|
+
v := l.get(37)!
|
|
29
|
+
assert v.x == 37.0
|
|
30
|
+
assert v.y == 74.0
|
|
31
|
+
l.set(37, Vec2(x: v.x + 1.0, y: v.y))
|
|
32
|
+
assert l.get(37)!.x == 38.0
|
|
33
|
+
assert l.get(37)!.y == 74.0
|
|
34
|
+
assert l.get(36)!.x == 36.0
|
|
35
|
+
assert l.get(99)!.y == 198.0
|
|
36
|
+
|
|
37
|
+
test "nested struct (Rect of Vec2s) in an Array"
|
|
38
|
+
a := Array[Rect]()
|
|
39
|
+
a = a.push(Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0)))
|
|
40
|
+
a = a.push(Rect(pos: Vec2(x: 5.0, y: 6.0), size: Vec2(x: 7.0, y: 8.0)))
|
|
41
|
+
assert a.length() == 2
|
|
42
|
+
r := a.get(1)
|
|
43
|
+
assert r.pos.x == 5.0
|
|
44
|
+
assert r.pos.y == 6.0
|
|
45
|
+
assert r.size.x == 7.0
|
|
46
|
+
assert r.size.y == 8.0
|
|
47
|
+
a.set(0, Rect(pos: r.size, size: r.pos))
|
|
48
|
+
assert a.get(0).pos.y == 8.0
|
|
49
|
+
assert a.get(0).size.x == 5.0
|
|
50
|
+
assert a.get(0).area() == 30.0
|
|
51
|
+
|
|
52
|
+
test "struct with Int, Bool and Float leaves"
|
|
53
|
+
a := Array[Tile]()
|
|
54
|
+
a = a.push(Tile(id: 7, solid: true, weight: 1.5))
|
|
55
|
+
a = a.push(Tile(id: 9, solid: false, weight: 2.5))
|
|
56
|
+
assert a.get(0).id == 7
|
|
57
|
+
assert a.get(0).solid
|
|
58
|
+
assert a.get(0).weight == 1.5
|
|
59
|
+
assert a.get(1).id == 9
|
|
60
|
+
assert !a.get(1).solid
|
|
61
|
+
assert a.get(1).weight == 2.5
|
|
62
|
+
b := a.resize(4)
|
|
63
|
+
assert b.length() == 4
|
|
64
|
+
assert b.get(1).id == 9
|
|
65
|
+
assert b.get(3).id == 0
|
|
66
|
+
assert !b.get(3).solid
|
|
67
|
+
|
|
68
|
+
test "copy semantics: a value read from the array is a copy until set"
|
|
69
|
+
l := List[Vec2]()
|
|
70
|
+
l.push(Vec2(x: 1.0, y: 1.0))
|
|
71
|
+
v := l.get(0)!
|
|
72
|
+
v = Vec2(x: 99.0, y: 99.0)
|
|
73
|
+
assert l.get(0)!.x == 1.0
|
|
74
|
+
w := l.get(0)!
|
|
75
|
+
l.set(0, Vec2(x: 5.0, y: 5.0))
|
|
76
|
+
assert w.x == 1.0
|
|
77
|
+
assert l.get(0)!.x == 5.0
|
|
78
|
+
assert v.x == 99.0
|
|
79
|
+
|
|
80
|
+
test "for x := range over Array[Vec2]"
|
|
81
|
+
a := Array[Vec2]().push(Vec2(x: 1.0, y: 10.0)).push(Vec2(x: 2.0, y: 20.0)).push(Vec2(x: 3.0, y: 30.0))
|
|
82
|
+
sx := 0.0
|
|
83
|
+
sy := 0.0
|
|
84
|
+
for v := range a
|
|
85
|
+
sx = sx + v.x
|
|
86
|
+
sy = sy + v.y
|
|
87
|
+
assert sx == 6.0
|
|
88
|
+
assert sy == 60.0
|
|
89
|
+
|
|
90
|
+
test "Array.filled with a struct"
|
|
91
|
+
a := Array.filled(5, Vec2(x: 2.0, y: 3.0))
|
|
92
|
+
assert a.length() == 5
|
|
93
|
+
assert a.get(4).y == 3.0
|
|
94
|
+
|
|
95
|
+
test "Grid[Tile] get/set"
|
|
96
|
+
g := Grid.filled(3, 2, Tile(id: 1, solid: false, weight: 0.5))
|
|
97
|
+
g.set(2, 1, Tile(id: 42, solid: true, weight: 9.0))
|
|
98
|
+
assert g.get(2, 1).id == 42
|
|
99
|
+
assert g.get(2, 1).solid
|
|
100
|
+
assert g.get(0, 0).id == 1
|
|
101
|
+
assert g.get(1, 1).weight == 0.5
|
|
102
|
+
|
|
103
|
+
test "a List[Vec2] inside a record field"
|
|
104
|
+
w := World(points: List[Vec2](), name: "w")
|
|
105
|
+
w.points.push(Vec2(x: 1.0, y: 2.0))
|
|
106
|
+
w.points.push(Vec2(x: 3.0, y: 4.0))
|
|
107
|
+
assert w.points.length() == 2
|
|
108
|
+
w.points.set(1, Vec2(x: 30.0, y: 40.0))
|
|
109
|
+
assert w.points.get(1)!.x == 30.0
|
|
110
|
+
assert w.points.get(0)!.y == 2.0
|
plum-std/Array.plum
CHANGED
|
@@ -11,7 +11,14 @@ import std/Str
|
|
|
11
11
|
# `array<mut ref null T>` (all reference types of the same wasm type share one
|
|
12
12
|
# array type). `get`/`set`/`push`/`resize`/`length`/`filled` on the value
|
|
13
13
|
# arrays are plain `array.get`/`array.set`/`array.copy` on the raw elements —
|
|
14
|
+
# nothing is boxed or allocated per element. `Array[S]` for a `struct` S is
|
|
15
|
+
# stored struct-of-arrays: ONE GC struct holding one native array per leaf of
|
|
16
|
+
# S (`Array[Vec2]` is `{xs: array<mut f64>, ys: array<mut f64>}`, a nested
|
|
17
|
+
# `Rect(pos: Vec2, size: Vec2)` has four f64 arrays, Bool leaves are i32
|
|
18
|
+
# arrays). `get` reads each leaf array and leaves the flattened value on the
|
|
19
|
+
# stack, `set` writes each leaf, `push`/`resize` allocate the leaf arrays
|
|
20
|
+
# together: no per-element allocation. (Records and other reference types
|
|
14
|
-
#
|
|
21
|
+
# stay arrays of references.) `init`/`get`/`set`/`length`/
|
|
15
22
|
# `push`/`resize` are compiler intrinsics (see `compileIntrinsicFnBody`) —
|
|
16
23
|
# there's no way to express raw array allocation/indexing in Plum source itself.
|
|
17
24
|
# `List[T]` is built on this.
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -773,6 +773,12 @@ fn nullableErr(ty: &PlumType) -> Option<&PlumType> {
|
|
|
773
773
|
plum_checker::nullableErrPayload(ty).filter(|p| !isValueTy(p))
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
+
/// `Array[S]` for a `struct` S (with at least one leaf) is stored field by
|
|
777
|
+
/// field: one GC struct holding one native array per leaf.
|
|
778
|
+
fn isSoaElem(ty: &PlumType) -> bool {
|
|
779
|
+
matches!(ty, PlumType::TNamed(_)) && valueFieldsOf(ty).is_some() && leafCount(ty) > 0
|
|
780
|
+
}
|
|
781
|
+
|
|
776
782
|
fn isValueTy(ty: &PlumType) -> bool {
|
|
777
783
|
valueFieldsOf(ty).is_some()
|
|
778
784
|
}
|
|
@@ -935,6 +941,9 @@ pub struct GcTypeRegistry {
|
|
|
935
941
|
/// harmlessly never read as such) until/unless the program actually
|
|
936
942
|
/// instantiates `Array` at least once.
|
|
937
943
|
pub array_type_idx: u32,
|
|
944
|
+
/// Leaf wasm value type -> native `array<mut leaf>` type index; used by the
|
|
945
|
+
/// struct-of-arrays layout of `Array[S]` for a `struct` S.
|
|
946
|
+
pub leaf_array_idx: HashMap<ValType, u32>,
|
|
938
947
|
/// Helper `(arr: array<i8>) -> Str` building `Str{arr}`; `None` when the program has no `Str`.
|
|
939
948
|
pub mk_str_fn: Option<u32>,
|
|
940
949
|
/// `!T` (keyed by `T`) -> its heap box struct `{leaves of T..., code: i32}`.
|
|
@@ -1063,6 +1072,7 @@ fn buildGcTypeRegistry(
|
|
|
1063
1072
|
EnumSuper(PlumType),
|
|
1064
1073
|
Variant(PlumType, String),
|
|
1065
1074
|
ArrayRef(ValType),
|
|
1075
|
+
ArraySoa(Vec<ValType>),
|
|
1066
1076
|
ErrBox(PlumType),
|
|
1067
1077
|
}
|
|
1068
1078
|
let mut slots: Vec<Slot> = vec![Slot::Str];
|
|
@@ -1079,6 +1089,7 @@ fn buildGcTypeRegistry(
|
|
|
1079
1089
|
closure_type_idx: 0,
|
|
1080
1090
|
variadic_array_type_idx: HashMap::new(),
|
|
1081
1091
|
array_type_idx: 0,
|
|
1092
|
+
leaf_array_idx: HashMap::new(),
|
|
1082
1093
|
mk_str_fn: None,
|
|
1083
1094
|
err_box: HashMap::new(),
|
|
1084
1095
|
};
|
|
@@ -1152,6 +1163,23 @@ fn buildGcTypeRegistry(
|
|
|
1152
1163
|
let mut by_vt: HashMap<ValType, u32> = HashMap::new();
|
|
1153
1164
|
for a in &arrays {
|
|
1154
1165
|
let PlumType::TApp(_, args) = a else { continue };
|
|
1166
|
+
if isSoaElem(&args[0]) {
|
|
1167
|
+
// Struct-of-arrays: one native array per leaf, held by one struct.
|
|
1168
|
+
let leaves = flatValtypes(&args[0], ®);
|
|
1169
|
+
for vt in &leaves {
|
|
1170
|
+
let idx = *by_vt.entry(*vt).or_insert_with(|| {
|
|
1171
|
+
let idx = slots.len() as u32;
|
|
1172
|
+
slots.push(Slot::ArrayRef(*vt));
|
|
1173
|
+
idx
|
|
1174
|
+
});
|
|
1175
|
+
reg.leaf_array_idx.insert(*vt, idx);
|
|
1176
|
+
}
|
|
1177
|
+
let idx = slots.len() as u32;
|
|
1178
|
+
slots.push(Slot::ArraySoa(leaves));
|
|
1179
|
+
reg.app_super.insert(a.clone(), idx);
|
|
1180
|
+
reg.app_class.insert(a.clone(), idx);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1155
1183
|
let vt = plumTypeToGcValtype(&args[0], ®);
|
|
1156
1184
|
let idx = *by_vt.entry(vt).or_insert_with(|| {
|
|
1157
1185
|
let idx = slots.len() as u32;
|
|
@@ -1258,6 +1286,23 @@ fn buildGcTypeRegistry(
|
|
|
1258
1286
|
},
|
|
1259
1287
|
}
|
|
1260
1288
|
}
|
|
1289
|
+
Slot::ArraySoa(leaves) => SubType {
|
|
1290
|
+
is_final: true,
|
|
1291
|
+
supertype_idx: None,
|
|
1292
|
+
composite_type: CompositeType {
|
|
1293
|
+
inner: CompositeInnerType::Struct(StructType {
|
|
1294
|
+
fields: leaves
|
|
1295
|
+
.iter()
|
|
1296
|
+
.map(|vt| FieldType {
|
|
1297
|
+
element_type: StorageType::Val(gcRef(reg.leaf_array_idx[vt])),
|
|
1298
|
+
mutable: false,
|
|
1299
|
+
})
|
|
1300
|
+
.collect::<Vec<_>>()
|
|
1301
|
+
.into(),
|
|
1302
|
+
}),
|
|
1303
|
+
shared: false,
|
|
1304
|
+
},
|
|
1305
|
+
},
|
|
1261
1306
|
Slot::ArrayRef(vt) => SubType {
|
|
1262
1307
|
is_final: true,
|
|
1263
1308
|
supertype_idx: None,
|
|
@@ -2790,6 +2835,9 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
|
|
|
2790
2835
|
let array_elem = elem.clone();
|
|
2791
2836
|
let array_ty = PlumType::TApp("Array".to_string(), vec![elem]);
|
|
2792
2837
|
let array_type_idx = withGcTypes(|r| r.superIdx(&array_ty))?;
|
|
2838
|
+
if isSoaElem(&array_elem) {
|
|
2839
|
+
return compileSoaArrayIntrinsic(f.name.as_str(), &array_elem, array_type_idx);
|
|
2840
|
+
}
|
|
2793
2841
|
match f.name.as_str() {
|
|
2794
2842
|
// init() -> Array[T] — always starts empty; see `push` for how a
|
|
2795
2843
|
// specific length is actually built up.
|
|
@@ -2933,6 +2981,124 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
|
|
|
2933
2981
|
}
|
|
2934
2982
|
}
|
|
2935
2983
|
|
|
2984
|
+
/// `Array[S]` (S a `struct`) intrinsics for the struct-of-arrays layout: the array
|
|
2985
|
+
/// is one GC struct whose field `k` is the native array of leaf `k`. `get`/`set`
|
|
2986
|
+
/// read/write each leaf array directly (no allocation, no box).
|
|
2987
|
+
fn compileSoaArrayIntrinsic(name: &str, elem: &PlumType, soa_idx: u32) -> Option<Vec<u8>> {
|
|
2988
|
+
let leaves = withGcTypes(|r| flatValtypes(elem, r));
|
|
2989
|
+
let leaf_arrs: Vec<u32> = withGcTypes(|r| leaves.iter().map(|vt| r.leaf_array_idx[vt]).collect());
|
|
2990
|
+
let n = leaves.len() as u32;
|
|
2991
|
+
let mut body = Vec::new();
|
|
2992
|
+
// Locals: declared groups, one entry per local (all single).
|
|
2993
|
+
let declare = |body: &mut Vec<u8>, tys: &[ValType]| {
|
|
2994
|
+
body.extend(encodeLeb128U32(tys.len() as u32));
|
|
2995
|
+
for t in tys {
|
|
2996
|
+
body.extend(encodeLeb128U32(1));
|
|
2997
|
+
t.encode(body);
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
// Reads leaf array `k` of the soa struct held in local `src`.
|
|
3001
|
+
let leaf_arr = |body: &mut Vec<u8>, src: u32, k: u32| {
|
|
3002
|
+
Instruction::LocalGet(src).encode(body);
|
|
3003
|
+
Instruction::StructGet { struct_type_index: soa_idx, field_index: k }.encode(body);
|
|
3004
|
+
};
|
|
3005
|
+
match name {
|
|
3006
|
+
"init" => {
|
|
3007
|
+
body.push(0);
|
|
3008
|
+
for a in &leaf_arrs {
|
|
3009
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
3010
|
+
Instruction::ArrayNewDefault(*a).encode(&mut body);
|
|
3011
|
+
}
|
|
3012
|
+
Instruction::StructNew(soa_idx).encode(&mut body);
|
|
3013
|
+
}
|
|
3014
|
+
"length" => {
|
|
3015
|
+
body.push(0);
|
|
3016
|
+
leaf_arr(&mut body, 0, 0);
|
|
3017
|
+
Instruction::ArrayLen.encode(&mut body);
|
|
3018
|
+
Instruction::I64ExtendI32U.encode(&mut body);
|
|
3019
|
+
}
|
|
3020
|
+
"get" => {
|
|
3021
|
+
body.push(0);
|
|
3022
|
+
for k in 0..n {
|
|
3023
|
+
leaf_arr(&mut body, 0, k);
|
|
3024
|
+
Instruction::LocalGet(1).encode(&mut body);
|
|
3025
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
3026
|
+
Instruction::ArrayGet(leaf_arrs[k as usize]).encode(&mut body);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
"set" => {
|
|
3030
|
+
body.push(0);
|
|
3031
|
+
for k in 0..n {
|
|
3032
|
+
leaf_arr(&mut body, 0, k);
|
|
3033
|
+
Instruction::LocalGet(1).encode(&mut body);
|
|
3034
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
3035
|
+
Instruction::LocalGet(2 + k).encode(&mut body);
|
|
3036
|
+
Instruction::ArraySet(leaf_arrs[k as usize]).encode(&mut body);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
"push" | "resize" => {
|
|
3040
|
+
let push = name == "push";
|
|
3041
|
+
// params: self=0, then v leaves (push) or n (resize, one i64 at 1).
|
|
3042
|
+
let nparams = if push { 1 + n } else { 2 };
|
|
3043
|
+
let old_len = nparams;
|
|
3044
|
+
let new_len = nparams + 1;
|
|
3045
|
+
let copy_len = nparams + 2;
|
|
3046
|
+
let first_arr = nparams + 3;
|
|
3047
|
+
let mut tys = vec![ValType::I32, ValType::I32, ValType::I32];
|
|
3048
|
+
tys.extend(leaf_arrs.iter().map(|a| gcRef(*a)));
|
|
3049
|
+
declare(&mut body, &tys);
|
|
3050
|
+
leaf_arr(&mut body, 0, 0);
|
|
3051
|
+
Instruction::ArrayLen.encode(&mut body);
|
|
3052
|
+
Instruction::LocalSet(old_len).encode(&mut body);
|
|
3053
|
+
if push {
|
|
3054
|
+
Instruction::LocalGet(old_len).encode(&mut body);
|
|
3055
|
+
Instruction::I32Const(1).encode(&mut body);
|
|
3056
|
+
Instruction::I32Add.encode(&mut body);
|
|
3057
|
+
} else {
|
|
3058
|
+
Instruction::LocalGet(1).encode(&mut body);
|
|
3059
|
+
Instruction::I32WrapI64.encode(&mut body);
|
|
3060
|
+
}
|
|
3061
|
+
Instruction::LocalSet(new_len).encode(&mut body);
|
|
3062
|
+
if push {
|
|
3063
|
+
Instruction::LocalGet(old_len).encode(&mut body);
|
|
3064
|
+
} else {
|
|
3065
|
+
Instruction::LocalGet(old_len).encode(&mut body);
|
|
3066
|
+
Instruction::LocalGet(new_len).encode(&mut body);
|
|
3067
|
+
Instruction::LocalGet(old_len).encode(&mut body);
|
|
3068
|
+
Instruction::LocalGet(new_len).encode(&mut body);
|
|
3069
|
+
Instruction::I32LtU.encode(&mut body);
|
|
3070
|
+
Instruction::Select.encode(&mut body);
|
|
3071
|
+
}
|
|
3072
|
+
Instruction::LocalSet(copy_len).encode(&mut body);
|
|
3073
|
+
for k in 0..n {
|
|
3074
|
+
let a = leaf_arrs[k as usize];
|
|
3075
|
+
Instruction::LocalGet(new_len).encode(&mut body);
|
|
3076
|
+
Instruction::ArrayNewDefault(a).encode(&mut body);
|
|
3077
|
+
Instruction::LocalSet(first_arr + k).encode(&mut body);
|
|
3078
|
+
Instruction::LocalGet(first_arr + k).encode(&mut body);
|
|
3079
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
3080
|
+
leaf_arr(&mut body, 0, k);
|
|
3081
|
+
Instruction::I32Const(0).encode(&mut body);
|
|
3082
|
+
Instruction::LocalGet(copy_len).encode(&mut body);
|
|
3083
|
+
Instruction::ArrayCopy { array_type_index_dst: a, array_type_index_src: a }.encode(&mut body);
|
|
3084
|
+
if push {
|
|
3085
|
+
Instruction::LocalGet(first_arr + k).encode(&mut body);
|
|
3086
|
+
Instruction::LocalGet(old_len).encode(&mut body);
|
|
3087
|
+
Instruction::LocalGet(1 + k).encode(&mut body);
|
|
3088
|
+
Instruction::ArraySet(a).encode(&mut body);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
for k in 0..n {
|
|
3092
|
+
Instruction::LocalGet(first_arr + k).encode(&mut body);
|
|
3093
|
+
}
|
|
3094
|
+
Instruction::StructNew(soa_idx).encode(&mut body);
|
|
3095
|
+
}
|
|
3096
|
+
_ => return None,
|
|
3097
|
+
}
|
|
3098
|
+
Instruction::End.encode(&mut body);
|
|
3099
|
+
Some(body)
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2936
3102
|
/// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`;
|
|
2937
3103
|
/// a single-letter generic name is a `TParam`.
|
|
2938
3104
|
/// The enum type (`TNamed`/`TApp`) a variant-constructing expression produces.
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -3267,3 +3267,87 @@ fn bufferToStrLengthExactAfterDoubling() {
|
|
|
3267
3267
|
let src = "import std/Str\nimport std/Buffer\nimport std/Int\nimport std/Float\n\nfun main() -> Int =\n b := Buffer()\n i := 0\n while i < 37\n b.write(\"abc\")\n i = i + 1\n b.toStr().length() * 1000 + b.length()\n";
|
|
3268
3268
|
assert_eq!(runWithCrash("bufexact", src).0.unwrap(), 111111);
|
|
3269
3269
|
}
|
|
3270
|
+
|
|
3271
|
+
// ---- struct-of-arrays layout for Array[struct] ----
|
|
3272
|
+
|
|
3273
|
+
#[test]
|
|
3274
|
+
fn array_of_struct_is_struct_of_f64_arrays_and_never_boxes() {
|
|
3275
|
+
let src = "\
|
|
3276
|
+
import std/Str
|
|
3277
|
+
import std/Int
|
|
3278
|
+
import std/Float
|
|
3279
|
+
import std/Array
|
|
3280
|
+
|
|
3281
|
+
struct Vec2(x: Float, y: Float)
|
|
3282
|
+
|
|
3283
|
+
fun main() -> Float =
|
|
3284
|
+
a := Array[Vec2]().push(Vec2(x: 1.0, y: 2.0)).push(Vec2(x: 3.0, y: 4.0))
|
|
3285
|
+
v := a.get(1)
|
|
3286
|
+
a.set(0, Vec2(x: v.x + 1.0, y: v.y))
|
|
3287
|
+
b := a.resize(5)
|
|
3288
|
+
a.get(0).x + b.get(1).y
|
|
3289
|
+
";
|
|
3290
|
+
let dir = std::env::temp_dir().join(format!("plum_soa_{}", std::process::id()));
|
|
3291
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
3292
|
+
let path = dir.join("main.plum");
|
|
3293
|
+
std::fs::write(&path, src).unwrap();
|
|
3294
|
+
let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
|
|
3295
|
+
plum_checker::checkSource(&source).expect("check");
|
|
3296
|
+
let bytes = compileSource(&source).expect("compile failed");
|
|
3297
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
3298
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
3299
|
+
|
|
3300
|
+
use wasmparser::{CompositeInnerType, HeapType, Operator, StorageType, ValType};
|
|
3301
|
+
let mut types: Vec<CompositeInnerType> = Vec::new();
|
|
3302
|
+
let mut struct_news: Vec<u32> = Vec::new();
|
|
3303
|
+
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
3304
|
+
match payload.unwrap() {
|
|
3305
|
+
wasmparser::Payload::TypeSection(s) => {
|
|
3306
|
+
for g in s {
|
|
3307
|
+
for t in g.unwrap().types() {
|
|
3308
|
+
types.push(t.composite_type.inner.clone());
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
wasmparser::Payload::CodeSectionEntry(body) => {
|
|
3313
|
+
let mut r = body.get_operators_reader().unwrap();
|
|
3314
|
+
while !r.eof() {
|
|
3315
|
+
if let Operator::StructNew { struct_type_index } = r.read().unwrap() {
|
|
3316
|
+
struct_news.push(struct_type_index);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
_ => {}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
let f64_array = |i: u32| match &types[i as usize] {
|
|
3324
|
+
CompositeInnerType::Array(a) => a.0.mutable && matches!(a.0.element_type, StorageType::Val(ValType::F64)),
|
|
3325
|
+
_ => false,
|
|
3326
|
+
};
|
|
3327
|
+
let soa: Vec<usize> = (0..types.len())
|
|
3328
|
+
.filter(|&i| match &types[i] {
|
|
3329
|
+
CompositeInnerType::Struct(s) => {
|
|
3330
|
+
s.fields.len() == 2
|
|
3331
|
+
&& s.fields.iter().all(|f| match f.element_type {
|
|
3332
|
+
StorageType::Val(ValType::Ref(r)) => match r.heap_type() {
|
|
3333
|
+
HeapType::Concrete(u) => f64_array(u.as_module_index().unwrap()),
|
|
3334
|
+
_ => false,
|
|
3335
|
+
},
|
|
3336
|
+
_ => false,
|
|
3337
|
+
})
|
|
3338
|
+
}
|
|
3339
|
+
_ => false,
|
|
3340
|
+
})
|
|
3341
|
+
.collect();
|
|
3342
|
+
assert_eq!(soa.len(), 1, "expected exactly one {{f64[], f64[]}} struct");
|
|
3343
|
+
// The per-element box of Vec2 is `{f64, f64}`: nothing may allocate it.
|
|
3344
|
+
for idx in &struct_news {
|
|
3345
|
+
if let CompositeInnerType::Struct(s) = &types[*idx as usize] {
|
|
3346
|
+
let is_box = s.fields.len() == 2
|
|
3347
|
+
&& s.fields.iter().all(|f| matches!(f.element_type, StorageType::Val(ValType::F64)));
|
|
3348
|
+
assert!(!is_box, "struct.new of a boxed Vec2");
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
assert!(struct_news.contains(&(soa[0] as u32)));
|
|
3352
|
+
assert_eq!(runMerged::<f64>("soa", src), 8.0);
|
|
3353
|
+
}
|