plum
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 +4 -1
- plum-core/src/ast.rs +3 -0
- plum-core/src/traits.rs +13 -6
- plum-core/src/union_lower.rs +36 -2
- plum-game/web-test/BENCHMARK.md +3 -0
- plum-wasm-codegen/src/lib.rs +237 -64
- plum-wasm-codegen/tests/traits_e2e_test.rs +122 -0
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 =
|
|
228
|
+
let variants = names
|
|
220
|
-
.get(&t.name)
|
|
221
|
-
.
|
|
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
|
|
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
|
|
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
|
-
|
|
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, ®)) {
|
|
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) =
|
|
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
|
-
|
|
5911
|
+
compileFieldPatterns(
|
|
5805
|
-
|
|
5912
|
+
fields,
|
|
5806
|
-
|
|
5913
|
+
&field_types,
|
|
5807
|
-
|
|
5914
|
+
&inst_types,
|
|
5808
|
-
|
|
5915
|
+
0,
|
|
5809
|
-
|
|
5916
|
+
narrow_local,
|
|
5810
|
-
|
|
5917
|
+
variant_idx,
|
|
5811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5980
|
+
compileFieldPatterns(
|
|
5981
|
+
fields,
|
|
5982
|
+
&types,
|
|
5983
|
+
&types,
|
|
5984
|
+
0,
|
|
5985
|
+
member_local,
|
|
5986
|
+
member_idx,
|
|
5821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6451
|
+
inner_fields,
|
|
6262
|
-
|
|
6452
|
+
&inner_field_types,
|
|
6263
|
-
|
|
6453
|
+
&inner_inst_types,
|
|
6264
|
-
|
|
6454
|
+
0,
|
|
6265
|
-
|
|
6455
|
+
nested_local,
|
|
6266
|
-
|
|
6456
|
+
inner_variant_idx,
|
|
6267
|
-
|
|
6457
|
+
None,
|
|
6268
|
-
|
|
6458
|
+
rest,
|
|
6269
|
-
|
|
6459
|
+
all_subjects,
|
|
6270
|
-
|
|
6460
|
+
result_vt,
|
|
6271
|
-
|
|
6461
|
+
exhaustive_fallback,
|
|
6272
|
-
|
|
6462
|
+
body,
|
|
6273
|
-
|
|
6463
|
+
ctx,
|
|
6274
|
-
|
|
6464
|
+
state,
|
|
6275
|
-
|
|
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
|
+
}
|