plum

#treesitter#compiler#wasm

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

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


3f7586e — Peter John 2026-10-26T11:44:55+05:30
feat(codegen)!: one compiled copy per generic instantiation, unboxed
plum-checker/src/lib.rs CHANGED
@@ -23,6 +23,13 @@ thread_local! {
23
23
  static INST_MODE: Cell<bool> = const { Cell::new(false) };
24
24
  /// Line of the declaration currently being checked (the "called at" line of recorded calls).
25
25
  static CUR_LINE: Cell<usize> = const { Cell::new(0) };
26
+ /// Address of the expression `inferExprWith` is currently inferring (the innermost one).
27
+ static CUR_EXPR: Cell<usize> = const { Cell::new(0) };
28
+ /// While checking the last statement of a value-producing tail (an fn body's
29
+ /// final `if`/`match`/expression): the type that tail value must have, so its
30
+ /// generic arguments are pinned like a directly-returned expression's.
31
+ static TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
32
+ static MATCH_TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
26
33
  }
27
34
 
28
35
  /// A call to a generic fn/method seen in some body: callee identity plus the
@@ -33,9 +40,23 @@ struct CallSite {
33
40
  name: String,
34
41
  args: BTreeMap<String, PlumType>,
35
42
  line: usize,
43
+ /// Address of the call expression (or, for an implicit call, of the expression it is made on).
44
+ expr: usize,
45
+ /// A call codegen makes on its own (string interpolation's `toStr`), not written in source.
46
+ implicit: bool,
36
47
  }
37
48
 
38
49
  fn recordCall(recv: Option<&str>, name: &str, args: BTreeMap<String, PlumType>) {
50
+ recordCallAt(CUR_EXPR.with(|e| e.get()), false, recv, name, args);
51
+ }
52
+
53
+ fn recordCallAt(
54
+ expr: usize,
55
+ implicit: bool,
56
+ recv: Option<&str>,
57
+ name: &str,
58
+ args: BTreeMap<String, PlumType>,
59
+ ) {
39
60
  if args.is_empty() {
40
61
  return;
41
62
  }
@@ -46,6 +67,8 @@ fn recordCall(recv: Option<&str>, name: &str, args: BTreeMap<String, PlumType>)
46
67
  name: name.to_string(),
47
68
  args,
48
69
  line: CUR_LINE.with(|l| l.get()),
70
+ expr,
71
+ implicit,
49
72
  });
50
73
  }
51
74
  });
@@ -69,6 +92,10 @@ pub struct TypedInfo {
69
92
  pub instantiations: Vec<Instantiation>,
70
93
  pub inst_expr_types: HashMap<(usize, usize), PlumType>,
71
94
  pub inst_index: HashMap<(String, Vec<PlumType>), usize>,
95
+ /// `(calling instantiation (None = a root fn), call expression address, implicit)` ->
96
+ /// the instantiation it calls. `implicit` marks calls codegen makes itself (the
97
+ /// `toStr` of a `"{x}"` interpolation, keyed by the interpolated expression).
98
+ pub call_inst: HashMap<(Option<usize>, usize, bool), usize>,
72
99
  }
73
100
 
74
101
  /// One reached instantiation of a generic fn or method of a generic type.
