plum

#treesitter#compiler#wasm

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 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. <!-- arrays-of-structs note --> 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).
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
- # nothing is boxed or allocated per element. `init`/`get`/`set`/`length`/
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], &reg);
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], &reg);
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
+ }