plum

#treesitter#compiler#wasm

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

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


aa923a9 — Peter John 2026-11-02T20:24:55+05:30
T10: box-free trait layout (wasm-gc subtyping)
plum-core/src/ast.rs CHANGED
@@ -172,12 +172,12 @@ 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.
178
175
  pub embeds: Vec<String>,
179
176
  /// A union enum produced by lowering a `trait`.
180
177
  pub isTrait: bool,
178
+ /// On a lowered trait enum: every embedder lists the trait first, so the layout
179
+ /// is wasm-gc subtyping (no box). The variants are the embedding records.
180
+ pub isSubtypedTrait: bool,
181
181
  }
182
182
 
183
183
  #[derive(Debug, Clone, PartialEq)]
plum-core/src/parser.rs CHANGED
@@ -300,6 +300,7 @@ impl<'a> AstParser<'a> {
300
300
  isRecord: false,
301
301
  embeds: Vec::new(),
302
302
  isTrait: false,
303
+ isSubtypedTrait: false,
303
304
  name,
304
305
  line: pos.row + 1,
305
306
  column: pos.column + 1,
@@ -376,6 +377,7 @@ impl<'a> AstParser<'a> {
376
377
  isRecord: false,
377
378
  embeds,
378
379
  isTrait: false,
380
+ isSubtypedTrait: false,
379
381
  name,
380
382
  line: pos.row + 1,
381
383
  column: pos.column + 1,
plum-core/src/traits.rs CHANGED
@@ -248,8 +248,9 @@ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
248
248
  variants,
249
249
  isValue: false,
250
250
  isRecord: false,
251
- embeds: if subtyped { names } else { vec![] },
251
+ embeds: vec![],
252
252
  isTrait: true,
253
+ isSubtypedTrait: subtyped,
253
254
  };
254
255
  source.traits.push(t.clone());
255
256
  *item = Item::Enum(e);
plum-core/src/union_lower.rs CHANGED
@@ -8,13 +8,13 @@ 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
11
+ // Subtyped traits (see `Enum.isSubtypedTrait`) keep records as their own subtypes: no
12
12
  // box, and trait field reads/writes stay plain field accesses for codegen.
13
13
  let subtyped: HashSet<String> = source
14
14
  .items
15
15
  .iter()
16
16
  .filter_map(|i| match i {
17
- Item::Enum(e) if e.isTrait && !e.embeds.is_empty() => Some(e.name.clone()),
17
+ Item::Enum(e) if e.isSubtypedTrait => Some(e.name.clone()),
18
18
  _ => None,
19
19
  })
20
20
  .collect();
plum-core/tests/traits_test.rs CHANGED
@@ -200,3 +200,18 @@ fn struct_typed_field_accessors_and_two_traits() {
200
200
  assert_eq!(enumOf(&s, "Aa").variants.len(), 1);
201
201
  assert_eq!(enumOf(&s, "Bb").variants[0].name, "Rr");
202
202
  }
203
+
204
+ #[test]
205
+ fn subtyped_flag_explicit_and_idempotent() {
206
+ let mut s = parseRaw(
207
+ "trait Aa(p: Int)\ntrait Bb(q: Int)\ntrait Cc(r: Int)\nrecord Rr(Aa, Bb, k: Int)\nrecord Ss(Bb, Cc)\n",
208
+ );
209
+ assert!(lowerTraits(&mut s).is_empty());
210
+ assert!(enumOf(&s, "Aa").isSubtypedTrait);
211
+ assert!(!enumOf(&s, "Bb").isSubtypedTrait);
212
+ assert!(!enumOf(&s, "Cc").isSubtypedTrait);
213
+ assert!(enumOf(&s, "Aa").embeds.is_empty());
214
+ let before = s.clone();
215
+ assert!(lowerTraits(&mut s).is_empty());
216
+ assert_eq!(s, before);
217
+ }
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1119,8 +1119,19 @@ fn subtypedTraits(source: &ast::Source) -> HashMap<String, Vec<String>> {
1119
1119
  .items
1120
1120
  .iter()
1121
1121
  .filter_map(|i| match i {
1122
- ast::Item::Enum(e) if e.isTrait && !e.embeds.is_empty() => {
1122
+ ast::Item::Enum(e) if e.isSubtypedTrait => {
1123
+ Some((
1123
- Some((e.name.clone(), e.embeds.clone()))
1124
+ e.name.clone(),
1125
+ e.variants
1126
+ .iter()
1127
+ .map(|v| {
1128
+ v.name
1129
+ .strip_prefix(&format!("{}__", e.name))
1130
+ .unwrap_or(&v.name)
1131
+ .to_string()
1132
+ })
1133
+ .collect(),
1134
+ ))
1124
1135
  }
1125
1136
  _ => None,
1126
1137
  })
plum-wasm-codegen/tests/traits_e2e_test.rs CHANGED
@@ -325,3 +325,57 @@ fun main() -> Int =
325
325
  // useExtra: n=11, b=8 -> 19; useBase: 4+5=9; t.n = 11
326
326
  assert_eq!(runInt("boxed2", &src), 19000 + 90 + 11);
327
327
  }
328
+
329
+ #[test]
330
+ fn nested_narrowing_two_levels() {
331
+ let src = prog("\
332
+ trait Actor(x: Int) =
333
+ fun hit(self) -> Int
334
+
335
+ record Dot(Actor, w: Int) =
336
+ fun hit(self) -> Int = self.w
337
+
338
+ record Cat(Actor) =
339
+ fun hit(self) -> Int = 1
340
+
341
+ record Holder(a: Actor)
342
+ record Outer(h: Holder)
343
+
344
+ fun f(o: Outer) -> Int =
345
+ match o
346
+ Outer(Holder(Dot(x, w))) => x * 100 + w
347
+ _ => 5
348
+
349
+ fun main() -> Int =
350
+ f(Outer(h: Holder(a: Dot(x: 4, w: 9)))) * 10 + f(Outer(h: Holder(a: Cat(x: 1))))
351
+ ");
352
+ assert_eq!(runInt("nest2", &src), 4090 + 5);
353
+ }
354
+
355
+ #[test]
356
+ fn subtyped_with_boxed_second_self_list_and_eq() {
357
+ let src = prog("\
358
+ trait Actor(x: Int, kids: List[Actor]) =
359
+ fun hit(self) -> Int
360
+
361
+ trait Extra(n: Int) =
362
+ fun b(self) -> Int
363
+
364
+ record Dot(Actor, Extra, w: Int) =
365
+ fun hit(self) -> Int = self.w
366
+ fun b(self) -> Int = self.n
367
+
368
+ fun main() -> Int =
369
+ d := Dot(x: 1, kids: List[Actor](), n: 6, w: 3)
370
+ a: Actor := d
371
+ e: Extra := d
372
+ e.n = 8
373
+ a.x = 2
374
+ a2: Actor := d
375
+ same := 0
376
+ if a == a2
377
+ same = 1
378
+ a.hit() * 1000 + d.n * 100 + d.x * 10 + same
379
+ ");
380
+ assert_eq!(runInt("mix", &src), 3000 + 800 + 20 + 1);
381
+ }