@@ -460,7 +487,15 @@ fn parseTagUnion(s: &str) -> Option<PlumType> {
460
487
  Some(p) => {
461
488
  let name = seg[..p].trim().to_string();
462
489
  let payload = seg[p + 1..].strip_suffix(')')?;
490
+ let payload = payload.trim();
491
+ let pty = if isGenericName(payload) {
492
+ TSUBST
493
+ .with(|s| s.borrow().as_ref().and_then(|m| m.get(payload).cloned()))
494
+ .unwrap_or_else(|| PlumType::TParam(payload.to_string()))
495
+ } else {
496
+ plumTypeFromName(payload)
497
+ };
463
- tags.insert(name, Some(Box::new(plumTypeFromName(payload.trim()))));
498
+ tags.insert(name, Some(Box::new(pty)));
464
499
  }
465
500
  None => {
466
501
  tags.insert(seg.to_string(), None);
@@ -1455,7 +1490,7 @@ fn drainCalls() -> Vec<CallSite> {
1455
1490
 
1456
1491
  /// Generic parameter names of `f`: its owner type's generics, then the free
1457
1492
  /// type parameters of its own signature. Empty = not generic.
1458
- fn fnGenericNames(f: &ast::Fn, ctx: &CheckCtx) -> Vec<String> {
1493
+ pub fn fnGenericNames(f: &ast::Fn, ctx: &CheckCtx) -> Vec<String> {
1459
1494
  let mut names: Vec<String> = match &f.type_param {
1460
1495
  Some(r) => enumGenericsOf(r, ctx).to_vec(),
1461
1496
  None => Vec::new(),
@@ -1521,7 +1556,8 @@ fn registerCalls(
1521
1556
  None => cs.name.clone(),
1522
1557
  };
1523
1558
  let key = (item.clone(), args.clone());
1524
- if st.info.inst_index.contains_key(&key) {
1559
+ if let Some(&existing) = st.info.inst_index.get(&key) {
1560
+ st.info.call_inst.insert((parent, cs.expr, cs.implicit), existing);
1525
1561
  continue;
1526
1562
  }
1527
1563
  let id = st.info.instantiations.len();
@@ -1545,6 +1581,7 @@ fn registerCalls(
1545
1581
  called_at_line: cs.line,
1546
1582
  });
1547
1583
  st.info.inst_index.insert(key, id);
1584
+ st.info.call_inst.insert((parent, cs.expr, cs.implicit), id);
1548
1585
  st.queue.push_back(id);
1549
1586
  }
1550
1587
  }
@@ -1686,7 +1723,11 @@ fn checkFn(
1686
1723
  Some((ast::Stmt::Expr(e, line, column), init)) => (Some((e, *line, *column)), init),
1687
1724
  _ => (None, &block.stmts[..]),
1688
1725
  };
1726
+ let n_init = init.len();
1689
- for stmt in init {
1727
+ for (i, stmt) in init.iter().enumerate() {
1728
+ if tail.is_none() && i + 1 == n_init && !matches!(declared_ret, PlumType::TUnit) {
1729
+ TAIL.with(|c| *c.borrow_mut() = Some(declared_ret.clone()));
1730
+ }
1690
1731
  errors.append(&mut checkStmt(
1691
1732
  stmt,
1692
1733
  &mut env,
@@ -1730,7 +1771,11 @@ fn checkBlock(
1730
1771
  ctx: &CheckCtx,
1731
1772
  ) -> Vec<CheckError> {
1732
1773
  let mut errors = Vec::new();
1774
+ let tail = TAIL.with(|t| t.borrow_mut().take());
1775
+ for (i, stmt) in block.stmts.iter().enumerate() {
1733
- for stmt in &block.stmts {
1776
+ if i + 1 == block.stmts.len() {
1777
+ TAIL.with(|t| *t.borrow_mut() = tail.clone());
1778
+ }
1734
1779
  let mut stmt_errors = checkStmt(stmt, env, declared_ret, fn_name, fn_pos, ctx);
1735
1780
  errors.append(&mut stmt_errors);
1736
1781
  }
@@ -1761,6 +1806,8 @@ fn checkStmt(
1761
1806
  ctx: &CheckCtx,
1762
1807
  ) -> Vec<CheckError> {
1763
1808
  let mut errors = Vec::new();
1809
+ let tail = TAIL.with(|t| t.borrow_mut().take());
1810
+ let set_tail = |t: &Option<PlumType>| TAIL.with(|c| *c.borrow_mut() = t.clone());
1764
1811
  match stmt {
1765
1812
  ast::Stmt::Assign(a) => {
1766
1813
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
@@ -1881,6 +1928,7 @@ fn checkStmt(
1881
1928
  format!("fn '{}': if condition: {}", fn_name, msg),
1882
1929
  )),
1883
1930
  }
1931
+ set_tail(&tail);
1884
1932
  errors.append(&mut checkBlock(
1885
1933
  &if_.body,
1886
1934
  env,
@@ -1909,7 +1957,8 @@ fn checkStmt(
1909
1957
  format!("fn '{}': else if condition: {}", fn_name, msg),
1910
1958
  )),
1911
1959
  }
1960
+ set_tail(&tail);
1912
- errors.append(&mut checkBlock(
1961
+ errors.append(&mut checkBlock(
1913
1962
  &ei.body,
1914
1963
  env,
1915
1964
  declared_ret,
@@ -1919,7 +1968,8 @@ fn checkStmt(
1919
1968
  ));
1920
1969
  }
1921
1970
  if let Some(else_block) = &if_.else_ {
1971
+ set_tail(&tail);
1922
- errors.append(&mut checkBlock(
1972
+ errors.append(&mut checkBlock(
1923
1973
  else_block,
1924
1974
  env,
1925
1975
  declared_ret,
@@ -1993,7 +2043,7 @@ fn checkStmt(
1993
2043
  ));
1994
2044
  }
1995
2045
  ast::Stmt::Expr(e, line, column) => {
1996
- if let Err(msg) = inferExpr(e, env, ctx) {
2046
+ if let Err(msg) = inferExprWith(e, env, ctx, tail.as_ref()) {
1997
2047
  errors.push(CheckError::at(
1998
2048
  *line,
1999
2049
  *column,
@@ -2018,6 +2068,7 @@ fn checkStmt(
2018
2068
  )),
2019
2069
  },
2020
2070
  ast::Stmt::Match(m) => {
2071
+ MATCH_TAIL.with(|c| *c.borrow_mut() = tail.clone());
2021
2072
  errors.append(&mut checkMatch(m, env, declared_ret, fn_name, fn_pos, ctx));
2022
2073
  }
2023
2074
  ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
@@ -2212,6 +2263,7 @@ fn checkMatch(
2212
2263
  fn_pos: (usize, usize),
2213
2264
  ctx: &CheckCtx,
2214
2265
  ) -> Vec<CheckError> {
2266
+ let match_tail = MATCH_TAIL.with(|c| c.borrow_mut().take());
2215
2267
  let mut errors = Vec::new();
2216
2268
 
2217
2269
  let mut subject_types: Vec<PlumType> = Vec::new();
@@ -2297,6 +2349,7 @@ fn checkMatch(
2297
2349
  )),
2298
2350
  }
2299
2351
  }
2352
+ TAIL.with(|c| *c.borrow_mut() = match_tail.clone());
2300
2353
  errors.append(&mut checkBlock(
2301
2354
  &case.body,
2302
2355
  &mut case_env,
@@ -3055,7 +3108,9 @@ pub fn inferExprWith(
3055
3108
  ctx: &CheckCtx,
3056
3109
  expected: Option<&PlumType>,
3057
3110
  ) -> Result<PlumType, String> {
3111
+ let prev = CUR_EXPR.with(|c| c.replace(expr as *const ast::Expr as usize));
3058
3112
  let r = inferExprInner(expr, env, ctx, expected);
3113
+ CUR_EXPR.with(|c| c.set(prev));
3059
3114
  if let Ok(t) = &r {
3060
3115
  recordType(expr, t);
3061
3116
  }
@@ -3079,7 +3134,20 @@ fn inferExprInner(
3079
3134
  // were never part of this check.
3080
3135
  for part in &s.parts {
3081
3136
  if let ast::StringPart::Interp(e) = part {
3082
- let _ = inferExprWith(e, env, ctx, None);
3137
+ if let Ok(PlumType::TApp(n, targs)) = inferExprWith(e, env, ctx, None) {
3138
+ let gens = enumGenericsOf(&n, ctx);
3139
+ if gens.len() == targs.len()
3140
+ && ctx.methods.contains_key(&(n.clone(), "toStr".to_string()))
3141
+ {
3142
+ recordCallAt(
3143
+ e as *const ast::Expr as usize,
3144
+ true,
3145
+ Some(&n),
3146
+ "toStr",
3147
+ appSubst(gens, &targs),
3148
+ );
3149
+ }
3150
+ }
3083
3151
  }
3084
3152
  }
3085
3153
  Ok(PlumType::TStr)
plum-cli/src/main.rs CHANGED
@@ -774,7 +774,7 @@ fn cmdRun(file: std::path::PathBuf, lib_path: std::path::PathBuf) -> Result<()>
774
774
  .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
775
775
 
776
776
  let module = wasmtime::Module::new(&engine, &wasm_bytes)
777
- .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e}"))?;
777
+ .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e:#}"))?;
778
778
  let mut store = wasmtime::Store::new(&engine, ());
779
779
  let imports = hostImports(&mut store, &module)?;
780
780
  let instance = wasmtime::Instance::new(&mut store, &module, &imports)
@@ -913,6 +913,9 @@ fn cmdTest(paths: Vec<std::path::PathBuf>, lib_path: std::path::PathBuf) -> Resu
913
913
  /// the single-file and directory-recursive paths of `cmdTest`.
914
914
  fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(usize, usize)> {
915
915
  let (wasm_bytes, names) = compileToTestWasm(file, lib_path)?;
916
+ if let Ok(path) = std::env::var("PLUM_DUMP_WASM") {
917
+ let _ = std::fs::write(path, &wasm_bytes);
918
+ }
916
919
 
917
920
  let mut config = wasmtime::Config::new();
918
921
  config.wasm_gc(true);
@@ -921,7 +924,7 @@ fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(us
921
924
  .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
922
925
 
923
926
  let module = wasmtime::Module::new(&engine, &wasm_bytes)
924
- .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e}"))?;
927
+ .map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e:#}"))?;
925
928
  let mut store = wasmtime::Store::new(&engine, ());
926
929
  let imports = hostImports(&mut store, &module)?;
927
930
  let instance = wasmtime::Instance::new(&mut store, &module, &imports)
plum-wasm-codegen/src/lib.rs CHANGED
@@ -48,6 +48,7 @@ pub struct WasmModule {
48
48
  pub func_count: u32,
49
49
  global_count: u32,
50
50
  start_function: Option<u32>,
51
+ func_names: Vec<(u32, String)>,
51
52
  }
52
53
 
53
54
  impl WasmModule {
@@ -65,6 +66,7 @@ impl WasmModule {
65
66
  func_count: 0,
66
67
  global_count: 0,
67
68
  start_function: None,
69
+ func_names: Vec::new(),
68
70
  }
69
71
  }
70
72
 
@@ -107,6 +109,11 @@ impl WasmModule {
107
109
  idx
108
110
  }
109
111
 
112
+ /// Records a debug name for function `idx` (wasm `name` section).
113
+ pub fn nameFunction(&mut self, idx: u32, name: &str) {
114
+ self.func_names.push((idx, name.to_string()));
115
+ }
116
+
110
117
  pub fn addExport(&mut self, name: &str, kind: ExportKind, idx: u32) {
111
118
  self.exports.push((name.to_string(), kind, idx));
112
119
  }
@@ -294,6 +301,19 @@ impl WasmModule {
294
301
  module.section(&data);
295
302
  }
296
303
 
304
+ if !self.func_names.is_empty() {
305
+ let mut names = wasm_encoder::NameSection::new();
306
+ let mut map = wasm_encoder::NameMap::new();
307
+ let mut sorted = self.func_names.clone();
308
+ sorted.sort_by_key(|(i, _)| *i);
309
+ sorted.dedup_by_key(|(i, _)| *i);
310
+ for (i, n) in &sorted {
311
+ map.append(*i, n);
312
+ }
313
+ names.functions(&map);
314
+ module.section(&names);
315
+ }
316
+
297
317
  module.finish()
298
318
  }
299
319
  }
@@ -356,33 +376,37 @@ pub struct ClosureInfo {
356
376
  /// `Unit` result is a null `anyref`. This keeps one wasm function per closure
357
377
  /// literal and needs no per-instantiation thunk: the "thunk" work is done in the
358
378
  /// closure's own prologue and the call site's epilogue.
359
- type ClosureSigKey = usize;
379
+ type ClosureSigKey = (Vec<ValType>, Option<ValType>);
360
380
 
361
381
  /// Global, read-only lookup tables shared by every function body being compiled.
362
382
  pub struct CompileCtx<'a> {
363
383
  pub func_ids: HashMap<String, u32>,
364
384
  pub func_sigs: HashMap<String, FuncSig>,
385
+ /// Instantiation id -> its wasm function and signature.
386
+ pub inst_funcs: HashMap<usize, (u32, FuncSig)>,
387
+ /// See `plum_checker::TypedInfo::call_inst`.
388
+ pub call_inst: HashMap<(Option<usize>, usize, bool), usize>,
389
+ /// Every free (non-method) fn name, generic or not.
390
+ pub known_fns: HashSet<String>,
391
+ pub eg: EnumGenerics,
365
392
  pub methods: MethodEnv,
366
393
  pub enum_variants: EnumVariants,
367
394
  pub enum_params: EnumParams,
368
395
  pub min_required: plum_checker::MinRequiredArgs,
369
396
  pub global_env: TypeEnv,
370
397
  /// Closure literal (keyed by `&Expr::Closure` pointer identity) -> its `ClosureInfo`.
371
- pub closures: HashMap<usize, ClosureInfo>,
398
+ pub closures: HashMap<(usize, usize), ClosureInfo>,
372
399
  /// The AST of each discovered closure literal, keyed the same way, so its body can
373
400
  /// be compiled in a second pass after all closures are registered.
374
- pub closure_asts: HashMap<usize, &'a ast::Closure>,
401
+ pub closure_asts: HashMap<(usize, usize), &'a ast::Closure>,
375
402
  /// Closure arity -> the erased `call_indirect` function-type index.
376
403
  pub closure_call_types: HashMap<ClosureSigKey, u32>,
377
- /// Instantiated type of every expression of the original source,
378
- /// keyed by expression address — from `plum_checker::typedSource`.
379
- pub types: HashMap<usize, PlumType>,
380
404
  /// Top-level function name -> global index holding its zero-capture "trampoline"
381
405
  /// closure struct `{table_idx, env=null}`, for using a plain named function
382
406
  /// wherever a `fn(...)`-typed value is expected (e.g. `each(double)`). Since the
383
407
  /// struct has no captures it never changes, so it's built once by the shared
384
408
  /// `start` function instead of being reconstructed per reference.
385
- pub named_fn_values: HashMap<String, u32>,
409
+ pub named_fn_values: HashMap<(String, PlumType), u32>,
386
410
  /// Shared runtime helper `(a: ref Str, b: ref Str) -> ref Str`: allocates a new
387
411
  /// `array<i8>` exactly long enough to hold `a`'s bytes followed by `b`'s, for
388
412
  /// lowering string interpolation (`"{expr}"`).
@@ -403,7 +427,7 @@ pub struct CompileCtx<'a> {
403
427
  pub gc_types: GcTypeRegistry,
404
428
  /// Payload-free variant name (True/False/End/...) -> the global index holding
405
429
  /// its one pre-allocated instance (see this migration plan's Decision 2).
406
- pub singleton_globals: HashMap<String, u32>,
430
+ pub singleton_globals: HashMap<(PlumType, String), u32>,
407
431
  }
408
432
 
409
433
  /// Per-module state that accumulates as function bodies are compiled: every static
@@ -435,14 +459,20 @@ struct LocalCtx<'a> {
435
459
  variadic_for_scratch: HashMap<usize, u32>,
436
460
  func_ids: &'a HashMap<String, u32>,
437
461
  func_sigs: &'a HashMap<String, FuncSig>,
462
+ inst_funcs: &'a HashMap<usize, (u32, FuncSig)>,
463
+ call_inst: &'a HashMap<(Option<usize>, usize, bool), usize>,
464
+ known_fns: &'a HashSet<String>,
465
+ /// Index of the unit being compiled (closures are keyed by it) and its instantiation.
466
+ unit_idx: usize,
467
+ unit_inst: Option<usize>,
438
468
  types: &'a HashMap<usize, PlumType>,
439
469
  /// Name -> the plum type each local was DECLARED with (so its wasm
440
470
  /// representation is `plumTypeToValtype` of this), as opposed to the
441
471
  /// type of a particular use of it.
442
472
  local_decl_types: HashMap<String, PlumType>,
443
- closures: &'a HashMap<usize, ClosureInfo>,
473
+ closures: &'a HashMap<(usize, usize), ClosureInfo>,
444
474
  closure_call_types: &'a HashMap<ClosureSigKey, u32>,
445
- named_fn_values: &'a HashMap<String, u32>,
475
+ named_fn_values: &'a HashMap<(String, PlumType), u32>,
446
476
  string_concat_func: u32,
447
477
  string_eq_func: u32,
448
478
  dyn_eq_func: u32,
@@ -452,7 +482,7 @@ struct LocalCtx<'a> {
452
482
  enum_params: &'a EnumParams,
453
483
  min_required: &'a plum_checker::MinRequiredArgs,
454
484
  gc_types: &'a GcTypeRegistry,
455
- singleton_globals: &'a HashMap<String, u32>,
485
+ singleton_globals: &'a HashMap<(PlumType, String), u32>,
456
486
  /// Tracks each binding's inferred type as compilation proceeds through
457
487
  /// statements in order, mirroring `plum-checker`'s own env evolution — needed
458
488
  /// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
@@ -847,27 +877,17 @@ fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
847
877
  })
848
878
  }
849
879
 
850
- /// A constructor-pattern name (`nested_class_scratch_types`'s recorded name, or any
851
- /// other spot a pattern's own concrete GC type index is needed) is always an enum
880
+ /// GC type index of a pattern/`?` scratch local: variant `vname` of enum type `ty`,
852
- /// variant — falls back to `class_type_idx` only for the record-shaped alias case
853
- /// (see `buildGcTypeRegistry`'s own alias comment), which `variant_type_idx` should
854
- /// already cover directly; kept as a defensive second lookup.
881
+ /// or (empty `vname`) the enum's own supertype.
855
- fn classOrVariantTypeIdx(r: &GcTypeRegistry, name: &str) -> u32 {
882
+ fn scratchTypeIdx(r: &GcTypeRegistry, ty: &PlumType, vname: &str) -> u32 {
883
+ let found = if vname.is_empty() {
884
+ r.superIdx(ty).or_else(|| r.classIdx(ty))
885
+ } else {
856
- *r.variant_type_idx
886
+ r.variantIdx(ty, vname)
857
- .get(name)
887
+ };
858
- .or_else(|| r.class_type_idx.get(name))
859
- // A scratch slot narrowed to a MULTI-variant enum's own wide supertype
860
- // (not any one variant) — the `?` operator's scratch local holds the
861
- // subject at its natural (possibly multi-variant, e.g. `Try`)
862
- // type, never narrower, since it ref.casts to whichever of the two
863
- // variants applies only at the point of use, not up front.
864
- .or_else(|| r.enum_super_type_idx.get(name))
865
- .unwrap_or_else(|| {
888
+ found.unwrap_or_else(|| {
866
- panic!(
867
- "internal codegen error: '{}' missing from the GC type registry",
889
+ panic!("internal codegen error: '{}' of {} missing from the GC type registry", vname, ty)
868
- name
869
- )
870
- })
890
+ })
871
891
  }
872
892
 
873
893
  /// Resolves an `ast::Type`/`ast::ParamType`'s bare name (e.g. from a function
@@ -927,54 +947,7 @@ fn pushSelfPlaceholder(vt: ValType, body: &mut Vec<u8>) {
927
947
  }
928
948
 
929
949
  fn plumTypeToValtype(t: &PlumType) -> ValType {
930
- match t {
931
- PlumType::TApp(n, _) => plumTypeToValtype(&PlumType::TNamed(n.clone())),
932
- PlumType::TInt => ValType::I64,
933
- PlumType::TFloat => ValType::F64,
934
- PlumType::TBool => ValType::I32,
935
- // `Str` is an ORDINARY class (`type Str(...) = data: Buffer ...` in
936
- // `str.plum`) — its own real struct type, registered exactly like any
937
- // other class (see `buildGcTypeRegistry`'s `Slot::Class` handling),
938
- // NOT the raw `byte_array_type_idx` (that's `Buffer`'s own `data`
939
- // field's type, two levels down) — except when `Str` was never
940
- // actually declared at all (an isolated snippet/unit test that never
941
- // went through `plum-core::loader::loadAndMerge`'s `std/str` prelude),
942
- // where `strGcValtypeOrFallback` falls back to treating `Str` as
943
- // that raw array directly, matching this function's pre-refactor
944
- // behavior.
945
- PlumType::TStr => withGcTypes(strGcValtypeOrFallback),
946
- PlumType::TByte => ValType::I32,
947
- // `[]Byte` is represented by the EXACT SAME wasm-gc array type as `Str`
948
- // (a mutable `array<i8>`) — they're structurally identical, and nothing
949
- // in this codegen needs to distinguish them at the wasm-type level
950
- // (no runtime `ref.test`/dynamic dispatch keys off it), so reusing
951
- // `byte_array_type_idx` avoids a second, redundant GC type-section entry.
952
- PlumType::TByteSlice => withGcTypes(|r| gcRef(r.byte_array_type_idx)),
953
- PlumType::TNamed(name) => withGcTypes(|r| {
954
- match r
955
- .class_type_idx
956
- .get(name)
957
- .or_else(|| r.enum_super_type_idx.get(name))
958
- {
959
- Some(idx) => gcRef(*idx),
960
- // Genuinely unmodeled type name (not a real class/enum) — permissive
961
- // fallback, matching this function's pre-wasm-gc "unmodeled type:
962
- // pointer" behavior; codegen sites that actually need a concrete
963
- // struct type still resolve it themselves and error clearly if absent.
964
- None => ValType::Ref(RefType::ANYREF),
965
- }
966
- }),
967
- // A closure value is a 2-field `{table_index: i32, env: anyref}` struct —
968
- // its OWN concrete GC type, registered per Task 2e (closures), not through
969
- // this general resolver; `anyref` here is a safe placeholder used only
970
- // where a closure's exact struct type isn't being constructed/destructured
971
- // directly (e.g. deciding a local's storage class), matching `TVariadic`'s
972
- // existing "not modeled as a concrete shape here" treatment.
973
- PlumType::TTags(_) => withGcTypes(|r| gcRef(r.tag_type_idx)),
974
- PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
975
- PlumType::TParam(_) => withGcTypes(|r| gcRef(r.obj_type_idx)),
976
- PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
950
+ withGcTypes(|r| plumTypeToGcValtype(t, r))
977
- }
978
951
  }
979
952
 
980
953
  /// Type-section indices for every wasm-gc composite type this program's
@@ -992,6 +965,17 @@ pub struct GcTypeRegistry {
992
965
  /// Variant name (flat namespace, matching `EnumVariants`) -> its concrete
993
966
  /// subtype `struct` type index.
994
967
  pub variant_type_idx: HashMap<String, u32>,
968
+ /// Concrete instance of a generic enum (`List[Int]`) -> its supertype index
969
+ /// (for `Array[T]`: the native array type of `T`'s representation).
970
+ pub app_super: HashMap<PlumType, u32>,
971
+ /// Record-shaped generic enum instance -> its one variant's struct index.
972
+ pub app_class: HashMap<PlumType, u32>,
973
+ /// (generic enum instance, variant name) -> variant struct index.
974
+ pub app_variant: HashMap<(PlumType, String), u32>,
975
+ /// Every registered enum instance (`TNamed` for non-generic, `TApp` for generic).
976
+ pub instances: Vec<PlumType>,
977
+ /// Names of every declared enum.
978
+ pub known_enums: HashSet<String>,
995
979
  /// The single shared `array<i8>` type index `[]Byte`/`Buffer`'s own
996
980
  /// `data` field use — NOT `Str`'s own representation (`Str` is an
997
981
  /// ordinary class, `data: Buffer`; see `libs/std/Str.plum`'s header).
@@ -1039,6 +1023,36 @@ pub struct GcTypeRegistry {
1039
1023
  pub dyn_fns: HashMap<(String, String), u32>,
1040
1024
  }
1041
1025
 
1026
+ impl GcTypeRegistry {
1027
+ /// Supertype struct index of enum type `ty` (`TNamed` or `TApp`).
1028
+ pub fn superIdx(&self, ty: &PlumType) -> Option<u32> {
1029
+ match ty {
1030
+ PlumType::TNamed(n) => self
1031
+ .enum_super_type_idx
1032
+ .get(n)
1033
+ .or_else(|| self.class_type_idx.get(n))
1034
+ .copied(),
1035
+ PlumType::TApp(..) => self.app_super.get(ty).copied(),
1036
+ _ => None,
1037
+ }
1038
+ }
1039
+ /// Struct index of the class-like (record-shaped) type `ty`.
1040
+ pub fn classIdx(&self, ty: &PlumType) -> Option<u32> {
1041
+ match ty {
1042
+ PlumType::TNamed(n) => self.class_type_idx.get(n).copied(),
1043
+ PlumType::TApp(..) => self.app_class.get(ty).copied(),
1044
+ _ => None,
1045
+ }
1046
+ }
1047
+ /// Struct index of variant `vname` of enum type `ty`.
1048
+ pub fn variantIdx(&self, ty: &PlumType, vname: &str) -> Option<u32> {
1049
+ match ty {
1050
+ PlumType::TApp(..) => self.app_variant.get(&(ty.clone(), vname.to_string())).copied(),
1051
+ _ => self.variant_type_idx.get(vname).copied(),
1052
+ }
1053
+ }
1054
+ }
1055
+
1042
1056
  /// Box type ids (see the plan's Global constraints): 0 is `$Obj` itself.
1043
1057
  pub const TYPE_ID_BOX_I64: i32 = 1;
1044
1058
  pub const TYPE_ID_BOX_F64: i32 = 2;
@@ -1082,45 +1096,27 @@ fn isArraySpecialization(name: &str) -> bool {
1082
1096
  /// structure: this function is only ever called during its second pass).
1083
1097
  fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
1084
1098
  match t {
1085
- PlumType::TApp(n, _) => plumTypeToGcValtype(&PlumType::TNamed(n.clone()), registry),
1099
+ PlumType::TApp(n, _) => match registry.classIdx(t).or_else(|| registry.superIdx(t)) {
1100
+ Some(idx) => gcRef(idx),
1101
+ None if hasFreeTy(t) || !registry.known_enums.contains(n) => {
1102
+ ValType::Ref(RefType::ANYREF)
1103
+ }
1104
+ None => panic!("internal codegen error: type {} was not registered", t),
1105
+ },
1086
1106
  PlumType::TInt => ValType::I64,
1087
1107
  PlumType::TFloat => ValType::F64,
1088
1108
  PlumType::TBool => ValType::I32,
1089
- // See the matching comment in `plumTypeToValtype` — `Str` is an
1090
- // ordinary class's own real struct type, not the raw
1091
- // `byte_array_type_idx`, except when `Str` was never actually
1092
- // declared at all (`strGcValtypeOrFallback`'s fallback case).
1093
1109
  PlumType::TStr => strGcValtypeOrFallback(registry),
1094
1110
  PlumType::TByte => ValType::I32,
1095
- // See the matching comment in `plumTypeToValtype` — `[]Byte` reuses
1096
- // `Buffer`'s own `array<i8>` GC type index rather than getting its own.
1097
- PlumType::TByteSlice => ValType::Ref(RefType {
1098
- nullable: true,
1099
- heap_type: HeapType::Concrete(registry.byte_array_type_idx),
1111
+ PlumType::TByteSlice => gcRef(registry.byte_array_type_idx),
1100
- }),
1101
1112
  PlumType::TNamed(name) => match registry
1102
1113
  .class_type_idx
1103
1114
  .get(name)
1104
1115
  .or_else(|| registry.enum_super_type_idx.get(name))
1105
1116
  {
1106
- Some(idx) => ValType::Ref(RefType {
1117
+ Some(idx) => gcRef(*idx),
1107
- nullable: true,
1108
- heap_type: HeapType::Concrete(*idx),
1109
- }),
1110
- // A field typed as a generic enum (e.g. `Node.next: Link[Node]`)
1111
- // resolves here to the BARE generic name — `plumTypeFromAst`
1112
- // has no representation for type arguments, so it can't know this means
1113
- // `Link[Int]` rather than bare `Link` (erased generics keep one
1114
- // compiled `Link`) — same permissive `anyref` fallback as
1115
- // `plumTypeToValtype` above, for the same "genuinely unmodeled" reason:
1116
- // `struct.get`/`ref.test`/`ref.cast` all work against `anyref` operands
1117
- // fine, so a field merely being STORED as `anyref` instead of the exact
1118
- // concrete type costs nothing but static precision.
1119
1118
  None => ValType::Ref(RefType::ANYREF),
1120
1119
  },
1121
- // TFun (closures) and TVariadic get their own concrete representation once
1122
- // Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
1123
- // yet-meaningful placeholder in the meantime, since nothing consumes it yet.
1124
1120
  PlumType::TTags(_) => gcRef(registry.tag_type_idx),
1125
1121
  PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
1126
1122
  PlumType::TParam(_) => gcRef(registry.obj_type_idx),
@@ -1128,86 +1124,92 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
1128
1124
  }
1129
1125
  }
1130
1126
 
1127
+ /// Free type variables / generic parameters anywhere inside `t`.
1128
+ fn hasFreeTy(t: &PlumType) -> bool {
1129
+ match t {
1130
+ PlumType::TVar(_) | PlumType::TParam(_) => true,
1131
+ PlumType::TApp(_, a) => a.iter().any(hasFreeTy),
1132
+ PlumType::TFun(ps, r) => ps.iter().any(hasFreeTy) || hasFreeTy(r),
1133
+ PlumType::TVariadic(i) => hasFreeTy(i),
1134
+ PlumType::TTags(m) => m.values().flatten().any(|t| hasFreeTy(t)),
1135
+ _ => false,
1136
+ }
1137
+ }
1138
+
1139
+ fn arrayDepth(t: &PlumType) -> usize {
1140
+ match t {
1141
+ PlumType::TApp(n, a) => {
1142
+ usize::from(isArraySpecialization(n)) + a.iter().map(arrayDepth).max().unwrap_or(0)
1143
+ }
1144
+ _ => 0,
1145
+ }
1146
+ }
1147
+
1131
- /// Builds the wasm-gc type registry for every concrete enum actually declared in
1148
+ /// Builds the wasm-gc type registry: one struct family per non-generic enum and per
1132
- /// `source` (`Bool`/`Str`/etc included, but only if imported — no builtin is
1133
- /// hardcoded here), plus the shared raw byte-array type, and declares them all as
1134
- /// ONE `rec` group via `module.addGcTypes` — a single group sidesteps every ordering
1135
- /// question about mutual/self-references (a record-shaped enum's field naming
1136
- /// another such enum's type, an enum variant field referencing its own enum,
1137
- /// `Node.next: Link[Node]`, etc.), since within one `rec` group members may
1149
+ /// distinct concrete instance (`apps`, e.g. `List[Int]`) of a generic enum, keyed by
1150
+ /// the structural `PlumType`; `Array[T]` instances become native wasm arrays of `T`'s
1138
- /// reference each other regardless of declaration order.
1151
+ /// representation. Everything is declared as ONE `rec` group.
1139
1152
  fn buildGcTypeRegistry(
1140
1153
  module: &mut WasmModule,
1141
1154
  source: &ast::Source,
1142
1155
  enum_variants: &EnumVariants,
1143
1156
  enum_params: &EnumParams,
1157
+ apps: &[PlumType],
1144
1158
  ) -> GcTypeRegistry {
1145
1159
  enum Slot {
1146
1160
  Str,
1147
- /// `$Obj {typeId: i32}` — supertype of every struct below.
1148
1161
  Obj,
1149
- EnumSuper(String),
1162
+ EnumSuper(PlumType),
1150
- Variant(String),
1163
+ Variant(PlumType, String),
1151
- /// The single shared `array<anyref>` type every `Array[T]`
1152
- /// aliases — see `isArraySpecialization`.
1153
- ArrayRef,
1164
+ ArrayRef(ValType),
1154
1165
  Tag,
1155
1166
  BoxI64,
1156
1167
  BoxF64,
1157
1168
  BoxI32,
1158
1169
  BoxBool,
1159
1170
  }
1160
-
1161
- // Pass 1: assign every entry a slot (and therefore a type index) up front, before
1162
- // any field list is built, so field-type resolution can reference ANY other entry.
1163
1171
  let mut slots: Vec<Slot> = vec![Slot::Str, Slot::Obj];
1164
1172
  let obj_type_idx: u32 = 1;
1173
+ let mut reg = GcTypeRegistry {
1174
+ class_type_idx: HashMap::new(),
1175
+ enum_super_type_idx: HashMap::new(),
1165
- let mut type_ids: HashMap<String, i32> = HashMap::new();
1176
+ variant_type_idx: HashMap::new(),
1177
+ app_super: HashMap::new(),
1178
+ app_class: HashMap::new(),
1179
+ app_variant: HashMap::new(),
1180
+ instances: Vec::new(),
1181
+ known_enums: HashSet::new(),
1182
+ byte_array_type_idx: 0,
1183
+ closure_type_idx: 0,
1184
+ variadic_array_type_idx: HashMap::new(),
1185
+ array_type_idx: 0,
1186
+ tag_type_idx: 0,
1187
+ box_i64_idx: 0,
1188
+ box_f64_idx: 0,
1189
+ box_i32_idx: 0,
1190
+ box_bool_idx: 0,
1191
+ obj_type_idx,
1192
+ type_ids: HashMap::new(),
1193
+ box_i64_fn: 0,
1194
+ box_f64_fn: 0,
1195
+ box_i32_fn: 0,
1196
+ box_bool_fn: 0,
1197
+ mk_str_fn: None,
1198
+ dyn_fns: HashMap::new(),
1199
+ };
1166
1200
  let mut next_type_id = TYPE_ID_FIRST_DECLARED;
1167
- let mut class_type_idx: HashMap<String, u32> = HashMap::new();
1201
+ let mut enum_decls: HashMap<String, &ast::Enum> = HashMap::new();
1168
- let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
1169
- let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
1170
- let mut array_ref_slot: Option<u32> = None;
1171
-
1172
- // `Bool` is an ordinary enum now (`type Bool = | True | False` in
1173
- // `libs/std/Bool.plum`) — registered exactly like any other enum
1174
- // declared in `source.items`, present only if actually imported.
1175
- let mut enum_decls: Vec<(String, Vec<String>)> = Vec::new();
1202
+ let mut instances: Vec<PlumType> = Vec::new();
1176
1203
  for item in &source.items {
1177
- if let ast::Item::Enum(e) = item {
1204
+ if let ast::Item::Trait(t) = item {
1178
- enum_decls.push((
1179
- e.name.clone(),
1180
- e.variants.iter().map(|v| v.name.clone()).collect(),
1205
+ reg.class_type_idx.insert(t.name.clone(), obj_type_idx);
1181
- ));
1182
1206
  }
1183
1207
  }
1184
- // A trait name used as a type lowers to `ref null $Obj`.
1185
1208
  for item in &source.items {
1186
- if let ast::Item::Trait(t) = item {
1209
+ let ast::Item::Enum(e) = item else { continue };
1187
- class_type_idx.insert(t.name.clone(), obj_type_idx);
1188
- }
1189
- }
1190
- for (enum_name, variant_names) in &enum_decls {
1191
- // `Array`/`Array$T` is a RECORD-shaped enum (single variant sharing the
1192
- // enum's own name — the `type Array[T] = ...` replacement) whose values
1193
- // are ALL represented by the one shared `array<anyref>` type regardless
1194
- // of `T` (see `isArraySpecialization`'s own doc comment) — alias its
1195
- // enum/variant/class names onto that single existing slot instead of
1196
- // giving it its own real struct type.
1197
- if isArraySpecialization(enum_name) {
1198
- let idx = *array_ref_slot.get_or_insert_with(|| {
1199
- let idx = slots.len() as u32;
1200
- slots.push(Slot::ArrayRef);
1201
- idx
1202
- });
1203
- enum_super_type_idx.insert(enum_name.clone(), idx);
1210
+ enum_decls.insert(e.name.clone(), e);
1204
- class_type_idx.insert(enum_name.clone(), idx);
1205
- for vname in variant_names {
1206
- variant_type_idx.insert(vname.clone(), idx);
1211
+ reg.known_enums.insert(e.name.clone());
1207
- }
1208
- continue;
1209
- }
1210
- let this_id = match enum_name.as_str() {
1212
+ let this_id = match e.name.as_str() {
1211
1213
  "Str" => TYPE_ID_STR,
1212
1214
  "Buffer" => TYPE_ID_BUFFER,
1213
1215
  _ => {
@@ -1216,67 +1218,101 @@ fn buildGcTypeRegistry(
1216
1218
  id
1217
1219
  }
1218
1220
  };
1219
- type_ids.insert(enum_name.clone(), this_id);
1221
+ reg.type_ids.insert(e.name.clone(), this_id);
1220
- for vname in variant_names {
1222
+ for v in &e.variants {
1221
- type_ids.insert(vname.clone(), this_id);
1223
+ reg.type_ids.insert(v.name.clone(), this_id);
1222
1224
  }
1223
- enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
1225
+ if e.generics.is_empty() && !isArraySpecialization(&e.name) {
1224
- slots.push(Slot::EnumSuper(enum_name.clone()));
1225
- for vname in variant_names {
1226
- variant_type_idx.insert(vname.clone(), slots.len() as u32);
1227
- slots.push(Slot::Variant(vname.clone()));
1226
+ instances.push(PlumType::TNamed(e.name.clone()));
1228
1227
  }
1229
- // A RECORD-shaped enum (the `type X = ...` replacement, e.g. `enum Cat =
1230
- // | Cat(name: Str)`) also gets a `class_type_idx` alias onto its one
1228
+ }
1231
- // variant's real struct slot — `plumTypeToValtype`'s `TNamed(name)` arm
1232
- // resolves a value of this type via `class_type_idx` (checked first,
1233
- // falling back to `enum_super_type_idx`), and every OTHER codegen site
1234
- // that already looks up a "class" by this exact name (field access,
1229
+ let mut arrays: Vec<PlumType> = Vec::new();
1230
+ for a in apps {
1235
- // construction, pattern destructuring, ...) keeps working unchanged.
1231
+ if let PlumType::TApp(n, _) = a {
1232
+ if isArraySpecialization(n) {
1233
+ arrays.push(a.clone());
1236
- if variant_names.len() == 1 && variant_names[0] == *enum_name {
1234
+ } else if enum_decls.contains_key(n) && !instances.contains(a) {
1237
- class_type_idx.insert(enum_name.clone(), variant_type_idx[&variant_names[0]]);
1235
+ instances.push(a.clone());
1236
+ }
1238
1237
  }
1239
1238
  }
1240
-
1239
+ for ty in &instances {
1240
+ let n = plum_checker::nominalName(ty).unwrap().to_string();
1241
+ let e = enum_decls[&n];
1242
+ let super_idx = slots.len() as u32;
1243
+ slots.push(Slot::EnumSuper(ty.clone()));
1244
+ match ty {
1245
+ PlumType::TNamed(_) => {
1246
+ reg.enum_super_type_idx.insert(n.clone(), super_idx);
1247
+ }
1248
+ _ => {
1249
+ reg.app_super.insert(ty.clone(), super_idx);
1250
+ }
1251
+ }
1252
+ for v in &e.variants {
1253
+ let idx = slots.len() as u32;
1254
+ slots.push(Slot::Variant(ty.clone(), v.name.clone()));
1255
+ match ty {
1256
+ PlumType::TNamed(_) => {
1257
+ reg.variant_type_idx.insert(v.name.clone(), idx);
1258
+ }
1259
+ _ => {
1260
+ reg.app_variant.insert((ty.clone(), v.name.clone()), idx);
1261
+ }
1262
+ }
1263
+ if e.variants.len() == 1 && v.name == n {
1264
+ match ty {
1265
+ PlumType::TNamed(_) => {
1266
+ reg.class_type_idx.insert(n.clone(), idx);
1267
+ }
1268
+ _ => {
1269
+ reg.app_class.insert(ty.clone(), idx);
1270
+ }
1271
+ }
1272
+ }
1273
+ }
1274
+ }
1275
+ reg.instances = instances.clone();
1241
- let tag_type_idx = slots.len() as u32;
1276
+ reg.tag_type_idx = slots.len() as u32;
1242
1277
  slots.push(Slot::Tag);
1243
- let box_i64_idx = slots.len() as u32;
1278
+ reg.box_i64_idx = slots.len() as u32;
1244
1279
  slots.push(Slot::BoxI64);
1245
- let box_f64_idx = slots.len() as u32;
1280
+ reg.box_f64_idx = slots.len() as u32;
1246
1281
  slots.push(Slot::BoxF64);
1247
- let box_i32_idx = slots.len() as u32;
1282
+ reg.box_i32_idx = slots.len() as u32;
1248
1283
  slots.push(Slot::BoxI32);
1249
- let box_bool_idx = slots.len() as u32;
1284
+ reg.box_bool_idx = slots.len() as u32;
1250
1285
  slots.push(Slot::BoxBool);
1251
1286
 
1252
- // The registry is fully index-complete after pass 1 (every name has an assigned
1287
+ // Native arrays: one wasm array type per distinct element representation,
1253
- // slot) even though no field lists exist yet — safe to hand to `plumTypeToGcValtype`
1254
- // for pass 2's field-type resolution.
1288
+ // innermost (least nested) element types first.
1255
- let registry = GcTypeRegistry {
1289
+ arrays.sort_by_key(arrayDepth);
1256
- class_type_idx,
1257
- enum_super_type_idx,
1258
- variant_type_idx,
1259
- byte_array_type_idx: 0,
1260
- closure_type_idx: 0,
1261
- variadic_array_type_idx: HashMap::new(),
1290
+ let mut by_vt: HashMap<ValType, u32> = HashMap::new();
1291
+ for a in &arrays {
1292
+ let PlumType::TApp(_, args) = a else { continue };
1293
+ let vt = plumTypeToGcValtype(&args[0], &reg);
1262
- array_type_idx: array_ref_slot.unwrap_or(0),
1294
+ let idx = *by_vt.entry(vt).or_insert_with(|| {
1263
- tag_type_idx,
1264
- box_i64_idx,
1265
- box_f64_idx,
1266
- box_i32_idx,
1267
- box_bool_idx,
1268
- obj_type_idx,
1269
- type_ids,
1270
- box_i64_fn: 0,
1271
- box_f64_fn: 0,
1272
- box_i32_fn: 0,
1273
- box_bool_fn: 0,
1274
- mk_str_fn: None,
1295
+ let idx = slots.len() as u32;
1275
- dyn_fns: HashMap::new(),
1296
+ slots.push(Slot::ArrayRef(vt));
1297
+ idx
1276
- };
1298
+ });
1299
+ reg.app_super.insert(a.clone(), idx);
1300
+ reg.app_class.insert(a.clone(), idx);
1301
+ reg.app_variant.insert((a.clone(), "Array".to_string()), idx);
1302
+ }
1277
1303
 
1304
+ let enum_generics = |n: &str| -> Vec<String> {
1305
+ enum_decls
1306
+ .get(n)
1307
+ .map(|e| e.generics.iter().map(|g| g.name.clone()).collect())
1308
+ .unwrap_or_default()
1309
+ };
1278
- // Pass 2: build the real SubType for every slot, now that every cross-reference
1310
+ let subst_for = |ty: &PlumType| -> std::collections::BTreeMap<String, PlumType> {
1311
+ match ty {
1312
+ PlumType::TApp(n, args) => enum_generics(n).into_iter().zip(args.iter().cloned()).collect(),
1279
- // resolves.
1313
+ _ => Default::default(),
1314
+ }
1315
+ };
1280
1316
  let subtypes: Vec<SubType> = slots
1281
1317
  .iter()
1282
1318
  .map(|slot| match slot {
@@ -1291,20 +1327,16 @@ fn buildGcTypeRegistry(
1291
1327
  shared: false,
1292
1328
  },
1293
1329
  },
1294
- // An ORDINARY enum's supertype declares zero fields (each variant adds its
1295
- // own distinct payload fields below it). A DISCRIMINANT enum (`enum
1296
- // Foo(n: Int) = ...`) is different: every variant shares the EXACT SAME
1297
- // field list (`plum-checker::buildGlobalTables` already gives every variant
1298
- // of such an enum identical `field_types`, equal to the shared params), so
1299
- // the supertype declares those fields directly — this is what lets `self.n`
1300
- // field access work on the plain supertype-typed reference with a
1301
- // `struct.get`, no `ref.cast` to one arbitrary variant required (which would
1302
- // trap at runtime whenever `self` isn't actually THAT variant).
1303
- Slot::EnumSuper(enum_name) => {
1330
+ Slot::EnumSuper(ty) => {
1331
+ let n = plum_checker::nominalName(ty).unwrap();
1332
+ let s = subst_for(ty);
1304
- let params = enum_params.get(enum_name).cloned().unwrap_or_default();
1333
+ let params = enum_params.get(n).cloned().unwrap_or_default();
1305
1334
  let mut field_types: Vec<FieldType> = vec![typeIdField()];
1306
- field_types.extend(params.iter().map(|(_, ty)| FieldType {
1335
+ field_types.extend(params.iter().map(|(_, fty)| FieldType {
1307
- element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1336
+ element_type: StorageType::Val(plumTypeToGcValtype(
1337
+ &plum_checker::types::substitute(fty, &s),
1338
+ &reg,
1339
+ )),
1308
1340
  mutable: true,
1309
1341
  }));
1310
1342
  SubType {
@@ -1318,29 +1350,14 @@ fn buildGcTypeRegistry(
1318
1350
  },
1319
1351
  }
1320
1352
  }
1321
- Slot::Variant(vname) => {
1353
+ Slot::Variant(ty, vname) => {
1322
1354
  let info = enum_variants.get(vname).unwrap_or_else(|| {
1323
- panic!(
1324
- "internal codegen error: variant '{}' missing from EnumVariants",
1355
+ panic!("internal codegen error: variant '{}' missing from EnumVariants", vname)
1325
- vname
1326
- )
1327
1356
  });
1328
- let super_idx = *registry
1329
- .enum_super_type_idx
1330
- .get(&info.enum_name)
1331
- .unwrap_or_else(|| {
1332
- panic!(
1333
- "internal codegen error: enum '{}' missing its supertype slot",
1357
+ let super_idx = reg.superIdx(ty).expect("enum supertype slot");
1334
- info.enum_name
1335
- )
1336
- });
1337
- // For a discriminant enum, `info.field_types` is ALREADY identical to the
1338
- // supertype's own fields (see the `EnumSuper` arm above) — a variant with
1339
- // zero ADDED fields beyond its supertype is still valid wasm-gc
1340
- // subtyping, so no special-casing is needed here.
1341
1358
  let mut field_types: Vec<FieldType> = vec![typeIdField()];
1342
- field_types.extend(info.field_types.iter().map(|ty| FieldType {
1359
+ field_types.extend(plum_checker::variantFields(info, ty).iter().map(|fty| FieldType {
1343
- element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
1360
+ element_type: StorageType::Val(plumTypeToGcValtype(fty, &reg)),
1344
1361
  mutable: true,
1345
1362
  }));
1346
1363
  SubType {
@@ -1389,12 +1406,12 @@ fn buildGcTypeRegistry(
1389
1406
  Slot::BoxF64 => boxSubType(ValType::F64, obj_type_idx),
1390
1407
  Slot::BoxI32 => boxSubType(ValType::I32, obj_type_idx),
1391
1408
  Slot::BoxBool => boxSubType(ValType::I32, obj_type_idx),
1392
- Slot::ArrayRef => SubType {
1409
+ Slot::ArrayRef(vt) => SubType {
1393
1410
  is_final: true,
1394
1411
  supertype_idx: None,
1395
1412
  composite_type: CompositeType {
1396
1413
  inner: CompositeInnerType::Array(ArrayType(FieldType {
1397
- element_type: StorageType::Val(ValType::Ref(RefType::ANYREF)),
1414
+ element_type: StorageType::Val(*vt),
1398
1415
  mutable: true,
1399
1416
  })),
1400
1417
  shared: false,
@@ -1402,10 +1419,8 @@ fn buildGcTypeRegistry(
1402
1419
  },
1403
1420
  })
1404
1421
  .collect();
1405
-
1406
1422
  module.addGcTypes(subtypes);
1407
-
1408
- registry
1423
+ reg
1409
1424
  }
1410
1425
 
1411
1426
  /// The immutable `typeId: i32` header field every `$Obj` subtype starts with.
@@ -1440,9 +1455,6 @@ fn blockTypeFor(result_vt: Option<ValType>) -> BlockType {
1440
1455
  result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty)
1441
1456
  }
1442
1457
 
1443
- fn retTypeToWasm(ret: Option<&ast::Type>) -> Option<ValType> {
1444
- ret.and_then(|r| astTypeToWasm(&r.name))
1445
- }
1446
1458
 
1447
1459
  fn encodeLeb128U32(mut val: u32) -> Vec<u8> {
1448
1460
  let mut bytes = Vec::new();
@@ -1752,18 +1764,305 @@ fn desugarAssertStmt(stmt: &ast::Stmt) -> ast::Stmt {
1752
1764
  }
1753
1765
  }
1754
1766
 
1767
+ /// One function body to compile: a non-generic fn (`inst: None`) or one concrete
1768
+ /// instantiation of a generic fn / method of a generic type.
1769
+ struct UnitDef<'a> {
1770
+ f: &'a ast::Fn,
1771
+ inst: Option<usize>,
1772
+ subst: std::collections::BTreeMap<String, PlumType>,
1773
+ /// Debug name in the wasm name section.
1774
+ name: String,
1775
+ }
1776
+
1777
+ type Subst = std::collections::BTreeMap<String, PlumType>;
1778
+ type EnumGenerics = HashMap<String, Vec<String>>;
1779
+
1780
+ /// The type of `self` for a method of `recv`, with the owner's generics substituted.
1781
+ fn recvPlumType(recv: &str, subst: &Subst, eg: &EnumGenerics) -> PlumType {
1782
+ if recv == "ByteSlice" {
1783
+ return PlumType::TByteSlice;
1784
+ }
1785
+ match eg.get(recv) {
1786
+ Some(g) if !g.is_empty() => PlumType::TApp(
1787
+ recv.to_string(),
1788
+ g.iter()
1789
+ .map(|n| subst.get(n).cloned().unwrap_or_else(|| PlumType::TParam(n.clone())))
1790
+ .collect(),
1791
+ ),
1792
+ _ => plum_checker::plumTypeFromName(recv),
1793
+ }
1794
+ }
1795
+
1796
+ /// `(self type, declared param types (variadic as TVariadic), return type)`, generics substituted.
1797
+ fn unitDeclTypes(f: &ast::Fn, subst: &Subst, eg: &EnumGenerics) -> (Option<PlumType>, Vec<PlumType>, PlumType) {
1798
+ let sub = |t: PlumType| plum_checker::types::substitute(&t, subst);
1799
+ let self_ty = f.type_param.as_deref().map(|r| recvPlumType(r, subst, eg));
1800
+ let params = f.params.iter().map(|p| sub(paramPlumType(&p.ty))).collect();
1801
+ let ret = sub(f
1802
+ .returns
1803
+ .as_ref()
1804
+ .map(plum_checker::plumTypeFromAstG)
1805
+ .unwrap_or(PlumType::TUnit));
1806
+ (self_ty, params, ret)
1807
+ }
1808
+
1809
+ fn unitFuncSig(f: &ast::Fn, subst: &Subst, eg: &EnumGenerics, g: &GcTypeRegistry) -> FuncSig {
1810
+ let (self_ty, params, ret) = unitDeclTypes(f, subst, eg);
1811
+ let mut vts: Vec<ValType> = Vec::new();
1812
+ if let Some(s) = &self_ty {
1813
+ vts.push(plumTypeToGcValtype(s, g));
1814
+ }
1815
+ for p in &params {
1816
+ vts.push(match p {
1817
+ PlumType::TVariadic(e) => {
1818
+ let ev = plumTypeToGcValtype(e, g);
1819
+ gcRef(*g.variadic_array_type_idx.get(&ev).unwrap_or_else(|| {
1820
+ panic!("internal codegen error: no variadic array type for element {}", e)
1821
+ }))
1822
+ }
1823
+ other => plumTypeToGcValtype(other, g),
1824
+ });
1825
+ }
1826
+ let ret_vt = if matches!(ret, PlumType::TUnit) { None } else { Some(plumTypeToGcValtype(&ret, g)) };
1827
+ let param_defaults = f
1828
+ .params
1829
+ .iter()
1830
+ .map(|p| match &p.ty {
1831
+ ast::ParamType::Type(_) => p.default.clone(),
1832
+ _ => None,
1833
+ })
1834
+ .collect();
1835
+ FuncSig {
1836
+ params: vts,
1837
+ ret: ret_vt,
1838
+ param_defaults,
1839
+ decl_params: params,
1840
+ decl_ret: ret,
1841
+ }
1842
+ }
1843
+
1844
+ /// Collects every concrete generic-enum instance, function type and variadic element
1845
+ /// type mentioned by the types it is shown.
1846
+ struct TypeCollector<'a> {
1847
+ apps: Vec<PlumType>,
1848
+ seen: HashSet<PlumType>,
1849
+ funs: Vec<PlumType>,
1850
+ funs_seen: HashSet<PlumType>,
1851
+ variadics: Vec<PlumType>,
1852
+ variadics_seen: HashSet<PlumType>,
1853
+ enum_variants: &'a EnumVariants,
1854
+ enum_params: &'a EnumParams,
1855
+ eg: &'a EnumGenerics,
1856
+ }
1857
+
1858
+ impl<'a> TypeCollector<'a> {
1859
+ fn visit(&mut self, t: &PlumType) {
1860
+ if hasFreeTy(t) {
1861
+ return;
1862
+ }
1863
+ match t {
1864
+ PlumType::TApp(n, args) => {
1865
+ for a in args {
1866
+ self.visit(a);
1867
+ }
1868
+ if !self.seen.insert(t.clone()) {
1869
+ return;
1870
+ }
1871
+ self.apps.push(t.clone());
1872
+ if isArraySpecialization(n) {
1873
+ return;
1874
+ }
1875
+ let infos: Vec<&EnumVariantInfo> = self
1876
+ .enum_variants
1877
+ .values()
1878
+ .filter(|i| i.enum_name == *n)
1879
+ .collect();
1880
+ for info in infos {
1881
+ for f in plum_checker::variantFields(info, t) {
1882
+ self.visit(&f);
1883
+ }
1884
+ }
1885
+ if let (Some(params), Some(g)) = (self.enum_params.get(n), self.eg.get(n)) {
1886
+ let s: Subst = g.iter().cloned().zip(args.iter().cloned()).collect();
1887
+ for (_, fty) in params.clone() {
1888
+ self.visit(&plum_checker::types::substitute(&fty, &s));
1889
+ }
1890
+ }
1891
+ }
1892
+ PlumType::TFun(ps, r) => {
1893
+ for p in ps {
1894
+ self.visit(p);
1895
+ }
1896
+ self.visit(r);
1897
+ if self.funs_seen.insert(t.clone()) {
1898
+ self.funs.push(t.clone());
1899
+ }
1900
+ }
1901
+ PlumType::TVariadic(e) => {
1902
+ self.visit(e);
1903
+ if self.variadics_seen.insert((**e).clone()) {
1904
+ self.variadics.push((**e).clone());
1905
+ }
1906
+ }
1907
+ PlumType::TTags(m) => {
1908
+ for v in m.values().flatten() {
1909
+ self.visit(v);
1910
+ }
1911
+ }
1912
+ _ => {}
1913
+ }
1914
+ }
1915
+ }
1916
+
1917
+ /// Everything a unit body needs to resolve calls, closures and expression types.
1918
+ struct UnitRef<'a> {
1919
+ idx: usize,
1920
+ inst: Option<usize>,
1921
+ subst: &'a Subst,
1922
+ types: &'a HashMap<usize, PlumType>,
1923
+ }
1924
+
1755
1925
  fn compileSourceInner(
1756
1926
  source: &ast::Source,
1757
1927
  extra_exports: &[(String, String)],
1758
1928
  typed: plum_checker::TypedInfo,
1759
1929
  ) -> Result<(Vec<u8>, Vec<String>), String> {
1760
- // The source is compiled as written (generics erased, not specialized): the
1930
+ let plum_checker::TypedInfo {
1931
+ expr_types,
1932
+ instantiations,
1933
+ inst_expr_types,
1934
+ call_inst,
1935
+ ..
1936
+ } = typed;
1761
- // checker's per-expression instantiated types drive every box/unbox decision.
1937
+ // Per-instantiation expression types, split out by instantiation id.
1938
+ let mut inst_types: Vec<HashMap<usize, PlumType>> = vec![HashMap::new(); instantiations.len()];
1762
- let types = typed.expr_types;
1939
+ for ((id, ptr), t) in inst_expr_types {
1940
+ inst_types[id].insert(ptr, t);
1941
+ }
1763
1942
  // `Bool.toStr` is a checker/codegen builtin: compiled as one extra method, never added to the source.
1764
1943
  let bool_to_str_fn = plum_checker::needsBuiltinBoolToStr(source).then(plum_checker::builtinBoolToStr);
1765
1944
  let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
1766
1945
  let min_required = plum_checker::buildMinRequiredArgs(source);
1946
+ let mut eg: EnumGenerics = HashMap::new();
1947
+ for item in &source.items {
1948
+ if let ast::Item::Enum(e) = item {
1949
+ eg.insert(e.name.clone(), e.generics.iter().map(|g| g.name.clone()).collect());
1950
+ }
1951
+ }
1952
+
1953
+ // ---- units: every non-generic fn, plus one per reached instantiation ----
1954
+ let all_fns: Vec<&ast::Fn> = source
1955
+ .items
1956
+ .iter()
1957
+ .filter_map(|item| match item {
1958
+ ast::Item::Fn(f) if !f.is_extern => Some(f),
1959
+ _ => None,
1960
+ })
1961
+ .chain(bool_to_str_fn.iter())
1962
+ .collect();
1963
+ let mut units: Vec<UnitDef> = Vec::new();
1964
+ let mut known_fns: HashSet<String> = HashSet::new();
1965
+ {
1966
+ let cctx = checkCtxOf(&methods, &enum_variants, &enum_params, &min_required);
1967
+ for f in &all_fns {
1968
+ if f.type_param.is_none() {
1969
+ known_fns.insert(f.name.clone());
1970
+ }
1971
+ if plum_checker::fnGenericNames(f, &cctx).is_empty() {
1972
+ units.push(UnitDef {
1973
+ f,
1974
+ inst: None,
1975
+ subst: Subst::new(),
1976
+ name: fnKey(f).replace("::", "."),
1977
+ });
1978
+ }
1979
+ }
1980
+ for inst in &instantiations {
1981
+ let Some(f) = all_fns
1982
+ .iter()
1983
+ .find(|f| f.type_param == inst.receiver && f.name == inst.name)
1984
+ else {
1985
+ continue;
1986
+ };
1987
+ let subst: Subst = inst
1988
+ .generics
1989
+ .iter()
1990
+ .cloned()
1991
+ .zip(inst.type_args.iter().cloned())
1992
+ .collect();
1993
+ let owner_n = inst.receiver.as_ref().and_then(|r| eg.get(r)).map(|g| g.len()).unwrap_or(0);
1994
+ let show = |a: &[PlumType]| a.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
1995
+ let name = match &inst.receiver {
1996
+ Some(r) => {
1997
+ let mut n = format!("{}[{}].{}", r, show(&inst.type_args[..owner_n.min(inst.type_args.len())]), inst.name);
1998
+ if inst.type_args.len() > owner_n {
1999
+ n.push_str(&format!("[{}]", show(&inst.type_args[owner_n..])));
2000
+ }
2001
+ if owner_n == 0 {
2002
+ n = format!("{}.{}", r, inst.name);
2003
+ }
2004
+ n
2005
+ }
2006
+ None => format!("{}[{}]", inst.name, show(&inst.type_args)),
2007
+ };
2008
+ units.push(UnitDef {
2009
+ f,
2010
+ inst: Some(inst.id),
2011
+ subst,
2012
+ name,
2013
+ });
2014
+ }
2015
+ }
2016
+
2017
+ // ---- collect the concrete generic types the program uses ----
2018
+ let mut tc = TypeCollector {
2019
+ apps: Vec::new(),
2020
+ seen: HashSet::new(),
2021
+ funs: Vec::new(),
2022
+ funs_seen: HashSet::new(),
2023
+ variadics: Vec::new(),
2024
+ variadics_seen: HashSet::new(),
2025
+ enum_variants: &enum_variants,
2026
+ enum_params: &enum_params,
2027
+ eg: &eg,
2028
+ };
2029
+ for info in enum_variants.values() {
2030
+ if info.generics.is_empty() {
2031
+ for t in &info.field_types {
2032
+ tc.visit(t);
2033
+ }
2034
+ }
2035
+ }
2036
+ for (name, params) in enum_params.iter() {
2037
+ if eg.get(name).is_none_or(|g| g.is_empty()) {
2038
+ for (_, t) in params {
2039
+ tc.visit(t);
2040
+ }
2041
+ }
2042
+ }
2043
+ for t in expr_types.values() {
2044
+ tc.visit(t);
2045
+ }
2046
+ for m in &inst_types {
2047
+ for t in m.values() {
2048
+ tc.visit(t);
2049
+ }
2050
+ }
2051
+ for u in &units {
2052
+ let (s, ps, r) = unitDeclTypes(u.f, &u.subst, &eg);
2053
+ if let Some(s) = s {
2054
+ tc.visit(&s);
2055
+ }
2056
+ for p in &ps {
2057
+ tc.visit(p);
2058
+ }
2059
+ tc.visit(&r);
2060
+ }
2061
+ for inst in &instantiations {
2062
+ for t in &inst.type_args {
2063
+ tc.visit(t);
2064
+ }
2065
+ }
1767
2066
 
1768
2067
  // Intern every tag name the program's types mention, in sorted order.
1769
2068
  {
@@ -1779,9 +2078,19 @@ fn compileSourceInner(
1779
2078
  for t in methods.values() {
1780
2079
  collectTagNamesTy(t, &mut names);
1781
2080
  }
1782
- for t in types.values() {
2081
+ for t in expr_types.values() {
1783
2082
  collectTagNamesTy(t, &mut names);
1784
2083
  }
2084
+ for m in &inst_types {
2085
+ for t in m.values() {
2086
+ collectTagNamesTy(t, &mut names);
2087
+ }
2088
+ }
2089
+ for inst in &instantiations {
2090
+ for t in &inst.type_args {
2091
+ collectTagNamesTy(t, &mut names);
2092
+ }
2093
+ }
1785
2094
  CURRENT_TAG_IDS.with(|c| {
1786
2095
  *c.borrow_mut() = names
1787
2096
  .into_iter()
@@ -1793,7 +2102,7 @@ fn compileSourceInner(
1793
2102
 
1794
2103
  let mut module = WasmModule::new();
1795
2104
 
1796
- let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params);
2105
+ let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params, &tc.apps);
1797
2106
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1798
2107
 
1799
2108
  CURRENT_CONSTS.with(|c| {
@@ -1805,22 +2114,12 @@ fn compileSourceInner(
1805
2114
  }
1806
2115
  });
1807
2116
 
1808
- // Register one shared `array<T>` GC type per distinct wasm value type used by a
2117
+ // One immutable `array<T>` per distinct wasm value type used by a `...T` variadic.
1809
- // `...T` variadic parameter anywhere in the program (almost always just one, e.g.
1810
- // `Int...`) — replaces the old bump-allocated `[count][elem...]` blob (`array.len`
1811
- // replaces the explicit count word). Must happen before function signatures are
1812
- // registered below, since a variadic param's wasm type is this array's `ref`.
1813
2118
  let mut variadic_elem_vts: Vec<ValType> = Vec::new();
1814
- for item in &source.items {
1815
- if let ast::Item::Fn(f) = item {
1816
- for p in &f.params {
2119
+ for e in &tc.variadics {
1817
- if let ast::ParamType::Variadic(t) = &p.ty {
1818
- let vt = astTypeToWasm(&t.name).unwrap_or(ValType::I64);
2120
+ let vt = plumTypeToGcValtype(e, &gc_types);
1819
- if !variadic_elem_vts.contains(&vt) {
2121
+ if !variadic_elem_vts.contains(&vt) {
1820
- variadic_elem_vts.push(vt);
2122
+ variadic_elem_vts.push(vt);
1821
- }
1822
- }
1823
- }
1824
2123
  }
1825
2124
  }
1826
2125
  let variadic_array_subtypes: Vec<SubType> = variadic_elem_vts
@@ -1845,13 +2144,7 @@ fn compileSourceInner(
1845
2144
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1846
2145
  }
1847
2146
 
1848
- // Register every `extern fun` (e.g. `plum-std/Os.plum`'s `printLn`) as a
2147
+ // Register every `extern fun` as a genuine wasm import BEFORE any function.
1849
- // genuine wasm import BEFORE any function (including the `start` function
1850
- // set up right below) — imports must occupy the low end of the function
1851
- // index space for every later `addFunction`'s index arithmetic to stay
1852
- // correct. `plum-checker` has already confirmed every extern fn has no
1853
- // receiver and no body, so `f.name` alone (no `fnKey` receiver-qualification) is
1854
- // always its unique key.
1855
2148
  let mut func_ids: HashMap<String, u32> = HashMap::new();
1856
2149
  let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
1857
2150
  let mut extern_wrappers: Vec<(String, u32, Vec<ValType>, Option<ValType>)> = Vec::new();
@@ -1860,38 +2153,28 @@ fn compileSourceInner(
1860
2153
  if !f.is_extern {
1861
2154
  continue;
1862
2155
  }
2156
+ known_fns.insert(f.name.clone());
2157
+ let sig = unitFuncSig(f, &Subst::new(), &eg, &gc_types);
1863
- let param_types = fnWasmParamTypes(f, &gc_types);
2158
+ let param_types = sig.params.clone();
1864
- let ret = retTypeToWasm(f.returns.as_ref());
2159
+ let ret = sig.ret;
1865
- // `Int` in an extern signature is narrowed to wasm i32 at the host
2160
+ // `Int` in an extern signature is narrowed to wasm i32 at the host boundary.
1866
- // boundary (the browser host never sees a BigInt). The import gets the
1867
- // narrowed type; a same-named wrapper with the plain i64 signature (added
1868
- // below, once all imports exist) range-checks/wraps args and sign-extends
1869
- // the result, so every Plum call site is unchanged.
1870
2161
  let narrow = |v: &ValType| if *v == ValType::I64 { ValType::I32 } else { *v };
1871
2162
  let imp_params: Vec<ValType> = param_types.iter().map(narrow).collect();
1872
2163
  let imp_results: Vec<ValType> = ret.iter().map(narrow).collect();
1873
2164
  let type_idx = module.addType(&imp_params, &imp_results);
1874
2165
  let func_idx = module.addImport("plum", &f.name, type_idx);
1875
- extern_wrappers.push((f.name.clone(), func_idx, param_types.clone(), ret));
2166
+ extern_wrappers.push((f.name.clone(), func_idx, param_types, ret));
1876
2167
  func_sigs.insert(
1877
2168
  f.name.clone(),
1878
2169
  FuncSig {
1879
- params: param_types,
1880
- ret,
1881
2170
  param_defaults: Vec::new(),
1882
- decl_params: f.params.iter().map(|p| paramPlumType(&p.ty)).collect(),
1883
- decl_ret: f
1884
- .returns
1885
- .as_ref()
2171
+ ..sig
1886
- .map(plum_checker::plumTypeFromAstG)
1887
- .unwrap_or(PlumType::TUnit),
1888
2172
  },
1889
2173
  );
1890
2174
  }
1891
2175
  }
1892
2176
 
1893
- // `plum::crash(Str)`: same wasm type as `printLn`; imported only when the
1894
- // program actually contains a `crash` statement.
2177
+ // `plum::crash(Str)`
1895
2178
  {
1896
2179
  let uses_crash = sourceUsesCrash(&format!("{:?}", source.items));
1897
2180
  if uses_crash {
@@ -1902,9 +2185,7 @@ fn compileSourceInner(
1902
2185
  }
1903
2186
  }
1904
2187
 
1905
- // Extern `Int` narrowing wrappers (see the import loop above). The i32 range
2188
+ // Extern `Int` narrowing wrappers.
1906
- // check is unconditional (one wrap/extend/compare per Int arg; the CLI has no
1907
- // separate debug mode): an `Int` that doesn't fit in i32 traps.
1908
2189
  for (name, imp_idx, params, ret) in &extern_wrappers {
1909
2190
  let mut b = vec![0u8];
1910
2191
  for (i, vt) in params.iter().enumerate() {
@@ -1933,54 +2214,39 @@ fn compileSourceInner(
1933
2214
  let results_vec: Vec<ValType> = ret.iter().cloned().collect();
1934
2215
  let t = module.addType(params, &results_vec);
1935
2216
  let w = module.addFunction(t, &b);
2217
+ module.nameFunction(w, name);
1936
2218
  func_ids.insert(name.clone(), w);
1937
2219
  }
1938
2220
 
1939
- // Pre-allocate one instance of every payload-free variant (True/False/End/...)
2221
+ // Pre-allocate one instance of every payload-free variant of every enum instance.
1940
- // as a global, populated once by a `start` function rather than reconstructed on
1941
- // every reference — see this migration plan's Decision 2. `struct.new` isn't
1942
- // allowed inside a global's own const-expr initializer (confirmed empirically in
1943
- // Task 1), so each global starts `ref.null` and a `start` function fills it in
1944
- // before any export is callable. The zero-capture "trampoline" closures
1945
- // registered below (once closures are discovered) append to this SAME start
1946
- // function, so its body isn't finalized/patched into the module until then.
1947
- let mut singleton_globals: HashMap<String, u32> = HashMap::new();
2222
+ let mut singleton_globals: HashMap<(PlumType, String), u32> = HashMap::new();
1948
2223
  let mut start_body = Vec::new();
2224
+ for ty in gc_types.instances.clone() {
2225
+ let en = plum_checker::nominalName(&ty).unwrap().to_string();
2226
+ let mut vnames: Vec<(&String, &EnumVariantInfo)> =
2227
+ enum_variants.iter().filter(|(_, i)| i.enum_name == en).collect();
2228
+ vnames.sort_by_key(|(_, i)| i.tag);
1949
- for (name, info) in &enum_variants {
2229
+ for (name, info) in vnames {
1950
- if !info.field_types.is_empty() {
2230
+ if !info.field_types.is_empty() {
1951
- continue;
2231
+ continue;
1952
- }
2232
+ }
2233
+ let variant_idx = gc_types.variantIdx(&ty, name).unwrap_or_else(|| {
1953
- // `Array`'s bare payload-free variant is aliased onto the
2234
+ panic!("internal codegen error: payload-free variant '{}' missing from GC type registry", name)
2235
+ });
1954
- // ONE shared `array<anyref>` type (see
2236
+ let mut init = Vec::new();
1955
- // `buildGcTypeRegistry`'s `isArraySpecialization` handling), not a
1956
- // real struct type — it's never actually instantiated this way
2237
+ Instruction::RefNull(HeapType::Concrete(variant_idx)).encode(&mut init);
1957
- // (construction goes through the `init`/`push` compiler intrinsics
2238
+ let global_idx = module.addGlobal(gcRef(variant_idx), true, &init);
1958
- // instead), so it must not get a singleton global here.
2239
+ singleton_globals.insert((ty.clone(), name.clone()), global_idx);
2240
+ Instruction::I32Const(typeIdOf(&gc_types, name)).encode(&mut start_body);
1959
- if isArraySpecialization(name) {
2241
+ Instruction::StructNew(variant_idx).encode(&mut start_body);
1960
- continue;
2242
+ Instruction::GlobalSet(global_idx).encode(&mut start_body);
1961
2243
  }
1962
- let variant_idx = *gc_types.variant_type_idx.get(name).unwrap_or_else(|| {
1963
- panic!(
1964
- "internal codegen error: payload-free variant '{}' missing from GC type registry",
1965
- name
1966
- )
1967
- });
1968
- let mut init = Vec::new();
1969
- Instruction::RefNull(HeapType::Concrete(variant_idx)).encode(&mut init);
1970
- let global_idx = module.addGlobal(gcRef(variant_idx), true, &init);
1971
- singleton_globals.insert(name.clone(), global_idx);
1972
-
1973
- // A payload-free variant is exactly `{typeId}`.
1974
- Instruction::I32Const(typeIdOf(&gc_types, name)).encode(&mut start_body);
1975
- Instruction::StructNew(variant_idx).encode(&mut start_body);
1976
- Instruction::GlobalSet(global_idx).encode(&mut start_body);
1977
2244
  }
1978
2245
  let start_type_idx = module.addType(&[], &[]);
1979
2246
  let start_func_idx = module.addFunction(start_type_idx, &[]);
2247
+ module.nameFunction(start_func_idx, "start");
1980
2248
  module.setStartFunction(start_func_idx);
1981
2249
 
1982
- // Box helpers (`i64 -> $BoxI64`, ...) and the `Str` builder push the header
1983
- // typeId themselves, so callers only supply the payload.
1984
2250
  gc_types.box_i64_fn =
1985
2251
  registerBoxHelper(&mut module, ValType::I64, TYPE_ID_BOX_I64, gc_types.box_i64_idx);
1986
2252
  gc_types.box_f64_fn =
@@ -1989,6 +2255,14 @@ fn compileSourceInner(
1989
2255
  registerBoxHelper(&mut module, ValType::I32, TYPE_ID_BOX_I32, gc_types.box_i32_idx);
1990
2256
  gc_types.box_bool_fn =
1991
2257
  registerBoxHelper(&mut module, ValType::I32, TYPE_ID_BOX_BOOL, gc_types.box_bool_idx);
2258
+ for (f, n) in [
2259
+ (gc_types.box_i64_fn, "box.i64"),
2260
+ (gc_types.box_f64_fn, "box.f64"),
2261
+ (gc_types.box_i32_fn, "box.i32"),
2262
+ (gc_types.box_bool_fn, "box.bool"),
2263
+ ] {
2264
+ module.nameFunction(f, n);
2265
+ }
1992
2266
  if let (Some(&buffer_idx), Some(&str_idx)) = (
1993
2267
  gc_types.class_type_idx.get("Buffer"),
1994
2268
  gc_types.class_type_idx.get("Str"),
@@ -2009,59 +2283,32 @@ fn compileSourceInner(
2009
2283
  }
2010
2284
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
2011
2285
 
2012
- // Closure ABI (see `ClosureSigKey`): one erased `call_indirect` type per arity,
2286
+ // Closure ABI: one `call_indirect` type per Plum function signature,
2013
- // `(env, anyref * n) -> anyref`, shared by every closure and call site.
2287
+ // `(env: anyref, params...) -> result`, with every value in its own representation.
2014
2288
  let mut closure_call_types: HashMap<ClosureSigKey, u32> = HashMap::new();
2015
- for arity in 0..=MAX_CLOSURE_ARITY {
2289
+ for t in &tc.funs {
2016
- let params = vec![ValType::Ref(RefType::ANYREF); arity + 1];
2017
- let t = module.addType(&params, &[ValType::Ref(RefType::ANYREF)]);
2018
- closure_call_types.insert(arity, t);
2290
+ if let PlumType::TFun(ps, r) = t {
2291
+ getClosureCallType(&mut module, &mut closure_call_types, &closureSigKey(ps, r, &gc_types));
2292
+ }
2019
2293
  }
2020
2294
 
2021
- // Register every function AND method signature up front (methods get an implicit
2295
+ // Register every unit's wasm function up front.
2022
- // leading `self: pointer` param and are keyed as "Receiver::method").
2296
+ let mut inst_funcs: HashMap<usize, (u32, FuncSig)> = HashMap::new();
2023
- for f in source
2297
+ for u in &units {
2024
- .items
2298
+ let sig = unitFuncSig(u.f, &u.subst, &eg, &gc_types);
2025
- .iter()
2299
+ let results_vec: Vec<ValType> = sig.ret.into_iter().collect();
2300
+ let type_idx = module.addType(&sig.params, &results_vec);
2026
- .filter_map(|item| match item {
2301
+ let func_idx = module.addFunction(type_idx, &[]);
2027
- ast::Item::Fn(f) => Some(f),
2302
+ module.nameFunction(func_idx, &u.name);
2303
+ match u.inst {
2028
- _ => None,
2304
+ Some(id) => {
2305
+ inst_funcs.insert(id, (func_idx, sig));
2029
- })
2306
+ }
2030
- .chain(bool_to_str_fn.iter())
2031
- {
2032
- {
2033
- if f.is_extern {
2034
- continue;
2307
+ None => {
2308
+ let key = fnKey(u.f);
2309
+ func_ids.insert(key.clone(), func_idx);
2310
+ func_sigs.insert(key, sig);
2035
2311
  }
2036
- let param_types = fnWasmParamTypes(f, &gc_types);
2037
- let ret = retTypeToWasm(f.returns.as_ref());
2038
- let results_vec: Vec<ValType> = ret.into_iter().collect();
2039
- let type_idx = module.addType(&param_types, &results_vec);
2040
- let func_idx = module.addFunction(type_idx, &[]);
2041
- let key = fnKey(f);
2042
- func_ids.insert(key.clone(), func_idx);
2043
- let param_defaults: Vec<Option<ast::Expr>> = f
2044
- .params
2045
- .iter()
2046
- .map(|p| match &p.ty {
2047
- ast::ParamType::Type(_) => p.default.clone(),
2048
- _ => None,
2049
- })
2050
- .collect();
2051
- func_sigs.insert(
2052
- key,
2053
- FuncSig {
2054
- params: param_types,
2055
- ret,
2056
- param_defaults,
2057
- decl_params: f.params.iter().map(|p| paramPlumType(&p.ty)).collect(),
2058
- decl_ret: f
2059
- .returns
2060
- .as_ref()
2061
- .map(plum_checker::plumTypeFromAstG)
2062
- .unwrap_or(PlumType::TUnit),
2063
- },
2064
- );
2065
2312
  }
2066
2313
  }
2067
2314
 
@@ -2076,44 +2323,30 @@ fn compileSourceInner(
2076
2323
  );
2077
2324
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
2078
2325
 
2079
- let fns: Vec<&ast::Fn> = source
2080
- .items
2081
- .iter()
2082
- .filter_map(|item| match item {
2083
- ast::Item::Fn(f) if !f.is_extern => Some(f),
2084
- _ => None,
2085
- })
2086
- .chain(bool_to_str_fn.iter())
2087
- .collect();
2088
-
2089
- // ---- Discovery pre-pass: find every closure literal in every function body. ----
2326
+ // ---- Discovery pre-pass: every closure literal in every unit body. ----
2090
- // (Runs on the original, erased source.) Registers each closure as its own wasm function + table
2091
- // element and records the free variables it must capture.
2092
- // Only genuine free functions (`type_param.is_none()`) — a bare `FnCall`
2093
- // (`each(...)`) can only ever resolve to a free function, never a method
2094
- // (those always dispatch through `Attribute`/`AttrKind::Method`, keyed by
2095
- // `(receiver, name)` instead — see that arm's own comment below). Keying
2096
- // this by bare name across free functions AND methods would let a
2097
- // method silently SHADOW a same-named free function here
2098
- // (e.g. a user's own top-level `each` losing to `List.each` once
2099
- // `List` happens to be in scope) — same-named methods on unrelated
2100
- // classes are already ambiguous by bare name, so methods have no business
2101
- // being in a bare-name lookup table at all.
2102
- let fn_decls: HashMap<String, &ast::Fn> = fns
2327
+ let fn_decls: HashMap<String, &ast::Fn> = units
2103
2328
  .iter()
2104
- .filter(|f| f.type_param.is_none())
2329
+ .filter(|u| u.inst.is_none() && u.f.type_param.is_none())
2105
- .map(|f| (f.name.clone(), *f))
2330
+ .map(|u| (u.f.name.clone(), u.f))
2106
2331
  .collect();
2332
+ let unit_types = |u: &UnitDef| -> &HashMap<usize, PlumType> {
2333
+ match u.inst {
2334
+ Some(id) => &inst_types[id],
2335
+ None => &expr_types,
2336
+ }
2337
+ };
2107
- let mut raw_closures: Vec<RawClosure> = Vec::new();
2338
+ let mut raw_closures: Vec<(usize, RawClosure)> = Vec::new();
2108
- let mut named_fn_refs: HashSet<String> = HashSet::new();
2339
+ let mut named_fn_refs: HashSet<(String, PlumType)> = HashSet::new();
2340
+ for (ui, u) in units.iter().enumerate() {
2109
- for f in &fns {
2341
+ let f = u.f;
2110
2342
  let mut locals: HashSet<String> = HashSet::new();
2111
2343
  for p in &f.params {
2112
2344
  locals.insert(p.name.clone());
2113
2345
  }
2114
2346
  let mut walker = ClosureWalker {
2115
- types: &types,
2347
+ types: unit_types(u),
2116
2348
  fn_decls: &fn_decls,
2349
+ func_sigs: &func_sigs,
2117
2350
  found: Vec::new(),
2118
2351
  locals,
2119
2352
  named_fn_refs: HashSet::new(),
@@ -2121,18 +2354,12 @@ fn compileSourceInner(
2121
2354
  match &f.body {
2122
2355
  ast::FnBody::Block(block) => walker.walkBlock(block),
2123
2356
  ast::FnBody::Expr(e) => walker.walkExpr(e),
2124
- // `fns` excludes every `extern fun` (no body to discover closures in).
2125
- ast::FnBody::Extern => unreachable!("extern fns are excluded from `fns`"),
2357
+ ast::FnBody::Extern => unreachable!("extern fns are excluded from units"),
2126
2358
  }
2127
- raw_closures.extend(walker.found);
2359
+ raw_closures.extend(walker.found.into_iter().map(|c| (ui, c)));
2128
2360
  named_fn_refs.extend(walker.named_fn_refs);
2129
2361
  }
2130
2362
 
2131
- // Register every closure literal's own env struct type, plus the ONE shared
2132
- // closure-value struct `{table_idx: i32, env: anyref}` every closure (and
2133
- // trampoline, below) wraps its env in — declared together as a single `rec`
2134
- // group now that every closure's free-variable types are known. The shared
2135
- // struct is always slot 0; closure `i`'s env type is slot `i + 1`.
2136
2363
  let mut closure_subtypes: Vec<SubType> = vec![SubType {
2137
2364
  is_final: true,
2138
2365
  supertype_idx: None,
@@ -2153,7 +2380,7 @@ fn compileSourceInner(
2153
2380
  shared: false,
2154
2381
  },
2155
2382
  }];
2156
- for rc in &raw_closures {
2383
+ for (_, rc) in &raw_closures {
2157
2384
  let field_types: Vec<FieldType> = rc
2158
2385
  .free_vars
2159
2386
  .iter()
@@ -2177,17 +2404,20 @@ fn compileSourceInner(
2177
2404
  gc_types.closure_type_idx = closure_type_indices[0];
2178
2405
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
2179
2406
 
2180
- let mut closures: HashMap<usize, ClosureInfo> = HashMap::new();
2407
+ let mut closures: HashMap<(usize, usize), ClosureInfo> = HashMap::new();
2181
- let mut closure_asts: HashMap<usize, &ast::Closure> = HashMap::new();
2408
+ let mut closure_asts: HashMap<(usize, usize), &ast::Closure> = HashMap::new();
2182
- for (i, rc) in raw_closures.iter().enumerate() {
2409
+ for (i, (ui, rc)) in raw_closures.iter().enumerate() {
2183
- let arity = rc.closure.params.len();
2184
- let type_idx = *closure_call_types
2410
+ let key = closureSigKey(
2411
+ &rc.param_ptypes,
2412
+ &rc.ret_ptype,
2185
- .get(&arity)
2413
+ &gc_types,
2414
+ );
2186
- .ok_or_else(|| format!("codegen: closures with {} params are not supported", arity))?;
2415
+ let type_idx = getClosureCallType(&mut module, &mut closure_call_types, &key);
2187
2416
  let func_idx = module.addFunction(type_idx, &[]);
2417
+ module.nameFunction(func_idx, &format!("{}.closure", units[*ui].name));
2188
2418
  let table_idx = module.addTableElement(func_idx);
2189
2419
  closures.insert(
2190
- rc.ptr,
2420
+ (*ui, rc.ptr),
2191
2421
  ClosureInfo {
2192
2422
  func_idx,
2193
2423
  table_idx,
@@ -2197,57 +2427,35 @@ fn compileSourceInner(
2197
2427
  free_vars: rc.free_vars.clone(),
2198
2428
  },
2199
2429
  );
2200
- closure_asts.insert(rc.ptr, rc.closure);
2430
+ closure_asts.insert((*ui, rc.ptr), rc.closure);
2201
- }
2431
+ }
2202
-
2432
+
2203
- // Register a zero-capture "trampoline" closure for every top-level function
2433
+ // Zero-capture trampolines for top-level functions used as values.
2204
- // referenced as a bare value (e.g. `each(double)`): a real wasm function with the
2205
- // erased closure ABI `(env, anyref...) -> anyref` that ignores its env, unboxes
2206
- // each argument to the real function's parameter representation, forwards to the
2207
- // real function and boxes its result, plus a funcref-table entry for it. Since
2208
- // it never captures anything, its `{table_idx, env=null}` closure struct is a
2209
- // compile-time constant — like every payload-free enum variant, built once by
2210
- // the shared `start` function and stored in its own global.
2211
- let mut named_fn_values: HashMap<String, u32> = HashMap::new();
2434
+ let mut named_fn_values: HashMap<(String, PlumType), u32> = HashMap::new();
2212
- for name in &named_fn_refs {
2435
+ for (name, fty) in &named_fn_refs {
2213
2436
  let sig = func_sigs
2214
2437
  .get(name)
2215
2438
  .expect("named fn ref must be a registered top-level function");
2216
2439
  let real_func_idx = *func_ids
2217
2440
  .get(name)
2218
2441
  .expect("named fn ref must be a registered top-level function");
2219
- let arity = sig.params.len();
2442
+ let PlumType::TFun(ps, r) = fty else { continue };
2220
- let type_idx = *closure_call_types
2443
+ let key = closureSigKey(ps, r, &gc_types);
2221
- .get(&arity)
2222
- .ok_or_else(|| format!("codegen: function '{}' has too many params to use as a value", name))?;
2444
+ let type_idx = getClosureCallType(&mut module, &mut closure_call_types, &key);
2223
-
2224
- let mut tbody = Vec::new();
2445
+ let mut tbody = vec![0u8];
2225
- tbody.push(0u8); // no locals beyond the params already in the signature
2226
- for (i, vt) in sig.params.iter().enumerate() {
2446
+ for i in 0..sig.params.len() {
2227
- Instruction::LocalGet((i + 1) as u32).encode(&mut tbody); // local 0 is env, ignored
2447
+ Instruction::LocalGet((i + 1) as u32).encode(&mut tbody);
2228
- match sig.decl_params.get(i) {
2229
- Some(PlumType::TBool) => {
2230
- coerceTy(&anyTy(), &PlumType::TBool, &mut tbody, &gc_types)
2231
- }
2232
- _ => coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut tbody, &gc_types),
2233
- }
2234
2448
  }
2235
2449
  Instruction::Call(real_func_idx).encode(&mut tbody);
2236
- match sig.ret {
2237
- Some(_) if matches!(sig.decl_ret, PlumType::TBool) => {
2238
- coerceTy(&PlumType::TBool, &anyTy(), &mut tbody, &gc_types)
2239
- }
2240
- Some(rvt) => coerceVt(rvt, ValType::Ref(RefType::ANYREF), &mut tbody, &gc_types),
2241
- None => Instruction::RefNull(HeapType::ANY).encode(&mut tbody),
2242
- }
2243
2450
  Instruction::End.encode(&mut tbody);
2244
2451
  let func_idx = module.addFunction(type_idx, &tbody);
2452
+ module.nameFunction(func_idx, &format!("{}.trampoline", name));
2245
2453
  let table_idx = module.addTableElement(func_idx);
2246
2454
 
2247
2455
  let mut init = Vec::new();
2248
2456
  Instruction::RefNull(HeapType::Concrete(gc_types.closure_type_idx)).encode(&mut init);
2249
2457
  let global_idx = module.addGlobal(gcRef(gc_types.closure_type_idx), true, &init);
2250
- named_fn_values.insert(name.clone(), global_idx);
2458
+ named_fn_values.insert((name.clone(), fty.clone()), global_idx);
2251
2459
 
2252
2460
  Instruction::I32Const(table_idx as i32).encode(&mut start_body);
2253
2461
  Instruction::RefNull(HeapType::ANY).encode(&mut start_body);
@@ -2255,19 +2463,12 @@ fn compileSourceInner(
2255
2463
  Instruction::GlobalSet(global_idx).encode(&mut start_body);
2256
2464
  }
2257
2465
 
2258
- // The `start` function is now complete — every payload-free variant singleton
2259
- // (above) and every trampoline closure (above) has appended its own init code.
2260
- // It declares no locals of its own, but a function body's raw bytes must still
2261
- // start with the (empty) locals-declaration vector — a single `0x00` — before
2262
- // the instruction stream, exactly like every other compiled function body below.
2263
2466
  Instruction::End.encode(&mut start_body);
2264
2467
  let mut final_start_body = vec![0u8];
2265
2468
  final_start_body.extend(start_body);
2266
2469
  let start_slot = (start_func_idx - module.func_import_count) as usize;
2267
2470
  module.functions[start_slot].1 = final_start_body;
2268
2471
 
2269
- // Runtime helpers backing string interpolation (`"{expr}"`): always registered
2270
- // (unconditionally, for simplicity) since they're cheap and self-contained.
2271
2472
  let string_concat_func = registerStringConcatHelper(&mut module, gc_types.byte_array_type_idx);
2272
2473
  let string_eq_func = registerStringEqHelper(&mut module, gc_types.byte_array_type_idx);
2273
2474
  let dyn_eq_func = registerDynEqHelper(&mut module, &gc_types, string_eq_func);
@@ -2276,6 +2477,10 @@ fn compileSourceInner(
2276
2477
  let ctx = CompileCtx {
2277
2478
  func_ids,
2278
2479
  func_sigs,
2480
+ inst_funcs,
2481
+ call_inst,
2482
+ known_fns,
2483
+ eg,
2279
2484
  methods,
2280
2485
  enum_variants,
2281
2486
  enum_params,
@@ -2284,7 +2489,6 @@ fn compileSourceInner(
2284
2489
  closures,
2285
2490
  closure_asts,
2286
2491
  closure_call_types,
2287
- types,
2288
2492
  named_fn_values,
2289
2493
  string_concat_func,
2290
2494
  string_eq_func,
@@ -2298,35 +2502,45 @@ fn compileSourceInner(
2298
2502
  passive_segments: Vec::new(),
2299
2503
  };
2300
2504
 
2301
- let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
2505
+ let mut compiled_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
2506
+ for (ui, u) in units.iter().enumerate() {
2507
+ let f = u.f;
2508
+ let uref = UnitRef {
2509
+ idx: ui,
2510
+ inst: u.inst,
2302
- for f in &fns {
2511
+ subst: &u.subst,
2512
+ types: unit_types(u),
2513
+ };
2303
- let body = match compileIntrinsicFnBody(f) {
2514
+ let body = match compileIntrinsicFnBody(f, &u.subst) {
2304
2515
  Some(body) => body,
2305
- None => compileFnBody(f, &ctx, &mut state).map_err(|e| {
2516
+ None => compileFnBody(f, &ctx, &mut state, &uref).map_err(|e| format!("{e} (in '{}')", u.name))?,
2306
- format!("{e} (in '{}{}')", f.type_param.as_ref().map(|r| format!("{r}.")).unwrap_or_default(), f.name)
2307
- })?,
2517
+ };
2518
+ let idx = match u.inst {
2519
+ Some(id) => ctx.inst_funcs[&id].0,
2520
+ None => ctx.func_ids[&fnKey(f)],
2308
2521
  };
2309
- compiled_bodies.push((fnKey(f), body));
2522
+ compiled_bodies.push((idx, body));
2310
2523
  }
2311
-
2312
- // Patch compiled bodies back into their pre-registered function slots.
2313
- for (name, body) in &compiled_bodies {
2524
+ for (idx, body) in &compiled_bodies {
2314
- let idx = *ctx
2315
- .func_ids
2316
- .get(name)
2317
- .expect("function was registered in the first pass");
2318
- let slot = (idx - module.func_import_count) as usize;
2525
+ let slot = (*idx - module.func_import_count) as usize;
2319
2526
  module.functions[slot].1 = body.clone();
2320
2527
  }
2321
2528
 
2322
2529
  // Compile each closure literal's own body into its reserved function slot.
2323
2530
  let mut closure_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
2324
- for (ptr, info) in &ctx.closures {
2531
+ for ((ui, ptr), info) in &ctx.closures {
2325
2532
  let cl = ctx
2326
2533
  .closure_asts
2327
- .get(ptr)
2534
+ .get(&(*ui, *ptr))
2328
2535
  .expect("every registered closure has its AST recorded");
2536
+ let u = &units[*ui];
2537
+ let uref = UnitRef {
2538
+ idx: *ui,
2539
+ inst: u.inst,
2540
+ subst: &u.subst,
2541
+ types: unit_types(u),
2542
+ };
2329
- let body = compileClosureBody(cl, info, &ctx, &mut state)?;
2543
+ let body = compileClosureBody(cl, info, &ctx, &mut state, &uref)?;
2330
2544
  closure_bodies.push((info.func_idx, body));
2331
2545
  }
2332
2546
  for (func_idx, body) in &closure_bodies {
@@ -2334,10 +2548,6 @@ fn compileSourceInner(
2334
2548
  module.functions[slot].1 = body.clone();
2335
2549
  }
2336
2550
 
2337
- // Flush every staged string literal into the module in the SAME order they were
2338
- // staged — `compileStaticString` already baked each one's index (its position in
2339
- // `state.passive_segments` at staging time) into an `array.new_data` instruction,
2340
- // so that order must be preserved exactly for those indices to still be correct.
2341
2551
  for bytes in &state.passive_segments {
2342
2552
  module.addPassiveDataSegment(bytes);
2343
2553
  }
@@ -2360,10 +2570,31 @@ fn compileSourceInner(
2360
2570
  .filter(|k| k.as_str() != CRASH_FN_KEY)
2361
2571
  .cloned()
2362
2572
  .collect();
2573
+ fn_keys.extend(units.iter().filter(|u| u.inst.is_some()).map(|u| u.name.clone()));
2363
2574
  fn_keys.sort();
2364
2575
  Ok((module.finish(), fn_keys))
2365
2576
  }
2366
2577
 
2578
+ /// `(param value types, result value type)` of a closure of Plum type `fn(ps) -> r`.
2579
+ fn closureSigKey(ps: &[PlumType], r: &PlumType, g: &GcTypeRegistry) -> ClosureSigKey {
2580
+ (
2581
+ ps.iter().map(|p| plumTypeToGcValtype(p, g)).collect(),
2582
+ if matches!(r, PlumType::TUnit) { None } else { Some(plumTypeToGcValtype(r, g)) },
2583
+ )
2584
+ }
2585
+
2586
+ fn getClosureCallType(module: &mut WasmModule, m: &mut HashMap<ClosureSigKey, u32>, key: &ClosureSigKey) -> u32 {
2587
+ if let Some(&t) = m.get(key) {
2588
+ return t;
2589
+ }
2590
+ let mut params = vec![ValType::Ref(RefType::ANYREF)];
2591
+ params.extend(key.0.iter().copied());
2592
+ let results: Vec<ValType> = key.1.into_iter().collect();
2593
+ let t = module.addType(&params, &results);
2594
+ m.insert(key.clone(), t);
2595
+ t
2596
+ }
2597
+
2367
2598
  /// `(payload) -> ref $Box*`: allocates a box with its header typeId set.
2368
2599
  fn registerBoxHelper(module: &mut WasmModule, vt: ValType, type_id: i32, box_idx: u32) -> u32 {
2369
2600
  let mut b = vec![0u8];
@@ -2956,10 +3187,10 @@ fn registerIntToStringHelper(module: &mut WasmModule, byte_array_type_idx: u32)
2956
3187
  /// registers their real signature and validates call sites), then intercepted
2957
3188
  /// here — by `fnKey` — instead of compiling their `todo` body to `unreachable`.
2958
3189
  /// Returns `None` for any other function, meaning "compile it normally."
2959
- fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
3190
+ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2960
3191
  if let Some(recv) = f.type_param.as_deref() {
2961
3192
  if isArraySpecialization(recv) {
2962
- return compileArrayIntrinsicFnBody(f);
3193
+ return compileArrayIntrinsicFnBody(f, subst);
2963
3194
  }
2964
3195
  }
2965
3196
  let byte_array_type_idx = withGcTypes(|r| r.byte_array_type_idx);
@@ -3129,8 +3360,10 @@ fn unwrapStrToBytes(body: &mut Vec<u8>) {
3129
3360
  /// `ref.cast` back from the raw stored `anyref` down to `T`'s own concrete
3130
3361
  /// type; `set`/`init` need no such cast the other way, since any concrete ref
3131
3362
  /// is already a valid subtype wherever `anyref` is expected.
3132
- fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
3363
+ fn compileArrayIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
3364
+ let elem = subst.get("T")?.clone();
3365
+ let array_ty = PlumType::TApp("Array".to_string(), vec![elem]);
3133
- let array_type_idx = withGcTypes(|r| r.array_type_idx);
3366
+ let array_type_idx = withGcTypes(|r| r.superIdx(&array_ty))?;
3134
3367
  match f.name.as_str() {
3135
3368
  // init() -> Array[T] — always starts empty; see `push` for how a
3136
3369
  // specific length is actually built up.
@@ -3244,15 +3477,6 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
3244
3477
  Instruction::LocalGet(1).encode(&mut body); // i
3245
3478
  Instruction::I32WrapI64.encode(&mut body);
3246
3479
  Instruction::ArrayGet(array_type_idx).encode(&mut body);
3247
- // Narrow the raw `anyref` element back to `T`'s own concrete type —
3248
- // skipped when `T` itself resolves to a bare `anyref` (nothing to cast to).
3249
- if let Some(ValType::Ref(RefType {
3250
- heap_type: HeapType::Concrete(idx),
3251
- ..
3252
- })) = f.returns.as_ref().and_then(|t| astTypeToWasm(&t.name))
3253
- {
3254
- Instruction::RefCastNullable(HeapType::Concrete(idx)).encode(&mut body);
3255
- }
3256
3480
  Instruction::End.encode(&mut body);
3257
3481
  Some(body)
3258
3482
  }
@@ -3280,6 +3504,15 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
3280
3504
 
3281
3505
  /// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`;
3282
3506
  /// a single-letter generic name is a `TParam`.
3507
+ /// The enum type (`TNamed`/`TApp`) a variant-constructing expression produces.
3508
+ fn enumTypeOfExpr(expr: &ast::Expr, info: &EnumVariantInfo, ctx: &LocalCtx) -> PlumType {
3509
+ let t = inferLocalType(expr, ctx);
3510
+ match &t {
3511
+ PlumType::TNamed(n) | PlumType::TApp(n, _) if *n == info.enum_name => t,
3512
+ _ => PlumType::TNamed(info.enum_name.clone()),
3513
+ }
3514
+ }
3515
+
3283
3516
  fn paramPlumType(pt: &ast::ParamType) -> PlumType {
3284
3517
  match pt {
3285
3518
  ast::ParamType::Type(t) => plum_checker::plumTypeFromAstG(t),
@@ -3307,6 +3540,22 @@ struct RawClosure<'a> {
3307
3540
  free_vars: Vec<(String, PlumType)>,
3308
3541
  }
3309
3542
 
3543
+ /// The `fn(...)` type a bare reference to top-level fn `name` is used at.
3544
+ fn namedRefType(
3545
+ name: &str,
3546
+ expr: &ast::Expr,
3547
+ types: &HashMap<usize, PlumType>,
3548
+ sigs: &HashMap<String, FuncSig>,
3549
+ ) -> PlumType {
3550
+ match types.get(&(expr as *const ast::Expr as usize)) {
3551
+ Some(t @ PlumType::TFun(..)) if !hasFreeTy(t) => t.clone(),
3552
+ _ => match sigs.get(name) {
3553
+ Some(s) => PlumType::TFun(s.decl_params.clone(), Box::new(s.decl_ret.clone())),
3554
+ None => PlumType::TFun(vec![], Box::new(PlumType::TUnit)),
3555
+ },
3556
+ }
3557
+ }
3558
+
3310
3559
  fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
3311
3560
  match arg {
3312
3561
  ast::Arg::Positional(e) => e,
@@ -3322,6 +3571,7 @@ fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
3322
3571
  struct ClosureWalker<'a, 't> {
3323
3572
  types: &'t HashMap<usize, PlumType>,
3324
3573
  fn_decls: &'a HashMap<String, &'a ast::Fn>,
3574
+ func_sigs: &'t HashMap<String, FuncSig>,
3325
3575
  found: Vec<RawClosure<'a>>,
3326
3576
  /// Local names bound in the function currently being walked (params, assign
3327
3577
  /// targets, for-loop vars) — used to tell a local variable reference apart from a
@@ -3332,7 +3582,7 @@ struct ClosureWalker<'a, 't> {
3332
3582
  /// `Expr::FnCall` and never reaches here). Each one needs a zero-capture
3333
3583
  /// "trampoline" closure so it can be used wherever a `fn(...)`-typed value is
3334
3584
  /// expected.
3335
- named_fn_refs: HashSet<String>,
3585
+ named_fn_refs: HashSet<(String, PlumType)>,
3336
3586
  }
3337
3587
 
3338
3588
  impl<'a, 't> ClosureWalker<'a, 't> {
@@ -3456,7 +3706,8 @@ impl<'a, 't> ClosureWalker<'a, 't> {
3456
3706
  if !self.locals.contains(name) {
3457
3707
  if let Some(f) = self.fn_decls.get(name) {
3458
3708
  if f.type_param.is_none() {
3709
+ let ty = namedRefType(name, expr, self.types, self.func_sigs);
3459
- self.named_fn_refs.insert(name.clone());
3710
+ self.named_fn_refs.insert((name.clone(), ty));
3460
3711
  }
3461
3712
  }
3462
3713
  }
@@ -3765,7 +4016,7 @@ struct Collector<'a> {
3765
4016
  /// Slot number (the `u32` values in `nested_class_scratch`) -> the variant name
3766
4017
  /// it narrows to, so its scratch local can be declared with that variant's exact
3767
4018
  /// concrete ref type instead of a uniform placeholder type.
3768
- nested_class_scratch_types: Vec<String>,
4019
+ nested_class_scratch_types: Vec<(PlumType, String)>,
3769
4020
  next_nested_class_slot: u32,
3770
4021
  /// `For` stmt identity (pointer address) -> a slot number; each slot reserves 2
3771
4022
  /// consecutive `i32` scratch locals for variadic iteration (`for v in nums`):
@@ -3821,7 +4072,7 @@ impl<'a> Collector<'a> {
3821
4072
  let slot = self.next_nested_class_slot;
3822
4073
  self.next_nested_class_slot += 1;
3823
4074
  self.nested_class_scratch.insert(key, slot);
3824
- self.nested_class_scratch_types.push(name.clone());
4075
+ self.nested_class_scratch_types.push((ty.clone(), name.clone()));
3825
4076
  if let Some(info) = self.enum_variants.get(name) {
3826
4077
  // The payload types as instantiated by the matched value's type
3827
4078
  // arguments (`Ok(v)` against `Try[Int, E]` binds `v: Int`).
@@ -3953,11 +4204,10 @@ impl<'a> Collector<'a> {
3953
4204
  ast::Expr::Try(inner) => {
3954
4205
  let key = expr as *const ast::Expr as usize;
3955
4206
  let ty = self.ty(inner);
3956
- let name = plum_checker::nominalName(&ty).unwrap_or("").to_string();
3957
4207
  let slot = self.next_nested_class_slot;
3958
4208
  self.next_nested_class_slot += 1;
3959
4209
  self.nested_class_scratch.insert(key, slot);
3960
- self.nested_class_scratch_types.push(name);
4210
+ self.nested_class_scratch_types.push((ty, String::new()));
3961
4211
  self.walkExpr(inner);
3962
4212
  }
3963
4213
  ast::Expr::Unary(u) => self.walkExpr(&u.operand),
@@ -3974,11 +4224,10 @@ impl<'a> Collector<'a> {
3974
4224
  ast::Expr::Elvis(e) => {
3975
4225
  let key = expr as *const ast::Expr as usize;
3976
4226
  let ty = self.ty(&e.left);
3977
- let name = plum_checker::nominalName(&ty).unwrap_or("").to_string();
3978
4227
  let slot = self.next_nested_class_slot;
3979
4228
  self.next_nested_class_slot += 1;
3980
4229
  self.nested_class_scratch.insert(key, slot);
3981
- self.nested_class_scratch_types.push(name);
4230
+ self.nested_class_scratch_types.push((ty, String::new()));
3982
4231
  self.walkExpr(&e.left);
3983
4232
  self.walkExpr(&e.right);
3984
4233
  }
@@ -4029,26 +4278,26 @@ fn compileFnBody(
4029
4278
  f: &ast::Fn,
4030
4279
  ctx: &CompileCtx,
4031
4280
  state: &mut ModuleState,
4281
+ unit: &UnitRef,
4032
4282
  ) -> Result<Vec<u8>, String> {
4033
4283
  let mut body = Vec::new();
4034
4284
 
4035
4285
  // Fallback type env (only consulted for synthesized nodes the checker never
4036
- // saw): globals, `self`, and the params under their DECLARED (generic) types.
4286
+ // saw): globals, `self`, and the params under their (instantiated) declared types.
4037
4287
  let mut base_env = ctx.global_env.clone();
4038
4288
  let mut decl_types: HashMap<String, PlumType> = HashMap::new();
4289
+ let (self_decl, param_decls, ret_decl) = unitDeclTypes(f, unit.subst, &ctx.eg);
4039
- if let Some(recv) = &f.type_param {
4290
+ if let Some(self_ty) = self_decl {
4040
- let self_ty = plum_checker::plumTypeFromName(recv);
4041
4291
  base_env.insert("self".to_string(), TypeScheme::mono(self_ty.clone()));
4042
4292
  decl_types.insert("self".to_string(), self_ty);
4043
4293
  }
4044
- for p in &f.params {
4045
- let ty = paramPlumType(&p.ty);
4294
+ for (p, ty) in f.params.iter().zip(param_decls.iter()) {
4046
4295
  base_env.insert(p.name.clone(), TypeScheme::mono(ty.clone()));
4047
- decl_types.insert(p.name.clone(), ty);
4296
+ decl_types.insert(p.name.clone(), ty.clone());
4048
4297
  }
4049
4298
 
4050
4299
  let mut collector = Collector {
4051
- types: &ctx.types,
4300
+ types: unit.types,
4052
4301
  enum_variants: &ctx.enum_variants,
4053
4302
  named: Vec::new(),
4054
4303
  named_set: Default::default(),
@@ -4104,8 +4353,8 @@ fn compileFnBody(
4104
4353
  // placeholder) — `struct.get` on a constructor-pattern match requires the local
4105
4354
  // holding the narrowed (`ref.cast`) value to be statically typed as that exact
4106
4355
  // variant, and different slots very likely narrow to different variants.
4107
- for vname in &collector.nested_class_scratch_types {
4356
+ for (sty, vname) in &collector.nested_class_scratch_types {
4108
- let variant_idx = withGcTypes(|r| classOrVariantTypeIdx(r, vname));
4357
+ let variant_idx = withGcTypes(|r| scratchTypeIdx(r, sty, vname));
4109
4358
  groups.push(gcRef(variant_idx));
4110
4359
  idx += 1;
4111
4360
  }
@@ -4144,7 +4393,12 @@ fn compileFnBody(
4144
4393
  variadic_for_scratch: collector.variadic_for_scratch,
4145
4394
  func_ids: &ctx.func_ids,
4146
4395
  func_sigs: &ctx.func_sigs,
4396
+ inst_funcs: &ctx.inst_funcs,
4397
+ call_inst: &ctx.call_inst,
4398
+ known_fns: &ctx.known_fns,
4399
+ unit_idx: unit.idx,
4400
+ unit_inst: unit.inst,
4147
- types: &ctx.types,
4401
+ types: unit.types,
4148
4402
  local_decl_types: decl_types,
4149
4403
  closures: &ctx.closures,
4150
4404
  closure_call_types: &ctx.closure_call_types,
@@ -4162,12 +4416,16 @@ fn compileFnBody(
4162
4416
  type_env: RefCell::new(base_env),
4163
4417
  loop_extra_depth: Cell::new(0),
4164
4418
  in_closure: false,
4165
- ret_type: f.returns.as_ref().map(plum_checker::plumTypeFromAstG),
4419
+ ret_type: f.returns.as_ref().map(|_| ret_decl.clone()),
4166
4420
  match_subject_types: RefCell::new(HashMap::new()),
4167
4421
  subject_ptypes: RefCell::new(HashMap::new()),
4168
4422
  };
4169
4423
 
4424
+ let result_vt = if f.returns.is_none() || matches!(ret_decl, PlumType::TUnit) {
4425
+ None
4426
+ } else {
4170
- let result_vt = retTypeToWasm(f.returns.as_ref());
4427
+ Some(plumTypeToValtype(&ret_decl))
4428
+ };
4171
4429
 
4172
4430
  match &f.body {
4173
4431
  ast::FnBody::Expr(e) => {
@@ -4425,18 +4683,27 @@ fn compileStmt(
4425
4683
  field_name, class_name
4426
4684
  )
4427
4685
  })?;
4428
- let super_type_idx = *ctx
4686
+ let super_type_idx = ctx
4429
4687
  .gc_types
4430
- .enum_super_type_idx
4431
- .get(&class_name)
4688
+ .superIdx(&obj_ty)
4432
4689
  .ok_or_else(|| {
4433
4690
  format!(
4434
4691
  "codegen: enum '{}' missing from the GC type registry",
4435
4692
  class_name
4436
4693
  )
4437
4694
  })?;
4695
+ let sub_s: Subst = ctx
4696
+ .enum_variants
4697
+ .values()
4698
+ .find(|i| i.enum_name == class_name)
4699
+ .map(|i| match &obj_ty {
4700
+ PlumType::TApp(_, a) => i.generics.iter().cloned().zip(a.iter().cloned()).collect(),
4701
+ _ => Subst::new(),
4702
+ })
4703
+ .unwrap_or_default();
4704
+ let fty = plum_checker::types::substitute(&params[field_idx].1, &sub_s);
4438
4705
  compileExpr(object, body, ctx, state)?;
4439
- compileExprInto(value, &params[field_idx].1, body, ctx, state)?;
4706
+ compileExprInto(value, &fty, body, ctx, state)?;
4440
4707
  Instruction::StructSet {
4441
4708
  struct_type_index: super_type_idx,
4442
4709
  field_index: fld(field_idx),
@@ -4462,12 +4729,13 @@ fn compileStmt(
4462
4729
  .iter()
4463
4730
  .position(|n| n == field_name)
4464
4731
  .expect("just found by this field name");
4465
- let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
4732
+ let variant_type_idx = ctx.gc_types.variantIdx(&obj_ty, variant_name)
4466
- .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
4733
+ .ok_or_else(|| format!("codegen: variant '{}' of {} missing from the GC type registry", variant_name, obj_ty))?;
4734
+ let fty = plum_checker::variantFields(info, &obj_ty)[field_idx].clone();
4467
4735
  compileExpr(object, body, ctx, state)?;
4468
4736
  Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))
4469
4737
  .encode(body);
4470
- compileExprInto(value, &info.field_types[field_idx], body, ctx, state)?;
4738
+ compileExprInto(value, &fty, body, ctx, state)?;
4471
4739
  Instruction::StructSet {
4472
4740
  struct_type_index: variant_type_idx,
4473
4741
  field_index: fld(field_idx),
@@ -4484,23 +4752,9 @@ fn compileStmt(
4484
4752
  Some(rt) => compileExprInto(e, rt, body, ctx, state)?,
4485
4753
  None => compileExpr(e, body, ctx, state)?,
4486
4754
  }
4487
- if ctx.in_closure {
4488
- // Closure ABI: the result leaves as an `anyref`.
4489
- let rt_vt = ctx.ret_type.as_ref().map(plumTypeToValtype);
4490
- if let Some(vt) = rt_vt {
4491
- if let Some(rt @ PlumType::TBool) = ctx.ret_type.as_ref() {
4492
- coerceTy(rt, &anyTy(), body, ctx.gc_types);
4493
- } else {
4494
- coerceVt(vt, ValType::Ref(RefType::ANYREF), body, ctx.gc_types);
4495
- }
4496
- }
4497
- }
4498
4755
  Instruction::Return.encode(body);
4499
4756
  }
4500
4757
  ast::Stmt::Return(None) => {
4501
- if ctx.in_closure {
4502
- Instruction::RefNull(HeapType::ANY).encode(body);
4503
- }
4504
4758
  Instruction::Return.encode(body);
4505
4759
  }
4506
4760
  ast::Stmt::If(if_) => {
@@ -4686,13 +4940,15 @@ fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
4686
4940
  {
4687
4941
  return !closureCallReturnsUnit(&call.name, ctx);
4688
4942
  }
4689
- ctx.func_sigs
4690
- .get(&call.name)
4943
+ resolveCallee(ctx, expr, false, &call.name)
4691
- .map(|s| s.ret.is_some())
4944
+ .map(|(_, s)| s.ret.is_some())
4692
4945
  .unwrap_or(true)
4693
4946
  }
4694
4947
  ast::Expr::Attribute(attr) => match &attr.attr {
4695
4948
  ast::AttrKind::Method(call) => {
4949
+ if let Some((_, s)) = resolveCallee(ctx, expr, false, "") {
4950
+ return s.ret.is_some();
4951
+ }
4696
4952
  let obj_ty = inferLocalType(&attr.object, ctx);
4697
4953
  // A call on an erased or trait-typed receiver dispatches through a
4698
4954
  // trait method whose declared return type decides.
@@ -5318,10 +5574,16 @@ fn compileVariantEqArm(
5318
5574
  name
5319
5575
  ));
5320
5576
  }
5577
+ let sty = ctx
5578
+ .subject_ptypes
5579
+ .borrow()
5580
+ .get(&scratch_local)
5581
+ .cloned()
5582
+ .unwrap_or_else(|| PlumType::TVar("_".to_string()));
5321
- let variant_idx = *ctx.gc_types.variant_type_idx.get(name).ok_or_else(|| {
5583
+ let variant_idx = ctx.gc_types.variantIdx(&sty, name).ok_or_else(|| {
5322
5584
  format!(
5323
- "codegen: variant '{}' missing from the GC type registry",
5585
+ "codegen: variant '{}' of {} missing from the GC type registry",
5324
- name
5586
+ name, sty
5325
5587
  )
5326
5588
  })?;
5327
5589
  // A single ref.test against the variant's exact concrete type replaces the old
@@ -5391,18 +5653,21 @@ fn compileVariantConstructorArm(
5391
5653
  fields.len()
5392
5654
  ));
5393
5655
  }
5656
+ let sty = ctx
5657
+ .subject_ptypes
5658
+ .borrow()
5659
+ .get(&scratch_local)
5660
+ .cloned()
5661
+ .unwrap_or_else(|| PlumType::TVar("_".to_string()));
5394
- let variant_idx = *ctx.gc_types.variant_type_idx.get(name).ok_or_else(|| {
5662
+ let variant_idx = ctx.gc_types.variantIdx(&sty, name).ok_or_else(|| {
5395
5663
  format!(
5396
- "codegen: variant '{}' missing from the GC type registry",
5664
+ "codegen: variant '{}' of {} missing from the GC type registry",
5397
- name
5665
+ name, sty
5398
5666
  )
5399
5667
  })?;
5400
- let field_types = info.field_types.clone();
5401
5668
  // Payload types instantiated by the subject's type arguments.
5402
- let inst_types = match ctx.subject_ptypes.borrow().get(&scratch_local) {
5403
- Some(sty) => instFields(info, sty),
5669
+ let inst_types = instFields(info, &sty);
5404
- None => field_types.clone(),
5670
+ let field_types = inst_types.clone();
5405
- };
5406
5671
 
5407
5672
  // This pattern's own narrowly-typed scratch local (declared with variant's exact
5408
5673
  // concrete ref type — see `nested_class_scratch_types`), distinct from
@@ -5498,7 +5763,7 @@ fn compileFieldPatterns(
5498
5763
  let pat = &fields[fpos];
5499
5764
  // The field's DECLARED type fixes its stored representation (an erased `T` slot
5500
5765
  // is a `ref null $Obj`); its INSTANTIATED type is what a binding/pattern sees.
5501
- let decl_ty = &field_types[fpos];
5766
+ let decl_ty = &inst_types[fpos];
5502
5767
  let field_ty = &inst_types[fpos];
5503
5768
  let decl_vt = plumTypeToValtype(decl_ty);
5504
5769
  let inst_vt = plumTypeToValtype(field_ty);
@@ -5663,8 +5928,8 @@ fn compileFieldPatterns(
5663
5928
  n
5664
5929
  ));
5665
5930
  }
5666
- let variant_idx = *ctx.gc_types.variant_type_idx.get(n).ok_or_else(|| {
5931
+ let variant_idx = ctx.gc_types.variantIdx(field_ty, n).ok_or_else(|| {
5667
- format!("codegen: variant '{}' missing from the GC type registry", n)
5932
+ format!("codegen: variant '{}' of {} missing from the GC type registry", n, field_ty)
5668
5933
  })?;
5669
5934
  Instruction::LocalGet(container_local).encode(body);
5670
5935
  Instruction::StructGet {
@@ -5774,10 +6039,10 @@ fn compileFieldPatterns(
5774
6039
  inner_fields.len()
5775
6040
  ));
5776
6041
  }
5777
- let inner_variant_idx = *ctx.gc_types.variant_type_idx.get(name).ok_or_else(|| {
6042
+ let inner_variant_idx = ctx.gc_types.variantIdx(field_ty, name).ok_or_else(|| {
5778
6043
  format!(
5779
- "codegen: variant '{}' missing from the GC type registry",
6044
+ "codegen: variant '{}' of {} missing from the GC type registry",
5780
- name
6045
+ name, field_ty
5781
6046
  )
5782
6047
  })?;
5783
6048
  let key = pat as *const ast::CasePattern as usize;
@@ -5786,8 +6051,8 @@ fn compileFieldPatterns(
5786
6051
  .to_string()
5787
6052
  })?;
5788
6053
  let nested_local = ctx.nested_class_scratch_base + slot;
5789
- let inner_field_types = info.field_types.clone();
5790
6054
  let inner_inst_types = instFields(info, field_ty);
6055
+ let inner_field_types = inner_inst_types.clone();
5791
6056
 
5792
6057
  Instruction::LocalGet(container_local).encode(body);
5793
6058
  Instruction::StructGet {
@@ -5891,9 +6156,10 @@ fn compileExpr(
5891
6156
  // value (e.g. `each(double)`) — push its zero-capture trampoline
5892
6157
  // closure's (pre-built by the `start` function) global.
5893
6158
  None => {
6159
+ let fty = namedRefType(name, expr, ctx.types, ctx.func_sigs);
5894
6160
  let global_idx = *ctx
5895
6161
  .named_fn_values
5896
- .get(name.as_str())
6162
+ .get(&(name.clone(), fty))
5897
6163
  .ok_or_else(|| format!("undeclared variable '{}'", name))?;
5898
6164
  Instruction::GlobalGet(global_idx).encode(body);
5899
6165
  }
@@ -5935,34 +6201,16 @@ fn compileExpr(
5935
6201
  enum_name
5936
6202
  )
5937
6203
  })?;
5938
- let success_idx =
5939
- *ctx.gc_types
5940
- .variant_type_idx
5941
- .get(success_name)
6204
+ let success_idx = ctx.gc_types.variantIdx(&inner_ty, success_name).ok_or_else(|| {
5942
- .ok_or_else(|| {
5943
- format!(
5944
- "codegen: variant '{}' missing from the GC type registry",
6205
+ format!("codegen: variant '{}' of {} missing from the GC type registry", success_name, inner_ty)
5945
- success_name
5946
- )
5947
- })?;
6206
+ })?;
5948
- let failure_idx =
5949
- *ctx.gc_types
5950
- .variant_type_idx
5951
- .get(failure_name)
6207
+ let failure_idx = ctx.gc_types.variantIdx(&inner_ty, failure_name).ok_or_else(|| {
5952
- .ok_or_else(|| {
5953
- format!(
5954
- "codegen: variant '{}' missing from the GC type registry",
6208
+ format!("codegen: variant '{}' of {} missing from the GC type registry", failure_name, inner_ty)
5955
- failure_name
5956
- )
5957
- })?;
6209
+ })?;
5958
6210
  // The payload slot is declared at its generic type (`T` -> `$Obj`); the
5959
6211
  // value this expression yields is at its instantiated type.
5960
- let raw_success_ty = success_info
5961
- .field_types
5962
- .first()
5963
- .cloned()
5964
- .unwrap_or(PlumType::TUnit);
5965
6212
  let success_ty = inferLocalType(expr, ctx);
6213
+ let raw_success_ty = success_ty.clone();
5966
6214
  let success_vt = plumTypeToValtype(&success_ty);
5967
6215
 
5968
6216
  let key = expr as *const ast::Expr as usize;
@@ -5979,22 +6227,20 @@ fn compileExpr(
5979
6227
  // Failure branch: take the inner `Err` payload and build the OUTER
5980
6228
  // function's `Err` variant from it (the Ok types may differ).
5981
6229
  let outer_err = match ctx.ret_type.as_ref().and_then(plum_checker::nominalName) {
5982
- Some(outer_enum) if outer_enum != enum_name => {
5983
- plum_checker::tryOperatorVariants(outer_enum, &cctx)
6230
+ Some(outer_enum) => plum_checker::tryOperatorVariants(outer_enum, &cctx)
5984
- .map(|(_, _, fname, _)| fname)
6231
+ .map(|(_, _, fname, _)| fname),
5985
- }
5986
6232
  _ => None,
5987
6233
  };
5988
6234
  let mut rewrap: Option<(u32, i32)> = None;
5989
6235
  if let Some(outer_err_name) = outer_err {
6236
+ let outer_ty = ctx.ret_type.clone().unwrap_or(PlumType::TUnit);
5990
- let outer_idx = *ctx
6237
+ let outer_idx = ctx
5991
6238
  .gc_types
5992
- .variant_type_idx
5993
- .get(outer_err_name)
6239
+ .variantIdx(&outer_ty, outer_err_name)
5994
6240
  .ok_or_else(|| {
5995
6241
  format!(
5996
- "codegen: variant '{}' missing from the GC type registry",
6242
+ "codegen: variant '{}' of {} missing from the GC type registry",
5997
- outer_err_name
6243
+ outer_err_name, outer_ty
5998
6244
  )
5999
6245
  })?;
6000
6246
  if outer_idx != failure_idx {
@@ -6057,32 +6303,14 @@ fn compileExpr(
6057
6303
  enum_name
6058
6304
  )
6059
6305
  })?;
6060
- let success_idx =
6061
- *ctx.gc_types
6062
- .variant_type_idx
6063
- .get(success_name)
6306
+ let success_idx = ctx.gc_types.variantIdx(&left_ty, success_name).ok_or_else(|| {
6064
- .ok_or_else(|| {
6065
- format!(
6066
- "codegen: variant '{}' missing from the GC type registry",
6307
+ format!("codegen: variant '{}' of {} missing from the GC type registry", success_name, left_ty)
6067
- success_name
6068
- )
6069
- })?;
6308
+ })?;
6070
- let failure_idx =
6071
- *ctx.gc_types
6072
- .variant_type_idx
6073
- .get(failure_name)
6309
+ let failure_idx = ctx.gc_types.variantIdx(&left_ty, failure_name).ok_or_else(|| {
6074
- .ok_or_else(|| {
6075
- format!(
6076
- "codegen: variant '{}' missing from the GC type registry",
6310
+ format!("codegen: variant '{}' of {} missing from the GC type registry", failure_name, left_ty)
6077
- failure_name
6078
- )
6079
- })?;
6311
+ })?;
6080
- let raw_success_ty = success_info
6081
- .field_types
6082
- .first()
6083
- .cloned()
6084
- .unwrap_or(PlumType::TUnit);
6085
6312
  let success_ty = inferLocalType(expr, ctx);
6313
+ let raw_success_ty = success_ty.clone();
6086
6314
  let success_vt = plumTypeToValtype(&success_ty);
6087
6315
 
6088
6316
  let key = expr as *const ast::Expr as usize;
@@ -6235,6 +6463,17 @@ fn compileExpr(
6235
6463
  }
6236
6464
  // `TVar`/`TUnit` share `Int`'s `i64` wasm representation (see
6237
6465
  // `plumTypeToValtype`) — an unresolved generic defaults the same way.
6466
+ PlumType::TByte => {
6467
+ match c.op {
6468
+ ast::CmpOp::Lt => Instruction::I32LtU,
6469
+ ast::CmpOp::Lte => Instruction::I32LeU,
6470
+ ast::CmpOp::Eq => Instruction::I32Eq,
6471
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I32Ne,
6472
+ ast::CmpOp::Gte => Instruction::I32GeU,
6473
+ ast::CmpOp::Gt => Instruction::I32GtU,
6474
+ }
6475
+ .encode(body);
6476
+ }
6238
6477
  PlumType::TInt | PlumType::TVar(_) | PlumType::TUnit => {
6239
6478
  match c.op {
6240
6479
  ast::CmpOp::Lt => Instruction::I64LtS,
@@ -6336,7 +6575,7 @@ fn compileExpr(
6336
6575
  .is_some_and(|info| info.field_types == [ty.clone()])
6337
6576
  });
6338
6577
  if (call.name == "Int" || call.name == "Float" || call.name == "Byte")
6339
- && !ctx.func_ids.contains_key(&call.name)
6578
+ && !ctx.known_fns.contains(&call.name)
6340
6579
  && !is_same_type_variant_wrap
6341
6580
  && single_arg_ty.is_some()
6342
6581
  {
@@ -6362,7 +6601,7 @@ fn compileExpr(
6362
6601
  compileClosureCall(call, expr, body, ctx, state)?;
6363
6602
  // `Bad(x)` / `Bad()` — an uppercase name the checker typed as a tag
6364
6603
  // union is a tag literal, even when a declared variant shares its name.
6365
- } else if !ctx.func_ids.contains_key(&call.name)
6604
+ } else if !ctx.known_fns.contains(&call.name)
6366
6605
  && call.args.len() <= 1
6367
6606
  && call
6368
6607
  .name
@@ -6404,6 +6643,7 @@ fn compileExpr(
6404
6643
  &init_call,
6405
6644
  &PlumType::TNamed(call.name.clone()),
6406
6645
  expr,
6646
+ false,
6407
6647
  body,
6408
6648
  ctx,
6409
6649
  state,
@@ -6411,16 +6651,10 @@ fn compileExpr(
6411
6651
  } else if let Some(info) = ctx.enum_variants.get(&call.name) {
6412
6652
  compileVariantConstruction(info, call, expr, body, ctx, state)?;
6413
6653
  } else {
6414
- let sig = ctx
6415
- .func_sigs
6416
- .get(&call.name)
6654
+ let (func_idx, sig) = resolveCallee(ctx, expr, false, &call.name)
6417
6655
  .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6418
6656
  compileCallArgs(&call.name, &call.args, sig, body, ctx, state)?;
6419
- let func_idx = ctx
6420
- .func_ids
6421
- .get(&call.name)
6422
- .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6423
- Instruction::Call(*func_idx).encode(body);
6657
+ Instruction::Call(func_idx).encode(body);
6424
6658
  coerceCallResult(&sig.decl_ret, expr, body, ctx);
6425
6659
  }
6426
6660
  }
@@ -6443,7 +6677,8 @@ fn compileExpr(
6443
6677
  compileTagLiteral(n, None, body, ctx, state)?
6444
6678
  }
6445
6679
  Some(info) if info.field_types.is_empty() => {
6680
+ let ety = enumTypeOfExpr(expr, info, ctx);
6446
- let global_idx = *ctx.singleton_globals.get(n)
6681
+ let global_idx = *ctx.singleton_globals.get(&(ety, n.clone()))
6447
6682
  .unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' has no singleton global", n));
6448
6683
  Instruction::GlobalGet(global_idx).encode(body);
6449
6684
  }
@@ -6511,6 +6746,7 @@ fn compileExpr(
6511
6746
  &init_call,
6512
6747
  &PlumType::TNamed(call.type_name.clone()),
6513
6748
  expr,
6749
+ false,
6514
6750
  body,
6515
6751
  ctx,
6516
6752
  state,
@@ -6535,21 +6771,22 @@ fn compileExpr(
6535
6771
  .enum_variants
6536
6772
  .get(&call.type_name)
6537
6773
  .expect("just checked is_some");
6774
+ let ety = enumTypeOfExpr(expr, info, ctx);
6775
+ let field_tys = plum_checker::variantFields(info, &ety);
6538
- let variant_idx = *ctx
6776
+ let variant_idx = ctx
6539
6777
  .gc_types
6540
- .variant_type_idx
6541
- .get(&call.type_name)
6778
+ .variantIdx(&ety, &call.type_name)
6542
6779
  .ok_or_else(|| {
6543
6780
  format!(
6544
- "codegen: variant '{}' missing from the GC type registry",
6781
+ "codegen: variant '{}' of {} missing from the GC type registry",
6545
- call.type_name
6782
+ call.type_name, ety
6546
6783
  )
6547
6784
  })?;
6548
6785
  let field_names = info.field_names.clone();
6549
6786
  Instruction::I32Const(typeIdOf(ctx.gc_types, &call.type_name)).encode(body);
6550
6787
  for (field_idx, field_name) in field_names.iter().enumerate() {
6551
6788
  match call.fields.iter().find(|fa| &fa.name == field_name) {
6552
- Some(fa) => compileExprInto(&fa.value, &info.field_types[field_idx], body, ctx, state)?,
6789
+ Some(fa) => compileExprInto(&fa.value, &field_tys[field_idx], body, ctx, state)?,
6553
6790
  // `Circle(..c, radius: 10)` — a field not overridden is read
6554
6791
  // straight off the spread source instead (checker only proved
6555
6792
  // `c` is SOME value of the owning enum, restricted to a bare
@@ -6595,7 +6832,7 @@ fn compileExpr(
6595
6832
  compileFieldRead(&attr.object, field_name, &obj_ty, expr, body, ctx, state)?
6596
6833
  }
6597
6834
  ast::AttrKind::Method(call) => {
6598
- compileMethodCall(&attr.object, call, &obj_ty, expr, body, ctx, state)?
6835
+ compileMethodCall(&attr.object, call, &obj_ty, expr, false, body, ctx, state)?
6599
6836
  }
6600
6837
  }
6601
6838
  }
@@ -6644,6 +6881,24 @@ fn coerceCallResultTo(decl_ret: &PlumType, want: &PlumType, body: &mut Vec<u8>,
6644
6881
  /// argument converted to its DECLARED param type (boxing into erased `T` slots,
6645
6882
  /// wrapping a bare `Int`/`Float` for a `Number` param), a trailing `...T` pack
6646
6883
  /// built into one GC array, and the declared defaults for omitted trailing params.
6884
+ /// The wasm function (and signature) a call expression invokes: its checked
6885
+ /// instantiation if the callee is generic, else the plain function `plain_key`.
6886
+ fn resolveCallee<'c>(
6887
+ ctx: &'c LocalCtx,
6888
+ call_expr: &ast::Expr,
6889
+ implicit: bool,
6890
+ plain_key: &str,
6891
+ ) -> Option<(u32, &'c FuncSig)> {
6892
+ let ptr = call_expr as *const ast::Expr as usize;
6893
+ if let Some(id) = ctx.call_inst.get(&(ctx.unit_inst, ptr, implicit)) {
6894
+ return ctx.inst_funcs.get(id).map(|(f, s)| (*f, s));
6895
+ }
6896
+ match (ctx.func_ids.get(plain_key), ctx.func_sigs.get(plain_key)) {
6897
+ (Some(f), Some(s)) => Some((*f, s)),
6898
+ _ => None,
6899
+ }
6900
+ }
6901
+
6647
6902
  fn compileCallArgs(
6648
6903
  func_key: &str,
6649
6904
  args: &[ast::Arg],
@@ -6682,7 +6937,7 @@ fn compileCallArgs(
6682
6937
  None => compileExpr(argExprOf(arg), body, ctx, state)?,
6683
6938
  }
6684
6939
  }
6685
- compileDefaultArgs(func_key, args.len(), body, ctx, state)?;
6940
+ compileDefaultArgs(func_key, sig, args.len(), body, ctx, state)?;
6686
6941
  }
6687
6942
  }
6688
6943
  Ok(())
@@ -6714,6 +6969,10 @@ fn compileFieldRead(
6714
6969
  }
6715
6970
  };
6716
6971
  let want = &inferLocalType(expr, ctx);
6972
+ let ety = match obj_ty {
6973
+ PlumType::TStr => PlumType::TNamed("Str".to_string()),
6974
+ t => t.clone(),
6975
+ };
6717
6976
  // A discriminant enum's shared params are declared directly on the enum's
6718
6977
  // SUPERTYPE (see `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to
6719
6978
  // any one variant needed.
@@ -6722,20 +6981,15 @@ fn compileFieldRead(
6722
6981
  .iter()
6723
6982
  .position(|(n, _)| n == field_name)
6724
6983
  .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
6725
- let super_type_idx = *ctx
6984
+ let super_type_idx = ctx.gc_types.superIdx(&ety).ok_or_else(|| {
6726
- .gc_types
6727
- .enum_super_type_idx
6728
- .get(&class_name)
6729
- .ok_or_else(|| {
6730
- format!("codegen: enum '{}' missing from the GC type registry", class_name)
6985
+ format!("codegen: enum '{}' missing from the GC type registry", class_name)
6731
- })?;
6986
+ })?;
6732
6987
  compileExpr(object, body, ctx, state)?;
6733
6988
  Instruction::StructGet {
6734
6989
  struct_type_index: super_type_idx,
6735
6990
  field_index: fld(field_idx),
6736
6991
  }
6737
6992
  .encode(body);
6738
- coerceTy(&params[field_idx].1, want, body, ctx.gc_types);
6739
6993
  return Ok(());
6740
6994
  }
6741
6995
  // Not a discriminant enum: `class_name` is an ordinary enum, and the checker
@@ -6755,16 +7009,16 @@ fn compileFieldRead(
6755
7009
  .iter()
6756
7010
  .position(|n| n == field_name)
6757
7011
  .expect("just found by this field name");
6758
- let variant_type_idx = *ctx
7012
+ let variant_type_idx = ctx
6759
7013
  .gc_types
6760
- .variant_type_idx
6761
- .get(variant_name)
7014
+ .variantIdx(&ety, variant_name)
6762
7015
  .ok_or_else(|| {
6763
7016
  format!(
6764
- "codegen: variant '{}' missing from the GC type registry",
7017
+ "codegen: variant '{}' of {} missing from the GC type registry",
6765
- variant_name
7018
+ variant_name, ety
6766
7019
  )
6767
7020
  })?;
7021
+ let _ = (info, want);
6768
7022
  compileExpr(object, body, ctx, state)?;
6769
7023
  Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
6770
7024
  Instruction::StructGet {
@@ -6772,7 +7026,6 @@ fn compileFieldRead(
6772
7026
  field_index: fld(field_idx),
6773
7027
  }
6774
7028
  .encode(body);
6775
- coerceTy(&info.field_types[field_idx], want, body, ctx.gc_types);
6776
7029
  Ok(())
6777
7030
  }
6778
7031
 
@@ -6824,10 +7077,29 @@ fn compileMethodCall(
6824
7077
  call: &ast::FnCall,
6825
7078
  obj_ty: &PlumType,
6826
7079
  result_expr: &ast::Expr,
7080
+ implicit: bool,
6827
7081
  body: &mut Vec<u8>,
6828
7082
  ctx: &LocalCtx,
6829
7083
  state: &mut ModuleState,
6830
7084
  ) -> Result<(), String> {
7085
+ // A call the checker resolved to a generic instantiation.
7086
+ if let Some((func_idx, sig)) = ctx
7087
+ .call_inst
7088
+ .get(&(ctx.unit_inst, result_expr as *const ast::Expr as usize, implicit))
7089
+ .and_then(|id| ctx.inst_funcs.get(id))
7090
+ .map(|(f, s)| (*f, s))
7091
+ {
7092
+ let recv = plum_checker::methodReceiverName(obj_ty).unwrap_or_default();
7093
+ if matches!(object, ast::Expr::TypeName(n) if *n == recv) {
7094
+ pushSelfPlaceholder(sig.params[0], body);
7095
+ } else {
7096
+ compileExpr(object, body, ctx, state)?;
7097
+ }
7098
+ compileCallArgs(&call.name, &call.args, sig, body, ctx, state)?;
7099
+ Instruction::Call(func_idx).encode(body);
7100
+ coerceCallResult(&sig.decl_ret, result_expr, body, ctx);
7101
+ return Ok(());
7102
+ }
6831
7103
  // A method on an erased generic parameter (`K: Hashable` -> `key.hash()`)
6832
7104
  // dispatches dynamically through the declaring trait's `$dyn$` dispatcher.
6833
7105
  if matches!(obj_ty, PlumType::TParam(_)) {
@@ -6892,7 +7164,12 @@ fn compileMethodCall(
6892
7164
  // the right type must still be pushed — the body simply never reads it.
6893
7165
  let is_static_call = matches!(object, ast::Expr::TypeName(n) if *n == class_name);
6894
7166
  if is_static_call {
6895
- let vt = astTypeToWasm(&dispatch_name).unwrap_or(ValType::I32);
7167
+ let _ = &dispatch_name;
7168
+ let vt = ctx
7169
+ .func_sigs
7170
+ .get(&key)
7171
+ .and_then(|s| s.params.first().copied())
7172
+ .unwrap_or(ValType::I32);
6896
7173
  pushSelfPlaceholder(vt, body);
6897
7174
  } else {
6898
7175
  if let Some(variant) = &box_variant {
@@ -6971,14 +7248,12 @@ fn compileDynCall(
6971
7248
  /// same as before this feature existed.
6972
7249
  fn compileDefaultArgs(
6973
7250
  func_key: &str,
7251
+ sig: &FuncSig,
6974
7252
  supplied_count: usize,
6975
7253
  body: &mut Vec<u8>,
6976
7254
  ctx: &LocalCtx,
6977
7255
  state: &mut ModuleState,
6978
7256
  ) -> Result<(), String> {
6979
- let Some(sig) = ctx.func_sigs.get(func_key) else {
6980
- return Ok(());
6981
- };
6982
7257
  for (i, default) in sig.param_defaults.iter().enumerate().skip(supplied_count) {
6983
7258
  let default_expr = default.as_ref().ok_or_else(|| {
6984
7259
  format!(
@@ -6997,11 +7272,13 @@ fn compileDefaultArgs(
6997
7272
  fn compileVariantConstruction(
6998
7273
  info: &EnumVariantInfo,
6999
7274
  call: &ast::FnCall,
7000
- _expr: &ast::Expr,
7275
+ expr: &ast::Expr,
7001
7276
  body: &mut Vec<u8>,
7002
7277
  ctx: &LocalCtx,
7003
7278
  state: &mut ModuleState,
7004
7279
  ) -> Result<(), String> {
7280
+ let ety = enumTypeOfExpr(expr, info, ctx);
7281
+ let field_tys = plum_checker::variantFields(info, &ety);
7005
7282
  if call.args.len() != info.field_types.len() {
7006
7283
  return Err(format!(
7007
7284
  "codegen: variant '{}' expects {} arg(s), got {}",
@@ -7010,20 +7287,19 @@ fn compileVariantConstruction(
7010
7287
  call.args.len()
7011
7288
  ));
7012
7289
  }
7013
- let variant_type_idx = *ctx
7290
+ let variant_type_idx = ctx
7014
7291
  .gc_types
7015
- .variant_type_idx
7016
- .get(&call.name)
7292
+ .variantIdx(&ety, &call.name)
7017
7293
  .ok_or_else(|| {
7018
7294
  format!(
7019
- "codegen: variant '{}' missing from the GC type registry",
7295
+ "codegen: variant '{}' of {} missing from the GC type registry",
7020
- call.name
7296
+ call.name, ety
7021
7297
  )
7022
7298
  })?;
7023
7299
  if info.field_types.is_empty() {
7024
7300
  // Pre-allocated singleton (this migration plan's Decision 2) — not a fresh
7025
7301
  // struct.new per reference.
7026
- let global_idx = *ctx.singleton_globals.get(&call.name).ok_or_else(|| {
7302
+ let global_idx = *ctx.singleton_globals.get(&(ety.clone(), call.name.clone())).ok_or_else(|| {
7027
7303
  format!(
7028
7304
  "codegen: payload-free variant '{}' has no singleton global",
7029
7305
  call.name
@@ -7044,7 +7320,7 @@ fn compileVariantConstruction(
7044
7320
  ast::Arg::Keyword { value, .. } => value,
7045
7321
  ast::Arg::Pair { value, .. } => value,
7046
7322
  };
7047
- match info.field_types.get(i) {
7323
+ match field_tys.get(i) {
7048
7324
  Some(d) => compileExprInto(arg_expr, d, body, ctx, state)?,
7049
7325
  None => compileExpr(arg_expr, body, ctx, state)?,
7050
7326
  }
@@ -7155,7 +7431,7 @@ fn compileInterpolatedString(
7155
7431
  } else if plum_checker::nominalName(&other)
7156
7432
  .is_some_and(|n| ctx.func_ids.contains_key(&format!("{}::toStr", n)))
7157
7433
  {
7158
- compileMethodCall(expr, &to_str, &other, expr, body, ctx, state)?;
7434
+ compileMethodCall(expr, &to_str, &other, expr, true, body, ctx, state)?;
7159
7435
  } else {
7160
7436
  return Err(format!(
7161
7437
  "codegen: interpolating a value of type {} is not yet supported",
@@ -7184,7 +7460,7 @@ fn compileClosureLiteral(
7184
7460
  ctx: &LocalCtx,
7185
7461
  _state: &mut ModuleState,
7186
7462
  ) -> Result<(), String> {
7187
- let key = cl as *const ast::Closure as usize;
7463
+ let key = (ctx.unit_idx, cl as *const ast::Closure as usize);
7188
7464
  let info = ctx.closures.get(&key).ok_or_else(|| {
7189
7465
  "internal codegen error: closure literal was not discovered by the discovery pre-pass"
7190
7466
  .to_string()
@@ -7210,18 +7486,12 @@ fn compileClosureLiteral(
7210
7486
  Ok(())
7211
7487
  }
7212
7488
 
7213
- /// Compiles a call to a closure-typed local via `call_indirect`, using the erased
7489
+ /// Compiles a call to a closure-typed local via `call_indirect` with the typed
7214
- /// closure ABI (`ClosureSigKey`): every argument is converted to `anyref`
7490
+ /// closure ABI: push the env, each argument in its own representation, then the
7215
- /// (primitives boxed), the result is converted from `anyref` to the call's own
7216
- /// instantiated type (or dropped when the closure's declared result is `Unit`).
7217
- /// The local is statically `anyref` (closures share that placeholder type — see
7218
- /// `plumTypeToValtype`), so every read of its fields `ref.cast`s down to the shared
7219
- /// concrete closure struct type first. Stack order matches the closure function's
7220
- /// signature `(env, ...args)`: push the env, then each argument, then the table
7221
- /// index (the `call_indirect` operand).
7491
+ /// table index (the `call_indirect` operand).
7222
7492
  fn compileClosureCall(
7223
7493
  call: &ast::FnCall,
7224
- call_expr: &ast::Expr,
7494
+ _call_expr: &ast::Expr,
7225
7495
  body: &mut Vec<u8>,
7226
7496
  ctx: &LocalCtx,
7227
7497
  state: &mut ModuleState,
@@ -7231,16 +7501,17 @@ fn compileClosureCall(
7231
7501
  .get(&call.name)
7232
7502
  .ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
7233
7503
  let closure_type_idx = ctx.gc_types.closure_type_idx;
7504
+ let Some(PlumType::TFun(ps, r)) = ctx.local_decl_types.get(&call.name) else {
7505
+ return Err(format!("codegen: '{}' is not a function-typed local", call.name));
7506
+ };
7507
+ let key = closureSigKey(ps, r, ctx.gc_types);
7234
- let type_index = *ctx.closure_call_types.get(&call.args.len()).ok_or_else(|| {
7508
+ let type_index = *ctx.closure_call_types.get(&key).ok_or_else(|| {
7235
7509
  format!(
7236
- "internal codegen error: no call_indirect type for closure '{}' with {} args",
7510
+ "internal codegen error: no call_indirect type for closure '{}' of type fn{:?}",
7237
- call.name,
7511
+ call.name, key
7238
- call.args.len()
7239
7512
  )
7240
7513
  })?;
7241
- let any = ValType::Ref(RefType::ANYREF);
7242
7514
 
7243
- // env (closure struct field 1)
7244
7515
  Instruction::LocalGet(closure_local).encode(body);
7245
7516
  Instruction::RefCastNonNull(HeapType::Concrete(closure_type_idx)).encode(body);
7246
7517
  Instruction::StructGet {
@@ -7248,11 +7519,12 @@ fn compileClosureCall(
7248
7519
  field_index: 1,
7249
7520
  }
7250
7521
  .encode(body);
7251
- // real arguments, boxed to the erased ABI
7252
- for arg in &call.args {
7522
+ for (i, arg) in call.args.iter().enumerate() {
7523
+ match ps.get(i) {
7524
+ Some(p) => compileExprInto(argExprOf(arg), p, body, ctx, state)?,
7253
- compileExprAs(argExprOf(arg), Some(any), body, ctx, state)?;
7525
+ None => compileExpr(argExprOf(arg), body, ctx, state)?,
7526
+ }
7254
7527
  }
7255
- // table index (closure struct field 0) — the call_indirect operand
7256
7528
  Instruction::LocalGet(closure_local).encode(body);
7257
7529
  Instruction::RefCastNonNull(HeapType::Concrete(closure_type_idx)).encode(body);
7258
7530
  Instruction::StructGet {
@@ -7265,13 +7537,6 @@ fn compileClosureCall(
7265
7537
  table_index: 0,
7266
7538
  }
7267
7539
  .encode(body);
7268
- // The result: `Unit` closures produce a null anyref that is dropped here, so
7269
- // a Unit-typed call leaves nothing on the stack (see `exprHasResult`).
7270
- if closureCallReturnsUnit(&call.name, ctx) {
7271
- Instruction::Drop.encode(body);
7272
- } else {
7273
- coerceTy(&anyTy(), &inferLocalType(call_expr, ctx), body, ctx.gc_types);
7274
- }
7275
7540
  Ok(())
7276
7541
  }
7277
7542
 
@@ -7287,22 +7552,19 @@ fn closureCallReturnsUnit(name: &str, ctx: &LocalCtx) -> bool {
7287
7552
  }
7288
7553
 
7289
7554
  /// Compiles a closure literal's own body into a standalone wasm function with the
7290
- /// erased closure ABI `(env: anyref, a1: anyref, ..., an: anyref) -> anyref`
7555
+ /// typed closure ABI `(env: anyref, p1: T1, ..., pn: Tn) -> R` (see `ClosureSigKey`).
7291
- /// (see `ClosureSigKey`). Local 0 is the env pointer; locals `1..=n` are the raw
7292
- /// incoming params; typed locals follow — one per closure param (holding the
7556
+ /// Local 0 is the env; locals `1..=n` are the params in their own representation;
7293
- /// raw param unboxed/cast to its real Plum type by the prologue), one per
7294
- /// captured free variable (restored from the env struct), then the body's own
7557
+ /// captured free variables (restored from the env struct) and the body's own named
7295
- /// named locals and scratch slots. The tail value is boxed back to `anyref` (a
7296
- /// `Unit` body yields a null `anyref`).
7558
+ /// locals and scratch slots follow.
7297
7559
  fn compileClosureBody(
7298
7560
  cl: &ast::Closure,
7299
7561
  info: &ClosureInfo,
7300
7562
  ctx: &CompileCtx,
7301
7563
  state: &mut ModuleState,
7564
+ unit: &UnitRef,
7302
7565
  ) -> Result<Vec<u8>, String> {
7303
7566
  let mut body = Vec::new();
7304
7567
 
7305
- // Fallback type env (synthesized nodes only) + declared local types.
7306
7568
  let mut base_env = ctx.global_env.clone();
7307
7569
  let mut decl_types: HashMap<String, PlumType> = HashMap::new();
7308
7570
  for (name, ty) in &info.free_vars {
@@ -7315,7 +7577,7 @@ fn compileClosureBody(
7315
7577
  }
7316
7578
 
7317
7579
  let mut collector = Collector {
7318
- types: &ctx.types,
7580
+ types: unit.types,
7319
7581
  enum_variants: &ctx.enum_variants,
7320
7582
  named: Vec::new(),
7321
7583
  named_set: Default::default(),
@@ -7328,19 +7590,12 @@ fn compileClosureBody(
7328
7590
  };
7329
7591
  collector.walkBlock(&cl.body);
7330
7592
 
7331
- // ---- local index layout ----
7332
- // [env_ptr][raw params][typed params][free-var locals][named...][match][scratch]
7593
+ // ---- local index layout: [env][params][free vars][named...][match][scratch] ----
7333
7594
  let mut locals: HashMap<String, u32> = HashMap::new();
7334
7595
  let mut groups: Vec<ValType> = Vec::new();
7335
- let mut idx = 0u32;
7596
+ let mut idx = 1u32;
7336
-
7337
- idx += 1 + cl.params.len() as u32; // env + raw params (all in the signature)
7338
- let mut param_prologue: Vec<(u32, u32, PlumType)> = Vec::new(); // (raw, typed, ptype)
7339
- for (i, (name, pty)) in cl.params.iter().zip(info.param_ptypes.iter()).enumerate() {
7340
- let vt = plumTypeToValtype(pty);
7597
+ for name in cl.params.iter() {
7341
7598
  locals.insert(name.clone(), idx);
7342
- groups.push(vt);
7343
- param_prologue.push((i as u32 + 1, idx, pty.clone()));
7344
7599
  idx += 1;
7345
7600
  }
7346
7601
  for (name, ty) in &info.free_vars {
@@ -7369,8 +7624,8 @@ fn compileClosureBody(
7369
7624
  }
7370
7625
 
7371
7626
  let nested_class_scratch_base = idx;
7372
- for vname in &collector.nested_class_scratch_types {
7627
+ for (sty, vname) in &collector.nested_class_scratch_types {
7373
- let variant_idx = withGcTypes(|r| classOrVariantTypeIdx(r, vname));
7628
+ let variant_idx = withGcTypes(|r| scratchTypeIdx(r, sty, vname));
7374
7629
  groups.push(gcRef(variant_idx));
7375
7630
  idx += 1;
7376
7631
  }
@@ -7388,6 +7643,7 @@ fn compileClosureBody(
7388
7643
  groups.push(ValType::I32); // loop index
7389
7644
  idx += 2;
7390
7645
  }
7646
+ let _ = idx;
7391
7647
 
7392
7648
  if groups.is_empty() {
7393
7649
  body.push(0);
@@ -7399,17 +7655,10 @@ fn compileClosureBody(
7399
7655
  }
7400
7656
  }
7401
7657
 
7402
- // Prologue: convert each raw `anyref` param to its real representation.
7403
- for (raw, typed, pty) in &param_prologue {
7404
- Instruction::LocalGet(*raw).encode(&mut body);
7405
- coerceTy(&anyTy(), pty, &mut body, &ctx.gc_types);
7406
- Instruction::LocalSet(*typed).encode(&mut body);
7407
- }
7408
- // Restore each captured free variable from the env struct (local 0, statically
7658
+ // Restore each captured free variable from the env struct at entry.
7409
- // `anyref` — `ref.cast` down to THIS closure's own concrete env type) at entry.
7410
7659
  for (i, (name, _)) in info.free_vars.iter().enumerate() {
7411
7660
  let local_idx = *locals.get(name).expect("free var local was assigned above");
7412
- Instruction::LocalGet(0).encode(&mut body); // env pointer
7661
+ Instruction::LocalGet(0).encode(&mut body);
7413
7662
  Instruction::RefCastNonNull(HeapType::Concrete(info.env_type_idx)).encode(&mut body);
7414
7663
  Instruction::StructGet {
7415
7664
  struct_type_index: info.env_type_idx,
@@ -7429,7 +7678,12 @@ fn compileClosureBody(
7429
7678
  variadic_for_scratch: collector.variadic_for_scratch,
7430
7679
  func_ids: &ctx.func_ids,
7431
7680
  func_sigs: &ctx.func_sigs,
7681
+ inst_funcs: &ctx.inst_funcs,
7682
+ call_inst: &ctx.call_inst,
7683
+ known_fns: &ctx.known_fns,
7684
+ unit_idx: unit.idx,
7685
+ unit_inst: unit.inst,
7432
- types: &ctx.types,
7686
+ types: unit.types,
7433
7687
  local_decl_types: decl_types,
7434
7688
  closures: &ctx.closures,
7435
7689
  closure_call_types: &ctx.closure_call_types,
@@ -7454,11 +7708,9 @@ fn compileClosureBody(
7454
7708
 
7455
7709
  if matches!(info.ret_ptype, PlumType::TUnit) {
7456
7710
  compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, None)?;
7457
- Instruction::RefNull(HeapType::ANY).encode(&mut body);
7458
7711
  } else {
7459
7712
  let ret_vt = plumTypeToValtype(&info.ret_ptype);
7460
7713
  compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, Some(ret_vt))?;
7461
- coerceTy(&info.ret_ptype, &anyTy(), &mut body, &ctx.gc_types);
7462
7714
  }
7463
7715
 
7464
7716
  Instruction::End.encode(&mut body);
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1221,7 +1221,7 @@ fn countTypes(bytes: &[u8]) -> usize {
1221
1221
  }
1222
1222
 
1223
1223
  #[test]
1224
- fn specializations_share_gc_type() {
1224
+ fn distinct_instances_get_distinct_gc_types() {
1225
1225
  let prog = |second: &str| {
1226
1226
  format!(
1227
1227
  "{}\
@@ -1238,7 +1238,7 @@ fun main() -> Int = 1
1238
1238
  };
1239
1239
  let same = assertValid(&prog("[A]"));
1240
1240
  let wider = assertValid(&prog("[A, B]"));
1241
- assert_eq!(countTypes(&same), countTypes(&wider));
1241
+ assert!(countTypes(&same) < countTypes(&wider));
1242
1242
  }
1243
1243
 
1244
1244
  #[test]
@@ -1742,29 +1742,221 @@ fun main() -> Int =
1742
1742
  assert_eq!(runMerged::<i64>("trylist", src), 41);
1743
1743
  }
1744
1744
 
1745
+ /// Compiles `src` (merged with the real std) and returns every compiled function key/name.
1746
+ fn compiledKeys(name: &str, src: &str) -> (Vec<u8>, Vec<String>) {
1747
+ let dir = std::env::temp_dir().join(format!("plum_t3k_{}_{}", name, std::process::id()));
1745
- #[test]
1748
+ std::fs::create_dir_all(&dir).unwrap();
1749
+ let path = dir.join("main.plum");
1746
- fn one_copy_per_generic() {
1750
+ std::fs::write(&path, src).unwrap();
1751
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
1752
+ let r = plum_wasm_codegen::compileSourceWithFunctions(&source).expect("compile");
1753
+ let _ = std::fs::remove_dir_all(&dir);
1754
+ wasmparser::validate(&r.0).expect("valid wasm");
1755
+ r
1756
+ }
1757
+
1747
- let src = "\
1758
+ const STD_HEAD: &str = "\
1748
1759
  import std/Str
1749
1760
  import std/Number
1750
1761
  import std/Try
1751
1762
  import std/List
1763
+ ";
1752
1764
 
1765
+ #[test]
1766
+ fn one_copy_per_distinct_type() {
1767
+ let src = format!("{}\
1753
1768
  fun main() -> Int =
1754
1769
  a := List(1, 2).map(|n| n + 1)
1755
1770
  b := List(\"x\", \"y\").map(|s| s + \"!\")
1756
1771
  a.length() + b.length()
1757
- ";
1772
+ ", STD_HEAD);
1758
- let dir = std::env::temp_dir().join(format!("plum_t4_copy_{}", std::process::id()));
1759
- std::fs::create_dir_all(&dir).unwrap();
1760
- let path = dir.join("main.plum");
1773
+ let (_, keys) = compiledKeys("copy2", &src);
1761
- std::fs::write(&path, src).unwrap();
1762
- let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
1763
- let (_, keys) = plum_wasm_codegen::compileSourceWithFunctions(&source).expect("compile");
1764
- let _ = std::fs::remove_dir_all(&dir);
1765
- let maps: Vec<&String> = keys.iter().filter(|k| k.as_str() == "List::map").collect();
1774
+ let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("List[") && k.contains("].map")).collect();
1766
- assert_eq!(maps.len(), 1, "got {:?}", keys);
1775
+ assert_eq!(maps.len(), 2, "got {:?}", maps);
1776
+ assert!(keys.iter().any(|k| k.starts_with("List[Int].map")), "{:?}", maps);
1777
+ assert!(keys.iter().any(|k| k.starts_with("List[Str].map")), "{:?}", maps);
1767
1778
  assert!(!keys.iter().any(|k| k.contains('$')), "mangled names remain");
1779
+
1780
+ let src = format!("{}\
1781
+ fun main() -> Int =
1782
+ a := List(1, 2).map(|n| n + 1)
1783
+ b := List(3).map(|n| n * 2)
1784
+ a.length() + b.length()
1785
+ ", STD_HEAD);
1786
+ let (_, keys) = compiledKeys("copy1", &src);
1787
+ let maps: Vec<&String> = keys.iter().filter(|k| k.starts_with("List[") && k.contains("].map")).collect();
1788
+ assert_eq!(maps.len(), 1, "got {:?}", maps);
1789
+ }
1790
+
1791
+ #[test]
1792
+ fn unreachable_generic_not_emitted() {
1793
+ let src = format!("{}\
1794
+ fun neverUsed(x: T) -> T =
1795
+ x
1796
+
1797
+ fun main() -> Int =
1798
+ List(1, 2).length()
1799
+ ", STD_HEAD);
1800
+ let (_, keys) = compiledKeys("unreach", &src);
1801
+ assert!(!keys.iter().any(|k| k.contains("neverUsed")), "{:?}", keys);
1802
+ }
1803
+
1804
+ /// `(function name, body operators as debug strings)` for every function with a name.
1805
+ fn namedBodies(bytes: &[u8]) -> Vec<(String, Vec<String>)> {
1806
+ let mut names: std::collections::HashMap<u32, String> = Default::default();
1807
+ let mut bodies = Vec::new();
1808
+ let mut imports = 0u32;
1809
+ for payload in wasmparser::Parser::new(0).parse_all(bytes) {
1810
+ match payload.unwrap() {
1811
+ wasmparser::Payload::ImportSection(s) => {
1812
+ imports = s.count();
1813
+ }
1814
+ wasmparser::Payload::CustomSection(c) => {
1815
+ if let wasmparser::KnownCustom::Name(r) = c.as_known() {
1816
+ for n in r {
1817
+ if let wasmparser::Name::Function(m) = n.unwrap() {
1818
+ for e in m {
1819
+ let e = e.unwrap();
1820
+ names.insert(e.index, e.name.to_string());
1821
+ }
1822
+ }
1823
+ }
1824
+ }
1825
+ }
1826
+ wasmparser::Payload::CodeSectionEntry(b) => {
1827
+ let mut ops = Vec::new();
1828
+ let mut r = b.get_operators_reader().unwrap();
1829
+ while !r.eof() {
1830
+ ops.push(format!("{:?}", r.read().unwrap()));
1831
+ }
1832
+ bodies.push(ops);
1833
+ }
1834
+ _ => {}
1835
+ }
1836
+ }
1837
+ bodies
1838
+ .into_iter()
1839
+ .enumerate()
1840
+ .map(|(i, ops)| (names.get(&(imports + i as u32)).cloned().unwrap_or_default(), ops))
1841
+ .collect()
1842
+ }
1843
+
1844
+ #[test]
1845
+ fn no_boxes_in_list_int_map() {
1846
+ let src = format!("{}\
1847
+ fun main() -> Int =
1848
+ List(1, 2).map(|n| n + 1).length()
1849
+ ", STD_HEAD);
1850
+ let (bytes, _) = compiledKeys("nobox", &src);
1851
+ let bodies = namedBodies(&bytes);
1852
+ let (name, ops) = bodies
1853
+ .iter()
1854
+ .find(|(n, _)| n.starts_with("List[Int].map"))
1855
+ .expect("List[Int].map compiled");
1856
+ assert!(!ops.is_empty(), "{name}");
1857
+ let box_idx: Vec<u32> = {
1858
+ // function indices of the box helpers
1859
+ let mut v = Vec::new();
1860
+ let mut names: Vec<(u32, String)> = Vec::new();
1861
+ for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
1862
+ if let wasmparser::Payload::CustomSection(c) = payload.unwrap() {
1863
+ if let wasmparser::KnownCustom::Name(r) = c.as_known() {
1864
+ for n in r {
1865
+ if let wasmparser::Name::Function(m) = n.unwrap() {
1866
+ for e in m {
1867
+ let e = e.unwrap();
1868
+ names.push((e.index, e.name.to_string()));
1869
+ }
1870
+ }
1871
+ }
1872
+ }
1873
+ }
1874
+ }
1875
+ for (i, n) in names {
1876
+ if n.starts_with("box.") {
1877
+ v.push(i);
1878
+ }
1879
+ }
1880
+ v
1881
+ };
1882
+ assert!(!box_idx.is_empty());
1883
+ for op in ops {
1884
+ for b in &box_idx {
1885
+ assert_ne!(*op, format!("Call {{ function_index: {} }}", b), "{name} boxes: {ops:?}");
1886
+ }
1887
+ }
1888
+ }
1889
+
1890
+ #[test]
1891
+ fn array_int_native() {
1892
+ let src = format!("{}\
1893
+ fun main() -> Int =
1894
+ List(1, 2).length()
1895
+ ", STD_HEAD);
1896
+ let (bytes, _) = compiledKeys("arrint", &src);
1897
+ let mut found_i64 = false;
1898
+ let mut found_anyref = false;
1899
+ for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
1900
+ if let wasmparser::Payload::TypeSection(s) = payload.unwrap() {
1901
+ for g in s {
1902
+ for t in g.unwrap().types() {
1903
+ if let wasmparser::CompositeInnerType::Array(a) = &t.composite_type.inner {
1904
+ if a.0.mutable && a.0.element_type == wasmparser::StorageType::Val(wasmparser::ValType::I64) {
1905
+ found_i64 = true;
1906
+ }
1907
+ if a.0.element_type == wasmparser::StorageType::Val(wasmparser::ValType::Ref(wasmparser::RefType::ANYREF)) {
1908
+ found_anyref = true;
1909
+ }
1910
+ }
1911
+ }
1912
+ }
1913
+ }
1914
+ }
1915
+ assert!(found_i64, "no `array (mut i64)` declared");
1916
+ assert!(!found_anyref, "an anyref array remains");
1917
+ }
1918
+
1919
+ #[test]
1920
+ fn generic_tag_payload() {
1921
+ let src = format!("{}\
1922
+ fun fail(x: T) -> Try[Int, [Bad(T)]] =
1923
+ Err(Bad(x))
1924
+
1925
+ fun main() -> Int =
1926
+ match fail(7)
1927
+ Ok(v) => v
1928
+ Err(Bad(n)) => n
1929
+ ", STD_HEAD);
1930
+ assert_eq!(runMerged::<i64>("gentag", &src), 7);
1931
+ }
1932
+
1933
+ #[test]
1934
+ fn closure_typed_abi() {
1935
+ let src = format!("{}\
1936
+ fun apply(f: fn(Int) -> Int, x: Int) -> Int =
1937
+ f(x)
1938
+
1939
+ fun main() -> Int =
1940
+ apply(|n| n + 1, 41)
1941
+ ", STD_HEAD);
1942
+ let (bytes, _) = compiledKeys("clabi", &src);
1943
+ let mut found = false;
1944
+ for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
1945
+ if let wasmparser::Payload::TypeSection(s) = payload.unwrap() {
1946
+ for g in s {
1947
+ for t in g.unwrap().types() {
1948
+ if let wasmparser::CompositeInnerType::Func(f) = &t.composite_type.inner {
1949
+ if f.params() == [wasmparser::ValType::Ref(wasmparser::RefType::ANYREF), wasmparser::ValType::I64]
1950
+ && f.results() == [wasmparser::ValType::I64]
1951
+ {
1952
+ found = true;
1953
+ }
1954
+ }
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+ assert!(found, "no (anyref, i64) -> i64 closure type");
1768
1960
  }
1769
1961
 
1770
1962
  // ---- built-in Bool (i32; no `import std/Bool`) ----