plum

#treesitter#compiler#wasm

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

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


538b1ee — Peter John 2026-11-02T20:19:57+05:30
T10: box-free trait layout (wasm-gc subtyping)
plum-checker/tests/trait_fields_tests.rs CHANGED
@@ -21,7 +21,10 @@ trait Ent(pos: Vec2, z: Float) =
21
21
  record Hut(Ent, k: Float) =
22
22
  fun draw(self) -> Float = self.z
23
23
 
24
+ # Orc lists Ent second, so Ent stays a boxed trait (accessor dispatch).
25
+ trait Mark(m: Int)
26
+
24
- record Orc(Ent) =
27
+ record Orc(Mark, Ent) =
25
28
  fun draw(self) -> Float = self.z + 1.0
26
29
 
27
30
  record Circle(r: Float)
plum-core/src/ast.rs CHANGED
@@ -172,6 +172,9 @@ pub struct Enum {
172
172
  /// `record X[T](..)`: a generic single-constructor heap type.
173
173
  pub isRecord: bool,
174
174
  /// Traits listed first in a `record`/`struct` field list (`record H(E, k: Int)`).
175
+ /// On a lowered trait enum (`isTrait`): the embedding records when the trait is
176
+ /// *subtyped* (every embedder lists it first, so their struct types extend the
177
+ /// trait's base struct and a conversion needs no box); empty otherwise.
175
178
  pub embeds: Vec<String>,
176
179
  /// A union enum produced by lowering a `trait`.
177
180
  pub isTrait: bool,
plum-core/src/traits.rs CHANGED
@@ -59,6 +59,8 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
59
59
  })
60
60
  .collect();
61
61
  let mut embedders: HashMap<String, Vec<String>> = HashMap::new();
62
+ // Records that list the trait FIRST: their struct layout starts with its fields.
63
+ let mut firstEmbedders: HashMap<String, usize> = HashMap::new();
62
64
  let mut newFns: Vec<Item> = Vec::new();
63
65
  // Record names and the file of any of their methods (for accessor `file`).
