plum

#treesitter#compiler#wasm

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

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


6c41056 — Peter John 2026-10-25T19:30:30+05:30
feat: trait names as types with typeId-header dyn dispatch
plum-checker/src/lib.rs CHANGED
@@ -432,6 +432,43 @@ const GLOBAL_KEY: &str = "$global:";
432
432
  const TRAIT_KEY: &str = "$trait:";
433
433
  /// `TypeEnv` key prefix for a generic parameter's trait bound (`$bound:K:Hashable`).
434
434
  const BOUND_KEY: &str = "$bound:";
435
+ /// `methods` method-name marking a receiver name as a trait (see `buildTables`).
436
+ pub const TRAIT_MARKER: &str = "$trait";
437
+
438
+ /// The name of the first method `trait_name` declares that `actual` lacks, or
439
+ /// `None` when `actual` implements the whole trait (or `trait_name` isn't one).
440
+ /// Reuses the per-receiver method table: a primitive/variant also counts when
441
+ /// its owning "union" enum (`Number` for `Int`) defines the method.
442
+ pub fn traitMissingMethod(
443
+ trait_name: &str,
444
+ actual: &PlumType,
445
+ ctx: &CheckCtx,
446
+ ) -> Option<String> {
447
+ let key = (trait_name.to_string(), TRAIT_MARKER.to_string());
448
+ if !ctx.methods.contains_key(&key) {
449
+ return None;
450
+ }
451
+ let recv = methodReceiverName(actual);
452
+ let owner = recv.as_ref().and_then(|r| {
453
+ ctx.enum_variants.get(r.as_str()).map(|i| i.enum_name.clone())
454
+ });
455
+ ctx.methods
456
+ .range((trait_name.to_string(), String::new())..)
457
+ .take_while(|((r, _), _)| r == trait_name)
458
+ .map(|((_, m), _)| m)
459
+ .filter(|m| m.as_str() != TRAIT_MARKER)
460
+ .find(|m| {
461
+ let has = |r: &String| ctx.methods.contains_key(&(r.clone(), (*m).clone()));
462
+ !(recv.as_ref().is_some_and(has) || owner.as_ref().is_some_and(has))
463
+ })
464
+ .cloned()
465
+ }
466
+
467
+ /// True if `name` is a declared trait.
468
+ pub fn isTraitName(name: &str, ctx: &CheckCtx) -> bool {
469
+ ctx.methods
470
+ .contains_key(&(name.to_string(), TRAIT_MARKER.to_string()))
471
+ }
435
472
 
436
473
  pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
437
474
  plumTypeFromName(&ty.name)
@@ -590,6 +627,20 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
590
627
  }
591
628
  unify(r1, r2, ctx)
592
629
  }
630
+ // A trait used as a type: any value implementing every trait method
631
+ // coerces to it (a bounded generic parameter is trusted).
632
+ (PlumType::TNamed(tr), actual) if isTraitName(tr, ctx) => {
633
+ if matches!(actual, PlumType::TParam(_)) {
634
+ return Ok(());
635
+ }
636
+ match traitMissingMethod(tr, actual, ctx) {
637
+ Some(m) => Err(format!(
638
+ "type '{}' does not implement trait '{}': missing '{}'",
639
+ actual, tr, m
640
+ )),
641
+ None => Ok(()),
642
+ }
643
+ }
593
644
  _ => Err(format!("type mismatch: expected {}, found {}", t1, t2)),
594
645
  }
595
646
  }
@@ -887,15 +938,40 @@ pub fn buildTables(
887
938
  };
888
939
  global_env.insert(c.name.clone(), TypeScheme::mono(ty));
889
940
  }
890
- ast::Item::Trait(t) if generic => {
941
+ ast::Item::Trait(t) => {
942
+ // A trait name is also usable as a type (`s: Shape`): register it
943
+ // as a receiver in `methods` (a `$trait` marker plus one entry per
944
+ // declared method) so calls on a trait-typed value resolve to the
891
- // Trait method signatures, looked up for calls on a bounded `TParam`.
945
+ // declared signature and `unify` can check implementers.
946
+ methods.insert(
947
+ (t.name.clone(), TRAIT_MARKER.to_string()),
948
+ PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit)),
949
+ );
892
950
  for m in &t.methods {
893
- let params: Vec<PlumType> = m.params.iter().map(|p| paramTypeG(&p.ty)).collect();
951
+ let params: Vec<PlumType> = m
952
+ .params
953
+ .iter()
954
+ .map(|p| match &p.ty {
955
+ ast::ParamType::Type(ty) => conv(ty),
956
+ ast::ParamType::Variadic(ty) => {
957
+ PlumType::TVariadic(Box::new(conv(ty)))
958
+ }
959
+ ast::ParamType::Fn(ps, ret) => PlumType::TFun(
960
+ ps.iter().map(&conv).collect(),
961
+ Box::new(ret.as_ref().map(|r| conv(r)).unwrap_or(PlumType::TUnit)),
962
+ ),
963
+ })
964
+ .collect();
894
965
  let ret = m.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
966
+ let sig = PlumType::TFun(params, Box::new(ret));
967
+ methods.insert((t.name.clone(), m.name.clone()), sig.clone());
968
+ if generic {
969
+ // Looked up for calls on a bounded `TParam`.
895
- global_env.insert(
970
+ global_env.insert(
896
- format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
971
+ format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
897
- TypeScheme::mono(PlumType::TFun(params, Box::new(ret))),
972
+ TypeScheme::mono(sig),
898
- );
973
+ );
974
+ }
899
975
  }
900
976
  }
901
977
  _ => {}
plum-checker/tests/checker_tests.rs CHANGED
@@ -2224,3 +2224,73 @@ fn probe_nested_generic_substitution() {
2224
2224
  let m2 = "fun f() -> List[Int] =\n List(items: 1).map(|x| x + 1)\n";
2225
2225
  assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, m2))).is_ok());
2226
2226
  }
2227
+
2228
+ // ---- trait types (T3) ----
2229
+
2230
+ const SHAPE_DEFS: &str = "\
2231
+ trait Shape =
2232
+ area() -> Float
2233
+
2234
+ enum Circle(Shape) =
2235
+ | Circle(r: Float)
2236
+
2237
+ fun area(self) -> Float =
2238
+ self.r
2239
+
2240
+ enum Plain =
2241
+ | Plain(n: Int)
2242
+
2243
+ ";
2244
+
2245
+ #[test]
2246
+ fn trait_type_accepts_implementer() {
2247
+ let src = format!(
2248
+ "{}fun total(s: Shape) -> Float =\n s.area()\n\nfun go() -> Float =\n total(Circle(r: 2.0))\n",
2249
+ SHAPE_DEFS
2250
+ );
2251
+ let r = checkSource(&parseClean(&src));
2252
+ assert!(r.is_ok(), "got {:?}", r.err());
2253
+ }
2254
+
2255
+ #[test]
2256
+ fn missing_trait_method_rejected_in_checker() {
2257
+ let src = format!(
2258
+ "{}fun total(s: Shape) -> Float =\n s.area()\n\nfun go() -> Float =\n total(Plain(n: 1))\n",
2259
+ SHAPE_DEFS
2260
+ );
2261
+ let msg = errText(checkSource(&parseClean(&src)));
2262
+ assert!(
2263
+ msg.contains("type 'Plain' does not implement trait 'Shape': missing 'area'"),
2264
+ "got {}",
2265
+ msg
2266
+ );
2267
+ }
2268
+
2269
+ #[test]
2270
+ fn trait_method_call_has_declared_signature() {
2271
+ let src = format!("{}fun f(s: Shape) -> Int =\n s.area()\n", SHAPE_DEFS);
2272
+ assert!(checkSource(&parseClean(&src)).is_err());
2273
+ }
2274
+
2275
+ #[test]
2276
+ fn primitive_coerces_to_trait_via_number() {
2277
+ let src = "\
2278
+ enum Number =
2279
+ | Int
2280
+ | Float
2281
+
2282
+ fun toStr(self) -> Str =
2283
+ \"n\"
2284
+
2285
+ trait ToStr =
2286
+ toStr() -> Str
2287
+
2288
+ fun show(x: ToStr) -> Str =
2289
+ x.toStr()
2290
+
2291
+ fun go() -> Str =
2292
+ show(5)
2293
+ ";
2294
+ let r = checkSource(&parseClean(src));
2295
+ assert!(r.is_ok(), "got {:?}", r.err());
2296
+ }
plum-cli/src/main.rs CHANGED
@@ -654,11 +654,11 @@ fn unwrapStrToArray(
654
654
  s: &wasmtime::Rooted<wasmtime::AnyRef>,
655
655
  ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
656
656
  let str_struct = s.unwrap_struct(&store)?;
657
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
657
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 1)? else {
658
658
  return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
659
659
  };
660
660
  let buffer_struct = buffer_ref.unwrap_struct(&store)?;