64
66
  let fnFile = |items: &[Item], rec: &str| -> String {
@@ -73,7 +75,7 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
73
75
 
74
76
  for ii in 0..source.items.len() {
75
77
  let (rname, isRecord, isValue, embeds) = match &source.items[ii] {
76
- Item::Enum(e) if !e.embeds.is_empty() => {
78
+ Item::Enum(e) if !e.embeds.is_empty() && !e.isTrait => {
77
79
  (e.name.clone(), e.isRecord, e.isValue, e.embeds.clone())
78
80
  }
79
81
  _ => continue,
@@ -156,6 +158,9 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
156
158
  copies.push(c);
157
159
  }
158
160
  embedders.entry(em.clone()).or_default().push(rname.clone());
161
+ if embeds.first() == Some(em) {
162
+ *firstEmbedders.entry(em.clone()).or_default() += 1;
163
+ }
159
164
  }
160
165
  for em in &embeds {
161
166
  let t = &traits[em];
@@ -216,10 +221,12 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
216
221
 
217
222
  for item in source.items.iter_mut() {
218
223
  let Item::Trait(t) = item else { continue };
224
+ let names = embedders.get(&t.name).cloned().unwrap_or_default();
225
+ // Subtyped layout: every embedder lists the trait first.
226
+ let subtyped = !names.is_empty()
227
+ && firstEmbedders.get(&t.name).copied().unwrap_or(0) == names.len();
219
- let variants = embedders
228
+ let variants = names
220
- .get(&t.name)
221
- .cloned()
229
+ .clone()
222
- .unwrap_or_default()
223
230
  .into_iter()
224
231
  .map(|r| EnumVariant {
225
232
  name: r.clone(),
@@ -241,7 +248,7 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
241
248
  variants,
242
249
  isValue: false,
243
250
  isRecord: false,
244
- embeds: vec![],
251
+ embeds: if subtyped { names } else { vec![] },
245
252
  isTrait: true,
246
253
  };
247
254
  source.traits.push(t.clone());
plum-core/src/union_lower.rs CHANGED
@@ -8,8 +8,19 @@ use std::collections::{HashMap, HashSet};
8
8
 
9
9
  /// Applies `plan` to `source`.
10
10
  pub fn applyPlan(source: &mut Source, plan: &UnionPlan) {
11
+ // Subtyped traits (see `Enum.embeds`) keep records as their own subtypes: no
12
+ // box, and trait field reads/writes stay plain field accesses for codegen.
13
+ let subtyped: HashSet<String> = source
14
+ .items
15
+ .iter()
16
+ .filter_map(|i| match i {
17
+ Item::Enum(e) if e.isTrait && !e.embeds.is_empty() => Some(e.name.clone()),
18
+ _ => None,
19
+ })
20
+ .collect();
11
21
  let mut lw = Lowerer {
12
22
  plan,
23
+ subtyped,
13
24
  pending: Vec::new(),
14
25
  needs: Vec::new(),
15
26
  };
@@ -45,6 +56,8 @@ pub fn applyPlan(source: &mut Source, plan: &UnionPlan) {
45
56
 
46
57
  struct Lowerer<'a> {
47
58
  plan: &'a UnionPlan,
59
+ /// Names of the subtyped (box-free) traits.
60
+ subtyped: HashSet<String>,
48
61
  /// Narrowings (from, to) found in the condition being visited.
49
62
  pending: Vec<(String, String)>,
50
63
  /// (variants, method name) of every dispatch seen.
@@ -56,6 +69,14 @@ fn addr<T>(t: &T) -> usize {
56
69
  }
57
70
 
58
71
  impl<'a> Lowerer<'a> {
72
+ /// Whether hidden variant `Enum__Member` belongs to a subtyped trait.
73
+ fn isSubtyped(&self, variant: &str) -> bool {
74
+ variant
75
+ .split("__")
76
+ .next()
77
+ .is_some_and(|e| self.subtyped.contains(e))
78
+ }
79
+
59
80
  fn block(&mut self, b: &mut Block) {
60
81
  for s in &mut b.stmts {
61
82
  self.stmt(s);
@@ -128,6 +149,9 @@ impl<'a> Lowerer<'a> {
128
149
  else {
129
150
  return;
130
151
  };
152
+ if variants.first().is_some_and(|v| self.isSubtyped(v)) {
153
+ return;
154
+ }
131
155
  if a.targets.len() != 1 || a.values.len() != 1 || a.declare {
132
156
  return;
133
157
  }
@@ -306,7 +330,12 @@ impl<'a> Lowerer<'a> {
306
330
  | Expr::Var(_)
307
331
  | Expr::TypeName(_) => {}
308
332
  }
309
- if let Some(variants) = self.plan.fieldReads.get(&a) {
333
+ if let Some(variants) = self
334
+ .plan
335
+ .fieldReads
336
+ .get(&a)
337
+ .filter(|vs| !vs.first().is_some_and(|v| self.isSubtyped(v)))
338
+ {
310
339
  if let Expr::Attribute(at) = e {
311
340
  if let AttrKind::Field(f) = &at.attr {
312
341
  let method = crate::traits::getter(f);
@@ -330,7 +359,12 @@ impl<'a> Lowerer<'a> {
330
359
  };
331
360
  self.needs.push((variants.clone(), method));
332
361
  }
333
- if let Some(variant) = self.plan.wraps.get(&a) {
362
+ if let Some(variant) = self
363
+ .plan
364
+ .wraps
365
+ .get(&a)
366
+ .filter(|v| !self.isSubtyped(v))
367
+ {
334
368
  let inner = std::mem::replace(e, Expr::Int(0));
335
369
  *e = Expr::FnCall(FnCall {
336
370
  name: variant.clone(),
plum-game/web-test/BENCHMARK.md CHANGED
@@ -123,7 +123,10 @@ With culling the frame costs about what the 2,000 visible sprites cost, not what
123
123
  | stress_scene, 5.8k nodes | `trait Actor` | 1.69, 1.69 | 1.8 | 0.039 | 1.27 | 0.26 |
124
124
  | stress_world, 20k nodes, culling on | closure `Node` | 1.09, 1.09 | 1.2 | 0.421 | 0.495 | 0.082 |
125
125
  | stress_world, 20k nodes, culling on | `trait Actor` | 2.09, 2.12 | 2.3-2.4 | 1.29-1.31 | 0.59 | 0.09 |
126
+ | stress_scene, 5.8k nodes | trait (box-free) | 1.67, 1.68 | 1.8 | 0.038 | 1.26 | 0.26 |
127
+ | stress_world, 20k nodes, culling on | trait (box-free) | 1.10, 1.11 | 1.2 | 0.37-0.38 | 0.54 | 0.085 |
126
128
 
127
129
  - `stress_scene`: +5% (1.60 -> 1.69 ms), at the edge of noise. The per-node closure call became a trait dispatch, which is cheaper (`update` 0.055 -> 0.039 ms); `draw` grew by 0.08 ms because the draw walk reads child fields through the trait.
128
130
  - `stress_world`: +92% (1.09 -> 2.09 ms), above the 10% threshold. All of it is `update`, which here is the y-sort: `sortChildren(|a, b| a.y < b.y)` makes 2 x 19,999 `a.y` reads per frame and every read of a trait field is a dispatch (`__get_y`) over the program's actor kinds, about 30-40 ns each against about 10 ns for a direct field read. Without the sort `update` is 0.01 ms. The cost scales with the number of actor kinds in the program: dropping two unused test-only kinds took `update` from 1.65 to 1.29 ms, which is why the Scene tests that need their own `act`/`draw` records live in `plum-examples/scene.plum` and not in `std/game/Scene.plum`. Draw is +0.1 ms (culling and transforms run as trait default methods, so they read fields directly, one dispatch per child).
131
+ - Box-free layout (T10): a trait whose every embedder lists it first is compiled as a wasm-gc base struct of the trait's fields with each embedding record's struct as a subtype. A record already is a trait value (no box, `==` is reference identity), a trait field read or write is one `struct.get`/`struct.set` on the base type, and only method dispatch and `match` narrowing use `ref.test`/`ref.cast`. `stress_world` goes 2.09 -> 1.10 ms (+1% against the closure version's 1.09, inside the 10% threshold; `update` 1.29 -> 0.37 ms) and `stress_scene` 1.69 -> 1.68 ms (+5% against 1.60, the same edge-of-noise gap as before). A trait that some record lists second or later keeps the boxed layout described above.
129
132
  - Reading the trait's fields from outside it (a game's sort key, hit tests on `.x`/`.y`) is the price of the trait. Default methods run on the concrete record and read its fields directly, which is why `local()`, `outside()`, `drawTree()` and `hitTree()` are trait methods rather than free functions.
plum-wasm-codegen/src/lib.rs CHANGED
@@ -999,6 +999,9 @@ pub struct GcTypeRegistry {
999
999
  pub mk_str_fn: Option<u32>,
1000
1000
  /// `!T` (keyed by `T`) -> its heap box struct `{leaves of T..., code: i32}`.
1001
1001
  pub err_box: HashMap<PlumType, u32>,
1002
+ /// Hidden member variants (`Actor__Box`) of subtyped traits: no struct of their
1003
+ /// own, they resolve to the member record's struct (see `subtypedTraits`).
1004
+ pub trait_members: HashSet<String>,
1002
1005
  }
1003
1006
 
1004
1007
  impl GcTypeRegistry {
@@ -1108,6 +1111,49 @@ fn arrayDepth(t: &PlumType) -> usize {
1108
1111
  }
1109
1112
  }
1110
1113
 
1114
+ /// Subtyped (box-free) traits: trait name -> its embedding records. Such a trait
1115
+ /// is a non-final base struct of its fields, each embedder's struct extends it,
1116
+ /// so a record already IS a trait value and trait fields are plain struct fields.
1117
+ fn subtypedTraits(source: &ast::Source) -> HashMap<String, Vec<String>> {
1118
+ source
1119
+ .items
1120
+ .iter()
1121
+ .filter_map(|i| match i {
1122
+ ast::Item::Enum(e) if e.isTrait && !e.embeds.is_empty() => {
1123
+ Some((e.name.clone(), e.embeds.clone()))
1124
+ }
1125
+ _ => None,
1126
+ })
1127
+ .collect()
1128
+ }
1129
+
1130
+ /// Registers each subtyped trait's fields as its enum params, so trait field
1131
+ /// reads and writes take the shared-field `struct.get`/`struct.set` path.
1132
+ fn addTraitParams(
1133
+ source: &ast::Source,
1134
+ enum_variants: &EnumVariants,
1135
+ enum_params: &mut EnumParams,
1136
+ ) {
1137
+ for (t, members) in subtypedTraits(source) {
1138
+ let Some(decl) = source.traits.iter().find(|d| d.name == t) else {
1139
+ continue;
1140
+ };
1141
+ let Some(info) = members.first().and_then(|m| enum_variants.get(m)) else {
1142
+ continue;
1143
+ };
1144
+ let params = info
1145
+ .field_names
1146
+ .iter()
1147
+ .cloned()
1148
+ .zip(info.field_types.iter().cloned())
1149
+ .take(decl.fields.len())
1150
+ .collect::<Vec<_>>();
1151
+ if !params.is_empty() {
1152
+ enum_params.insert(t, params);
1153
+ }
1154
+ }
1155
+ }
1156
+
1111
1157
  /// Builds the wasm-gc type registry: one struct family per non-generic enum and per
1112
1158
  /// distinct concrete instance (`apps`, e.g. `List[Int]`) of a generic enum, keyed by
1113
1159
  /// the structural `PlumType`; `Array[T]` instances become native wasm arrays of `T`'s
@@ -1145,7 +1191,15 @@ fn buildGcTypeRegistry(
1145
1191
  leaf_array_idx: HashMap::new(),
1146
1192
  mk_str_fn: None,
1147
1193
  err_box: HashMap::new(),
1194
+ trait_members: HashSet::new(),
1148
1195
  };
1196
+ let subtyped = subtypedTraits(source);
1197
+ let mut parent_of: HashMap<String, String> = HashMap::new();
1198
+ for (t, members) in &subtyped {
1199
+ for m in members {
1200
+ parent_of.insert(m.clone(), t.clone());
1201
+ }
1202
+ }
1149
1203
  let mut enum_decls: HashMap<String, &ast::Enum> = HashMap::new();
1150
1204
  let mut instances: Vec<PlumType> = Vec::new();
1151
1205
  for item in &source.items {
@@ -1166,6 +1220,10 @@ fn buildGcTypeRegistry(
1166
1220
  }
1167
1221
  }
1168
1222
  }
1223
+ // A trait's base struct must precede the records that extend it.
1224
+ instances.sort_by_key(|t| {
1225
+ !matches!(t, PlumType::TNamed(n) if subtyped.contains_key(n))
1226
+ });
1169
1227
  for ty in &instances {
1170
1228
  let n = plum_checker::nominalName(ty).unwrap().to_string();
1171
1229
  let e = enum_decls[&n];
@@ -1179,6 +1237,10 @@ fn buildGcTypeRegistry(
1179
1237
  reg.app_super.insert(ty.clone(), super_idx);
1180
1238
  }
1181
1239
  }
1240
+ if subtyped.contains_key(&n) {
1241
+ // Box-free trait: only the base struct; members are the records.
1242
+ continue;
1243
+ }
1182
1244
  for v in &e.variants {
1183
1245
  let idx = slots.len() as u32;
1184
1246
  slots.push(Slot::Variant(ty.clone(), v.name.clone()));
@@ -1202,6 +1264,15 @@ fn buildGcTypeRegistry(
1202
1264
  }
1203
1265
  }
1204
1266
  }
1267
+ for (t, members) in &subtyped {
1268
+ for m in members {
1269
+ if let Some(&idx) = reg.class_type_idx.get(m) {
1270
+ let hidden = plum_core::unions::hiddenVariant(t, m);
1271
+ reg.variant_type_idx.insert(hidden.clone(), idx);
1272
+ reg.trait_members.insert(hidden);
1273
+ }
1274
+ }
1275
+ }
1205
1276
  reg.instances = instances.clone();
1206
1277
  for t in errs {
1207
1278
  if !reg.err_box.contains_key(t) {
@@ -1316,7 +1387,12 @@ fn buildGcTypeRegistry(
1316
1387
  vname
1317
1388
  )
1318
1389
  });
1390
+ let super_idx = match ty {
1391
+ PlumType::TNamed(n) if *vname == *n && parent_of.contains_key(n) => {
1392
+ reg.enum_super_type_idx[&parent_of[n]]
1393
+ }
1319
- let super_idx = reg.superIdx(ty).expect("enum supertype slot");
1394
+ _ => reg.superIdx(ty).expect("enum supertype slot"),
1395
+ };
1320
1396
  let mut field_types: Vec<FieldType> = Vec::new();
1321
1397
  if let Some(leaves) = valueFieldsOf(ty).map(|_| flatValtypes(ty, &reg)) {
1322
1398
  // A value enum's box holds its flattened leaves directly.
@@ -2025,7 +2101,9 @@ fn compileSourceInner(
2025
2101
  for ((id, ptr), t) in inst_expr_types {
2026
2102
  inst_types[id].insert(ptr, t);
2027
2103
  }
2028
- let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
2104
+ let (global_env, methods, enum_variants, mut enum_params) =
2105
+ plum_checker::buildGlobalTables(source);
2106
+ addTraitParams(source, &enum_variants, &mut enum_params);
2029
2107
  let min_required = plum_checker::buildMinRequiredArgs(source);
2030
2108
  let primitive_generics = plum_checker::buildPrimitiveGenerics(source);
2031
2109
  let mut eg: EnumGenerics = HashMap::new();
@@ -5801,14 +5879,55 @@ fn compileVariantConstructorArm(
5801
5879
  Instruction::LocalGet(scratch_local).encode(body);
5802
5880
  Instruction::RefCastNonNull(HeapType::Concrete(variant_idx)).encode(body);
5803
5881
  Instruction::LocalSet(narrow_local).encode(body);
5882
+ let mut onMatch = |body: &mut Vec<u8>, state: &mut ModuleState| {
5883
+ compileCasePositions(
5884
+ case,
5885
+ pos + 1,
5886
+ all_subjects,
5887
+ rest,
5888
+ result_vt,
5889
+ exhaustive_fallback,
5890
+ body,
5891
+ ctx,
5892
+ state,
5893
+ )
5894
+ };
5895
+ if ctx.gc_types.trait_members.contains(name) {
5896
+ compileTraitMemberInner(
5897
+ &fields[0],
5898
+ &inst_types[0],
5899
+ narrow_local,
5900
+ variant_idx,
5901
+ rest,
5902
+ all_subjects,
5903
+ result_vt,
5904
+ exhaustive_fallback,
5905
+ body,
5906
+ ctx,
5907
+ state,
5908
+ &mut onMatch,
5909
+ )?;
5910
+ } else {
5804
- compileFieldPatterns(
5911
+ compileFieldPatterns(
5805
- fields,
5912
+ fields,
5806
- &field_types,
5913
+ &field_types,
5807
- &inst_types,
5914
+ &inst_types,
5808
- 0,
5915
+ 0,
5809
- narrow_local,
5916
+ narrow_local,
5810
- variant_idx,
5917
+ variant_idx,
5811
- value_leaf_map.as_deref(),
5918
+ value_leaf_map.as_deref(),
5919
+ rest,
5920
+ all_subjects,
5921
+ result_vt,
5922
+ exhaustive_fallback,
5923
+ body,
5924
+ ctx,
5925
+ state,
5926
+ &mut onMatch,
5927
+ )?;
5928
+ }
5929
+ Instruction::Else.encode(body);
5930
+ compileMatchArmsMulti(
5812
5931
  rest,
5813
5932
  all_subjects,
5814
5933
  result_vt,
@@ -5816,32 +5935,68 @@ fn compileVariantConstructorArm(
5816
5935
  body,
5817
5936
  ctx,
5818
5937
  state,
5938
+ )?;
5939
+ Instruction::End.encode(body);
5940
+ Ok(())
5941
+ }
5942
+
5943
+ /// Matches `inner` (the payload pattern of a hidden member variant `Actor__Box` of
5944
+ /// a subtyped trait) against the member record held in `member_local`. The record
5945
+ /// IS the trait value, so there is no payload field to read.
5946
+ #[allow(clippy::too_many_arguments)]
5947
+ fn compileTraitMemberInner(
5948
+ inner: &ast::CasePattern,
5949
+ member_ty: &PlumType,
5950
+ member_local: u32,
5951
+ member_idx: u32,
5952
+ rest: &[ast::Case],
5953
+ all_subjects: &[(ValType, u32)],
5954
+ result_vt: Option<ValType>,
5955
+ exhaustive_fallback: bool,
5956
+ body: &mut Vec<u8>,
5957
+ ctx: &LocalCtx,
5958
+ state: &mut ModuleState,
5959
+ on_match: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
5960
+ ) -> Result<(), String> {
5961
+ match inner {
5962
+ ast::CasePattern::Wildcard => on_match(body, state),
5963
+ ast::CasePattern::Name(n) if !ctx.enum_variants.contains_key(n) => {
5964
+ let idx = ctx.locals.get(n).copied().ok_or_else(|| {
5965
+ format!("internal codegen error: missing binding local '{}'", n)
5966
+ })?;
5967
+ Instruction::LocalGet(member_local).encode(body);
5968
+ Instruction::LocalSet(idx).encode(body);
5969
+ ctx.type_env
5970
+ .borrow_mut()
5971
+ .insert(n.to_string(), TypeScheme::mono(member_ty.clone()));
5819
- &mut |body, state| {
5972
+ on_match(body, state)
5973
+ }
5974
+ ast::CasePattern::Class { name, fields } => {
5975
+ let info = ctx
5976
+ .enum_variants
5977
+ .get(name)
5978
+ .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
5979
+ let types = instFields(info, member_ty);
5820
- compileCasePositions(
5980
+ compileFieldPatterns(
5981
+ fields,
5982
+ &types,
5983
+ &types,
5984
+ 0,
5985
+ member_local,
5986
+ member_idx,
5821
- case,
5987
+ None,
5822
- pos + 1,
5823
- all_subjects,
5824
5988
  rest,
5989
+ all_subjects,
5825
5990
  result_vt,
5826
5991
  exhaustive_fallback,
5827
5992
  body,
5828
5993
  ctx,
5829
5994
  state,
5995
+ on_match,
5830
5996
  )
5831
- },
5997
+ }
5832
- )?;
5833
- Instruction::Else.encode(body);
5998
+ _ => Err("codegen: unsupported pattern on a trait member".to_string()),
5834
- compileMatchArmsMulti(
5835
- rest,
5836
- all_subjects,
5837
- result_vt,
5838
- exhaustive_fallback,
5839
- body,
5840
- ctx,
5841
- state,
5842
- )?;
5999
+ }
5843
- Instruction::End.encode(body);
5844
- Ok(())
5845
6000
  }
5846
6001
 
5847
6002
  /// Pushes field `fpos` of the pattern container in `container_local`.
@@ -6257,41 +6412,59 @@ fn compileFieldPatterns(
6257
6412
  );
6258
6413
  Instruction::RefCastNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
6259
6414
  Instruction::LocalSet(nested_local).encode(body);
6415
+ let mut onNested = |body: &mut Vec<u8>, state: &mut ModuleState| {
6260
- compileFieldPatterns(
6416
+ compileFieldPatterns(
6417
+ fields,
6418
+ field_types,
6419
+ inst_types,
6420
+ fpos + 1,
6421
+ container_local,
6422
+ container_type_idx,
6423
+ leaf_map,
6424
+ rest,
6425
+ all_subjects,
6426
+ result_vt,
6427
+ exhaustive_fallback,
6428
+ body,
6429
+ ctx,
6430
+ state,
6431
+ on_match,
6432
+ )
6433
+ };
6434
+ if ctx.gc_types.trait_members.contains(name) {
6435
+ compileTraitMemberInner(
6436
+ &inner_fields[0],
6437
+ &inner_inst_types[0],
6438
+ nested_local,
6439
+ inner_variant_idx,
6440
+ rest,
6441
+ all_subjects,
6442
+ result_vt,
6443
+ exhaustive_fallback,
6444
+ body,
6445
+ ctx,
6446
+ state,
6447
+ &mut onNested,
6448
+ )?;
6449
+ } else {
6450
+ compileFieldPatterns(
6261
- inner_fields,
6451
+ inner_fields,
6262
- &inner_field_types,
6452
+ &inner_field_types,
6263
- &inner_inst_types,
6453
+ &inner_inst_types,
6264
- 0,
6454
+ 0,
6265
- nested_local,
6455
+ nested_local,
6266
- inner_variant_idx,
6456
+ inner_variant_idx,
6267
- None,
6457
+ None,
6268
- rest,
6458
+ rest,
6269
- all_subjects,
6459
+ all_subjects,
6270
- result_vt,
6460
+ result_vt,
6271
- exhaustive_fallback,
6461
+ exhaustive_fallback,
6272
- body,
6462
+ body,
6273
- ctx,
6463
+ ctx,
6274
- state,
6464
+ state,
6275
- &mut |body, state| {
6465
+ &mut onNested,
6276
- compileFieldPatterns(
6277
- fields,
6278
- field_types,
6279
- inst_types,
6280
- fpos + 1,
6281
- container_local,
6282
- container_type_idx,
6283
- leaf_map,
6284
- rest,
6285
- all_subjects,
6286
- result_vt,
6287
- exhaustive_fallback,
6288
- body,
6289
- ctx,
6290
- state,
6291
- on_match,
6292
- )
6293
- },
6294
- )?;
6466
+ )?;
6467
+ }
6295
6468
  Instruction::Else.encode(body);
6296
6469
  compileMatchArmsMulti(
6297
6470
  rest,
plum-wasm-codegen/tests/traits_e2e_test.rs CHANGED
@@ -203,3 +203,125 @@ fun main() -> Int =
203
203
  ");
204
204
  assert_eq!(runInt("narrow", &src), 4090 + 5);
205
205
  }
206
+
207
+ /// Operator names (Debug form) of the function named `name` in `bytes` (found
208
+ /// through the name section).
209
+ fn opsOfExport(bytes: &[u8], name: &str) -> Vec<String> {
210
+ use wasmparser::{Name, NameSectionReader, Parser, Payload};
211
+ let mut funcIdx = None;
212
+ let mut imported = 0u32;
213
+ let mut bodies = Vec::new();
214
+ for p in Parser::new(0).parse_all(bytes) {
215
+ match p.unwrap() {
216
+ Payload::ImportSection(s) => {
217
+ for i in s {
218
+ if matches!(i.unwrap().ty, wasmparser::TypeRef::Func(_)) {
219
+ imported += 1;
220
+ }
221
+ }
222
+ }
223
+ Payload::CustomSection(c) if c.name() == "name" => {
224
+ for n in NameSectionReader::new(wasmparser::BinaryReader::new(c.data(), c.data_offset())) {
225
+ if let Name::Function(m) = n.unwrap() {
226
+ for a in m {
227
+ let a = a.unwrap();
228
+ if a.name == name {
229
+ funcIdx = Some(a.index);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ Payload::CodeSectionEntry(b) => bodies.push(b),
236
+ _ => {}
237
+ }
238
+ }
239
+ let idx = funcIdx.unwrap_or_else(|| panic!("no function named {name}")) - imported;
240
+ let mut ops = Vec::new();
241
+ let mut r = bodies[idx as usize].get_operators_reader().unwrap();
242
+ while !r.eof() {
243
+ ops.push(format!("{:?}", r.read().unwrap()));
244
+ }
245
+ ops
246
+ }
247
+
248
+ fn compileOk(name: &str, src: &str) -> Vec<u8> {
249
+ let source = load(name, src).expect("load");
250
+ let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
251
+ wasmparser::validate(&bytes).expect("valid wasm");
252
+ bytes
253
+ }
254
+
255
+ const ACTORS: &str = "\
256
+ trait Actor(x: Int, y: Int) =
257
+ fun hit(self) -> Int
258
+
259
+ record Box(Actor, w: Int) =
260
+ fun hit(self) -> Int = self.w
261
+
262
+ record Dot(Actor) =
263
+ fun hit(self) -> Int = 1
264
+ ";
265
+
266
+ #[test]
267
+ fn subtyped_field_read_direct() {
268
+ let src = prog(&format!("{ACTORS}
269
+ fun readY(a: Actor) -> Int = a.y
270
+
271
+ fun main() -> Int =
272
+ readY(Box(x: 1, y: 7, w: 3))
273
+ "));
274
+ let bytes = compileOk("direct_read", &src);
275
+ let ops = opsOfExport(&bytes, "readY");
276
+ assert!(ops.iter().any(|o| o.starts_with("StructGet")), "{ops:?}");
277
+ assert!(!ops.iter().any(|o| o.starts_with("Call") || o.starts_with("RefTest") || o.starts_with("RefCast")), "{ops:?}");
278
+ assert_eq!(runInt("direct_read_run", &src), 7);
279
+ }
280
+
281
+ #[test]
282
+ fn conversion_is_identity() {
283
+ let src = prog(&format!("{ACTORS}
284
+ fun asActor(b: Box) -> Actor = b
285
+
286
+ fun main() -> Int =
287
+ b := Box(x: 1, y: 7, w: 3)
288
+ a: Actor := b
289
+ c: Actor := b
290
+ a.y = 9
291
+ same := 0
292
+ if a == c
293
+ same = 1
294
+ same * 100 + b.y
295
+ "));
296
+ let bytes = compileOk("identity", &src);
297
+ let ops = opsOfExport(&bytes, "asActor");
298
+ assert!(!ops.iter().any(|o| o.starts_with("StructNew")), "{ops:?}");
299
+ assert_eq!(runInt("identity_run", &src), 109);
300
+ }
301
+
302
+ #[test]
303
+ fn second_trait_stays_boxed() {
304
+ let src = prog("\
305
+ trait Base(id: Int) =
306
+ fun a(self) -> Int
307
+
308
+ trait Extra(n: Int) =
309
+ fun b(self) -> Int
310
+
311
+ record Thing(Base, Extra, k: Int) =
312
+ fun a(self) -> Int = self.k
313
+ fun b(self) -> Int = self.k * 2
314
+
315
+ fun useExtra(e: Extra) -> Int =
316
+ e.n = e.n + 1
317
+ e.b() + e.n
318
+
319
+ fun useBase(x: Base) -> Int = x.a() + x.id
320
+
321
+ fun main() -> Int =
322
+ t := Thing(id: 5, n: 10, k: 4)
323
+ useExtra(t) * 1000 + useBase(t) * 10 + t.n
324
+ ");
325
+ // useExtra: n=11, b=8 -> 19; useBase: 4+5=9; t.n = 11
326
+ assert_eq!(runInt("boxed2", &src), 19000 + 90 + 11);
327
+ }