661
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
661
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 1)? else {
662
662
  return Err(wasmtime::Error::msg(
663
663
  "Buffer.data is not a []Byte reference",
664
664
  ));
@@ -708,7 +708,7 @@ fn makeStrResult(
708
708
  .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
709
709
  .clone();
710
710
  let buffer_ty = str_ty
711
- .field(0)
711
+ .field(1)
712
712
  .and_then(|f| match f.element_type() {
713
713
  wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
714
714
  rt.heap_type().as_concrete_struct().cloned()
@@ -719,7 +719,7 @@ fn makeStrResult(
719
719
  wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type")
720
720
  })?;
721
721
  let array_ty = buffer_ty
722
- .field(0)
722
+ .field(1)
723
723
  .and_then(|f| match f.element_type() {
724
724
  wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => {
725
725
  rt.heap_type().as_concrete_array().cloned()
@@ -738,6 +738,7 @@ fn makeStrResult(
738
738
  &mut caller,
739
739
  &buffer_pre,
740
740
  &[
741
+ wasmtime::Val::I32(6), // Buffer typeId (see plum_wasm_codegen::TYPE_ID_BUFFER)
741
742
  wasmtime::Val::AnyRef(Some(arr.to_anyref())),
742
743
  wasmtime::Val::I64(bytes.len() as i64),
743
744
  ],
@@ -747,7 +748,7 @@ fn makeStrResult(
747
748
  let str_val = wasmtime::StructRef::new(
748
749
  &mut caller,
749
750
  &str_pre,
750
- &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))],
751
+ &[wasmtime::Val::I32(5), wasmtime::Val::AnyRef(Some(buffer.to_anyref()))], // 5 = Str typeId
751
752
  )?;
752
753
  Ok(str_val.to_anyref())
753
754
  }
plum-examples/traits.plum ADDED
@@ -0,0 +1,66 @@
1
+ import std/Bool
2
+ import std/Str
3
+ import std/Number
4
+ import std/Try
5
+ import std/List
6
+
7
+ # A trait name is a type. Any value whose type implements every method the
8
+ # trait declares can be passed where the trait is expected, and calling a
9
+ # method on it dispatches on the value's runtime type id.
10
+ trait Shape =
11
+ area() -> Float
12
+ name() -> Str
13
+
14
+ enum Circle(Shape) =
15
+ | Circle(r: Float)
16
+
17
+ fun area(self) -> Float =
18
+ self.r * self.r * 3.0
19
+
20
+ fun name(self) -> Str =
21
+ "circle"
22
+
23
+ enum Square(Shape) =
24
+ | Square(s: Float)
25
+
26
+ fun area(self) -> Float =
27
+ self.s * self.s
28
+
29
+ fun name(self) -> Str =
30
+ "square"
31
+
32
+ enum Tagged(ToStr) =
33
+ | Tagged(label: Str)
34
+
35
+ fun toStr(self) -> Str =
36
+ "<{self.label}>"
37
+
38
+ fun totalArea(shapes: List[Shape]) -> Float =
39
+ shapes.reduce(0.0, |acc, s| acc + s.area())
40
+
41
+ fun describe(s: Shape) -> Str =
42
+ "{s.name()}"
43
+
44
+ # Int, Float, Str and a user enum all flow through the same `ToStr` slot;
45
+ # the Int and Float are boxed on the way in.
46
+ fun show(x: ToStr) -> Str =
47
+ x.toStr()
48
+
49
+ test "a heterogeneous List[Shape] sums its areas through dynamic dispatch"
50
+ shapes := List[Shape]()
51
+ shapes.add(Circle(r: 1.5))
52
+ shapes.add(Square(s: 1.5))
53
+ shapes.add(Circle(r: 0.5))
54
+ # 6.75 + 2.25 + 0.75
55
+ assert totalArea(shapes) == 9.75
56
+
57
+ test "a method on a trait-typed value picks the implementor by runtime type"
58
+ assert describe(Circle(r: 1.0)) == "circle"
59
+ assert describe(Square(s: 1.0)) == "square"
60
+
61
+ test "ToStr on mixed values: boxed Int, boxed Float, Str and an enum"
62
+ assert show(5) == "5"
63
+ assert show(-12) == "-12"
64
+ assert show(2.5) == "2.5"
65
+ assert show("hi") == "hi"
66
+ assert show(Tagged(label: "x")) == "<x>"
plum-runtime/src/main.rs CHANGED
@@ -282,11 +282,11 @@ fn unwrapStrToArray(
282
282
  s: &wasmtime::Rooted<wasmtime::AnyRef>,
283
283
  ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
284
284
  let str_struct = s.unwrap_struct(&store)?;
285
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
285
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 1)? else {
286
286
  return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
287
287
  };
288
288
  let buffer_struct = buffer_ref.unwrap_struct(&store)?;
289
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
289
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 1)? else {
290
290
  return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
291
291
  };
292
292
  bytes_ref.unwrap_array(&store)
@@ -321,13 +321,13 @@ fn makeStrResult(
321
321
  let str_ty = ref_ty.heap_type().as_concrete_struct()
322
322
  .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
323
323
  .clone();
324
- let buffer_ty = str_ty.field(0)
324
+ let buffer_ty = str_ty.field(1)
325
325
  .and_then(|f| match f.element_type() {
326
326
  wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(),
327
327
  _ => None,
328
328
  })
329
329
  .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?;
330
- let array_ty = buffer_ty.field(0)
330
+ let array_ty = buffer_ty.field(1)
331
331
  .and_then(|f| match f.element_type() {
332
332
  wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(),
333
333
  _ => None,
@@ -341,11 +341,11 @@ fn makeStrResult(
341
341
  let buffer = wasmtime::StructRef::new(
342
342
  &mut caller,
343
343
  &buffer_pre,
344
- &[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)],
344
+ &[wasmtime::Val::I32(6), wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)], // 6 = Buffer typeId
345
345
  )?;
346
346
 
347
347
  let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
348
- let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?;
348
+ let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::I32(5), wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?; // 5 = Str typeId
349
349
  Ok(str_val.to_anyref())
350
350
  }
351
351
 
plum-wasm-codegen/src/lib.rs CHANGED
@@ -629,20 +629,52 @@ fn collectTagNames(s: &str, out: &mut std::collections::BTreeSet<String>) {
629
629
  /// reference values are stored as-is.
630
630
  fn boxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
631
631
  match plumTypeToValtype(ty) {
632
- ValType::I64 => Instruction::StructNew(ctx.gc_types.box_i64_idx).encode(body),
632
+ ValType::I64 => Instruction::Call(ctx.gc_types.box_i64_fn).encode(body),
633
- ValType::F64 => Instruction::StructNew(ctx.gc_types.box_f64_idx).encode(body),
633
+ ValType::F64 => Instruction::Call(ctx.gc_types.box_f64_fn).encode(body),
634
- ValType::I32 => Instruction::StructNew(ctx.gc_types.box_i32_idx).encode(body),
634
+ ValType::I32 => Instruction::Call(ctx.gc_types.box_i32_fn).encode(body),
635
635
  _ => {}
636
636
  }
637
637
  }
638
638
 
639
+ /// Boxes the primitive on top of the stack (`ty`) into its `$Box*` struct when
640
+ /// it flows into a slot whose wasm type is `expected` and `ty` itself is not a
641
+ /// reference — i.e. an `Int`/`Float`/`Byte` entering a trait-typed (`$Obj`) slot.
642
+ fn boxIfPrimitiveIntoObj(ty: &PlumType, expected: ValType, body: &mut Vec<u8>, ctx: &LocalCtx) {
643
+ let ValType::Ref(rt) = expected else { return };
644
+ if rt.heap_type != HeapType::Concrete(ctx.gc_types.obj_type_idx) {
645
+ return;
646
+ }
647
+ if matches!(ty, PlumType::TInt | PlumType::TFloat | PlumType::TByte) {
648
+ boxTagPayload(ty, body, ctx);
649
+ }
650
+ }
651
+
652
+ /// Compiles `expr` and, if it is a primitive flowing into a trait-typed slot of
653
+ /// wasm type `expected`, boxes it.
654
+ fn compileExprAs(
655
+ expr: &ast::Expr,
656
+ expected: Option<ValType>,
657
+ body: &mut Vec<u8>,
658
+ ctx: &LocalCtx,
659
+ state: &mut ModuleState,
660
+ ) -> Result<(), String> {
661
+ compileExpr(expr, body, ctx, state)?;
662
+ if let Some(vt @ ValType::Ref(rt)) = expected {
663
+ if rt.heap_type == HeapType::Concrete(ctx.gc_types.obj_type_idx) {
664
+ let ty = inferLocalType(expr, ctx);
665
+ boxIfPrimitiveIntoObj(&ty, vt, body, ctx);
666
+ }
667
+ }
668
+ Ok(())
669
+ }
670
+
639
671
  /// Inverse of `boxTagPayload`: an `anyref` payload on the stack becomes a `ty` value.
640
672
  fn unboxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
641
673
  let unbox = |idx: u32, body: &mut Vec<u8>| {
642
674
  Instruction::RefCastNonNull(HeapType::Concrete(idx)).encode(body);
643
675
  Instruction::StructGet {
644
676
  struct_type_index: idx,
645
- field_index: 0,
677
+ field_index: fld(0),
646
678
  }
647
679
  .encode(body);
648
680
  };
@@ -668,6 +700,7 @@ fn compileTagLiteral(
668
700
  ctx: &LocalCtx,
669
701
  state: &mut ModuleState,
670
702
  ) -> Result<(), String> {
703
+ Instruction::I32Const(TYPE_ID_TAG).encode(body);
671
704
  Instruction::I32Const(tagId(name)).encode(body);
672
705
  match payload {
673
706
  Some(e) => {
@@ -866,6 +899,46 @@ pub struct GcTypeRegistry {
866
899
  pub box_i64_idx: u32,
867
900
  pub box_f64_idx: u32,
868
901
  pub box_i32_idx: u32,
902
+ /// The `$Obj {typeId: i32}` supertype of every class/enum/tag/box struct.
903
+ pub obj_type_idx: u32,
904
+ /// Enum name and variant name -> the runtime `typeId` stored in field 0 of
905
+ /// every value of that type (all variants of an enum share the enum's id).
906
+ pub type_ids: HashMap<String, i32>,
907
+ /// Box-helper function indices `(i64) -> $BoxI64`, `(f64) -> $BoxF64`,
908
+ /// `(i32) -> $BoxI32` (they push the header typeId themselves).
909
+ pub box_i64_fn: u32,
910
+ pub box_f64_fn: u32,
911
+ pub box_i32_fn: u32,
912
+ /// Helper `(arr: array<i8>, len: i64) -> Str` building `Str{Buffer{arr,len}}`
913
+ /// with correct headers; `None` when the program has no `Str`/`Buffer`.
914
+ pub mk_str_fn: Option<u32>,
915
+ /// `(trait, method)` -> its `$dyn$<Trait>$<method>` dispatcher function.
916
+ pub dyn_fns: HashMap<(String, String), u32>,
917
+ }
918
+
919
+ /// Box type ids (see the plan's Global constraints): 0 is `$Obj` itself.
920
+ pub const TYPE_ID_BOX_I64: i32 = 1;
921
+ pub const TYPE_ID_BOX_F64: i32 = 2;
922
+ pub const TYPE_ID_BOX_I32: i32 = 3;
923
+ pub const TYPE_ID_TAG: i32 = 4;
924
+ /// `Str` and `Buffer` get fixed ids so hosts can build them without a lookup.
925
+ pub const TYPE_ID_STR: i32 = 5;
926
+ pub const TYPE_ID_BUFFER: i32 = 6;
927
+ /// First id handed to a declared class/enum, in declaration order.
928
+ pub const TYPE_ID_FIRST_DECLARED: i32 = 16;
929
+
930
+ /// Every class/enum/tag/box struct carries `typeId: i32` as field 0, so a
931
+ /// declared field `i` lives at struct field `i + OBJ_HEADER_FIELDS`.
932
+ const OBJ_HEADER_FIELDS: u32 = 1;
933
+
934
+ /// Struct field index of declared payload field `i` (skipping the typeId header).
935
+ fn fld(i: usize) -> u32 {
936
+ i as u32 + OBJ_HEADER_FIELDS
937
+ }
938
+
939
+ /// The runtime typeId of a variant/enum, or 0 when unknown.
940
+ fn typeIdOf(r: &GcTypeRegistry, name: &str) -> i32 {
941
+ r.type_ids.get(name).copied().unwrap_or(0)
869
942
  }
870
943
 
871
944
  /// Whether `name` is `Array` itself or one of its monomorphized specializations
@@ -955,6 +1028,8 @@ fn buildGcTypeRegistry(
955
1028
  ) -> GcTypeRegistry {
956
1029
  enum Slot {
957
1030
  Str,
1031
+ /// `$Obj {typeId: i32}` — supertype of every struct below.
1032
+ Obj,
958
1033
  EnumSuper(String),
959
1034
  Variant(String),
960
1035
  /// The single shared `array<anyref>` type every `Array[T]`
@@ -968,13 +1043,17 @@ fn buildGcTypeRegistry(
968
1043
 
969
1044
  // Pass 1: assign every entry a slot (and therefore a type index) up front, before
970
1045
  // any field list is built, so field-type resolution can reference ANY other entry.
971
- let mut slots: Vec<Slot> = vec![Slot::Str];
1046
+ let mut slots: Vec<Slot> = vec![Slot::Str, Slot::Obj];
1047
+ let obj_type_idx: u32 = 1;
1048
+ let mut type_ids: HashMap<String, i32> = HashMap::new();
1049
+ let mut next_type_id = TYPE_ID_FIRST_DECLARED;
972
1050
  let mut class_type_idx: HashMap<String, u32> = HashMap::new();
973
1051
  let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
974
1052
  let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
975
1053
  let mut array_ref_slot: Option<u32> = None;
976
1054
  let mut erased_enum_super: HashMap<String, u32> = HashMap::new();
977
1055
  let mut erased_variant_idx: HashMap<String, u32> = HashMap::new();
1056
+ let mut erased_enum_id: HashMap<String, i32> = HashMap::new();
978
1057
 
979
1058
  // `Bool` is an ordinary enum now (`type Bool = | True | False` in
980
1059
  // `libs/std/Bool.plum`) — registered exactly like any other enum
@@ -988,6 +1067,12 @@ fn buildGcTypeRegistry(
988
1067
  ));
989
1068
  }
990
1069
  }
1070
+ // A trait name used as a type lowers to `ref null $Obj`.
1071
+ for item in &source.items {
1072
+ if let ast::Item::Trait(t) = item {
1073
+ class_type_idx.insert(t.name.clone(), obj_type_idx);
1074
+ }
1075
+ }
991
1076
  for (enum_name, variant_names) in &enum_decls {
992
1077
  // `Array`/`Array$T` is a RECORD-shaped enum (single variant sharing the
993
1078
  // enum's own name — the `type Array[T] = ...` replacement) whose values
@@ -1016,6 +1101,13 @@ fn buildGcTypeRegistry(
1016
1101
  let erased_enum = plum_checker::erasedEnumName(enum_name);
1017
1102
  if let Some(existing_super) = erased_enum_super.get(&erased_enum) {
1018
1103
  enum_super_type_idx.insert(enum_name.clone(), *existing_super);
1104
+ // Shares the first specialization's wasm types, and so its typeId.
1105
+ if let Some(first_id) = erased_enum_id.get(&erased_enum).copied() {
1106
+ type_ids.insert(enum_name.clone(), first_id);
1107
+ for vname in variant_names {
1108
+ type_ids.insert(vname.clone(), first_id);
1109
+ }
1110
+ }
1019
1111
  for vname in variant_names {
1020
1112
  let ev = plum_checker::erasedEnumName(vname);
1021
1113
  if let Some(idx) = erased_variant_idx.get(&ev) {
@@ -1028,6 +1120,20 @@ fn buildGcTypeRegistry(
1028
1120
  }
1029
1121
  continue;
1030
1122
  }
1123
+ let this_id = match enum_name.as_str() {
1124
+ "Str" => TYPE_ID_STR,
1125
+ "Buffer" => TYPE_ID_BUFFER,
1126
+ _ => {
1127
+ let id = next_type_id;
1128
+ next_type_id += 1;
1129
+ id
1130
+ }
1131
+ };
1132
+ type_ids.insert(enum_name.clone(), this_id);
1133
+ for vname in variant_names {
1134
+ type_ids.insert(vname.clone(), this_id);
1135
+ }
1136
+ erased_enum_id.insert(erased_enum.clone(), this_id);
1031
1137
  enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
1032
1138
  erased_enum_super.insert(erased_enum, slots.len() as u32);
1033
1139
  slots.push(Slot::EnumSuper(enum_name.clone()));
@@ -1072,6 +1178,13 @@ fn buildGcTypeRegistry(
1072
1178
  box_i64_idx,
1073
1179
  box_f64_idx,
1074
1180
  box_i32_idx,
1181
+ obj_type_idx,
1182
+ type_ids,
1183
+ box_i64_fn: 0,
1184
+ box_f64_fn: 0,
1185
+ box_i32_fn: 0,
1186
+ mk_str_fn: None,
1187
+ dyn_fns: HashMap::new(),
1075
1188
  };
1076
1189
 
1077
1190
  // Pass 2: build the real SubType for every slot, now that every cross-reference
@@ -1101,16 +1214,14 @@ fn buildGcTypeRegistry(
1101
1214
  // trap at runtime whenever `self` isn't actually THAT variant).
1102
1215
  Slot::EnumSuper(enum_name) => {
1103
1216
  let params = enum_params.get(enum_name).cloned().unwrap_or_default();
1104
- let field_types: Vec<FieldType> = params
1217
+ let mut field_types: Vec<FieldType> = vec![typeIdField()];
1105
- .iter()
1106
- .map(|(_, ty)| FieldType {
1218
+ field_types.extend(params.iter().map(|(_, ty)| FieldType {
1107
- element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1219
+ element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1108
- mutable: true,
1220
+ mutable: true,
1109
- })
1221
+ }));
1110
- .collect();
1111
1222
  SubType {
1112
1223
  is_final: false,
1113
- supertype_idx: None,
1224
+ supertype_idx: Some(obj_type_idx),
1114
1225
  composite_type: CompositeType {
1115
1226
  inner: CompositeInnerType::Struct(StructType {
1116
1227
  fields: field_types.into(),
@@ -1139,14 +1250,11 @@ fn buildGcTypeRegistry(
1139
1250
  // supertype's own fields (see the `EnumSuper` arm above) — a variant with
1140
1251
  // zero ADDED fields beyond its supertype is still valid wasm-gc
1141
1252
  // subtyping, so no special-casing is needed here.
1142
- let field_types: Vec<FieldType> = info
1253
+ let mut field_types: Vec<FieldType> = vec![typeIdField()];
1143
- .field_types
1144
- .iter()
1145
- .map(|ty| FieldType {
1254
+ field_types.extend(info.field_types.iter().map(|ty| FieldType {
1146
- element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1255
+ element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1147
- mutable: true,
1256
+ mutable: true,
1148
- })
1257
+ }));
1149
- .collect();
1150
1258
  SubType {
1151
1259
  is_final: true,
1152
1260
  supertype_idx: Some(super_idx),
@@ -1158,12 +1266,23 @@ fn buildGcTypeRegistry(
1158
1266
  },
1159
1267
  }
1160
1268
  }
1269
+ Slot::Obj => SubType {
1270
+ is_final: false,
1271
+ supertype_idx: None,
1272
+ composite_type: CompositeType {
1273
+ inner: CompositeInnerType::Struct(StructType {
1274
+ fields: vec![typeIdField()].into(),
1275
+ }),
1276
+ shared: false,
1277
+ },
1278
+ },
1161
1279
  Slot::Tag => SubType {
1162
1280
  is_final: true,
1163
- supertype_idx: None,
1281
+ supertype_idx: Some(obj_type_idx),
1164
1282
  composite_type: CompositeType {
1165
1283
  inner: CompositeInnerType::Struct(StructType {
1166
1284
  fields: vec![
1285
+ typeIdField(),
1167
1286
  FieldType {
1168
1287
  element_type: StorageType::Val(ValType::I32),
1169
1288
  mutable: false,
@@ -1178,9 +1297,9 @@ fn buildGcTypeRegistry(
1178
1297
  shared: false,
1179
1298
  },
1180
1299
  },
1181
- Slot::BoxI64 => boxSubType(ValType::I64),
1300
+ Slot::BoxI64 => boxSubType(ValType::I64, obj_type_idx),
1182
- Slot::BoxF64 => boxSubType(ValType::F64),
1301
+ Slot::BoxF64 => boxSubType(ValType::F64, obj_type_idx),
1183
- Slot::BoxI32 => boxSubType(ValType::I32),
1302
+ Slot::BoxI32 => boxSubType(ValType::I32, obj_type_idx),
1184
1303
  Slot::ArrayRef => SubType {
1185
1304
  is_final: true,
1186
1305
  supertype_idx: None,
@@ -1200,16 +1319,27 @@ fn buildGcTypeRegistry(
1200
1319
  registry
1201
1320
  }
1202
1321
 
1322
+ /// The immutable `typeId: i32` header field every `$Obj` subtype starts with.
1323
+ fn typeIdField() -> FieldType {
1324
+ FieldType {
1325
+ element_type: StorageType::Val(ValType::I32),
1326
+ mutable: false,
1327
+ }
1328
+ }
1329
+
1203
- fn boxSubType(vt: ValType) -> SubType {
1330
+ fn boxSubType(vt: ValType, obj_type_idx: u32) -> SubType {
1204
1331
  SubType {
1205
1332
  is_final: true,
1206
- supertype_idx: None,
1333
+ supertype_idx: Some(obj_type_idx),
1207
1334
  composite_type: CompositeType {
1208
1335
  inner: CompositeInnerType::Struct(StructType {
1209
- fields: vec![FieldType {
1336
+ fields: vec![
1337
+ typeIdField(),
1338
+ FieldType {
1210
- element_type: StorageType::Val(vt),
1339
+ element_type: StorageType::Val(vt),
1211
- mutable: false,
1340
+ mutable: false,
1212
- }]
1341
+ },
1342
+ ]
1213
1343
  .into(),
1214
1344
  }),
1215
1345
  shared: false,
@@ -1647,13 +1777,43 @@ fn compileSourceInner(
1647
1777
  let global_idx = module.addGlobal(gcRef(variant_idx), true, &init);
1648
1778
  singleton_globals.insert(name.clone(), global_idx);
1649
1779
 
1780
+ // A payload-free variant is exactly `{typeId}`.
1781
+ Instruction::I32Const(typeIdOf(&gc_types, name)).encode(&mut start_body);
1650
- Instruction::StructNewDefault(variant_idx).encode(&mut start_body);
1782
+ Instruction::StructNew(variant_idx).encode(&mut start_body);
1651
1783
  Instruction::GlobalSet(global_idx).encode(&mut start_body);
1652
1784
  }
1653
1785
  let start_type_idx = module.addType(&[], &[]);
1654
1786
  let start_func_idx = module.addFunction(start_type_idx, &[]);
1655
1787
  module.setStartFunction(start_func_idx);
1656
1788
 
1789
+ // Box helpers (`i64 -> $BoxI64`, ...) and the `Str` builder push the header
1790
+ // typeId themselves, so callers only supply the payload.
1791
+ gc_types.box_i64_fn =
1792
+ registerBoxHelper(&mut module, ValType::I64, TYPE_ID_BOX_I64, gc_types.box_i64_idx);
1793
+ gc_types.box_f64_fn =
1794
+ registerBoxHelper(&mut module, ValType::F64, TYPE_ID_BOX_F64, gc_types.box_f64_idx);
1795
+ gc_types.box_i32_fn =
1796
+ registerBoxHelper(&mut module, ValType::I32, TYPE_ID_BOX_I32, gc_types.box_i32_idx);
1797
+ if let (Some(&buffer_idx), Some(&str_idx)) = (
1798
+ gc_types.class_type_idx.get("Buffer"),
1799
+ gc_types.class_type_idx.get("Str"),
1800
+ ) {
1801
+ let mut b = vec![0u8];
1802
+ Instruction::I32Const(TYPE_ID_STR).encode(&mut b);
1803
+ Instruction::I32Const(TYPE_ID_BUFFER).encode(&mut b);
1804
+ Instruction::LocalGet(0).encode(&mut b);
1805
+ Instruction::LocalGet(1).encode(&mut b);
1806
+ Instruction::StructNew(buffer_idx).encode(&mut b);
1807
+ Instruction::StructNew(str_idx).encode(&mut b);
1808
+ Instruction::End.encode(&mut b);
1809
+ let t = module.addType(
1810
+ &[gcRef(gc_types.byte_array_type_idx), ValType::I64],
1811
+ &[gcRef(str_idx)],
1812
+ );
1813
+ gc_types.mk_str_fn = Some(module.addFunction(t, &b));
1814
+ }
1815
+ CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1816
+
1657
1817
  // Closure wasm signature -> function-type index, deduped so every closure/call site
1658
1818
  // of the same shape shares one `call_indirect` type.
1659
1819
  let mut closure_call_types: HashMap<ClosureSigKey, u32> = HashMap::new();
@@ -1706,6 +1866,17 @@ fn compileSourceInner(
1706
1866
  }
1707
1867
  }
1708
1868
 
1869
+ registerDynDispatchers(
1870
+ &mut module,
1871
+ source,
1872
+ &methods,
1873
+ &enum_variants,
1874
+ &func_ids,
1875
+ &func_sigs,
1876
+ &mut gc_types,
1877
+ );
1878
+ CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1879
+
1709
1880
  let fns: Vec<&ast::Fn> = source
1710
1881
  .items
1711
1882
  .iter()
@@ -1995,6 +2166,177 @@ fn compileSourceInner(
1995
2166
  Ok(module.finish())
1996
2167
  }
1997
2168
 
2169
+ /// `(payload) -> ref $Box*`: allocates a box with its header typeId set.
2170
+ fn registerBoxHelper(module: &mut WasmModule, vt: ValType, type_id: i32, box_idx: u32) -> u32 {
2171
+ let mut b = vec![0u8];
2172
+ Instruction::I32Const(type_id).encode(&mut b);
2173
+ Instruction::LocalGet(0).encode(&mut b);
2174
+ Instruction::StructNew(box_idx).encode(&mut b);
2175
+ Instruction::End.encode(&mut b);
2176
+ let t = module.addType(&[vt], &[gcRef(box_idx)]);
2177
+ module.addFunction(t, &b)
2178
+ }
2179
+
2180
+ /// Emits one `$dyn$<Trait>$<method>(self: ref null $Obj, args...)` dispatcher per
2181
+ /// trait method that has at least one implementor: it reads the receiver's
2182
+ /// `typeId` and calls the matching concrete method. Declared enums dispatch by
2183
+ /// their own id; boxed `Int`/`Float`/`Byte` unbox and call the primitive's method
2184
+ /// (or, for `Int`/`Float`, wrap into the "union" enum such as `Number` that
2185
+ /// defines it — the same fallback a direct `x.toStr()` call uses). Only methods
2186
+ /// present in `func_ids` (i.e. actually compiled) are ever referenced.
2187
+ fn registerDynDispatchers(
2188
+ module: &mut WasmModule,
2189
+ source: &ast::Source,
2190
+ methods: &MethodEnv,
2191
+ enum_variants: &EnumVariants,
2192
+ func_ids: &HashMap<String, u32>,
2193
+ func_sigs: &HashMap<String, FuncSig>,
2194
+ gc_types: &mut GcTypeRegistry,
2195
+ ) {
2196
+ let obj_idx = gc_types.obj_type_idx;
2197
+ let enum_names: Vec<&str> = source
2198
+ .items
2199
+ .iter()
2200
+ .filter_map(|i| match i {
2201
+ ast::Item::Enum(e) if !isArraySpecialization(&e.name) => Some(e.name.as_str()),
2202
+ _ => None,
2203
+ })
2204
+ .collect();
2205
+ let mut registered: Vec<((String, String), u32)> = Vec::new();
2206
+ for item in &source.items {
2207
+ let ast::Item::Trait(t) = item else { continue };
2208
+ for m in &t.methods {
2209
+ let key = (t.name.clone(), m.name.clone());
2210
+ let Some(PlumType::TFun(ps, ret)) = methods.get(&key) else {
2211
+ continue;
2212
+ };
2213
+ if ps
2214
+ .iter()
2215
+ .any(|p| matches!(p, PlumType::TVariadic(_) | PlumType::TFun(..)))
2216
+ {
2217
+ continue;
2218
+ }
2219
+ let params_vt: Vec<ValType> = ps.iter().map(plumTypeToValtype).collect();
2220
+ let ret_vt: Option<ValType> = if **ret == PlumType::TUnit {
2221
+ None
2222
+ } else {
2223
+ Some(plumTypeToValtype(ret))
2224
+ };
2225
+ let sig_matches = |fkey: &str| {
2226
+ func_sigs
2227
+ .get(fkey)
2228
+ .is_some_and(|s| s.params.get(1..) == Some(&params_vt[..]) && s.ret == ret_vt)
2229
+ };
2230
+ // (typeId, arm code) — code leaves the call result on the stack.
2231
+ let mut arms: Vec<(i32, Vec<u8>)> = Vec::new();
2232
+ let push_args = |b: &mut Vec<u8>| {
2233
+ for i in 0..params_vt.len() {
2234
+ Instruction::LocalGet(i as u32 + 1).encode(b);
2235
+ }
2236
+ };
2237
+ let mut seen_ids = std::collections::HashSet::new();
2238
+ for en in &enum_names {
2239
+ let fkey = format!("{}::{}", en, m.name);
2240
+ let Some(&fidx) = func_ids.get(&fkey) else { continue };
2241
+ let id = typeIdOf(gc_types, en);
2242
+ if id == 0 || !sig_matches(&fkey) || !seen_ids.insert(id) {
2243
+ continue;
2244
+ }
2245
+ let Some(ValType::Ref(self_rt)) = func_sigs.get(&fkey).and_then(|s| s.params.first().copied())
2246
+ else {
2247
+ continue;
2248
+ };
2249
+ let mut b = Vec::new();
2250
+ Instruction::LocalGet(0).encode(&mut b);
2251
+ Instruction::RefCastNullable(self_rt.heap_type).encode(&mut b);
2252
+ push_args(&mut b);
2253
+ Instruction::Call(fidx).encode(&mut b);
2254
+ arms.push((id, b));
2255
+ }
2256
+ // Boxed primitives.
2257
+ for (prim, vt, box_idx, tid) in [
2258
+ ("Int", ValType::I64, gc_types.box_i64_idx, TYPE_ID_BOX_I64),
2259
+ ("Float", ValType::F64, gc_types.box_f64_idx, TYPE_ID_BOX_F64),
2260
+ ("Byte", ValType::I32, gc_types.box_i32_idx, TYPE_ID_BOX_I32),
2261
+ ] {
2262
+ let direct = format!("{}::{}", prim, m.name);
2263
+ let mut b = Vec::new();
2264
+ if func_ids.contains_key(&direct)
2265
+ && sig_matches(&direct)
2266
+ && func_sigs.get(&direct).and_then(|s| s.params.first().copied()) == Some(vt)
2267
+ {
2268
+ Instruction::LocalGet(0).encode(&mut b);
2269
+ Instruction::RefCastNonNull(HeapType::Concrete(box_idx)).encode(&mut b);
2270
+ Instruction::StructGet {
2271
+ struct_type_index: box_idx,
2272
+ field_index: fld(0),
2273
+ }
2274
+ .encode(&mut b);
2275
+ push_args(&mut b);
2276
+ Instruction::Call(func_ids[&direct]).encode(&mut b);
2277
+ } else if let Some(info) = enum_variants.get(prim) {
2278
+ let wrap_key = format!("{}::{}", info.enum_name, m.name);
2279
+ let (Some(&fidx), Some(&variant_idx)) =
2280
+ (func_ids.get(&wrap_key), gc_types.variant_type_idx.get(prim))
2281
+ else {
2282
+ continue;
2283
+ };
2284
+ if !sig_matches(&wrap_key) {
2285
+ continue;
2286
+ }
2287
+ Instruction::I32Const(typeIdOf(gc_types, prim)).encode(&mut b);
2288
+ Instruction::LocalGet(0).encode(&mut b);
2289
+ Instruction::RefCastNonNull(HeapType::Concrete(box_idx)).encode(&mut b);
2290
+ Instruction::StructGet {
2291
+ struct_type_index: box_idx,
2292
+ field_index: fld(0),
2293
+ }
2294
+ .encode(&mut b);
2295
+ Instruction::StructNew(variant_idx).encode(&mut b);
2296
+ push_args(&mut b);
2297
+ Instruction::Call(fidx).encode(&mut b);
2298
+ } else {
2299
+ continue;
2300
+ }
2301
+ arms.push((tid, b));
2302
+ }
2303
+ if arms.is_empty() {
2304
+ continue;
2305
+ }
2306
+ let mut all_params = vec![gcRef(obj_idx)];
2307
+ all_params.extend(params_vt.iter().copied());
2308
+ let results: Vec<ValType> = ret_vt.into_iter().collect();
2309
+ let tid_local = all_params.len() as u32;
2310
+ let mut body = Vec::new();
2311
+ body.extend(encodeLeb128U32(1));
2312
+ body.extend(encodeLeb128U32(1));
2313
+ ValType::I32.encode(&mut body);
2314
+ Instruction::LocalGet(0).encode(&mut body);
2315
+ Instruction::StructGet {
2316
+ struct_type_index: obj_idx,
2317
+ field_index: 0,
2318
+ }
2319
+ .encode(&mut body);
2320
+ Instruction::LocalSet(tid_local).encode(&mut body);
2321
+ for (id, code) in &arms {
2322
+ Instruction::LocalGet(tid_local).encode(&mut body);
2323
+ Instruction::I32Const(*id).encode(&mut body);
2324
+ Instruction::I32Eq.encode(&mut body);
2325
+ Instruction::If(BlockType::Empty).encode(&mut body);
2326
+ body.extend(code);
2327
+ Instruction::Return.encode(&mut body);
2328
+ Instruction::End.encode(&mut body);
2329
+ }
2330
+ Instruction::Unreachable.encode(&mut body);
2331
+ Instruction::End.encode(&mut body);
2332
+ let t = module.addType(&all_params, &results);
2333
+ let fidx = module.addFunction(t, &body);
2334
+ registered.push((key, fidx));
2335
+ }
2336
+ }
2337
+ gc_types.dyn_fns.extend(registered);
2338
+ }
2339
+
1998
2340
  /// Registers `__string_concat(a: ref Str, b: ref Str) -> ref Str`, a hand-written
1999
2341
  /// runtime helper backing string interpolation. Unwraps both `Str` params down
2000
2342
  /// to their raw `array<i8>`s (`unwrapStrToBytes`) — which each track their own
@@ -2451,9 +2793,10 @@ fn wrapBytesAsStr(body: &mut Vec<u8>, push_len: impl FnOnce(&mut Vec<u8>)) {
2451
2793
  let Some((buffer_type_idx, str_type_idx)) = types else {
2452
2794
  return;
2453
2795
  };
2796
+ let _ = (buffer_type_idx, str_type_idx);
2454
2797
  push_len(body);
2455
- Instruction::StructNew(buffer_type_idx).encode(body);
2798
+ let mk = withGcTypes(|r| r.mk_str_fn).expect("mk_str_fn registered with Str/Buffer");
2456
- Instruction::StructNew(str_type_idx).encode(body);
2799
+ Instruction::Call(mk).encode(body);
2457
2800
  }
2458
2801
 
2459
2802
  /// The inverse of `wrapBytesAsStr`: given a `Str` value already on the stack,
@@ -2474,12 +2817,12 @@ fn unwrapStrToBytes(body: &mut Vec<u8>) {
2474
2817
  };
2475
2818
  Instruction::StructGet {
2476
2819
  struct_type_index: str_type_idx,
2477
- field_index: 0,
2820
+ field_index: fld(0),
2478
2821
  }
2479
2822
  .encode(body);
2480
2823
  Instruction::StructGet {
2481
2824
  struct_type_index: buffer_type_idx,
2482
- field_index: 0,
2825
+ field_index: fld(0),
2483
2826
  }
2484
2827
  .encode(body);
2485
2828
  }
@@ -3879,7 +4222,7 @@ fn compileStmtInValuePosition(
3879
4222
  state: &mut ModuleState,
3880
4223
  ) -> Result<(), String> {
3881
4224
  match stmt {
3882
- ast::Stmt::Expr(e, ..) => compileExpr(e, body, ctx, state),
4225
+ ast::Stmt::Expr(e, ..) => compileExprAs(e, Some(result_vt), body, ctx, state),
3883
4226
  ast::Stmt::Return(_) | ast::Stmt::Todo | ast::Stmt::Crash(_) => {
3884
4227
  compileStmt(stmt, body, ctx, state)
3885
4228
  }
@@ -4116,7 +4459,7 @@ fn compileStmt(
4116
4459
  compileExpr(value, body, ctx, state)?;
4117
4460
  Instruction::StructSet {
4118
4461
  struct_type_index: super_type_idx,
4119
- field_index: field_idx as u32,
4462
+ field_index: fld(field_idx),
4120
4463
  }
4121
4464
  .encode(body);
4122
4465
  }
@@ -4147,7 +4490,7 @@ fn compileStmt(
4147
4490
  compileExpr(value, body, ctx, state)?;
4148
4491
  Instruction::StructSet {
4149
4492
  struct_type_index: variant_type_idx,
4150
- field_index: field_idx as u32,
4493
+ field_index: fld(field_idx),
4151
4494
  }
4152
4495
  .encode(body);
4153
4496
  }
@@ -4157,7 +4500,7 @@ fn compileStmt(
4157
4500
  }
4158
4501
  }
4159
4502
  ast::Stmt::Return(Some(e)) => {
4160
- compileExpr(e, body, ctx, state)?;
4503
+ compileExprAs(e, ctx.ret_type.as_ref().map(plumTypeToValtype), body, ctx, state)?;
4161
4504
  Instruction::Return.encode(body);
4162
4505
  }
4163
4506
  ast::Stmt::Return(None) => {
@@ -4501,7 +4844,7 @@ fn compileTagPattern(
4501
4844
  load(body);
4502
4845
  Instruction::StructGet {
4503
4846
  struct_type_index: tag_idx,
4504
- field_index: 0,
4847
+ field_index: fld(0),
4505
4848
  }
4506
4849
  .encode(body);
4507
4850
  Instruction::I32Const(tagId(name)).encode(body);
@@ -4516,7 +4859,7 @@ fn compileTagPattern(
4516
4859
  load(body);
4517
4860
  Instruction::StructGet {
4518
4861
  struct_type_index: tag_idx,
4519
- field_index: 1,
4862
+ field_index: fld(1),
4520
4863
  }
4521
4864
  .encode(body);
4522
4865
  unboxTagPayload(pty, body, ctx);
@@ -5056,7 +5399,7 @@ fn compileFieldPatterns(
5056
5399
  Instruction::LocalGet(container_local).encode(b);
5057
5400
  Instruction::StructGet {
5058
5401
  struct_type_index: container_type_idx,
5059
- field_index: fpos as u32,
5402
+ field_index: fld(fpos),
5060
5403
  }
5061
5404
  .encode(b);
5062
5405
  },
@@ -5125,7 +5468,7 @@ fn compileFieldPatterns(
5125
5468
  Instruction::LocalGet(container_local).encode(body);
5126
5469
  Instruction::StructGet {
5127
5470
  struct_type_index: container_type_idx,
5128
- field_index: fpos as u32,
5471
+ field_index: fld(fpos),
5129
5472
  }
5130
5473
  .encode(body);
5131
5474
  Instruction::LocalSet(idx).encode(body);
@@ -5166,7 +5509,7 @@ fn compileFieldPatterns(
5166
5509
  Instruction::LocalGet(container_local).encode(body);
5167
5510
  Instruction::StructGet {
5168
5511
  struct_type_index: container_type_idx,
5169
- field_index: fpos as u32,
5512
+ field_index: fld(fpos),
5170
5513
  }
5171
5514
  .encode(body);
5172
5515
  Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
@@ -5206,7 +5549,7 @@ fn compileFieldPatterns(
5206
5549
  Instruction::LocalGet(container_local).encode(body);
5207
5550
  Instruction::StructGet {
5208
5551
  struct_type_index: container_type_idx,
5209
- field_index: fpos as u32,
5552
+ field_index: fld(fpos),
5210
5553
  }
5211
5554
  .encode(body);
5212
5555
  Instruction::I64Const(*n).encode(body);
@@ -5285,7 +5628,7 @@ fn compileFieldPatterns(
5285
5628
  Instruction::LocalGet(container_local).encode(body);
5286
5629
  Instruction::StructGet {
5287
5630
  struct_type_index: container_type_idx,
5288
- field_index: fpos as u32,
5631
+ field_index: fld(fpos),
5289
5632
  }
5290
5633
  .encode(body);
5291
5634
  Instruction::RefTestNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
@@ -5294,7 +5637,7 @@ fn compileFieldPatterns(
5294
5637
  Instruction::LocalGet(container_local).encode(body);
5295
5638
  Instruction::StructGet {
5296
5639
  struct_type_index: container_type_idx,
5297
- field_index: fpos as u32,
5640
+ field_index: fld(fpos),
5298
5641
  }
5299
5642
  .encode(body);
5300
5643
  Instruction::RefCastNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
@@ -5457,8 +5800,6 @@ fn compileExpr(
5457
5800
  Instruction::LocalTee(scratch_local).encode(body);
5458
5801
  Instruction::RefTestNonNull(HeapType::Concrete(failure_idx)).encode(body);
5459
5802
  Instruction::If(BlockType::Result(success_vt)).encode(body);
5460
- Instruction::LocalGet(scratch_local).encode(body);
5461
- Instruction::RefCastNonNull(HeapType::Concrete(failure_idx)).encode(body);
5462
5803
  // Failure branch: take the inner `Err` payload and build the OUTER
5463
5804
  // function's `Err` variant from it (the Ok types may differ).
5464
5805
  let outer_err = match &ctx.ret_type {
@@ -5468,6 +5809,7 @@ fn compileExpr(
5468
5809
  }
5469
5810
  _ => None,
5470
5811
  };
5812
+ let mut rewrap: Option<(u32, i32)> = None;
5471
5813
  if let Some(outer_err_name) = outer_err {
5472
5814
  let outer_idx = *ctx
5473
5815
  .gc_types
@@ -5480,14 +5822,22 @@ fn compileExpr(
5480
5822
  )
5481
5823
  })?;
5482
5824
  if outer_idx != failure_idx {
5483
- Instruction::StructGet {
5484
- struct_type_index: failure_idx,
5485
- field_index: 0,
5486
- }
5487
- .encode(body);
5488
- Instruction::StructNew(outer_idx).encode(body);
5825
+ rewrap = Some((outer_idx, typeIdOf(ctx.gc_types, outer_err_name)));
5489
5826
  }
5490
5827
  }
5828
+ if let Some((_, tid)) = rewrap {
5829
+ Instruction::I32Const(tid).encode(body);
5830
+ }
5831
+ Instruction::LocalGet(scratch_local).encode(body);
5832
+ Instruction::RefCastNonNull(HeapType::Concrete(failure_idx)).encode(body);
5833
+ if let Some((outer_idx, _)) = rewrap {
5834
+ Instruction::StructGet {
5835
+ struct_type_index: failure_idx,
5836
+ field_index: fld(0),
5837
+ }
5838
+ .encode(body);
5839
+ Instruction::StructNew(outer_idx).encode(body);
5840
+ }
5491
5841
  Instruction::Return.encode(body);
5492
5842
  Instruction::Else.encode(body);
5493
5843
  Instruction::LocalGet(scratch_local).encode(body);
@@ -5495,7 +5845,7 @@ fn compileExpr(
5495
5845
  if !success_info.field_types.is_empty() {
5496
5846
  Instruction::StructGet {
5497
5847
  struct_type_index: success_idx,
5498
- field_index: 0,
5848
+ field_index: fld(0),
5499
5849
  }
5500
5850
  .encode(body);
5501
5851
  }
@@ -5576,7 +5926,7 @@ fn compileExpr(
5576
5926
  if !success_info.field_types.is_empty() {
5577
5927
  Instruction::StructGet {
5578
5928
  struct_type_index: success_idx,
5579
- field_index: 0,
5929
+ field_index: fld(0),
5580
5930
  }
5581
5931
  .encode(body);
5582
5932
  }
@@ -5660,7 +6010,7 @@ fn compileExpr(
5660
6010
  compileExpr(side, body, ctx, state)?;
5661
6011
  Instruction::StructGet {
5662
6012
  struct_type_index: tag_idx,
5663
- field_index: 0,
6013
+ field_index: fld(0),
5664
6014
  }
5665
6015
  .encode(body);
5666
6016
  }
@@ -5883,10 +6233,15 @@ fn compileExpr(
5883
6233
  },
5884
6234
  _ => None,
5885
6235
  };
6236
+ let sig_params: Vec<ValType> = ctx
6237
+ .func_sigs
6238
+ .get(&call.name)
6239
+ .map(|s| s.params.clone())
6240
+ .unwrap_or_default();
5886
6241
  match variadic_split {
5887
6242
  Some((elem_ty, fixed_count)) => {
5888
- for arg in call.args.iter().take(fixed_count) {
6243
+ for (i, arg) in call.args.iter().take(fixed_count).enumerate() {
5889
- compileExpr(argExprOf(arg), body, ctx, state)?;
6244
+ compileExprAs(argExprOf(arg), sig_params.get(i).copied(), body, ctx, state)?;
5890
6245
  }
5891
6246
  let trailing: Vec<&ast::Expr> =
5892
6247
  call.args.iter().skip(fixed_count).map(argExprOf).collect();
@@ -5897,7 +6252,7 @@ fn compileExpr(
5897
6252
  .get(&elem_vt)
5898
6253
  .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
5899
6254
  for arg_expr in &trailing {
5900
- compileExpr(arg_expr, body, ctx, state)?;
6255
+ compileExprAs(arg_expr, Some(elem_vt), body, ctx, state)?;
5901
6256
  }
5902
6257
  Instruction::ArrayNewFixed {
5903
6258
  array_type_index: array_type_idx,
@@ -5912,8 +6267,8 @@ fn compileExpr(
5912
6267
  Instruction::Call(*func_idx).encode(body);
5913
6268
  }
5914
6269
  None => {
5915
- for arg in &call.args {
6270
+ for (i, arg) in call.args.iter().enumerate() {
5916
- compileExpr(argExprOf(arg), body, ctx, state)?;
6271
+ compileExprAs(argExprOf(arg), sig_params.get(i).copied(), body, ctx, state)?;
5917
6272
  }
5918
6273
  compileDefaultArgs(&call.name, call.args.len(), body, ctx, state)?;
5919
6274
  let func_idx = ctx
@@ -6033,9 +6388,16 @@ fn compileExpr(
6033
6388
  )
6034
6389
  })?;
6035
6390
  let field_names = info.field_names.clone();
6391
+ Instruction::I32Const(typeIdOf(ctx.gc_types, &call.type_name)).encode(body);
6036
6392
  for (field_idx, field_name) in field_names.iter().enumerate() {
6037
6393
  match call.fields.iter().find(|fa| &fa.name == field_name) {
6038
- Some(fa) => compileExpr(&fa.value, body, ctx, state)?,
6394
+ Some(fa) => compileExprAs(
6395
+ &fa.value,
6396
+ info.field_types.get(field_idx).map(plumTypeToValtype),
6397
+ body,
6398
+ ctx,
6399
+ state,
6400
+ )?,
6039
6401
  // `Circle(..c, radius: 10)` — a field not overridden is read
6040
6402
  // straight off the spread source instead (checker only proved
6041
6403
  // `c` is SOME value of the owning enum, restricted to a bare
@@ -6056,7 +6418,7 @@ fn compileExpr(
6056
6418
  Instruction::RefCastNonNull(HeapType::Concrete(variant_idx)).encode(body);
6057
6419
  Instruction::StructGet {
6058
6420
  struct_type_index: variant_idx,
6059
- field_index: field_idx as u32,
6421
+ field_index: fld(field_idx),
6060
6422
  }
6061
6423
  .encode(body);
6062
6424
  }
@@ -6123,7 +6485,7 @@ fn compileExpr(
6123
6485
  compileExpr(&attr.object, body, ctx, state)?;
6124
6486
  Instruction::StructGet {
6125
6487
  struct_type_index: super_type_idx,
6126
- field_index: field_idx as u32,
6488
+ field_index: fld(field_idx),
6127
6489
  }
6128
6490
  .encode(body);
6129
6491
  }
@@ -6172,7 +6534,7 @@ fn compileExpr(
6172
6534
  .encode(body);
6173
6535
  Instruction::StructGet {
6174
6536
  struct_type_index: variant_type_idx,
6175
- field_index: field_idx as u32,
6537
+ field_index: fld(field_idx),
6176
6538
  }
6177
6539
  .encode(body);
6178
6540
  }
@@ -6198,6 +6560,59 @@ fn compileExpr(
6198
6560
  ))
6199
6561
  }
6200
6562
  };
6563
+ // A method call on a trait-typed value goes through the trait's
6564
+ // `$dyn$<Trait>$<method>` dispatcher (reads the receiver's typeId).
6565
+ let is_trait_recv = ctx.gc_types.class_type_idx.get(&class_name)
6566
+ == Some(&ctx.gc_types.obj_type_idx)
6567
+ && ctx.methods.contains_key(&(
6568
+ class_name.clone(),
6569
+ plum_checker::TRAIT_MARKER.to_string(),
6570
+ ));
6571
+ // A method the receiver's own trait doesn't declare (e.g. a
6572
+ // `T: ToStr` bound's `toStr` on a `Shape`-typed value inside a
6573
+ // specialized generic) dispatches through whichever trait does.
6574
+ let dyn_owner = if is_trait_recv {
6575
+ if ctx.gc_types.dyn_fns.contains_key(&(class_name.clone(), call.name.clone())) {
6576
+ Some(class_name.clone())
6577
+ } else {
6578
+ ctx.gc_types
6579
+ .dyn_fns
6580
+ .keys()
6581
+ .find(|(_, m)| *m == call.name)
6582
+ .map(|(t, _)| t.clone())
6583
+ }
6584
+ } else {
6585
+ None
6586
+ };
6587
+ if is_trait_recv && dyn_owner.is_none() {
6588
+ // No implementor of this method exists at all: unreachable at
6589
+ // runtime, but the call must still type-check structurally.
6590
+ Instruction::Unreachable.encode(body);
6591
+ return Ok(());
6592
+ }
6593
+ if let Some(&dyn_idx) = dyn_owner.as_ref().and_then(|t| {
6594
+ ctx.gc_types
6595
+ .dyn_fns
6596
+ .get(&(t.clone(), call.name.clone()))
6597
+ }) {
6598
+ let owner = dyn_owner.clone().unwrap_or_default();
6599
+ let param_tys: Vec<PlumType> =
6600
+ match ctx.methods.get(&(owner, call.name.clone())) {
6601
+ Some(PlumType::TFun(ps, _)) => ps.clone(),
6602
+ _ => Vec::new(),
6603
+ };
6604
+ compileExpr(&attr.object, body, ctx, state)?;
6605
+ for (i, arg) in call.args.iter().enumerate() {
6606
+ let e = match arg {
6607
+ ast::Arg::Positional(e) => e,
6608
+ ast::Arg::Keyword { value, .. } => value,
6609
+ ast::Arg::Pair { value, .. } => value,
6610
+ };
6611
+ compileExprAs(e, param_tys.get(i).map(plumTypeToValtype), body, ctx, state)?;
6612
+ }
6613
+ Instruction::Call(dyn_idx).encode(body);
6614
+ return Ok(());
6615
+ }
6201
6616
  let direct_key = format!("{}::{}", class_name, call.name);
6202
6617
  // Fallback: a primitive/class receiver's method might be
6203
6618
  // defined once on the "union" enum that bare-wraps it
@@ -6254,6 +6669,10 @@ fn compileExpr(
6254
6669
  let vt = astTypeToWasm(&dispatch_name).unwrap_or(ValType::I32);
6255
6670
  pushSelfPlaceholder(vt, body);
6256
6671
  } else {
6672
+ if box_variant_idx.is_some() {
6673
+ // The wrap-enum variant's header typeId goes below its payload.
6674
+ Instruction::I32Const(typeIdOf(ctx.gc_types, &class_name)).encode(body);
6675
+ }
6257
6676
  compileExpr(&attr.object, body, ctx, state)?; // push self
6258
6677
  if let Some(variant_idx) = box_variant_idx {
6259
6678
  // Dispatching to the wrap-enum's method (see
@@ -6285,10 +6704,15 @@ fn compileExpr(
6285
6704
  },
6286
6705
  _ => None,
6287
6706
  };
6707
+ let sig_params: Vec<ValType> = ctx
6708
+ .func_sigs
6709
+ .get(&key)
6710
+ .map(|s| s.params.clone())
6711
+ .unwrap_or_default();
6288
6712
  match variadic_split {
6289
6713
  Some((elem_ty, fixed_count)) => {
6290
- for arg in call.args.iter().take(fixed_count) {
6714
+ for (i, arg) in call.args.iter().take(fixed_count).enumerate() {
6291
- compileExpr(argExprOf(arg), body, ctx, state)?;
6715
+ compileExprAs(argExprOf(arg), sig_params.get(i + 1).copied(), body, ctx, state)?;
6292
6716
  }
6293
6717
  let trailing: Vec<&ast::Expr> =
6294
6718
  call.args.iter().skip(fixed_count).map(argExprOf).collect();
@@ -6299,7 +6723,7 @@ fn compileExpr(
6299
6723
  .get(&elem_vt)
6300
6724
  .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
6301
6725
  for arg_expr in &trailing {
6302
- compileExpr(arg_expr, body, ctx, state)?;
6726
+ compileExprAs(arg_expr, Some(elem_vt), body, ctx, state)?;
6303
6727
  }
6304
6728
  Instruction::ArrayNewFixed {
6305
6729
  array_type_index: array_type_idx,
@@ -6308,8 +6732,8 @@ fn compileExpr(
6308
6732
  .encode(body);
6309
6733
  }
6310
6734
  None => {
6311
- for arg in &call.args {
6735
+ for (i, arg) in call.args.iter().enumerate() {
6312
- compileExpr(argExprOf(arg), body, ctx, state)?;
6736
+ compileExprAs(argExprOf(arg), sig_params.get(i + 1).copied(), body, ctx, state)?;
6313
6737
  }
6314
6738
  compileDefaultArgs(&key, call.args.len(), body, ctx, state)?;
6315
6739
  }
@@ -6421,13 +6845,20 @@ fn compileVariantConstruction(
6421
6845
  // single construction instruction — positional args already match field
6422
6846
  // declaration order (unlike named class-field construction), so no reordering
6423
6847
  // is needed here.
6848
+ Instruction::I32Const(typeIdOf(ctx.gc_types, &call.name)).encode(body);
6424
- for arg in &call.args {
6849
+ for (i, arg) in call.args.iter().enumerate() {
6425
6850
  let arg_expr = match arg {
6426
6851
  ast::Arg::Positional(e) => e,
6427
6852
  ast::Arg::Keyword { value, .. } => value,
6428
6853
  ast::Arg::Pair { value, .. } => value,
6429
6854
  };
6855
+ compileExprAs(
6856
+ arg_expr,
6430
- compileExpr(arg_expr, body, ctx, state)?;
6857
+ info.field_types.get(i).map(plumTypeToValtype),
6858
+ body,
6859
+ ctx,
6860
+ state,
6861
+ )?;
6431
6862
  }
6432
6863
  Instruction::StructNew(variant_type_idx).encode(body);
6433
6864
  Ok(())
@@ -6503,6 +6934,7 @@ fn compileInterpolatedString(
6503
6934
  // `Float` at all already must `import std/Number` (see
6504
6935
  // `plum-core::loader::checkBuiltinImports`), so these
6505
6936
  // lookups are always present.
6937
+ Instruction::I32Const(typeIdOf(ctx.gc_types, "Float")).encode(body);
6506
6938
  compileExpr(expr, body, ctx, state)?;
6507
6939
  let variant_idx = *ctx.gc_types.variant_type_idx.get("Float")
6508
6940
  .ok_or_else(|| "codegen: interpolating a Float value requires `Number` (import std/Number)".to_string())?;
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1557,3 +1557,118 @@ fun main() -> Int =
1557
1557
  );
1558
1558
  assert_eq!(got, 9);
1559
1559
  }
1560
+
1561
+ // ---- trait types + typeId (T3) ----
1562
+
1563
+ fn repoRoot() -> std::path::PathBuf {
1564
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
1565
+ }
1566
+
1567
+ /// Compiles `src` as a real file (so `import std/...` resolves against the repo's
1568
+ /// `plum-std`) and returns the wasm instance's `main` export plus its store.
1569
+ fn runMerged<R: wasmtime::WasmResults>(name: &str, src: &str) -> R {
1570
+ let dir = std::env::temp_dir().join(format!("plum_t3_{}_{}", name, std::process::id()));
1571
+ std::fs::create_dir_all(&dir).unwrap();
1572
+ let path = dir.join("main.plum");
1573
+ std::fs::write(&path, src).unwrap();
1574
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
1575
+ plum_checker::checkSource(&source).unwrap_or_else(|e| {
1576
+ panic!("check errors: {:?}", e.iter().map(|x| x.message.clone()).collect::<Vec<_>>())
1577
+ });
1578
+ let bytes = compileSource(&source).expect("compile failed");
1579
+ wasmparser::validate(&bytes).expect("valid wasm");
1580
+ let engine = gcEngine();
1581
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
1582
+ let mut store = wasmtime::Store::new(&engine, ());
1583
+ // std pulls in host externs (printLn, ...) the test never calls: stub them.
1584
+ let mut linker = wasmtime::Linker::new(&engine);
1585
+ linker
1586
+ .define_unknown_imports_as_default_values(&mut store, &module)
1587
+ .unwrap();
1588
+ let instance = linker.instantiate(&mut store, &module).unwrap();
1589
+ let main = instance.get_typed_func::<(), R>(&mut store, "main").unwrap();
1590
+ let r = main.call(&mut store, ()).unwrap();
1591
+ let _ = std::fs::remove_dir_all(&dir);
1592
+ r
1593
+ }
1594
+
1595
+ #[test]
1596
+ fn heterogeneous_list_dispatch() {
1597
+ let src = "\
1598
+ import std/Bool
1599
+ import std/Str
1600
+ import std/Number
1601
+ import std/Try
1602
+ import std/List
1603
+
1604
+ trait Shape =
1605
+ area() -> Float
1606
+
1607
+ enum Circle(Shape) =
1608
+ | Circle(r: Float)
1609
+
1610
+ fun area(self) -> Float =
1611
+ self.r * self.r * 3.0
1612
+
1613
+ enum Square(Shape) =
1614
+ | Square(s: Float)
1615
+
1616
+ fun area(self) -> Float =
1617
+ self.s * self.s
1618
+
1619
+ fun total(xs: List[Shape]) -> Float =
1620
+ xs.reduce(0.0, |acc, x| acc + x.area())
1621
+
1622
+ fun main() -> Float =
1623
+ xs := List[Shape]()
1624
+ xs.add(Circle(r: 1.5))
1625
+ xs.add(Square(s: 1.5))
1626
+ xs.add(Circle(r: 0.5))
1627
+ total(xs)
1628
+ ";
1629
+ // 6.75 + 2.25 + 0.75, exact in binary floating point.
1630
+ assert_eq!(runMerged::<f64>("hetero", src), 9.75);
1631
+ }
1632
+
1633
+ #[test]
1634
+ fn missing_trait_method_rejected() {
1635
+ let src = "\
1636
+ trait Shape =
1637
+ area() -> Float
1638
+
1639
+ enum Plain =
1640
+ | Plain(n: Int)
1641
+
1642
+ fun total(s: Shape) -> Float =
1643
+ s.area()
1644
+
1645
+ fun main() -> Float =
1646
+ total(Plain(n: 1))
1647
+ ";
1648
+ let errs = plum_checker::checkSource(&parse(src)).unwrap_err();
1649
+ assert!(
1650
+ errs.iter().any(|e| e
1651
+ .message
1652
+ .contains("type 'Plain' does not implement trait 'Shape': missing 'area'")),
1653
+ "got {:?}",
1654
+ errs.iter().map(|e| e.message.clone()).collect::<Vec<_>>()
1655
+ );
1656
+ }
1657
+
1658
+ #[test]
1659
+ fn boxed_int_dispatch() {
1660
+ let src = "\
1661
+ import std/Bool
1662
+ import std/Str
1663
+ import std/Number
1664
+
1665
+ fun show(x: ToStr) -> Str =
1666
+ x.toStr()
1667
+
1668
+ fun main() -> Int =
1669
+ if show(5) == \"5\" && show(2.5) == \"2.5\" && show(\"hi\") == \"hi\"
1670
+ return 1
1671
+ return 0
1672
+ ";
1673
+ assert_eq!(runMerged::<i64>("boxed", src), 1);
1674
+ }
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -135,11 +135,11 @@ fn ioExampleCompilesAndRunsCorrectly() {
135
135
  return Err(wasmtime::Error::msg("printLn expects a Str argument"));
136
136
  };
137
137
  let str_struct = s.unwrap_struct(&caller)?;
138
- let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut caller, 0)? else {
138
+ let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut caller, 1)? else {
139
139
  return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
140
140
  };
141
141
  let buffer_struct = buffer_ref.unwrap_struct(&caller)?;
142
- let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut caller, 0)?
142
+ let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut caller, 1)?
143
143
  else {
144
144
  return Err(wasmtime::Error::msg(
145
145
  "Buffer.data is not a []Byte reference",