plum

#treesitter#compiler#wasm

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

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


55dceee — Peter John 2026-10-25T22:50:52+05:30
feat(codegen)!: compile erased generics; stop monomorphizing
plum-checker/src/lib.rs CHANGED
@@ -25,6 +25,14 @@ pub struct TypedInfo {
25
25
  pub expr_types: HashMap<usize, PlumType>,
26
26
  }
27
27
 
28
+ /// Both operands of a binary/compare operator have the same type, so an
29
+ /// unresolved inference variable on one side takes the other side's type.
30
+ fn bindAcross(a: &PlumType, b: &PlumType) {
31
+ let mut scratch = Binds::new();
32
+ bindMatch(a, b, &mut scratch);
33
+ bindMatch(b, a, &mut scratch);
34
+ }
35
+
28
36
  fn freshVar() -> PlumType {
29
37
  FRESH.with(|c| {
30
38
  let n = c.get();
@@ -225,7 +233,7 @@ fn appSubst(generics: &[String], args: &[PlumType]) -> BTreeMap<String, PlumType
225
233
 
226
234
  /// A variant's payload field types with the enum's generics replaced by the
227
235
  /// type arguments of `ty` (when it is a `TApp`).
228
- fn variantFields(info: &EnumVariantInfo, ty: &PlumType) -> Vec<PlumType> {
236
+ pub fn variantFields(info: &EnumVariantInfo, ty: &PlumType) -> Vec<PlumType> {
229
237
  match ty {
230
238
  PlumType::TApp(_, args) if args.len() == info.generics.len() => {
231
239
  let s = appSubst(&info.generics, args);
@@ -288,7 +296,7 @@ fn recordType(expr: &ast::Expr, ty: &PlumType) {
288
296
  }
289
297
 
290
298
  /// Base name of a nominal type (`TNamed`/`TApp`).
291
- fn nominalName(t: &PlumType) -> Option<&str> {
299
+ pub fn nominalName(t: &PlumType) -> Option<&str> {
292
300
  match t {
293
301
  PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n),
294
302
  _ => None,
@@ -2522,6 +2530,9 @@ fn checkCallArgs(
2522
2530
  format!("{} arg {}: {}", label, i, err)
2523
2531
  }
2524
2532
  })?;
2533
+ // An argument whose own type still has unresolved variables (an unused
2534
+ // closure parameter, an empty literal) learns them from the parameter.
2535
+ bindMatch(&actual, &want, binds);
2525
2536
  }
2526
2537
  }
2527
2538
  Ok(())
@@ -2563,7 +2574,8 @@ fn tryParts(ty: &PlumType, ctx: &CheckCtx) -> Option<(PlumType, Option<PlumType>
2563
2574
 
2564
2575
  fn inferMethodCall(
2565
2576
  obj_ty: &PlumType,
2577
+ name: &str,
2566
- call: &ast::FnCall,
2578
+ args: &[ast::Arg],
2567
2579
  env: &TypeEnv,
2568
2580
  ctx: &CheckCtx,
2569
2581
  expected: Option<&PlumType>,
@@ -2575,19 +2587,19 @@ fn inferMethodCall(
2575
2587
  let Some(tr) = key.strip_prefix(&prefix) else {
2576
2588
  continue;
2577
2589
  };
2578
- let Some(sch) = env.get(&format!("{}{}:{}", TRAIT_KEY, tr, call.name)) else {
2590
+ let Some(sch) = env.get(&format!("{}{}:{}", TRAIT_KEY, tr, name)) else {
2579
2591
  continue;
2580
2592
  };
2581
2593
  if let PlumType::TFun(params, ret) = &*sch.body {
2582
2594
  let mut binds = Binds::new();
2583
- let label = format!("method '{}.{}'", p, call.name);
2595
+ let label = format!("method '{}.{}'", p, name);
2584
- checkCallArgs(&label, params, &call.args, params.len(), env, ctx, &mut binds)?;
2596
+ checkCallArgs(&label, params, args, params.len(), env, ctx, &mut binds)?;
2585
2597
  return Ok(substVars(ret, &binds));
2586
2598
  }
2587
2599
  }
2588
2600
  return Err(format!(
2589
2601
  "'{}' has no method '{}' (add a bound)",
2590
- p, call.name
2602
+ p, name
2591
2603
  ));
2592
2604
  }
2593
2605
  let Some(class_name) = methodReceiverName(obj_ty) else {
@@ -2597,20 +2609,24 @@ fn inferMethodCall(
2597
2609
  // the "union" enum that bare-wraps it instead (`enum Number = | Int | Float`).
2598
2610
  let class_name = if ctx
2599
2611
  .methods
2600
- .contains_key(&(class_name.clone(), call.name.clone()))
2612
+ .contains_key(&(class_name.to_string(), name.to_string()))
2601
2613
  {
2602
2614
  class_name
2603
2615
  } else {
2604
2616
  ctx.enum_variants
2605
2617
  .get(class_name.as_str())
2606
- .map(|info| info.enum_name.clone())
2618
+ .map(|info| info.enum_name.to_string())
2607
2619
  .filter(|enum_name| {
2608
2620
  ctx.methods
2609
- .contains_key(&(enum_name.clone(), call.name.clone()))
2621
+ .contains_key(&(enum_name.to_string(), name.to_string()))
2610
2622
  })
2611
2623
  .unwrap_or(class_name)
2612
2624
  };
2613
- let Some(sig) = ctx.methods.get(&(class_name.clone(), call.name.clone())) else {
2625
+ let Some(sig) = ctx.methods.get(&(class_name.to_string(), name.to_string())) else {
2626
+ // A trait-typed value only has the methods its trait declares.
2627
+ if isTraitName(&class_name, ctx) {
2628
+ return Err(format!("trait '{}' has no method '{}'", class_name, name));
2629
+ }
2614
2630
  // Unmodeled method (e.g. builtin/std): allow, codegen will catch.
2615
2631
  return Ok(PlumType::TVar("_".to_string()));
2616
2632
  };
@@ -2632,11 +2648,11 @@ fn inferMethodCall(
2632
2648
  }
2633
2649
  let required = ctx
2634
2650
  .min_required
2635
- .get(&(Some(class_name.clone()), call.name.clone()))
2651
+ .get(&(Some(class_name.to_string()), name.to_string()))
2636
2652
  .copied()
2637
2653
  .unwrap_or(params.len());
2638
- let label = format!("method '{}.{}'", class_name, call.name);
2654
+ let label = format!("method '{}.{}'", class_name, name);
2639
- checkCallArgs(&label, &params, &call.args, required, env, ctx, &mut binds)?;
2655
+ checkCallArgs(&label, &params, args, required, env, ctx, &mut binds)?;
2640
2656
  Ok(substVars(&ret, &binds))
2641
2657
  }
2642
2658
 
@@ -2837,7 +2853,7 @@ fn inferExprInner(
2837
2853
  resolved_from_usage
2838
2854
  .get(p)
2839
2855
  .cloned()
2840
- .unwrap_or_else(|| PlumType::TVar(format!("_closure_{}", p)))
2856
+ .unwrap_or_else(freshVar)
2841
2857
  });
2842
2858
  closure_env.insert(p.clone(), TypeScheme::mono(t.clone()));
2843
2859
  t
@@ -2856,13 +2872,37 @@ fn inferExprInner(
2856
2872
  closure_env.insert(name.clone(), TypeScheme::mono(ty));
2857
2873
  }
2858
2874
  }
2875
+ } else {
2876
+ // Walk every other statement (`if`, loops, `match`, plain
2877
+ // calls) so the types of the expressions inside are
2878
+ // recorded for codegen. Errors are deliberately dropped:
2879
+ // the closure's params may still be unresolved here.
2880
+ let _ = checkStmt(
2881
+ stmt,
2882
+ &mut closure_env,
2883
+ &PlumType::TVar("_".into()),
2884
+ "closure",
2885
+ (0, 0),
2886
+ ctx,
2887
+ );
2859
2888
  }
2860
2889
  }
2861
2890
  }
2862
2891
  let body_ty = match &cl.body.stmts.last() {
2863
2892
  Some(ast::Stmt::Expr(e, ..)) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
2864
2893
  Some(ast::Stmt::Return(Some(e))) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
2894
+ Some(last) => {
2895
+ let _ = checkStmt(
2896
+ last,
2897
+ &mut closure_env,
2898
+ &PlumType::TVar("_".into()),
2899
+ "closure",
2900
+ (0, 0),
2901
+ ctx,
2902
+ );
2903
+ PlumType::TUnit
2904
+ }
2865
- _ => PlumType::TUnit,
2905
+ None => PlumType::TUnit,
2866
2906
  };
2867
2907
  Ok(PlumType::TFun(param_types, Box::new(body_ty)))
2868
2908
  }
@@ -2876,6 +2916,7 @@ fn inferExprInner(
2876
2916
  let lt = inferExpr(&b.left, env, ctx)?;
2877
2917
  let rt = inferExpr(&b.right, env, ctx)?;
2878
2918
  unify(&lt, &rt, ctx).map_err(|e| format!("binary op: {}", e))?;
2919
+ bindAcross(&lt, &rt);
2879
2920
  if matches!(lt, PlumType::TVar(_)) && !matches!(rt, PlumType::TVar(_)) {
2880
2921
  return Ok(rt);
2881
2922
  }
@@ -2902,6 +2943,7 @@ fn inferExprInner(
2902
2943
  return Ok(PlumType::TBool);
2903
2944
  }
2904
2945
  unify(&lt, &rt, ctx).map_err(|e| format!("compare op: {}", e))?;
2946
+ bindAcross(&lt, &rt);
2905
2947
  Ok(PlumType::TBool)
2906
2948
  }
2907
2949
  ast::Expr::Ternary(t) => {
@@ -2972,14 +3014,11 @@ fn inferExprInner(
2972
3014
  .methods
2973
3015
  .contains_key(&(call.name.clone(), "init".to_string()))
2974
3016
  {
3017
+ // The receiver is a temporary (never recorded); the ARGUMENTS stay
2975
- let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
3018
+ // the original nodes so their types are recorded for codegen.
2976
- object: ast::Expr::TypeName(call.name.clone()),
3019
+ let receiver = ast::Expr::TypeName(call.name.clone());
2977
- attr: ast::AttrKind::Method(ast::FnCall {
2978
- name: "init".to_string(),
2979
- args: call.args.clone(),
2980
- }),
2981
- }));
2982
- return inferExprWith(&synthetic, env, ctx, expected);
3020
+ let obj_ty = inferExprInner(&receiver, env, ctx, None)?;
3021
+ return inferMethodCall(&obj_ty, "init", &call.args, env, ctx, expected);
2983
3022
  }
2984
3023
  if let Some(info) = ctx.enum_variants.get(&call.name) {
2985
3024
  if call.args.len() != info.field_types.len() {
@@ -3075,7 +3114,9 @@ fn inferExprInner(
3075
3114
  field_name, other
3076
3115
  )),
3077
3116
  },
3117
+ ast::AttrKind::Method(call) => {
3078
- ast::AttrKind::Method(call) => inferMethodCall(&obj_ty, call, env, ctx, expected),
3118
+ inferMethodCall(&obj_ty, &call.name, &call.args, env, ctx, expected)
3119
+ }
3079
3120
  }
3080
3121
  }
3081
3122
  }
plum-checker/tests/checker_tests.rs CHANGED
@@ -2089,6 +2089,14 @@ fn unbounded_tparam_method_error() {
2089
2089
  );
2090
2090
  }
2091
2091
 
2092
+ #[test]
2093
+ fn trait_typed_receiver_undeclared_method_error() {
2094
+ let src = "trait Shape =\n area() -> Float\n\nfun f(s: Shape) -> Float =\n s.bogus()\n";
2095
+ assert!(
2096
+ errText(checkSource(&parseClean(src))).contains("trait 'Shape' has no method 'bogus'")
2097
+ );
2098
+ }
2099
+
2092
2100
  #[test]
2093
2101
  fn bounded_ok() {
2094
2102
  let src = "\
plum-cli/src/main.rs CHANGED
@@ -181,7 +181,13 @@ fn compileToTestWasm(
181
181
  anyhow::bail!(messages.join("\n"));
182
182
  }
183
183
 
184
+ let compiled = plum_wasm_codegen::compileTestSource(&ast)
184
- plum_wasm_codegen::compileTestSource(&ast).map_err(|e| anyhow::anyhow!("codegen error: {e}"))
185
+ .map_err(|e| anyhow::anyhow!("codegen error: {e}"))?;
186
+ // Debug aid: `PLUM_DUMP_WASM=out.wasm plum test file.plum` keeps the module.
187
+ if let Some(path) = std::env::var_os("PLUM_DUMP_WASM") {
188
+ let _ = fs::write(path, &compiled.0);
189
+ }
190
+ Ok(compiled)
185
191
  }
186
192
 
187
193
  fn cmdCompile(
plum-wasm-codegen/src/lib.rs CHANGED
@@ -314,6 +314,11 @@ pub struct FuncSig {
314
314
  /// TRAILING ones compiled from here (see `compileCallArgs`). Always
315
315
  /// `vec![]` for an `extern fun` (the checker rejects a default there).
316
316
  pub param_defaults: Vec<Option<ast::Expr>>,
317
+ /// The DECLARED (generic, un-instantiated) param types, excluding any
318
+ /// implicit receiver, and the declared return type. A `TParam` slot is
319
+ /// erased to `ref null $Obj`, so the caller boxes/unboxes against these.
320
+ pub decl_params: Vec<PlumType>,
321
+ pub decl_ret: PlumType,
317
322
  }
318
323
 
319
324
  /// Everything codegen needs to know about one closure *literal* found in the program.
@@ -332,20 +337,26 @@ pub struct ClosureInfo {
332
337
  /// order) — assigned once all closures are discovered, alongside every other
333
338
  /// closure's env type and the shared closure-value struct, in one `rec` group.
334
339
  pub env_type_idx: u32,
335
- /// Closure param val types (NOT including the implicit leading env pointer).
336
- pub param_vts: Vec<ValType>,
337
340
  /// Closure param plum types (for the closure body's own type env).
338
341
  pub param_ptypes: Vec<PlumType>,
339
- /// Closure return val type (`None` for a `Unit`-returning closure).
342
+ /// Closure return plum type.
340
- pub ret_vt: Option<ValType>,
343
+ pub ret_ptype: PlumType,
341
344
  /// Free variables captured by value, in a stable (first-appearance) order; the
342
345
  /// index into this vec IS the variable's field index in the env struct.
343
346
  pub free_vars: Vec<(String, PlumType)>,
344
347
  }
345
348
 
346
- /// Key for deduplicating `call_indirect` function-type indices: the full wasm
349
+ /// Closure ABI (the "erased" calling convention): EVERY closure, whatever its
347
- /// signature (leading env-ptr param included) of a closure.
350
+ /// Plum signature, compiles to the wasm function type `(env: anyref, a1: anyref,
351
+ /// ..., an: anyref) -> anyref`, so a closure value is callable from any
352
+ /// `fn(...)`-typed slot (including a generic `fn(T) -> U` one) through a single
353
+ /// `call_indirect` type per arity. A primitive argument/result is boxed at that
354
+ /// edge (`$BoxI64`/`$BoxF64`/`$BoxI32`) and unboxed again inside the closure body
355
+ /// (prologue) / at the call site (result); reference values pass as-is, and a
356
+ /// `Unit` result is a null `anyref`. This keeps one wasm function per closure
357
+ /// literal and needs no per-instantiation thunk: the "thunk" work is done in the
358
+ /// closure's own prologue and the call site's epilogue.
348
- type ClosureSigKey = (Vec<ValType>, Option<ValType>);
359
+ type ClosureSigKey = usize;
349
360
 
350
361
  /// Global, read-only lookup tables shared by every function body being compiled.
351
362
  pub struct CompileCtx<'a> {
@@ -361,8 +372,11 @@ pub struct CompileCtx<'a> {
361
372
  /// The AST of each discovered closure literal, keyed the same way, so its body can
362
373
  /// be compiled in a second pass after all closures are registered.
363
374
  pub closure_asts: HashMap<usize, &'a ast::Closure>,
364
- /// Closure wasm signature -> function-type index, for `call_indirect` at call sites.
375
+ /// Closure arity -> the erased `call_indirect` function-type index.
365
376
  pub closure_call_types: HashMap<ClosureSigKey, u32>,
377
+ /// Instantiated type of every expression of the (un-monomorphized) source,
378
+ /// keyed by expression address — from `plum_checker::typedSource`.
379
+ pub types: HashMap<usize, PlumType>,
366
380
  /// Top-level function name -> global index holding its zero-capture "trampoline"
367
381
  /// closure struct `{table_idx, env=null}`, for using a plain named function
368
382
  /// wherever a `fn(...)`-typed value is expected (e.g. `each(double)`). Since the
@@ -377,6 +391,10 @@ pub struct CompileCtx<'a> {
377
391
  /// content equality (1/0) — see the comment on its registration site for
378
392
  /// why `==`/`!=` on `Str` needs this instead of plain `ref.eq`.
379
393
  pub string_eq_func: u32,
394
+ /// Shared runtime helper `(a: ref null $Obj, b: ref null $Obj) -> i32`: `==` on
395
+ /// two erased (`TParam`-typed) values — identity, else by value for boxed
396
+ /// `Int`/`Float`/`Byte` and content for `Str`.
397
+ pub dyn_eq_func: u32,
380
398
  /// Shared runtime helper `(n: i64) -> ref Str`: allocates a new `array<i8>`
381
399
  /// holding `n`'s decimal representation, for interpolating an `Int`.
382
400
  pub int_to_string_func: u32,
@@ -417,11 +435,17 @@ struct LocalCtx<'a> {
417
435
  variadic_for_scratch: HashMap<usize, u32>,
418
436
  func_ids: &'a HashMap<String, u32>,
419
437
  func_sigs: &'a HashMap<String, FuncSig>,
438
+ types: &'a HashMap<usize, PlumType>,
439
+ /// Name -> the plum type each local was DECLARED with (so its wasm
440
+ /// representation is `plumTypeToValtype` of this), as opposed to the
441
+ /// type of a particular use of it.
442
+ local_decl_types: HashMap<String, PlumType>,
420
443
  closures: &'a HashMap<usize, ClosureInfo>,
421
444
  closure_call_types: &'a HashMap<ClosureSigKey, u32>,
422
445
  named_fn_values: &'a HashMap<String, u32>,
423
446
  string_concat_func: u32,
424
447
  string_eq_func: u32,
448
+ dyn_eq_func: u32,
425
449
  int_to_string_func: u32,
426
450
  methods: &'a MethodEnv,
427
451
  enum_variants: &'a EnumVariants,
@@ -433,15 +457,6 @@ struct LocalCtx<'a> {
433
457
  /// statements in order, mirroring `plum-checker`'s own env evolution — needed
434
458
  /// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
435
459
  type_env: RefCell<TypeEnv>,
436
- /// Local name -> the exact wasm `call_indirect` signature of the closure literal
437
- /// assigned to it (populated when compiling that `Stmt::Assign`, straight from
438
- /// the already-correct `ClosureInfo` the discovery pass computed). Exists so
439
- /// `compileClosureCall` doesn't have to re-derive the signature via
440
- /// `plum_checker::inferExpr` on the closure a second time — which, unlike the
441
- /// discovery pass, doesn't have `resolveClosureParamTypesFromUsage`'s fix
442
- /// and would fall back to its old TVar-defaults-to-Int behavior, disagreeing
443
- /// with the (now correct) signature the closure's body was actually compiled with.
444
- closure_local_sigs: RefCell<HashMap<String, ClosureSigKey>>,
445
460
  /// How many wasm structured blocks (`if`/`else`) currently lie between the
446
461
  /// point being compiled and its NEAREST ENCLOSING LOOP's own `block`/`loop`
447
462
  /// pair — used by `Stmt::Break`/`Stmt::Continue` to compute the right `br`
@@ -457,9 +472,15 @@ struct LocalCtx<'a> {
457
472
  /// The declared return type of the function being compiled (`None` inside a
458
473
  /// closure body) — `?` builds the OUTER function's `Err` variant from it.
459
474
  ret_type: Option<PlumType>,
475
+ /// Whether this body is a closure literal's: `return` then yields an erased
476
+ /// (`anyref`) value per the closure ABI instead of `ret_type`'s own repr.
477
+ in_closure: bool,
460
478
  /// Match-subject scratch local -> its tag-union type, for subjects whose
461
479
  /// static type is a `TTags` (their `Name`/`Class` arms test `$Tag.id`).
462
480
  match_subject_types: RefCell<HashMap<u32, PlumType>>,
481
+ /// Match-subject scratch local -> the subject's full plum type (type arguments
482
+ /// included), so constructor patterns can instantiate their payload types.
483
+ subject_ptypes: RefCell<HashMap<u32, PlumType>>,
463
484
  }
464
485
 
465
486
  fn fnKey(f: &ast::Fn) -> String {
@@ -492,6 +513,12 @@ fn fnWasmParamTypes(f: &ast::Fn, gc_types: &GcTypeRegistry) -> Vec<ValType> {
492
513
  param_types
493
514
  }
494
515
 
516
+ /// A single uppercase letter is the grammar's only spelling of a generic parameter.
517
+ fn isGenericName(name: &str) -> bool {
518
+ let mut chars = name.chars();
519
+ matches!((chars.next(), chars.next()), (Some(c), None) if c.is_ascii_uppercase())
520
+ }
521
+
495
522
  fn paramTypeName(pt: &ast::ParamType) -> &str {
496
523
  match pt {
497
524
  ast::ParamType::Type(t) => t.name.as_str(),
@@ -531,6 +558,24 @@ thread_local! {
531
558
  const { RefCell::new(std::collections::BTreeMap::new()) };
532
559
  }
533
560
 
561
+ /// A variant's payload field types as instantiated by `subject_ty`'s type
562
+ /// arguments. A field whose argument the checker could not resolve (matching
563
+ /// through a bare `Parcel` annotation) keeps its declared, erased type, so a
564
+ /// binding of it stays a `ref null $Obj` instead of guessing a representation.
565
+ fn instFields(info: &EnumVariantInfo, subject_ty: &PlumType) -> Vec<PlumType> {
566
+ plum_checker::variantFields(info, subject_ty)
567
+ .into_iter()
568
+ .zip(info.field_types.iter())
569
+ .map(|(inst, decl)| match inst {
570
+ PlumType::TVar(_) => decl.clone(),
571
+ other => other,
572
+ })
573
+ .collect()
574
+ }
575
+
576
+ /// Largest closure arity with a pre-registered erased `call_indirect` type.
577
+ const MAX_CLOSURE_ARITY: usize = 8;
578
+
534
579
  /// Import key of the `plum::crash(Str)` host function in `func_ids`.
535
580
  const CRASH_FN_KEY: &str = "__crash";
536
581
 
@@ -572,55 +617,25 @@ fn tagId(name: &str) -> i32 {
572
617
  })
573
618
  }
574
619
 
575
- /// Collects every tag name inside every `[...]` tag-union segment of `s`
620
+ /// Collects every tag name appearing anywhere inside `ty` (tag-union keys and,
576
- /// (a canonical type or mangled name), recursing into payload types.
621
+ /// recursively, their payload types and every type argument).
577
- fn collectTagNames(s: &str, out: &mut std::collections::BTreeSet<String>) {
622
+ fn collectTagNamesTy(ty: &PlumType, out: &mut std::collections::BTreeSet<String>) {
578
- let chars: Vec<char> = s.chars().collect();
579
- let mut i = 0;
580
- while i < chars.len() {
581
- if chars[i] == '[' && chars.get(i + 1) != Some(&']') {
582
- let start = i;
583
- let mut depth = 0;
584
- while i < chars.len() {
585
- match chars[i] {
586
- '[' => depth += 1,
587
- ']' => depth -= 1,
588
- _ => {}
589
- }
590
- i += 1;
591
- if depth == 0 {
592
- break;
593
- }
594
- }
595
- let inner: String = chars[start + 1..i.saturating_sub(1)].iter().collect();
596
- let mut item = String::new();
597
- let mut d = 0;
598
- let mut items = Vec::new();
599
- for ch in inner.chars() {
600
- match ch {
623
+ match ty {
601
- '(' | '[' => d += 1,
602
- ')' | ']' => d -= 1,
603
- _ => {}
604
- }
605
- if ch == ',' && d == 0 {
606
- items.push(std::mem::take(&mut item));
624
+ PlumType::TTags(tags) => {
607
- } else {
608
- item.push(ch);
609
- }
610
- }
611
- items.push(item);
612
- for it in items {
625
+ for (name, payload) in tags {
613
- let name = it.split('(').next().unwrap_or("").trim().to_string();
614
- if !name.is_empty() {
615
- out.insert(name);
626
+ out.insert(name.clone());
616
- }
617
- if let Some(pos) = it.find('(') {
627
+ if let Some(p) = payload {
618
- collectTagNames(&it[pos + 1..], out);
628
+ collectTagNamesTy(p, out);
619
629
  }
620
630
  }
621
- } else {
622
- i += 1;
623
631
  }
632
+ PlumType::TApp(_, args) => args.iter().for_each(|a| collectTagNamesTy(a, out)),
633
+ PlumType::TFun(ps, r) => {
634
+ ps.iter().for_each(|p| collectTagNamesTy(p, out));
635
+ collectTagNamesTy(r, out);
636
+ }
637
+ PlumType::TVariadic(i) => collectTagNamesTy(i, out),
638
+ _ => {}
624
639
  }
625
640
  }
626
641
 
@@ -636,21 +651,53 @@ fn boxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
636
651
  }
637
652
  }
638
653
 
639
- /// Boxes the primitive on top of the stack (`ty`) into its `$Box*` struct when
654
+ /// Converts the value on top of the stack from representation `from` to `to`
640
- /// it flows into a slot whose wasm type is `expected` and `ty` itself is not a
641
- /// reference — i.e. an `Int`/`Float`/`Byte` entering a trait-typed (`$Obj`) slot.
655
+ /// (the erased-generics edge conversions): a primitive entering a reference slot
656
+ /// (`ref null $Obj` / `anyref`) is boxed; a reference leaving such a slot for a
657
+ /// primitive is cast to the matching box and unboxed; a reference leaving the
658
+ /// erased supertype for a concrete struct/enum/tag type is `ref.cast` down.
659
+ /// Identical representations and upcasts emit nothing.
642
- fn boxIfPrimitiveIntoObj(ty: &PlumType, expected: ValType, body: &mut Vec<u8>, ctx: &LocalCtx) {
660
+ fn coerceVt(from: ValType, to: ValType, body: &mut Vec<u8>, g: &GcTypeRegistry) {
643
- let ValType::Ref(rt) = expected else { return };
661
+ if from == to {
644
- if rt.heap_type != HeapType::Concrete(ctx.gc_types.obj_type_idx) {
645
662
  return;
646
663
  }
664
+ match (from, to) {
665
+ (ValType::I64, ValType::Ref(_)) => Instruction::Call(g.box_i64_fn).encode(body),
666
+ (ValType::F64, ValType::Ref(_)) => Instruction::Call(g.box_f64_fn).encode(body),
667
+ (ValType::I32, ValType::Ref(_)) => Instruction::Call(g.box_i32_fn).encode(body),
647
- if matches!(ty, PlumType::TInt | PlumType::TFloat | PlumType::TByte) {
668
+ (ValType::Ref(_), prim @ (ValType::I64 | ValType::F64 | ValType::I32)) => {
669
+ let idx = match prim {
648
- boxTagPayload(ty, body, ctx);
670
+ ValType::I64 => g.box_i64_idx,
671
+ ValType::F64 => g.box_f64_idx,
672
+ _ => g.box_i32_idx,
673
+ };
674
+ Instruction::RefCastNonNull(HeapType::Concrete(idx)).encode(body);
675
+ Instruction::StructGet {
676
+ struct_type_index: idx,
677
+ field_index: fld(0),
678
+ }
679
+ .encode(body);
680
+ }
681
+ (ValType::Ref(f), ValType::Ref(t)) => {
682
+ // Only a downcast out of the erased supertype (`$Obj`) or an abstract
683
+ // ref (`anyref`) needs an instruction; anything else is an upcast.
684
+ let from_erased = match f.heap_type {
685
+ HeapType::Concrete(fi) => fi == g.obj_type_idx,
686
+ _ => true,
687
+ };
688
+ if let HeapType::Concrete(ti) = t.heap_type {
689
+ if from_erased && !matches!(f.heap_type, HeapType::Concrete(fi) if fi == ti) {
690
+ Instruction::RefCastNullable(t.heap_type).encode(body);
691
+ }
692
+ }
693
+ }
694
+ _ => {}
649
695
  }
650
696
  }
651
697
 
652
- /// Compiles `expr` and, if it is a primitive flowing into a trait-typed slot of
698
+ /// Compiles `expr` and converts its value to representation `expected` (when given):
699
+ /// boxes a primitive flowing into an erased (`ref null $Obj`) slot, casts an
653
- /// wasm type `expected`, boxes it.
700
+ /// erased value down to the concrete type a slot expects.
654
701
  fn compileExprAs(
655
702
  expr: &ast::Expr,
656
703
  expected: Option<ValType>,
@@ -659,15 +706,52 @@ fn compileExprAs(
659
706
  state: &mut ModuleState,
660
707
  ) -> Result<(), String> {
661
708
  compileExpr(expr, body, ctx, state)?;
662
- if let Some(vt @ ValType::Ref(rt)) = expected {
709
+ if let Some(vt) = expected {
663
- if rt.heap_type == HeapType::Concrete(ctx.gc_types.obj_type_idx) {
664
- let ty = inferLocalType(expr, ctx);
710
+ let own = plumTypeToValtype(&inferLocalType(expr, ctx));
665
- boxIfPrimitiveIntoObj(&ty, vt, body, ctx);
711
+ coerceVt(own, vt, body, ctx.gc_types);
666
- }
667
712
  }
668
713
  Ok(())
669
714
  }
670
715
 
716
+ /// The variant of enum `decl` that bare-wraps the primitive type `actual`
717
+ /// (`enum Number = | Int | Float`), if `decl` names such an enum.
718
+ fn primitiveWrapVariant(decl: &PlumType, actual: &PlumType, ctx: &LocalCtx) -> Option<String> {
719
+ if !matches!(actual, PlumType::TInt | PlumType::TFloat) {
720
+ return None;
721
+ }
722
+ let enum_name = plum_checker::nominalName(decl)?;
723
+ ctx.enum_variants
724
+ .iter()
725
+ .find(|(_, info)| {
726
+ info.enum_name == enum_name
727
+ && info.field_names.is_empty()
728
+ && info.field_types == [actual.clone()]
729
+ })
730
+ .map(|(n, _)| n.clone())
731
+ }
732
+
733
+ /// Like `compileExprAs`, for a slot whose DECLARED plum type is `decl`: a bare
734
+ /// `Int`/`Float` flowing into a `Number`-like union enum is wrapped in the
735
+ /// matching variant (the old monomorphizer's `wrapPrimitiveAgainstExpected`).
736
+ fn compileExprInto(
737
+ expr: &ast::Expr,
738
+ decl: &PlumType,
739
+ body: &mut Vec<u8>,
740
+ ctx: &LocalCtx,
741
+ state: &mut ModuleState,
742
+ ) -> Result<(), String> {
743
+ let actual = inferLocalType(expr, ctx);
744
+ if let Some(variant) = primitiveWrapVariant(decl, &actual, ctx) {
745
+ if let Some(&vidx) = ctx.gc_types.variant_type_idx.get(&variant) {
746
+ Instruction::I32Const(typeIdOf(ctx.gc_types, &variant)).encode(body);
747
+ compileExpr(expr, body, ctx, state)?;
748
+ Instruction::StructNew(vidx).encode(body);
749
+ return Ok(());
750
+ }
751
+ }
752
+ compileExprAs(expr, Some(plumTypeToValtype(decl)), body, ctx, state)
753
+ }
754
+
671
755
  /// Inverse of `boxTagPayload`: an `anyref` payload on the stack becomes a `ty` value.
672
756
  fn unboxTagPayload(ty: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
673
757
  let unbox = |idx: u32, body: &mut Vec<u8>| {
@@ -771,6 +855,8 @@ fn astTypeToWasm(name: &str) -> Option<ValType> {
771
855
  other if other.starts_with('[') && !other.starts_with("[]") => {
772
856
  Some(withGcTypes(|r| gcRef(r.tag_type_idx)))
773
857
  }
858
+ // A generic parameter (`T`) is erased to the common `$Obj` supertype.
859
+ other if isGenericName(other) => Some(plumTypeToValtype(&PlumType::TParam(other.to_string()))),
774
860
  other => Some(plumTypeToValtype(&PlumType::TNamed(other.to_string()))),
775
861
  }
776
862
  }
@@ -853,7 +939,8 @@ fn plumTypeToValtype(t: &PlumType) -> ValType {
853
939
  // existing "not modeled as a concrete shape here" treatment.
854
940
  PlumType::TTags(_) => withGcTypes(|r| gcRef(r.tag_type_idx)),
855
941
  PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
942
+ PlumType::TParam(_) => withGcTypes(|r| gcRef(r.obj_type_idx)),
856
- PlumType::TVar(_) | PlumType::TParam(_) | PlumType::TUnit => ValType::I64,
943
+ PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
857
944
  }
858
945
  }
859
946
 
@@ -948,7 +1035,7 @@ fn typeIdOf(r: &GcTypeRegistry, name: &str) -> i32 {
948
1035
  /// compiler intrinsics rather than compiled `todo` bodies (see
949
1036
  /// `compileIntrinsicFnBody`).
950
1037
  fn isArraySpecialization(name: &str) -> bool {
951
- name == "Array" || name.starts_with("Array$")
1038
+ name == "Array"
952
1039
  }
953
1040
 
954
1041
  /// Resolves a plum type to the wasm-gc `ValType` its values are represented as, given
@@ -1008,7 +1095,8 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
1008
1095
  // yet-meaningful placeholder in the meantime, since nothing consumes it yet.
1009
1096
  PlumType::TTags(_) => gcRef(registry.tag_type_idx),
1010
1097
  PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
1098
+ PlumType::TParam(_) => gcRef(registry.obj_type_idx),
1011
- PlumType::TVar(_) | PlumType::TParam(_) | PlumType::TUnit => ValType::I64,
1099
+ PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
1012
1100
  }
1013
1101
  }
1014
1102
 
@@ -1051,9 +1139,6 @@ fn buildGcTypeRegistry(
1051
1139
  let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
1052
1140
  let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
1053
1141
  let mut array_ref_slot: Option<u32> = None;
1054
- let mut erased_enum_super: HashMap<String, u32> = HashMap::new();
1055
- let mut erased_variant_idx: HashMap<String, u32> = HashMap::new();
1056
- let mut erased_enum_id: HashMap<String, i32> = HashMap::new();
1057
1142
 
1058
1143
  // `Bool` is an ordinary enum now (`type Bool = | True | False` in
1059
1144
  // `libs/std/Bool.plum`) — registered exactly like any other enum
@@ -1094,32 +1179,6 @@ fn buildGcTypeRegistry(
1094
1179
  }
1095
1180
  continue;
1096
1181
  }
1097
- // Specializations that differ only in their tag-union segments
1098
- // (`Try$Int$[A]` / `Try$Int$[A,B]`) share ONE set of wasm types — every
1099
- // tag value is the same `$Tag` struct — so a later one is aliased onto
1100
- // the first one's slots instead of getting its own.
1101
- let erased_enum = plum_checker::erasedEnumName(enum_name);
1102
- if let Some(existing_super) = erased_enum_super.get(&erased_enum) {
1103
- enum_super_type_idx.insert(enum_name.clone(), *existing_super);
1104
- // Shares the first specialization's wasm types, and so its typeId.
1105
- if let Some(first_id) = erased_enum_id.get(&erased_enum).copied() {
1106
- type_ids.insert(enum_name.clone(), first_id);
1107
- for vname in variant_names {
1108
- type_ids.insert(vname.clone(), first_id);
1109
- }
1110
- }
1111
- for vname in variant_names {
1112
- let ev = plum_checker::erasedEnumName(vname);
1113
- if let Some(idx) = erased_variant_idx.get(&ev) {
1114
- variant_type_idx.insert(vname.clone(), *idx);
1115
- continue;
1116
- }
1117
- variant_type_idx.insert(vname.clone(), slots.len() as u32);
1118
- erased_variant_idx.insert(ev, slots.len() as u32);
1119
- slots.push(Slot::Variant(vname.clone()));
1120
- }
1121
- continue;
1122
- }
1123
1182
  let this_id = match enum_name.as_str() {
1124
1183
  "Str" => TYPE_ID_STR,
1125
1184
  "Buffer" => TYPE_ID_BUFFER,
@@ -1133,13 +1192,10 @@ fn buildGcTypeRegistry(
1133
1192
  for vname in variant_names {
1134
1193
  type_ids.insert(vname.clone(), this_id);
1135
1194
  }
1136
- erased_enum_id.insert(erased_enum.clone(), this_id);
1137
1195
  enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
1138
- erased_enum_super.insert(erased_enum, slots.len() as u32);
1139
1196
  slots.push(Slot::EnumSuper(enum_name.clone()));
1140
1197
  for vname in variant_names {
1141
1198
  variant_type_idx.insert(vname.clone(), slots.len() as u32);
1142
- erased_variant_idx.insert(plum_checker::erasedEnumName(vname), slots.len() as u32);
1143
1199
  slots.push(Slot::Variant(vname.clone()));
1144
1200
  }
1145
1201
  // A RECORD-shaped enum (the `type X = ...` replacement, e.g. `enum Cat =
@@ -1347,21 +1403,6 @@ fn boxSubType(vt: ValType, obj_type_idx: u32) -> SubType {
1347
1403
  }
1348
1404
  }
1349
1405
 
1350
- /// Maps an `ast::ParamType::Fn(params, ret)` to the wasm signature of the *closure
1351
- /// function* it compiles to: an implicit leading `env: anyref`, then one param per
1352
- /// declared param type, returning `ret`'s val type (or nothing for `Unit`).
1353
- fn fnParamTypeToWasmSig(
1354
- params: &[ast::Type],
1355
- ret: &Option<Box<ast::Type>>,
1356
- ) -> (Vec<ValType>, Option<ValType>) {
1357
- let mut vts = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
1358
- for p in params {
1359
- vts.push(astTypeToWasm(&p.name).unwrap_or(ValType::I32));
1360
- }
1361
- let ret_vt = ret.as_ref().and_then(|t| astTypeToWasm(&t.name));
1362
- (vts, ret_vt)
1363
- }
1364
-
1365
1406
  fn blockTypeFor(result_vt: Option<ValType>) -> BlockType {
1366
1407
  result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty)
1367
1408
  }
@@ -1414,6 +1455,16 @@ fn checkCtxOf<'a>(
1414
1455
  /// already accepted by `plum_checker::checkSource`, so a failure here would indicate
1415
1456
  /// codegen is being driven directly on unchecked input (as some tests do).
1416
1457
  fn inferLocalType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
1458
+ // The checker's instantiated type for this exact expression node, if any;
1459
+ // synthesized nodes (desugarings built during codegen) fall back to
1460
+ // re-inference against the local type env.
1461
+ if let Some(t) = ctx.types.get(&(expr as *const ast::Expr as usize)) {
1462
+ return t.clone();
1463
+ }
1464
+ inferFallbackType(expr, ctx)
1465
+ }
1466
+
1467
+ fn inferFallbackType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
1417
1468
  let env = ctx.type_env.borrow();
1418
1469
  let cctx = checkCtxOf(
1419
1470
  ctx.methods,
@@ -1425,6 +1476,14 @@ fn inferLocalType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
1425
1476
  }
1426
1477
 
1427
1478
  pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
1479
+ compileSourceInner(source, &[]).map(|(bytes, _)| bytes)
1480
+ }
1481
+
1482
+ /// Like `compileSource`, also returning the key of every compiled function
1483
+ /// (`name` or `Receiver::method`) — one entry per source function, since a
1484
+ /// generic function/method compiles exactly once however many types it is
1485
+ /// instantiated at.
1486
+ pub fn compileSourceWithFunctions(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
1428
1487
  compileSourceInner(source, &[])
1429
1488
  }
1430
1489
 
@@ -1446,7 +1505,7 @@ pub fn compileTestSource(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>),
1446
1505
  .iter()
1447
1506
  .map(|(_, export_name)| export_name["test#".len()..].to_string())
1448
1507
  .collect();
1449
- let wasm = compileSourceInner(&desugared, &extra_exports)?;
1508
+ let (wasm, _) = compileSourceInner(&desugared, &extra_exports)?;
1450
1509
  Ok((wasm, names))
1451
1510
  }
1452
1511
 
@@ -1616,26 +1675,35 @@ fn desugarAssertStmt(stmt: &ast::Stmt) -> ast::Stmt {
1616
1675
  fn compileSourceInner(
1617
1676
  source: &ast::Source,
1618
1677
  extra_exports: &[(String, String)],
1619
- ) -> Result<Vec<u8>, String> {
1678
+ ) -> Result<(Vec<u8>, Vec<String>), String> {
1679
+ // The source is compiled as written (generics erased, not specialized): the
1680
+ // checker's per-expression instantiated types drive every box/unbox decision.
1620
- let source = &plum_checker::monomorphize::monomorphizeSource(source)?;
1681
+ let typed = plum_checker::typedSource(source).map_err(|errs| {
1682
+ errs.iter()
1683
+ .map(|e| format!("type error: {}", e.message))
1684
+ .collect::<Vec<_>>()
1685
+ .join("\n")
1686
+ })?;
1687
+ let types = typed.expr_types;
1621
- let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
1688
+ let (global_env, methods, enum_variants, enum_params) = plum_checker::buildTables(source, true);
1622
1689
  let min_required = plum_checker::buildMinRequiredArgs(source);
1623
1690
 
1624
1691
  // Intern every tag name the program's types mention, in sorted order.
1625
1692
  {
1626
1693
  let mut names = std::collections::BTreeSet::new();
1627
- for (k, info) in &enum_variants {
1694
+ for info in enum_variants.values() {
1628
- collectTagNames(k, &mut names);
1629
- collectTagNames(&info.enum_name, &mut names);
1630
1695
  for t in &info.field_types {
1631
- collectTagNames(&t.to_string(), &mut names);
1696
+ collectTagNamesTy(t, &mut names);
1632
1697
  }
1633
1698
  }
1634
1699
  for scheme in global_env.values() {
1635
- collectTagNames(&scheme.body.to_string(), &mut names);
1700
+ collectTagNamesTy(&scheme.body, &mut names);
1636
1701
  }
1637
1702
  for t in methods.values() {
1638
- collectTagNames(&t.to_string(), &mut names);
1703
+ collectTagNamesTy(t, &mut names);
1704
+ }
1705
+ for t in types.values() {
1706
+ collectTagNamesTy(t, &mut names);
1639
1707
  }
1640
1708
  CURRENT_TAG_IDS.with(|c| {
1641
1709
  *c.borrow_mut() = names
@@ -1726,6 +1794,12 @@ fn compileSourceInner(
1726
1794
  params: param_types,
1727
1795
  ret,
1728
1796
  param_defaults: Vec::new(),
1797
+ decl_params: f.params.iter().map(|p| paramPlumType(&p.ty)).collect(),
1798
+ decl_ret: f
1799
+ .returns
1800
+ .as_ref()
1801
+ .map(plum_checker::plumTypeFromAstG)
1802
+ .unwrap_or(PlumType::TUnit),
1729
1803
  },
1730
1804
  );
1731
1805
  }
@@ -1814,9 +1888,14 @@ fn compileSourceInner(
1814
1888
  }
1815
1889
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1816
1890
 
1891
+ // Closure ABI (see `ClosureSigKey`): one erased `call_indirect` type per arity,
1817
- // Closure wasm signature -> function-type index, deduped so every closure/call site
1892
+ // `(env, anyref * n) -> anyref`, shared by every closure and call site.
1818
- // of the same shape shares one `call_indirect` type.
1819
1893
  let mut closure_call_types: HashMap<ClosureSigKey, u32> = HashMap::new();
1894
+ for arity in 0..=MAX_CLOSURE_ARITY {
1895
+ let params = vec![ValType::Ref(RefType::ANYREF); arity + 1];
1896
+ let t = module.addType(&params, &[ValType::Ref(RefType::ANYREF)]);
1897
+ closure_call_types.insert(arity, t);
1898
+ }
1820
1899
 
1821
1900
  // Register every function AND method signature up front (methods get an implicit
1822
1901
  // leading `self: pointer` param and are keyed as "Receiver::method").
@@ -1846,23 +1925,14 @@ fn compileSourceInner(
1846
1925
  params: param_types,
1847
1926
  ret,
1848
1927
  param_defaults,
1928
+ decl_params: f.params.iter().map(|p| paramPlumType(&p.ty)).collect(),
1929
+ decl_ret: f
1930
+ .returns
1931
+ .as_ref()
1932
+ .map(plum_checker::plumTypeFromAstG)
1933
+ .unwrap_or(PlumType::TUnit),
1849
1934
  },
1850
1935
  );
1851
-
1852
- // Any `fn(...) -> ...`-typed param is callable via `call_indirect`; register
1853
- // its wasm signature (leading env-ptr param included) so call sites can
1854
- // resolve a consistent type index even if no matching closure literal exists.
1855
- for p in &f.params {
1856
- if let ast::ParamType::Fn(params, ret) = &p.ty {
1857
- let (sig_params, ret_vt) = fnParamTypeToWasmSig(params, ret);
1858
- let sig_key: ClosureSigKey = (sig_params.clone(), ret_vt);
1859
- if !closure_call_types.contains_key(&sig_key) {
1860
- let results: Vec<ValType> = ret_vt.into_iter().collect();
1861
- let tidx = module.addType(&sig_params, &results);
1862
- closure_call_types.insert(sig_key, tidx);
1863
- }
1864
- }
1865
- }
1866
1936
  }
1867
1937
  }
1868
1938
 
@@ -1908,21 +1978,12 @@ fn compileSourceInner(
1908
1978
  let mut raw_closures: Vec<RawClosure> = Vec::new();
1909
1979
  let mut named_fn_refs: HashSet<String> = HashSet::new();
1910
1980
  for f in &fns {
1911
- let mut env = global_env.clone();
1912
- if let Some(recv) = &f.type_param {
1913
- env.insert(
1914
- "self".to_string(),
1915
- TypeScheme::mono(plum_checker::plumTypeFromName(recv)),
1916
- );
1917
- }
1918
1981
  let mut locals: HashSet<String> = HashSet::new();
1919
1982
  for p in &f.params {
1920
- env.insert(p.name.clone(), TypeScheme::mono(paramPlumType(&p.ty)));
1921
1983
  locals.insert(p.name.clone());
1922
1984
  }
1923
1985
  let mut walker = ClosureWalker {
1924
- env,
1925
- cctx: checkCtxOf(&methods, &enum_variants, &enum_params, &min_required),
1986
+ types: &types,
1926
1987
  fn_decls: &fn_decls,
1927
1988
  found: Vec::new(),
1928
1989
  locals,
@@ -1930,7 +1991,7 @@ fn compileSourceInner(
1930
1991
  };
1931
1992
  match &f.body {
1932
1993
  ast::FnBody::Block(block) => walker.walkBlock(block),
1933
- ast::FnBody::Expr(e) => walker.walkExpr(e, None),
1994
+ ast::FnBody::Expr(e) => walker.walkExpr(e),
1934
1995
  // `fns` excludes every `extern fun` (no body to discover closures in).
1935
1996
  ast::FnBody::Extern => unreachable!("extern fns are excluded from `fns`"),
1936
1997
  }
@@ -1990,18 +2051,10 @@ fn compileSourceInner(
1990
2051
  let mut closures: HashMap<usize, ClosureInfo> = HashMap::new();
1991
2052
  let mut closure_asts: HashMap<usize, &ast::Closure> = HashMap::new();
1992
2053
  for (i, rc) in raw_closures.iter().enumerate() {
1993
- let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
1994
- sig_params.extend(rc.param_vts.iter().copied());
1995
- let sig_key: ClosureSigKey = (sig_params.clone(), rc.ret_vt);
2054
+ let arity = rc.closure.params.len();
1996
- let type_idx = match closure_call_types.get(&sig_key) {
2055
+ let type_idx = *closure_call_types
1997
- Some(t) => *t,
2056
+ .get(&arity)
1998
- None => {
1999
- let results: Vec<ValType> = rc.ret_vt.into_iter().collect();
2057
+ .ok_or_else(|| format!("codegen: closures with {} params are not supported", arity))?;
2000
- let t = module.addType(&sig_params, &results);
2001
- closure_call_types.insert(sig_key, t);
2002
- t
2003
- }
2004
- };
2005
2058
  let func_idx = module.addFunction(type_idx, &[]);
2006
2059
  let table_idx = module.addTableElement(func_idx);
2007
2060
  closures.insert(
@@ -2010,9 +2063,8 @@ fn compileSourceInner(
2010
2063
  func_idx,
2011
2064
  table_idx,
2012
2065
  env_type_idx: closure_type_indices[i + 1],
2013
- param_vts: rc.param_vts.clone(),
2014
2066
  param_ptypes: rc.param_ptypes.clone(),
2015
- ret_vt: rc.ret_vt,
2067
+ ret_ptype: rc.ret_ptype.clone(),
2016
2068
  free_vars: rc.free_vars.clone(),
2017
2069
  },
2018
2070
  );
@@ -2021,10 +2073,11 @@ fn compileSourceInner(
2021
2073
 
2022
2074
  // Register a zero-capture "trampoline" closure for every top-level function
2023
2075
  // referenced as a bare value (e.g. `each(double)`): a real wasm function with the
2024
- // closure calling convention `(env, ...real_params) -> ret` that ignores its env
2076
+ // erased closure ABI `(env, anyref...) -> anyref` that ignores its env, unboxes
2077
+ // each argument to the real function's parameter representation, forwards to the
2025
- // and forwards straight to the real function, plus a funcref-table entry for it.
2078
+ // real function and boxes its result, plus a funcref-table entry for it. Since
2026
- // Since it never captures anything, its `{table_idx, env=null}` closure struct is
2079
+ // it never captures anything, its `{table_idx, env=null}` closure struct is a
2027
- // a compile-time constant — like every payload-free enum variant, built once by
2080
+ // compile-time constant — like every payload-free enum variant, built once by
2028
2081
  // the shared `start` function and stored in its own global.
2029
2082
  let mut named_fn_values: HashMap<String, u32> = HashMap::new();
2030
2083
  for name in &named_fn_refs {
@@ -2034,26 +2087,22 @@ fn compileSourceInner(
2034
2087
  let real_func_idx = *func_ids
2035
2088
  .get(name)
2036
2089
  .expect("named fn ref must be a registered top-level function");
2037
-
2038
- let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
2039
- sig_params.extend(sig.params.iter().copied());
2090
+ let arity = sig.params.len();
2040
- let sig_key: ClosureSigKey = (sig_params.clone(), sig.ret);
2041
- let type_idx = match closure_call_types.get(&sig_key) {
2091
+ let type_idx = *closure_call_types
2042
- Some(t) => *t,
2092
+ .get(&arity)
2043
- None => {
2044
- let results: Vec<ValType> = sig.ret.into_iter().collect();
2093
+ .ok_or_else(|| format!("codegen: function '{}' has too many params to use as a value", name))?;
2045
- let t = module.addType(&sig_params, &results);
2046
- closure_call_types.insert(sig_key, t);
2047
- t
2048
- }
2049
- };
2050
2094
 
2051
2095
  let mut tbody = Vec::new();
2052
2096
  tbody.push(0u8); // no locals beyond the params already in the signature
2053
- for i in 0..sig.params.len() {
2097
+ for (i, vt) in sig.params.iter().enumerate() {
2054
2098
  Instruction::LocalGet((i + 1) as u32).encode(&mut tbody); // local 0 is env, ignored
2099
+ coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut tbody, &gc_types);
2055
2100
  }
2056
2101
  Instruction::Call(real_func_idx).encode(&mut tbody);
2102
+ match sig.ret {
2103
+ Some(rvt) => coerceVt(rvt, ValType::Ref(RefType::ANYREF), &mut tbody, &gc_types),
2104
+ None => Instruction::RefNull(HeapType::ANY).encode(&mut tbody),
2105
+ }
2057
2106
  Instruction::End.encode(&mut tbody);
2058
2107
  let func_idx = module.addFunction(type_idx, &tbody);
2059
2108
  let table_idx = module.addTableElement(func_idx);
@@ -2084,6 +2133,7 @@ fn compileSourceInner(
2084
2133
  // (unconditionally, for simplicity) since they're cheap and self-contained.
2085
2134
  let string_concat_func = registerStringConcatHelper(&mut module, gc_types.byte_array_type_idx);
2086
2135
  let string_eq_func = registerStringEqHelper(&mut module, gc_types.byte_array_type_idx);
2136
+ let dyn_eq_func = registerDynEqHelper(&mut module, &gc_types, string_eq_func);
2087
2137
  let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.byte_array_type_idx);
2088
2138
 
2089
2139
  let ctx = CompileCtx {
@@ -2097,9 +2147,11 @@ fn compileSourceInner(
2097
2147
  closures,
2098
2148
  closure_asts,
2099
2149
  closure_call_types,
2150
+ types,
2100
2151
  named_fn_values,
2101
2152
  string_concat_func,
2102
2153
  string_eq_func,
2154
+ dyn_eq_func,
2103
2155
  int_to_string_func,
2104
2156
  gc_types,
2105
2157
  singleton_globals,
@@ -2163,7 +2215,14 @@ fn compileSourceInner(
2163
2215
  module.addExport(export_name, ExportKind::Func, idx);
2164
2216
  }
2165
2217
 
2218
+ let mut fn_keys: Vec<String> = ctx
2219
+ .func_ids
2220
+ .keys()
2221
+ .filter(|k| k.as_str() != CRASH_FN_KEY)
2222
+ .cloned()
2223
+ .collect();
2224
+ fn_keys.sort();
2166
- Ok(module.finish())
2225
+ Ok((module.finish(), fn_keys))
2167
2226
  }
2168
2227
 
2169
2228
  /// `(payload) -> ref $Box*`: allocates a box with its header typeId set.
@@ -2337,6 +2396,98 @@ fn registerDynDispatchers(
2337
2396
  gc_types.dyn_fns.extend(registered);
2338
2397
  }
2339
2398
 
2399
+ /// Registers `__dyn_eq(a: ref null $Obj, b: ref null $Obj) -> i32`, the `==` of two
2400
+ /// erased (`TParam`-typed) values: reference identity first, then — for two
2401
+ /// values of the same `typeId` — payload equality for boxed `Int`/`Float`/`Byte`
2402
+ /// and byte-for-byte content equality for `Str`. Everything else (class/enum
2403
+ /// instances) is identity, exactly like `==` on a concrete reference type.
2404
+ fn registerDynEqHelper(module: &mut WasmModule, g: &GcTypeRegistry, string_eq_func: u32) -> u32 {
2405
+ let obj = g.obj_type_idx;
2406
+ let mut b = vec![0u8];
2407
+ let tid = |b: &mut Vec<u8>, local: u32| {
2408
+ Instruction::LocalGet(local).encode(b);
2409
+ Instruction::StructGet {
2410
+ struct_type_index: obj,
2411
+ field_index: 0,
2412
+ }
2413
+ .encode(b);
2414
+ };
2415
+ let i32r = BlockType::Result(ValType::I32);
2416
+ Instruction::LocalGet(0).encode(&mut b);
2417
+ Instruction::LocalGet(1).encode(&mut b);
2418
+ Instruction::RefEq.encode(&mut b);
2419
+ Instruction::If(i32r).encode(&mut b);
2420
+ Instruction::I32Const(1).encode(&mut b);
2421
+ Instruction::Else.encode(&mut b);
2422
+ // either null -> not equal
2423
+ Instruction::LocalGet(0).encode(&mut b);
2424
+ Instruction::RefIsNull.encode(&mut b);
2425
+ Instruction::LocalGet(1).encode(&mut b);
2426
+ Instruction::RefIsNull.encode(&mut b);
2427
+ Instruction::I32Or.encode(&mut b);
2428
+ Instruction::If(i32r).encode(&mut b);
2429
+ Instruction::I32Const(0).encode(&mut b);
2430
+ Instruction::Else.encode(&mut b);
2431
+ // different dynamic types -> not equal
2432
+ tid(&mut b, 0);
2433
+ tid(&mut b, 1);
2434
+ Instruction::I32Ne.encode(&mut b);
2435
+ Instruction::If(i32r).encode(&mut b);
2436
+ Instruction::I32Const(0).encode(&mut b);
2437
+ Instruction::Else.encode(&mut b);
2438
+ let mut ends = 0;
2439
+ for (id, box_idx, eq) in [
2440
+ (TYPE_ID_BOX_I64, g.box_i64_idx, Instruction::I64Eq),
2441
+ (TYPE_ID_BOX_F64, g.box_f64_idx, Instruction::F64Eq),
2442
+ (TYPE_ID_BOX_I32, g.box_i32_idx, Instruction::I32Eq),
2443
+ ] {
2444
+ tid(&mut b, 0);
2445
+ Instruction::I32Const(id).encode(&mut b);
2446
+ Instruction::I32Eq.encode(&mut b);
2447
+ Instruction::If(i32r).encode(&mut b);
2448
+ for local in [0, 1] {
2449
+ Instruction::LocalGet(local).encode(&mut b);
2450
+ Instruction::RefCastNonNull(HeapType::Concrete(box_idx)).encode(&mut b);
2451
+ Instruction::StructGet {
2452
+ struct_type_index: box_idx,
2453
+ field_index: fld(0),
2454
+ }
2455
+ .encode(&mut b);
2456
+ }
2457
+ eq.encode(&mut b);
2458
+ Instruction::Else.encode(&mut b);
2459
+ ends += 1;
2460
+ }
2461
+ match g.class_type_idx.get("Str") {
2462
+ Some(&str_idx) => {
2463
+ tid(&mut b, 0);
2464
+ Instruction::I32Const(TYPE_ID_STR).encode(&mut b);
2465
+ Instruction::I32Eq.encode(&mut b);
2466
+ Instruction::If(i32r).encode(&mut b);
2467
+ for local in [0, 1] {
2468
+ Instruction::LocalGet(local).encode(&mut b);
2469
+ Instruction::RefCastNonNull(HeapType::Concrete(str_idx)).encode(&mut b);
2470
+ }
2471
+ Instruction::Call(string_eq_func).encode(&mut b);
2472
+ Instruction::Else.encode(&mut b);
2473
+ Instruction::I32Const(0).encode(&mut b);
2474
+ Instruction::End.encode(&mut b);
2475
+ }
2476
+ None => Instruction::I32Const(0).encode(&mut b),
2477
+ }
2478
+ for _ in 0..ends {
2479
+ Instruction::End.encode(&mut b);
2480
+ }
2481
+ // close: type-id-ne `if`, null `if`, identity `if`
2482
+ Instruction::End.encode(&mut b);
2483
+ Instruction::End.encode(&mut b);
2484
+ Instruction::End.encode(&mut b);
2485
+ Instruction::End.encode(&mut b);
2486
+ let obj_ref = gcRef(obj);
2487
+ let t = module.addType(&[obj_ref, obj_ref], &[ValType::I32]);
2488
+ module.addFunction(t, &b)
2489
+ }
2490
+
2340
2491
  /// Registers `__string_concat(a: ref Str, b: ref Str) -> ref Str`, a hand-written
2341
2492
  /// runtime helper backing string interpolation. Unwraps both `Str` params down
2342
2493
  /// to their raw `array<i8>`s (`unwrapStrToBytes`) — which each track their own
@@ -2938,18 +3089,19 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
2938
3089
  }
2939
3090
  }
2940
3091
 
2941
- /// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`.
3092
+ /// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`;
3093
+ /// a single-letter generic name is a `TParam`.
2942
3094
  fn paramPlumType(pt: &ast::ParamType) -> PlumType {
2943
3095
  match pt {
2944
- ast::ParamType::Type(t) => plum_checker::plumTypeFromAst(t),
3096
+ ast::ParamType::Type(t) => plum_checker::plumTypeFromAstG(t),
2945
3097
  ast::ParamType::Variadic(t) => {
2946
- PlumType::TVariadic(Box::new(plum_checker::plumTypeFromAst(t)))
3098
+ PlumType::TVariadic(Box::new(plum_checker::plumTypeFromAstG(t)))
2947
3099
  }
2948
3100
  ast::ParamType::Fn(params, ret) => {
2949
- let param_types = params.iter().map(plum_checker::plumTypeFromAst).collect();
3101
+ let param_types = params.iter().map(plum_checker::plumTypeFromAstG).collect();
2950
3102
  let ret_ty = ret
2951
3103
  .as_ref()
2952
- .map(|r| plum_checker::plumTypeFromAst(r))
3104
+ .map(|r| plum_checker::plumTypeFromAstG(r))
2953
3105
  .unwrap_or(PlumType::TUnit);
2954
3106
  PlumType::TFun(param_types, Box::new(ret_ty))
2955
3107
  }
@@ -2961,20 +3113,25 @@ fn paramPlumType(pt: &ast::ParamType) -> PlumType {
2961
3113
  struct RawClosure<'a> {
2962
3114
  ptr: usize,
2963
3115
  closure: &'a ast::Closure,
2964
- param_vts: Vec<ValType>,
2965
3116
  param_ptypes: Vec<PlumType>,
2966
- ret_vt: Option<ValType>,
3117
+ ret_ptype: PlumType,
2967
3118
  free_vars: Vec<(String, PlumType)>,
2968
3119
  }
2969
3120
 
3121
+ fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
3122
+ match arg {
3123
+ ast::Arg::Positional(e) => e,
3124
+ ast::Arg::Keyword { value, .. } => value,
3125
+ ast::Arg::Pair { value, .. } => value,
3126
+ }
3127
+ }
3128
+
2970
- /// Walks a function body (maintaining an evolving type env, exactly like `Collector`)
3129
+ /// Walks a function body to find every closure literal and the free variables it
3130
+ /// captures. A closure's Plum signature is the checker's instantiated `TFun` type
2971
- /// to find every closure literal and determine its concrete signature and captured
3131
+ /// of the literal (`types`); its wasm signature is the erased closure ABI
2972
- /// free variables. A closure passed directly as a `fn(...)`-typed call argument takes
2973
- /// its signature from that declared param type; any other closure (e.g. one assigned to
2974
- /// a local) falls back to the checker's inference of the closure expression itself.
3132
+ /// (`ClosureSigKey`), independent of those types.
2975
- struct ClosureWalker<'a, 'c> {
3133
+ struct ClosureWalker<'a, 't> {
2976
- env: TypeEnv,
2977
- cctx: plum_checker::CheckCtx<'c>,
3134
+ types: &'t HashMap<usize, PlumType>,
2978
3135
  fn_decls: &'a HashMap<String, &'a ast::Fn>,
2979
3136
  found: Vec<RawClosure<'a>>,
2980
3137
  /// Local names bound in the function currently being walked (params, assign
@@ -2989,7 +3146,7 @@ struct ClosureWalker<'a, 'c> {
2989
3146
  named_fn_refs: HashSet<String>,
2990
3147
  }
2991
3148
 
2992
- impl<'a, 'c> ClosureWalker<'a, 'c> {
3149
+ impl<'a, 't> ClosureWalker<'a, 't> {
2993
3150
  fn walkBlock(&mut self, block: &'a ast::Block) {
2994
3151
  for s in &block.stmts {
2995
3152
  self.walkStmt(s);
@@ -3000,27 +3157,24 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
3000
3157
  match stmt {
3001
3158
  ast::Stmt::Assign(a) => {
3002
3159
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
3003
- self.walkExpr(value, None);
3160
+ self.walkExpr(value);
3004
3161
  match target {
3005
3162
  ast::AssignTarget::Var(name) => {
3006
- let ty = plum_checker::inferExpr(value, &self.env, &self.cctx)
3007
- .unwrap_or(PlumType::TInt);
3008
- self.env.insert(name.clone(), TypeScheme::mono(ty));
3009
3163
  self.locals.insert(name.clone());
3010
3164
  }
3011
3165
  ast::AssignTarget::Field(object, _) => {
3012
- self.walkExpr(object, None);
3166
+ self.walkExpr(object);
3013
3167
  }
3014
3168
  }
3015
3169
  }
3016
3170
  }
3017
- ast::Stmt::Return(Some(e)) => self.walkExpr(e, None),
3171
+ ast::Stmt::Return(Some(e)) => self.walkExpr(e),
3018
3172
  ast::Stmt::Return(None) => {}
3019
3173
  ast::Stmt::If(i) => {
3020
- self.walkExpr(&i.condition, None);
3174
+ self.walkExpr(&i.condition);
3021
3175
  self.walkBlock(&i.body);
3022
3176
  for ei in &i.else_ifs {
3023
- self.walkExpr(&ei.condition, None);
3177
+ self.walkExpr(&ei.condition);
3024
3178
  self.walkBlock(&ei.body);
3025
3179
  }
3026
3180
  if let Some(e) = &i.else_ {
@@ -3028,125 +3182,80 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
3028
3182
  }
3029
3183
  }
3030
3184
  ast::Stmt::While(w) => {
3031
- self.walkExpr(&w.condition, None);
3185
+ self.walkExpr(&w.condition);
3032
3186
  self.walkBlock(&w.body);
3033
3187
  }
3034
3188
  ast::Stmt::For(f) => {
3035
- self.walkExpr(&f.iter, None);
3189
+ self.walkExpr(&f.iter);
3036
- let elem_ty = match plum_checker::inferExpr(&f.iter, &self.env, &self.cctx) {
3037
- Ok(PlumType::TVariadic(elem)) => *elem,
3038
- _ => PlumType::TInt,
3039
- };
3040
3190
  for v in &f.vars {
3041
- self.env
3042
- .insert(v.clone(), TypeScheme::mono(elem_ty.clone()));
3043
3191
  self.locals.insert(v.clone());
3044
3192
  }
3045
3193
  self.walkBlock(&f.body);
3046
3194
  }
3047
- ast::Stmt::Expr(e, ..) => self.walkExpr(e, None),
3195
+ ast::Stmt::Expr(e, ..) => self.walkExpr(e),
3048
- ast::Stmt::Assert(c) => self.walkExpr(&c.cond, None),
3196
+ ast::Stmt::Assert(c) => self.walkExpr(&c.cond),
3049
3197
  ast::Stmt::Match(m) => {
3050
3198
  for s in &m.subjects {
3051
- self.walkExpr(s, None);
3199
+ self.walkExpr(s);
3052
3200
  }
3053
3201
  for case in &m.cases {
3202
+ if let Some(g) = &case.guard {
3203
+ self.walkExpr(g);
3204
+ }
3054
3205
  self.walkBlock(&case.body);
3055
3206
  }
3056
3207
  }
3057
3208
  ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
3058
- ast::Stmt::Crash(e) => self.walkExpr(e, None),
3209
+ ast::Stmt::Crash(e) => self.walkExpr(e),
3059
3210
  }
3060
3211
  }
3061
3212
 
3062
- /// `expected_fn` carries the declared `fn(params) -> ret` type when this expression is
3063
- /// a call argument in a `fn`-typed parameter position, giving a closure literal its
3064
- /// concrete signature.
3065
- fn walkExpr(&mut self, expr: &'a ast::Expr, expected_fn: Option<PlumType>) {
3213
+ fn walkExpr(&mut self, expr: &'a ast::Expr) {
3066
3214
  match expr {
3067
- ast::Expr::Closure(cl) => self.recordClosure(cl, expected_fn),
3215
+ ast::Expr::Closure(cl) => self.recordClosure(expr, cl),
3068
3216
  ast::Expr::Binary(b) => {
3069
- self.walkExpr(&b.left, None);
3217
+ self.walkExpr(&b.left);
3070
- self.walkExpr(&b.right, None);
3218
+ self.walkExpr(&b.right);
3071
3219
  }
3072
3220
  ast::Expr::Bool(b) => {
3073
- self.walkExpr(&b.left, None);
3221
+ self.walkExpr(&b.left);
3074
- self.walkExpr(&b.right, None);
3222
+ self.walkExpr(&b.right);
3075
3223
  }
3076
3224
  ast::Expr::Compare(c) => {
3077
- self.walkExpr(&c.left, None);
3225
+ self.walkExpr(&c.left);
3078
- self.walkExpr(&c.right, None);
3226
+ self.walkExpr(&c.right);
3079
3227
  }
3080
- ast::Expr::Not(inner) => self.walkExpr(inner, None),
3228
+ ast::Expr::Not(inner) => self.walkExpr(inner),
3081
- ast::Expr::Try(inner) => self.walkExpr(inner, None),
3229
+ ast::Expr::Try(inner) => self.walkExpr(inner),
3082
- ast::Expr::Unary(u) => self.walkExpr(&u.operand, None),
3230
+ ast::Expr::Unary(u) => self.walkExpr(&u.operand),
3083
- ast::Expr::Paren(inner) => self.walkExpr(inner, None),
3231
+ ast::Expr::Paren(inner) => self.walkExpr(inner),
3084
3232
  ast::Expr::Ternary(t) => {
3085
- self.walkExpr(&t.condition, None);
3233
+ self.walkExpr(&t.condition);
3086
- self.walkExpr(&t.then, None);
3234
+ self.walkExpr(&t.then);
3087
- self.walkExpr(&t.else_, None);
3235
+ self.walkExpr(&t.else_);
3088
3236
  }
3089
3237
  ast::Expr::Elvis(e) => {
3090
- self.walkExpr(&e.left, None);
3238
+ self.walkExpr(&e.left);
3091
- self.walkExpr(&e.right, None);
3239
+ self.walkExpr(&e.right);
3092
3240
  }
3093
3241
  ast::Expr::FnCall(call) => {
3094
- let callee = self.fn_decls.get(&call.name).copied();
3095
- for (i, arg) in call.args.iter().enumerate() {
3096
- let arg_expr = argExprOf(arg);
3242
+ for arg in &call.args {
3097
- let expected = callee
3098
- .and_then(|f| f.params.get(i))
3099
- .and_then(|p| match &p.ty {
3100
- ast::ParamType::Fn(params, ret) => Some(PlumType::TFun(
3101
- params.iter().map(plum_checker::plumTypeFromAst).collect(),
3102
- Box::new(
3103
- ret.as_deref()
3104
- .map(plum_checker::plumTypeFromAst)
3105
- .unwrap_or(PlumType::TUnit),
3106
- ),
3107
- )),
3108
- _ => None,
3109
- });
3110
- self.walkExpr(arg_expr, expected);
3243
+ self.walkExpr(argExprOf(arg));
3111
3244
  }
3112
3245
  }
3113
3246
  ast::Expr::ClassCall(call) => {
3114
3247
  for fa in &call.fields {
3115
- self.walkExpr(&fa.value, None);
3248
+ self.walkExpr(&fa.value);
3249
+ }
3250
+ if let Some(sp) = &call.spread {
3251
+ self.walkExpr(sp);
3116
3252
  }
3117
3253
  }
3118
3254
  ast::Expr::Attribute(a) => {
3119
- self.walkExpr(&a.object, None);
3255
+ self.walkExpr(&a.object);
3120
3256
  if let ast::AttrKind::Method(call) = &a.attr {
3121
- // Unlike the `FnCall` arm above (which can look a callee up
3122
- // directly by bare name in `fn_decls`), a method's signature
3123
- // must be resolved via its RECEIVER's type first — `ctx.methods`
3124
- // is keyed `(class_name, method_name)`, and two unrelated
3125
- // classes' methods sharing a bare name (`List.each`/`Map.each`)
3126
- // would otherwise be ambiguous. Without this lookup, a closure
3127
- // argument to ANY method fell back to guessing its param types
3128
- // from how they're used in its OWN body (see `recordClosure`'s
3129
- // fallback arm) — which silently defaults a non-numeric param
3130
- // (e.g. `reduce`'s `Str` accumulator) to `Int`, miscompiling the
3131
- // closure with the wrong signature entirely.
3132
- let method_sig = plum_checker::inferExpr(&a.object, &self.env, &self.cctx)
3133
- .ok()
3134
- .and_then(|obj_ty| plum_checker::methodReceiverName(&obj_ty))
3135
- .and_then(|class_name| {
3136
- self.cctx
3137
- .methods
3138
- .get(&(class_name, call.name.clone()))
3139
- .cloned()
3140
- });
3141
- for (i, arg) in call.args.iter().enumerate() {
3257
+ for arg in &call.args {
3142
- let expected = match &method_sig {
3143
- Some(PlumType::TFun(params, _)) => params
3144
- .get(i)
3145
- .cloned()
3146
- .filter(|p| matches!(p, PlumType::TFun(_, _))),
3147
- _ => None,
3148
- };
3149
- self.walkExpr(argExprOf(arg), expected);
3258
+ self.walkExpr(argExprOf(arg));
3150
3259
  }
3151
3260
  }
3152
3261
  }
@@ -3165,14 +3274,10 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
3165
3274
  }
3166
3275
  ast::Expr::String(s) => {
3167
3276
  // A `"{...}"` interpolation can embed an arbitrary expression,
3168
- // including a closure literal passed straight to a method call
3277
+ // including a closure literal passed straight to a method call.
3169
- // (`"{l.any(|x| x % 2 == 0)}"`) — walk into it like any other
3170
- // sub-expression, or such a closure is never discovered by this
3171
- // pre-pass and codegen later traps with "closure literal was not
3172
- // discovered by the discovery pre-pass".
3173
3278
  for part in &s.parts {
3174
3279
  if let ast::StringPart::Interp(e) = part {
3175
- self.walkExpr(e, None);
3280
+ self.walkExpr(e);
3176
3281
  }
3177
3282
  }
3178
3283
  }
@@ -3181,301 +3286,40 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
3181
3286
  }
3182
3287
  }
3183
3288
 
3184
- fn recordClosure(&mut self, cl: &'a ast::Closure, expected_fn: Option<PlumType>) {
3289
+ fn recordClosure(&mut self, expr: &'a ast::Expr, cl: &'a ast::Closure) {
3185
- let (param_vts, param_ptypes, ret_vt) = match expected_fn {
3290
+ let (param_ptypes, ret_ptype) = match self.types.get(&(expr as *const ast::Expr as usize)) {
3186
- Some(PlumType::TFun(params, ret)) => {
3291
+ Some(PlumType::TFun(ps, r)) if ps.len() == cl.params.len() => (ps.clone(), (**r).clone()),
3187
- let param_vts = params.iter().map(plumTypeToValtype).collect();
3188
- let ret_vt = match *ret {
3189
- PlumType::TUnit => None,
3190
- other => Some(plumTypeToValtype(&other)),
3191
- };
3192
- (param_vts, params, ret_vt)
3193
- }
3194
- _ => {
3195
- // Not a direct `fn`-typed call argument (e.g. assigned to a local
3292
+ // No usable checker type (never expected for checked source): treat
3196
- // first, then called/passed on later): the checker's own closure
3197
- // inference (`plum_checker::inferExpr` on `Expr::Closure`) gives
3198
- // each param a fresh `TVar` and never actually unifies it against
3293
+ // every param as an Int and the result as Unit.
3199
- // how the param is used in the body — `unify` is a no-op for any
3200
- // `TVar` — so a param that's genuinely Float or a class/pointer
3201
- // silently comes back as an unresolved `TVar`, which
3202
- // `plumTypeToValtype` then defaults to `Int`. If the closure is
3203
- // later called at its real (non-Int) type, the wasm function actually
3204
- // compiled for its body (built from this wrong, Int-assumed signature)
3205
- // won't match the `call_indirect` type the real call site expects —
3206
- // a mismatch `call_indirect` only traps on at runtime, not at compile
3207
- // or validation time.
3208
- //
3209
- // Fix: resolve each param's type from a direct usage in the body
3210
- // first (see `resolveClosureParamTypesFromUsage`) — e.g. `cb = |v|
3211
- // x + v` with a captured `Float` `x` resolves `v` to `Float` from that
3212
- // `Binary` op — then infer the return type against an env where the
3213
- // params are already bound concretely (bypassing the checker's
3214
- // Closure-inference arm entirely, since it always re-binds params to
3215
- // fresh TVars regardless of what's already in the env passed to it).
3216
- let resolved_params = resolveClosureParamTypesFromUsage(cl, &self.env, &self.cctx);
3217
- let param_ptypes: Vec<PlumType> = cl
3218
- .params
3219
- .iter()
3220
- .map(|p| resolved_params.get(p).cloned().unwrap_or(PlumType::TInt))
3294
+ _ => (vec![PlumType::TInt; cl.params.len()], PlumType::TUnit),
3221
- .collect();
3222
- let param_vts: Vec<ValType> = param_ptypes.iter().map(plumTypeToValtype).collect();
3223
-
3224
- let mut body_env = self.env.clone();
3225
- for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
3226
- body_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
3227
- }
3228
- let ret_ty = match cl.body.stmts.last() {
3229
- Some(ast::Stmt::Expr(e, ..)) => {
3230
- plum_checker::inferExpr(e, &body_env, &self.cctx).ok()
3231
- }
3232
- Some(ast::Stmt::Return(Some(e))) => {
3233
- plum_checker::inferExpr(e, &body_env, &self.cctx).ok()
3234
- }
3235
- _ => Some(PlumType::TUnit),
3236
- };
3237
- let ret_vt = match ret_ty {
3238
- Some(PlumType::TUnit) => None,
3239
- Some(PlumType::TVar(_)) | None => Some(ValType::I64), // unresolved: preserve prior Int-default behavior
3240
- Some(other) => Some(plumTypeToValtype(&other)),
3241
- };
3242
- (param_vts, param_ptypes, ret_vt)
3243
- }
3244
3295
  };
3245
- let free_vars = collectFreeVars(cl, &self.env, self.fn_decls);
3296
+ let free_vars = collectFreeVars(cl, self.types, self.fn_decls);
3246
3297
  self.found.push(RawClosure {
3247
3298
  ptr: cl as *const ast::Closure as usize,
3248
3299
  closure: cl,
3249
- param_vts,
3300
+ param_ptypes,
3250
- param_ptypes: param_ptypes.clone(),
3251
- ret_vt,
3301
+ ret_ptype,
3252
3302
  free_vars,
3253
3303
  });
3254
3304
 
3255
3305
  // Recurse into this closure's own body to find any closure literals nested
3256
- // inside it (`|v| |w| v + w`, or a closure literal used inside a `match`/`if`
3257
- // within this one's body). Each nested closure gets registered exactly like a
3258
- // top-level one, seeing this closure's own params as locals in scope — which
3306
+ // inside it, seeing this closure's own params as locals in scope.
3259
- // is also what makes its free-variable analysis correctly capture a name from
3260
- // *this* closure's scope (rather than silently missing it): once discovery
3261
- // finishes, `ctx.closures` holds every closure at every depth before any body
3262
- // is compiled, so the single fixed "compile each registered closure" pass in
3263
- // `compileSource` already handles arbitrary nesting with no further changes.
3264
- let saved_env = self.env.clone();
3265
3307
  let saved_locals = self.locals.clone();
3266
- for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
3308
+ for p in &cl.params {
3267
- self.env.insert(p.clone(), TypeScheme::mono(ty.clone()));
3268
3309
  self.locals.insert(p.clone());
3269
3310
  }
3270
3311
  self.walkBlock(&cl.body);
3271
- self.env = saved_env;
3272
3312
  self.locals = saved_locals;
3273
3313
  }
3274
3314
  }
3275
3315
 
3276
- fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
3277
- match arg {
3278
- ast::Arg::Positional(e) => e,
3279
- ast::Arg::Keyword { value, .. } => value,
3280
- ast::Arg::Pair { value, .. } => value,
3281
- }
3282
- }
3283
-
3284
- /// Resolves as many of a closure's param types as possible from how they're actually
3285
- /// used in its body — e.g. `|v| x + v` with a captured `Float` `x` resolves `v` to
3286
- /// `Float` from that `Binary` op, or `|v| helper(v)` where `helper`'s declared param
3287
- /// type is concrete resolves `v` to that. A param never used in a way that pins down
3288
- /// a concrete type simply doesn't appear in the returned map (callers fall back to
3289
- /// `Int`, matching the prior default). This is intentionally a shallow, best-effort
3290
- /// scan — not full unification — scoped to fixing the specific `call_indirect`
3291
- /// signature-mismatch gap this exists for, not replacing the checker's inference.
3292
- fn resolveClosureParamTypesFromUsage(
3293
- cl: &ast::Closure,
3294
- env: &TypeEnv,
3295
- cctx: &plum_checker::CheckCtx,
3296
- ) -> HashMap<String, PlumType> {
3297
- let params: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
3298
- let mut resolved: HashMap<String, PlumType> = HashMap::new();
3299
- for stmt in &cl.body.stmts {
3300
- scanStmtForParamTypes(stmt, &params, env, cctx, &mut resolved);
3301
- }
3302
- resolved
3303
- }
3304
-
3305
- fn scanStmtForParamTypes(
3306
- stmt: &ast::Stmt,
3307
- params: &std::collections::HashSet<String>,
3308
- env: &TypeEnv,
3309
- cctx: &plum_checker::CheckCtx,
3310
- resolved: &mut HashMap<String, PlumType>,
3311
- ) {
3312
- match stmt {
3313
- ast::Stmt::Assign(a) => {
3314
- for v in &a.values {
3315
- scanExprForParamTypes(v, params, env, cctx, resolved);
3316
- }
3317
- }
3318
- ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e, ..) => {
3319
- scanExprForParamTypes(e, params, env, cctx, resolved);
3320
- }
3321
- ast::Stmt::Assert(c) => {
3322
- scanExprForParamTypes(&c.cond, params, env, cctx, resolved);
3323
- }
3324
- ast::Stmt::If(i) => {
3325
- scanExprForParamTypes(&i.condition, params, env, cctx, resolved);
3326
- for s in &i.body.stmts {
3327
- scanStmtForParamTypes(s, params, env, cctx, resolved);
3328
- }
3329
- for ei in &i.else_ifs {
3330
- scanExprForParamTypes(&ei.condition, params, env, cctx, resolved);
3331
- for s in &ei.body.stmts {
3332
- scanStmtForParamTypes(s, params, env, cctx, resolved);
3333
- }
3334
- }
3335
- if let Some(e) = &i.else_ {
3336
- for s in &e.stmts {
3337
- scanStmtForParamTypes(s, params, env, cctx, resolved);
3338
- }
3339
- }
3340
- }
3341
- ast::Stmt::While(w) => {
3342
- scanExprForParamTypes(&w.condition, params, env, cctx, resolved);
3343
- for s in &w.body.stmts {
3344
- scanStmtForParamTypes(s, params, env, cctx, resolved);
3345
- }
3346
- }
3347
- ast::Stmt::For(f) => {
3348
- scanExprForParamTypes(&f.iter, params, env, cctx, resolved);
3349
- for s in &f.body.stmts {
3350
- scanStmtForParamTypes(s, params, env, cctx, resolved);
3351
- }
3352
- }
3353
- _ => {}
3354
- }
3355
- }
3356
-
3357
- fn scanExprForParamTypes(
3358
- expr: &ast::Expr,
3359
- params: &std::collections::HashSet<String>,
3360
- env: &TypeEnv,
3361
- cctx: &plum_checker::CheckCtx,
3362
- resolved: &mut HashMap<String, PlumType>,
3363
- ) {
3364
- match expr {
3365
- ast::Expr::Binary(b) => {
3366
- tryResolveParamFromPair(&b.left, &b.right, params, env, cctx, resolved);
3367
- scanExprForParamTypes(&b.left, params, env, cctx, resolved);
3368
- scanExprForParamTypes(&b.right, params, env, cctx, resolved);
3369
- }
3370
- ast::Expr::Compare(c) => {
3371
- tryResolveParamFromPair(&c.left, &c.right, params, env, cctx, resolved);
3372
- scanExprForParamTypes(&c.left, params, env, cctx, resolved);
3373
- scanExprForParamTypes(&c.right, params, env, cctx, resolved);
3374
- }
3375
- ast::Expr::Bool(b) => {
3376
- scanExprForParamTypes(&b.left, params, env, cctx, resolved);
3377
- scanExprForParamTypes(&b.right, params, env, cctx, resolved);
3378
- }
3379
- ast::Expr::Not(inner) => scanExprForParamTypes(inner, params, env, cctx, resolved),
3380
- ast::Expr::Unary(u) => scanExprForParamTypes(&u.operand, params, env, cctx, resolved),
3381
- ast::Expr::Paren(inner) => scanExprForParamTypes(inner, params, env, cctx, resolved),
3382
- ast::Expr::Ternary(t) => {
3383
- scanExprForParamTypes(&t.condition, params, env, cctx, resolved);
3384
- scanExprForParamTypes(&t.then, params, env, cctx, resolved);
3385
- scanExprForParamTypes(&t.else_, params, env, cctx, resolved);
3386
- }
3387
- ast::Expr::Elvis(e) => {
3388
- scanExprForParamTypes(&e.left, params, env, cctx, resolved);
3389
- scanExprForParamTypes(&e.right, params, env, cctx, resolved);
3390
- }
3391
- ast::Expr::FnCall(call) => {
3392
- if let Ok(PlumType::TFun(param_types, _)) = plum_checker::lookup(env, &call.name) {
3393
- for (arg, expected) in call.args.iter().zip(param_types.iter()) {
3394
- let arg_expr = argExprOf(arg);
3395
- if let ast::Expr::Var(n) = arg_expr {
3396
- if params.contains(n)
3397
- && !resolved.contains_key(n)
3398
- && !matches!(expected, PlumType::TVar(_))
3399
- {
3400
- resolved.insert(n.clone(), expected.clone());
3401
- }
3402
- }
3403
- }
3404
- }
3405
- for arg in &call.args {
3406
- scanExprForParamTypes(argExprOf(arg), params, env, cctx, resolved);
3407
- }
3408
- }
3409
- ast::Expr::ClassCall(call) => {
3410
- for fa in &call.fields {
3411
- scanExprForParamTypes(&fa.value, params, env, cctx, resolved);
3412
- }
3413
- }
3414
- ast::Expr::Attribute(a) => {
3415
- // `c.field` on a bare, unresolved param implies `c`'s type is whichever
3416
- // enum declares a variant with that field name — ambiguous if more than
3417
- // one variant has a field by that name, but resolvable in the common case.
3418
- if let ast::AttrKind::Field(field_name) = &a.attr {
3419
- if let ast::Expr::Var(n) = &a.object {
3420
- if params.contains(n) && !resolved.contains_key(n) {
3421
- let mut matches = cctx.enum_variants.values().filter(|info| {
3422
- info.field_names.iter().any(|fname| fname == field_name)
3423
- });
3424
- if let (Some(info), None) = (matches.next(), matches.next()) {
3425
- resolved.insert(n.clone(), PlumType::TNamed(info.enum_name.clone()));
3426
- }
3427
- }
3428
- }
3429
- }
3430
- scanExprForParamTypes(&a.object, params, env, cctx, resolved);
3431
- if let ast::AttrKind::Method(call) = &a.attr {
3432
- for arg in &call.args {
3433
- scanExprForParamTypes(argExprOf(arg), params, env, cctx, resolved);
3434
- }
3435
- }
3436
- }
3437
- _ => {}
3438
- }
3439
- }
3440
-
3441
- /// If either side of a `Binary`/`Compare` operand pair is a bare reference to an
3442
- /// unresolved param and the *other* side has a concrete (non-`TVar`) inferred type,
3443
- /// binds the param to that type.
3444
- fn tryResolveParamFromPair(
3445
- left: &ast::Expr,
3446
- right: &ast::Expr,
3447
- params: &std::collections::HashSet<String>,
3448
- env: &TypeEnv,
3449
- cctx: &plum_checker::CheckCtx,
3450
- resolved: &mut HashMap<String, PlumType>,
3451
- ) {
3452
- if let ast::Expr::Var(n) = left {
3453
- if params.contains(n) && !resolved.contains_key(n) {
3454
- if let Ok(ty) = plum_checker::inferExpr(right, env, cctx) {
3455
- if !matches!(ty, PlumType::TVar(_)) {
3456
- resolved.insert(n.clone(), ty);
3457
- }
3458
- }
3459
- }
3460
- }
3461
- if let ast::Expr::Var(n) = right {
3462
- if params.contains(n) && !resolved.contains_key(n) {
3463
- if let Ok(ty) = plum_checker::inferExpr(left, env, cctx) {
3464
- if !matches!(ty, PlumType::TVar(_)) {
3465
- resolved.insert(n.clone(), ty);
3466
- }
3467
- }
3468
- }
3469
- }
3470
- }
3471
-
3472
3316
  /// Determines a closure's captured free variables: every `Var` referenced in its body
3473
3317
  /// that is neither one of the closure's own params nor assigned locally inside the body,
3474
3318
  /// in first-appearance order. Each free variable's type is looked up in the *enclosing*
3475
3319
  /// scope's type env.
3476
3320
  fn collectFreeVars(
3477
3321
  cl: &ast::Closure,
3478
- env: &TypeEnv,
3322
+ env: &HashMap<usize, PlumType>,
3479
3323
  fn_decls: &HashMap<String, &ast::Fn>,
3480
3324
  ) -> Vec<(String, PlumType)> {
3481
3325
  let mut bound: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
@@ -3545,7 +3389,7 @@ fn fvCollectRefsBlock(
3545
3389
  bound: &std::collections::HashSet<String>,
3546
3390
  seen: &mut std::collections::HashSet<String>,
3547
3391
  free: &mut Vec<(String, PlumType)>,
3548
- env: &TypeEnv,
3392
+ env: &HashMap<usize, PlumType>,
3549
3393
  fn_decls: &HashMap<String, &ast::Fn>,
3550
3394
  ) {
3551
3395
  for s in &block.stmts {
@@ -3606,7 +3450,7 @@ fn fvCollectRefsExpr(
3606
3450
  bound: &std::collections::HashSet<String>,
3607
3451
  seen: &mut std::collections::HashSet<String>,
3608
3452
  free: &mut Vec<(String, PlumType)>,
3609
- env: &TypeEnv,
3453
+ env: &HashMap<usize, PlumType>,
3610
3454
  fn_decls: &HashMap<String, &ast::Fn>,
3611
3455
  ) {
3612
3456
  match expr {
@@ -3616,10 +3460,22 @@ fn fvCollectRefsExpr(
3616
3460
  // reference (see `named_fn_values`), not loaded from an enclosing local.
3617
3461
  let is_named_fn_ref = fn_decls.get(name).is_some_and(|f| f.type_param.is_none());
3618
3462
  if !is_named_fn_ref && !bound.contains(name) && seen.insert(name.clone()) {
3463
+ let ty = env
3464
+ .get(&(expr as *const ast::Expr as usize))
3465
+ .cloned()
3619
- let ty = plum_checker::lookup(env, name).unwrap_or(PlumType::TInt);
3466
+ .unwrap_or(PlumType::TInt);
3620
3467
  free.push((name.clone(), ty));
3621
3468
  }
3622
3469
  }
3470
+ ast::Expr::Self_ => {
3471
+ if !bound.contains("self") && seen.insert("self".to_string()) {
3472
+ let ty = env
3473
+ .get(&(expr as *const ast::Expr as usize))
3474
+ .cloned()
3475
+ .unwrap_or(PlumType::TInt);
3476
+ free.push(("self".to_string(), ty));
3477
+ }
3478
+ }
3623
3479
  ast::Expr::Binary(b) => {
3624
3480
  fvCollectRefsExpr(&b.left, bound, seen, free, env, fn_decls);
3625
3481
  fvCollectRefsExpr(&b.right, bound, seen, free, env, fn_decls);
@@ -3654,9 +3510,19 @@ fn fvCollectRefsExpr(
3654
3510
  for fa in &call.fields {
3655
3511
  fvCollectRefsExpr(&fa.value, bound, seen, free, env, fn_decls);
3656
3512
  }
3513
+ if let Some(sp) = &call.spread {
3514
+ fvCollectRefsExpr(sp, bound, seen, free, env, fn_decls);
3515
+ }
3657
3516
  }
3517
+ ast::Expr::String(sx) => {
3518
+ for part in &sx.parts {
3519
+ if let ast::StringPart::Interp(e) = part {
3520
+ fvCollectRefsExpr(e, bound, seen, free, env, fn_decls);
3521
+ }
3522
+ }
3523
+ }
3658
- ast::Expr::Attribute(a) => {
3524
+ ast::Expr::Attribute(a) => {
3659
- fvCollectRefsExpr(&a.object, bound, seen, free, env, fn_decls);
3525
+ fvCollectRefsExpr(&a.object, bound, seen, free, env, fn_decls);
3660
3526
  if let ast::AttrKind::Method(call) = &a.attr {
3661
3527
  for arg in &call.args {
3662
3528
  fvCollectRefsExpr(argExprOf(arg), bound, seen, free, env, fn_decls);
@@ -3690,8 +3556,9 @@ fn fvCollectRefsExpr(
3690
3556
  /// its inferred type, (2) how many `ClassCall` scratch temporaries it needs, and (3)
3691
3557
  /// the subject type for every `match` statement (for its own scratch temporary).
3692
3558
  struct Collector<'a> {
3693
- env: TypeEnv,
3559
+ /// The checker's instantiated type of every expression.
3694
- cctx: plum_checker::CheckCtx<'a>,
3560
+ types: &'a HashMap<usize, PlumType>,
3561
+ enum_variants: &'a EnumVariants,
3695
3562
  named: Vec<(String, PlumType)>,
3696
3563
  named_set: std::collections::HashSet<String>,
3697
3564
  /// One scratch-local type per subject (usually one, more for `match a, b, ...`).
@@ -3723,9 +3590,16 @@ struct Collector<'a> {
3723
3590
  impl<'a> Collector<'a> {
3724
3591
  fn bind(&mut self, name: &str, ty: PlumType) {
3725
3592
  if self.named_set.insert(name.to_string()) {
3726
- self.named.push((name.to_string(), ty.clone()));
3593
+ self.named.push((name.to_string(), ty));
3727
3594
  }
3595
+ }
3596
+
3597
+ /// The checker's type of `expr` (`Int` if the node was never recorded).
3598
+ fn ty(&self, expr: &ast::Expr) -> PlumType {
3599
+ self.types
3728
- self.env.insert(name.to_string(), TypeScheme::mono(ty));
3600
+ .get(&(expr as *const ast::Expr as usize))
3601
+ .cloned()
3602
+ .unwrap_or(PlumType::TInt)
3729
3603
  }
3730
3604
 
3731
3605
  /// Binds every `Name` sub-pattern anywhere inside `pat` (at any nesting depth) to
@@ -3748,7 +3622,7 @@ impl<'a> Collector<'a> {
3748
3622
  match pat {
3749
3623
  ast::CasePattern::Name(n) => {
3750
3624
  let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
3751
- && self.cctx.enum_variants.contains_key(n);
3625
+ && self.enum_variants.contains_key(n);
3752
3626
  if !is_variant {
3753
3627
  self.bind(n, ty.clone());
3754
3628
  }
@@ -3759,8 +3633,10 @@ impl<'a> Collector<'a> {
3759
3633
  self.next_nested_class_slot += 1;
3760
3634
  self.nested_class_scratch.insert(key, slot);
3761
3635
  self.nested_class_scratch_types.push(name.clone());
3762
- if let Some(info) = self.cctx.enum_variants.get(name) {
3636
+ if let Some(info) = self.enum_variants.get(name) {
3637
+ // The payload types as instantiated by the matched value's type
3638
+ // arguments (`Ok(v)` against `Try[Int, E]` binds `v: Int`).
3763
- let field_types = info.field_types.clone();
3639
+ let field_types = instFields(info, ty);
3764
3640
  for (f, fty) in fields.iter().zip(field_types.iter()) {
3765
3641
  self.collectPattern(f, fty);
3766
3642
  }
@@ -3783,23 +3659,7 @@ impl<'a> Collector<'a> {
3783
3659
  self.walkExpr(value);
3784
3660
  match target {
3785
3661
  ast::AssignTarget::Var(name) => {
3786
- let ty = if matches!(value, ast::Expr::Closure(_)) {
3662
+ let ty = self.ty(value);
3787
- // The checker's own closure inference (`inferExpr` on
3788
- // `Expr::Closure`) infers the return type by recursively
3789
- // inferring the body's tail expression with each param bound
3790
- // to a fresh, unconstrained `TVar` — e.g. a captured/param
3791
- // attribute access (`c.age`) on a `TVar`-typed object isn't a
3792
- // known class, so it errors out entirely, and this call site
3793
- // then silently defaults to `TInt` — the *wrong* wasm local
3794
- // width for what's actually always an `i32` pointer. All that
3795
- // actually matters here is the local's wasm width, and every
3796
- // closure value is an i32 pointer regardless of its
3797
- // parameter/return types, so skip inference entirely.
3798
- PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
3799
- } else {
3800
- plum_checker::inferExpr(value, &self.env, &self.cctx)
3801
- .unwrap_or(PlumType::TInt)
3802
- };
3803
3663
  self.bind(name, ty);
3804
3664
  }
3805
3665
  ast::AssignTarget::Field(object, _) => {
@@ -3827,8 +3687,7 @@ impl<'a> Collector<'a> {
3827
3687
  }
3828
3688
  ast::Stmt::For(f) => {
3829
3689
  self.walkExpr(&f.iter);
3830
- let iter_ty = plum_checker::inferExpr(&f.iter, &self.env, &self.cctx)
3690
+ let iter_ty = self.ty(&f.iter);
3831
- .unwrap_or(PlumType::TInt);
3832
3691
  if let PlumType::TVariadic(elem) = &iter_ty {
3833
3692
  let idx = self.next_variadic_for_slot;
3834
3693
  self.next_variadic_for_slot += 1;
@@ -3852,13 +3711,12 @@ impl<'a> Collector<'a> {
3852
3711
  .iter()
3853
3712
  .map(|s| {
3854
3713
  self.walkExpr(s);
3855
- plum_checker::inferExpr(s, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
3714
+ self.ty(s)
3856
3715
  })
3857
3716
  .collect();
3858
3717
  self.match_scratch
3859
3718
  .insert(m as *const ast::Match as usize, subject_types.clone());
3860
3719
  for case in &m.cases {
3861
- let saved = self.env.clone();
3862
3720
  for (pat, subject_ty) in case.patterns.iter().zip(subject_types.iter()) {
3863
3721
  self.collectPattern(pat, subject_ty);
3864
3722
  }
@@ -3866,7 +3724,6 @@ impl<'a> Collector<'a> {
3866
3724
  self.walkExpr(g);
3867
3725
  }
3868
3726
  self.walkBlock(&case.body);
3869
- self.env = saved;
3870
3727
  }
3871
3728
  }
3872
3729
  ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
@@ -3906,12 +3763,8 @@ impl<'a> Collector<'a> {
3906
3763
  // in each branch, not by declaring this local pre-narrowed.
3907
3764
  ast::Expr::Try(inner) => {
3908
3765
  let key = expr as *const ast::Expr as usize;
3766
+ let ty = self.ty(inner);
3909
- let ty = plum_checker::inferExpr(inner, &self.env, &self.cctx)
3767
+ let name = plum_checker::nominalName(&ty).unwrap_or("").to_string();
3910
- .unwrap_or(PlumType::TVar("_".to_string()));
3911
- let name = match &ty {
3912
- PlumType::TNamed(n) => n.clone(),
3913
- _ => String::new(),
3914
- };
3915
3768
  let slot = self.next_nested_class_slot;
3916
3769
  self.next_nested_class_slot += 1;
3917
3770
  self.nested_class_scratch.insert(key, slot);
@@ -3931,12 +3784,8 @@ impl<'a> Collector<'a> {
3931
3784
  // `return`.
3932
3785
  ast::Expr::Elvis(e) => {
3933
3786
  let key = expr as *const ast::Expr as usize;
3787
+ let ty = self.ty(&e.left);
3934
- let ty = plum_checker::inferExpr(&e.left, &self.env, &self.cctx)
3788
+ let name = plum_checker::nominalName(&ty).unwrap_or("").to_string();
3935
- .unwrap_or(PlumType::TVar("_".to_string()));
3936
- let name = match &ty {
3937
- PlumType::TNamed(n) => n.clone(),
3938
- _ => String::new(),
3939
- };
3940
3789
  let slot = self.next_nested_class_slot;
3941
3790
  self.next_nested_class_slot += 1;
3942
3791
  self.nested_class_scratch.insert(key, slot);
@@ -3994,39 +3843,24 @@ fn compileFnBody(
3994
3843
  ) -> Result<Vec<u8>, String> {
3995
3844
  let mut body = Vec::new();
3996
3845
 
3846
+ // Fallback type env (only consulted for synthesized nodes the checker never
3847
+ // saw): globals, `self`, and the params under their DECLARED (generic) types.
3997
3848
  let mut base_env = ctx.global_env.clone();
3849
+ let mut decl_types: HashMap<String, PlumType> = HashMap::new();
3998
3850
  if let Some(recv) = &f.type_param {
3999
- base_env.insert(
4000
- "self".to_string(),
4001
- TypeScheme::mono(plum_checker::plumTypeFromName(recv)),
3851
+ let self_ty = plum_checker::plumTypeFromName(recv);
3852
+ base_env.insert("self".to_string(), TypeScheme::mono(self_ty.clone()));
4002
- );
3853
+ decl_types.insert("self".to_string(), self_ty);
4003
3854
  }
4004
3855
  for p in &f.params {
4005
- let ty = match &p.ty {
3856
+ let ty = paramPlumType(&p.ty);
4006
- ast::ParamType::Type(t) => plum_checker::plumTypeFromAst(t),
4007
- ast::ParamType::Variadic(t) => {
4008
- PlumType::TVariadic(Box::new(plum_checker::plumTypeFromAst(t)))
4009
- }
4010
- ast::ParamType::Fn(params, ret) => {
4011
- let param_types = params.iter().map(plum_checker::plumTypeFromAst).collect();
4012
- let ret_ty = ret
4013
- .as_ref()
4014
- .map(|r| plum_checker::plumTypeFromAst(r))
4015
- .unwrap_or(PlumType::TUnit);
4016
- PlumType::TFun(param_types, Box::new(ret_ty))
4017
- }
4018
- };
4019
- base_env.insert(p.name.clone(), TypeScheme::mono(ty));
3857
+ base_env.insert(p.name.clone(), TypeScheme::mono(ty.clone()));
3858
+ decl_types.insert(p.name.clone(), ty);
4020
3859
  }
4021
3860
 
4022
3861
  let mut collector = Collector {
4023
- env: base_env.clone(),
4024
- cctx: checkCtxOf(
4025
- &ctx.methods,
3862
+ types: &ctx.types,
4026
- &ctx.enum_variants,
3863
+ enum_variants: &ctx.enum_variants,
4027
- &ctx.enum_params,
4028
- &ctx.min_required,
4029
- ),
4030
3864
  named: Vec::new(),
4031
3865
  named_set: Default::default(),
4032
3866
  match_scratch: HashMap::new(),
@@ -4043,6 +3877,9 @@ fn compileFnBody(
4043
3877
  // `main() -> Int = each(|v| v)`), which needs construction scratch slots.
4044
3878
  collector.walkExpr(e);
4045
3879
  }
3880
+ for (name, ty) in &collector.named {
3881
+ decl_types.entry(name.clone()).or_insert_with(|| ty.clone());
3882
+ }
4046
3883
  // ---- assign local indices: [self?][params][named...][classcall scratch...][match scratch...] ----
4047
3884
  let mut locals: HashMap<String, u32> = HashMap::new();
4048
3885
  let mut groups: Vec<ValType> = Vec::new();
@@ -4118,11 +3955,14 @@ fn compileFnBody(
4118
3955
  variadic_for_scratch: collector.variadic_for_scratch,
4119
3956
  func_ids: &ctx.func_ids,
4120
3957
  func_sigs: &ctx.func_sigs,
3958
+ types: &ctx.types,
3959
+ local_decl_types: decl_types,
4121
3960
  closures: &ctx.closures,
4122
3961
  closure_call_types: &ctx.closure_call_types,
4123
3962
  named_fn_values: &ctx.named_fn_values,
4124
3963
  string_concat_func: ctx.string_concat_func,
4125
3964
  string_eq_func: ctx.string_eq_func,
3965
+ dyn_eq_func: ctx.dyn_eq_func,
4126
3966
  int_to_string_func: ctx.int_to_string_func,
4127
3967
  methods: &ctx.methods,
4128
3968
  enum_variants: &ctx.enum_variants,
@@ -4131,10 +3971,11 @@ fn compileFnBody(
4131
3971
  gc_types: &ctx.gc_types,
4132
3972
  singleton_globals: &ctx.singleton_globals,
4133
3973
  type_env: RefCell::new(base_env),
4134
- closure_local_sigs: RefCell::new(HashMap::new()),
4135
3974
  loop_extra_depth: Cell::new(0),
3975
+ in_closure: false,
4136
- ret_type: f.returns.as_ref().map(|r| plum_checker::plumTypeFromAst(r)),
3976
+ ret_type: f.returns.as_ref().map(plum_checker::plumTypeFromAstG),
4137
3977
  match_subject_types: RefCell::new(HashMap::new()),
3978
+ subject_ptypes: RefCell::new(HashMap::new()),
4138
3979
  };
4139
3980
 
4140
3981
  let result_vt = retTypeToWasm(f.returns.as_ref());
@@ -4382,47 +4223,36 @@ fn compileStmt(
4382
4223
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
4383
4224
  match target {
4384
4225
  ast::AssignTarget::Var(name) => {
4385
- // See the matching comment in `Collector::walkStmt`: the checker's
4386
- // closure inference is unreliable (can error out entirely depending
4387
- // on the body), but every closure value is an i32 pointer regardless
4388
- // of its real signature, so don't bother inferring it at all here.
4389
- let vty = if matches!(value, ast::Expr::Closure(_)) {
4390
- PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
4391
- } else {
4392
- inferLocalType(value, ctx)
4226
+ let vty = inferLocalType(value, ctx);
4393
- };
4394
4227
  compileExpr(value, body, ctx, state)?;
4395
4228
  let idx = ctx
4396
4229
  .locals
4397
4230
  .get(name)
4398
4231
  .copied()
4399
4232
  .ok_or_else(|| format!("undeclared local '{}'", name))?;
4233
+ // Convert to the representation the local was declared with
4234
+ // (differs only across an erased/instantiated edge).
4235
+ if let Some(decl) = ctx.local_decl_types.get(name) {
4236
+ coerceVt(
4237
+ plumTypeToValtype(&vty),
4238
+ plumTypeToValtype(decl),
4239
+ body,
4240
+ ctx.gc_types,
4241
+ );
4242
+ }
4400
4243
  Instruction::LocalSet(idx).encode(body);
4401
4244
  ctx.type_env
4402
4245
  .borrow_mut()
4403
4246
  .insert(name.clone(), TypeScheme::mono(vty));
4404
- // If this assigns a closure *literal*, remember its exact, already-
4405
- // correct signature (computed by the discovery pass) so a later call
4406
- // to it doesn't have to re-derive one — see `closure_local_sigs`.
4407
- if let ast::Expr::Closure(cl) = value {
4408
- let key = cl.as_ref() as *const ast::Closure as usize;
4409
- if let Some(info) = ctx.closures.get(&key) {
4410
- let mut sig_params = vec![ValType::Ref(RefType::ANYREF)];
4411
- sig_params.extend(info.param_vts.iter().copied());
4412
- ctx.closure_local_sigs
4413
- .borrow_mut()
4414
- .insert(name.clone(), (sig_params, info.ret_vt));
4415
- }
4416
- }
4417
4247
  }
4418
4248
  ast::AssignTarget::Field(object, field_name) => {
4419
4249
  let obj_ty = inferLocalType(object, ctx);
4420
- let class_name = match &obj_ty {
4250
+ let class_name = match plum_checker::nominalName(&obj_ty) {
4421
- PlumType::TNamed(n) => n.clone(),
4251
+ Some(n) => n.to_string(),
4422
- other => {
4252
+ None => {
4423
4253
  return Err(format!(
4424
4254
  "codegen: cannot assign field '{}' on non-class type {}",
4425
- field_name, other
4255
+ field_name, obj_ty
4426
4256
  ))
4427
4257
  }
4428
4258
  };
@@ -4456,7 +4286,7 @@ fn compileStmt(
4456
4286
  )
4457
4287
  })?;
4458
4288
  compileExpr(object, body, ctx, state)?;
4459
- compileExpr(value, body, ctx, state)?;
4289
+ compileExprInto(value, &params[field_idx].1, body, ctx, state)?;
4460
4290
  Instruction::StructSet {
4461
4291
  struct_type_index: super_type_idx,
4462
4292
  field_index: fld(field_idx),
@@ -4487,7 +4317,7 @@ fn compileStmt(
4487
4317
  compileExpr(object, body, ctx, state)?;
4488
4318
  Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))
4489
4319
  .encode(body);
4490
- compileExpr(value, body, ctx, state)?;
4320
+ compileExprInto(value, &info.field_types[field_idx], body, ctx, state)?;
4491
4321
  Instruction::StructSet {
4492
4322
  struct_type_index: variant_type_idx,
4493
4323
  field_index: fld(field_idx),
@@ -4500,10 +4330,23 @@ fn compileStmt(
4500
4330
  }
4501
4331
  }
4502
4332
  ast::Stmt::Return(Some(e)) => {
4333
+ match &ctx.ret_type {
4334
+ Some(rt) => compileExprInto(e, rt, body, ctx, state)?,
4335
+ None => compileExpr(e, body, ctx, state)?,
4336
+ }
4337
+ if ctx.in_closure {
4338
+ // Closure ABI: the result leaves as an `anyref`.
4503
- compileExprAs(e, ctx.ret_type.as_ref().map(plumTypeToValtype), body, ctx, state)?;
4339
+ let rt_vt = ctx.ret_type.as_ref().map(plumTypeToValtype);
4340
+ if let Some(vt) = rt_vt {
4341
+ coerceVt(vt, ValType::Ref(RefType::ANYREF), body, ctx.gc_types);
4342
+ }
4343
+ }
4504
4344
  Instruction::Return.encode(body);
4505
4345
  }
4506
4346
  ast::Stmt::Return(None) => {
4347
+ if ctx.in_closure {
4348
+ Instruction::RefNull(HeapType::ANY).encode(body);
4349
+ }
4507
4350
  Instruction::Return.encode(body);
4508
4351
  }
4509
4352
  ast::Stmt::If(if_) => {
@@ -4681,12 +4524,13 @@ fn compileStmt(
4681
4524
  fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
4682
4525
  match expr {
4683
4526
  ast::Expr::FnCall(call) => {
4684
- if ctx.locals.contains_key(&call.name) {
4527
+ if ctx.locals.contains_key(&call.name)
4528
+ && matches!(
4529
+ ctx.local_decl_types.get(&call.name),
4685
- if let PlumType::TFun(_, ret) =
4530
+ Some(PlumType::TFun(_, _))
4686
- inferLocalType(&ast::Expr::Var(call.name.clone()), ctx)
4531
+ )
4687
- {
4532
+ {
4688
- return !matches!(*ret, PlumType::TUnit);
4533
+ return !closureCallReturnsUnit(&call.name, ctx);
4689
- }
4690
4534
  }
4691
4535
  ctx.func_sigs
4692
4536
  .get(&call.name)
@@ -4695,17 +4539,38 @@ fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
4695
4539
  }
4696
4540
  ast::Expr::Attribute(attr) => match &attr.attr {
4697
4541
  ast::AttrKind::Method(call) => {
4542
+ let obj_ty = inferLocalType(&attr.object, ctx);
4543
+ // A call on an erased or trait-typed receiver dispatches through a
4544
+ // trait method whose declared return type decides.
4545
+ let trait_owner = match &obj_ty {
4546
+ PlumType::TParam(_) => dynOwnerForMethod(&call.name, call.args.len(), ctx),
4547
+ t => plum_checker::nominalName(t)
4548
+ .filter(|n| {
4549
+ ctx.gc_types.class_type_idx.get(*n)
4550
+ == Some(&ctx.gc_types.obj_type_idx)
4551
+ })
4552
+ .map(|n| {
4553
+ if ctx.methods.contains_key(&(n.to_string(), call.name.clone())) {
4554
+ n.to_string()
4555
+ } else {
4556
+ dynOwnerForMethod(&call.name, call.args.len(), ctx)
4557
+ .unwrap_or_else(|| n.to_string())
4558
+ }
4559
+ }),
4560
+ };
4561
+ if let Some(owner) = trait_owner {
4562
+ return match ctx.methods.get(&(owner, call.name.clone())) {
4563
+ Some(PlumType::TFun(_, ret)) => !matches!(**ret, PlumType::TUnit),
4564
+ _ => true,
4565
+ };
4566
+ }
4698
4567
  // `methodReceiverName` (not a bare `TNamed` match) so this also
4699
4568
  // covers a Unit-returning method called in statement position on
4700
4569
  // a BUILTIN primitive receiver (`Int`/`Float`/`Bool`/`Str`/`Byte`/
4701
4570
  // `[]Byte`) — e.g. `self.data.set(...)` on a `[]Byte` field — not
4702
- // just an ordinary class. Without this, such a call was wrongly
4703
- // assumed to leave a value on the stack, emitting a `Drop` with
4704
- // nothing to drop.
4571
+ // just an ordinary class.
4705
- if let Some(class_name) =
4706
- plum_checker::methodReceiverName(&inferLocalType(&attr.object, ctx))
4572
+ if let Some(class_name) = plum_checker::methodReceiverName(&obj_ty) {
4707
- {
4708
- let key = format!("{}::{}", class_name, call.name);
4573
+ let (_, key, _) = resolveMethodKey(&class_name, &call.name, ctx);
4709
4574
  ctx.func_sigs
4710
4575
  .get(&key)
4711
4576
  .map(|s| s.ret.is_some())
@@ -4902,6 +4767,9 @@ fn compileMatch(
4902
4767
  let subject_ty = inferLocalType(subject, ctx);
4903
4768
  let subject_vt = plumTypeToValtype(&subject_ty);
4904
4769
  let scratch_local = ctx.match_scratch_base + base_slot + i as u32;
4770
+ ctx.subject_ptypes
4771
+ .borrow_mut()
4772
+ .insert(scratch_local, subject_ty.clone());
4905
4773
  if matches!(subject_ty, PlumType::TTags(_)) {
4906
4774
  ctx.match_subject_types
4907
4775
  .borrow_mut()
@@ -5297,6 +5165,11 @@ fn compileVariantConstructorArm(
5297
5165
  )
5298
5166
  })?;
5299
5167
  let field_types = info.field_types.clone();
5168
+ // Payload types instantiated by the subject's type arguments.
5169
+ let inst_types = match ctx.subject_ptypes.borrow().get(&scratch_local) {
5170
+ Some(sty) => instFields(info, sty),
5171
+ None => field_types.clone(),
5172
+ };
5300
5173
 
5301
5174
  // This pattern's own narrowly-typed scratch local (declared with variant's exact
5302
5175
  // concrete ref type — see `nested_class_scratch_types`), distinct from
@@ -5323,6 +5196,7 @@ fn compileVariantConstructorArm(
5323
5196
  compileFieldPatterns(
5324
5197
  fields,
5325
5198
  &field_types,
5199
+ &inst_types,
5326
5200
  0,
5327
5201
  narrow_local,
5328
5202
  variant_idx,
@@ -5372,6 +5246,7 @@ fn compileVariantConstructorArm(
5372
5246
  fn compileFieldPatterns(
5373
5247
  fields: &[ast::CasePattern],
5374
5248
  field_types: &[PlumType],
5249
+ inst_types: &[PlumType],
5375
5250
  fpos: usize,
5376
5251
  container_local: u32,
5377
5252
  container_type_idx: u32,
@@ -5388,7 +5263,12 @@ fn compileFieldPatterns(
5388
5263
  return on_match(body, state);
5389
5264
  }
5390
5265
  let pat = &fields[fpos];
5266
+ // The field's DECLARED type fixes its stored representation (an erased `T` slot
5267
+ // is a `ref null $Obj`); its INSTANTIATED type is what a binding/pattern sees.
5391
- let field_ty = &field_types[fpos];
5268
+ let decl_ty = &field_types[fpos];
5269
+ let field_ty = &inst_types[fpos];
5270
+ let decl_vt = plumTypeToValtype(decl_ty);
5271
+ let inst_vt = plumTypeToValtype(field_ty);
5392
5272
 
5393
5273
  if let PlumType::TTags(tags) = field_ty {
5394
5274
  if isTagPattern(pat) {
@@ -5402,6 +5282,7 @@ fn compileFieldPatterns(
5402
5282
  field_index: fld(fpos),
5403
5283
  }
5404
5284
  .encode(b);
5285
+ coerceVt(decl_vt, inst_vt, b, ctx.gc_types);
5405
5286
  },
5406
5287
  result_vt,
5407
5288
  body,
@@ -5411,6 +5292,7 @@ fn compileFieldPatterns(
5411
5292
  compileFieldPatterns(
5412
5293
  fields,
5413
5294
  field_types,
5295
+ inst_types,
5414
5296
  fpos + 1,
5415
5297
  container_local,
5416
5298
  container_type_idx,
@@ -5443,6 +5325,7 @@ fn compileFieldPatterns(
5443
5325
  ast::CasePattern::Wildcard => compileFieldPatterns(
5444
5326
  fields,
5445
5327
  field_types,
5328
+ inst_types,
5446
5329
  fpos + 1,
5447
5330
  container_local,
5448
5331
  container_type_idx,
@@ -5471,6 +5354,7 @@ fn compileFieldPatterns(
5471
5354
  field_index: fld(fpos),
5472
5355
  }
5473
5356
  .encode(body);
5357
+ coerceVt(decl_vt, inst_vt, body, ctx.gc_types);
5474
5358
  Instruction::LocalSet(idx).encode(body);
5475
5359
  ctx.type_env
5476
5360
  .borrow_mut()
@@ -5478,6 +5362,7 @@ fn compileFieldPatterns(
5478
5362
  compileFieldPatterns(
5479
5363
  fields,
5480
5364
  field_types,
5365
+ inst_types,
5481
5366
  fpos + 1,
5482
5367
  container_local,
5483
5368
  container_type_idx,
@@ -5497,7 +5382,7 @@ fn compileFieldPatterns(
5497
5382
  ctx.enum_variants
5498
5383
  .get(n)
5499
5384
  .ok_or_else(|| format!("codegen: unknown enum variant '{}'", n))?;
5500
- if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
5385
+ if !matches!(decl_vt, ValType::Ref(_)) {
5501
5386
  return Err(format!(
5502
5387
  "codegen: enum tag pattern '{}' against a non-enum field",
5503
5388
  n
@@ -5517,6 +5402,7 @@ fn compileFieldPatterns(
5517
5402
  compileFieldPatterns(
5518
5403
  fields,
5519
5404
  field_types,
5405
+ inst_types,
5520
5406
  fpos + 1,
5521
5407
  container_local,
5522
5408
  container_type_idx,
@@ -5543,7 +5429,7 @@ fn compileFieldPatterns(
5543
5429
  Ok(())
5544
5430
  }
5545
5431
  ast::CasePattern::Int(n) => {
5546
- if plumTypeToValtype(field_ty) != ValType::I64 {
5432
+ if inst_vt != ValType::I64 {
5547
5433
  return Err("codegen: integer match pattern against a non-Int field".to_string());
5548
5434
  }
5549
5435
  Instruction::LocalGet(container_local).encode(body);
@@ -5552,12 +5438,14 @@ fn compileFieldPatterns(
5552
5438
  field_index: fld(fpos),
5553
5439
  }
5554
5440
  .encode(body);
5441
+ coerceVt(decl_vt, inst_vt, body, ctx.gc_types);
5555
5442
  Instruction::I64Const(*n).encode(body);
5556
5443
  Instruction::I64Eq.encode(body);
5557
5444
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5558
5445
  compileFieldPatterns(
5559
5446
  fields,
5560
5447
  field_types,
5448
+ inst_types,
5561
5449
  fpos + 1,
5562
5450
  container_local,
5563
5451
  container_type_idx,
@@ -5597,7 +5485,7 @@ fn compileFieldPatterns(
5597
5485
  .enum_variants
5598
5486
  .get(name)
5599
5487
  .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
5600
- if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
5488
+ if !matches!(decl_vt, ValType::Ref(_)) {
5601
5489
  return Err(format!(
5602
5490
  "codegen: constructor pattern '{}' against a non-enum field",
5603
5491
  name
@@ -5624,6 +5512,7 @@ fn compileFieldPatterns(
5624
5512
  })?;
5625
5513
  let nested_local = ctx.nested_class_scratch_base + slot;
5626
5514
  let inner_field_types = info.field_types.clone();
5515
+ let inner_inst_types = instFields(info, field_ty);
5627
5516
 
5628
5517
  Instruction::LocalGet(container_local).encode(body);
5629
5518
  Instruction::StructGet {
@@ -5645,6 +5534,7 @@ fn compileFieldPatterns(
5645
5534
  compileFieldPatterns(
5646
5535
  inner_fields,
5647
5536
  &inner_field_types,
5537
+ &inner_inst_types,
5648
5538
  0,
5649
5539
  nested_local,
5650
5540
  inner_variant_idx,
@@ -5659,6 +5549,7 @@ fn compileFieldPatterns(
5659
5549
  compileFieldPatterns(
5660
5550
  fields,
5661
5551
  field_types,
5552
+ inst_types,
5662
5553
  fpos + 1,
5663
5554
  container_local,
5664
5555
  container_type_idx,
@@ -5712,7 +5603,16 @@ fn compileExpr(
5712
5603
  }
5713
5604
  ast::Expr::Var(name) => {
5714
5605
  match ctx.locals.get(name.as_str()) {
5606
+ Some(idx) => {
5715
- Some(idx) => Instruction::LocalGet(*idx).encode(body),
5607
+ Instruction::LocalGet(*idx).encode(body);
5608
+ // The local holds its DECLARED representation; this use site's
5609
+ // type may differ across an erased edge (a generic `T` slot read
5610
+ // at its instantiated type).
5611
+ if let Some(decl) = ctx.local_decl_types.get(name.as_str()) {
5612
+ let want = plumTypeToValtype(&inferLocalType(expr, ctx));
5613
+ coerceVt(plumTypeToValtype(decl), want, body, ctx.gc_types);
5614
+ }
5615
+ }
5716
5616
  // Not a local: a bare reference to a top-level function used as a
5717
5617
  // value (e.g. `each(double)`) — push its zero-capture trampoline
5718
5618
  // closure's (pre-built by the `start` function) global.
@@ -5739,12 +5639,12 @@ fn compileExpr(
5739
5639
  // inside an expression here for the first time.
5740
5640
  ast::Expr::Try(inner) => {
5741
5641
  let inner_ty = inferLocalType(inner, ctx);
5742
- let enum_name = match &inner_ty {
5642
+ let enum_name = match plum_checker::nominalName(&inner_ty) {
5743
- PlumType::TNamed(n) => n.clone(),
5643
+ Some(n) => n.to_string(),
5744
- other => {
5644
+ None => {
5745
5645
  return Err(format!(
5746
5646
  "codegen: '?' requires a Try value, found {}",
5747
- other
5647
+ inner_ty
5748
5648
  ))
5749
5649
  }
5750
5650
  };
@@ -5781,13 +5681,16 @@ fn compileExpr(
5781
5681
  failure_name
5782
5682
  )
5783
5683
  })?;
5684
+ // The payload slot is declared at its generic type (`T` -> `$Obj`); the
5685
+ // value this expression yields is at its instantiated type.
5784
- let success_vt = plumTypeToValtype(
5686
+ let raw_success_vt = plumTypeToValtype(
5785
5687
  &success_info
5786
5688
  .field_types
5787
5689
  .first()
5788
5690
  .cloned()
5789
5691
  .unwrap_or(PlumType::TUnit),
5790
5692
  );
5693
+ let success_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
5791
5694
 
5792
5695
  let key = expr as *const ast::Expr as usize;
5793
5696
  let slot = *ctx
@@ -5802,8 +5705,8 @@ fn compileExpr(
5802
5705
  Instruction::If(BlockType::Result(success_vt)).encode(body);
5803
5706
  // Failure branch: take the inner `Err` payload and build the OUTER
5804
5707
  // function's `Err` variant from it (the Ok types may differ).
5805
- let outer_err = match &ctx.ret_type {
5708
+ let outer_err = match ctx.ret_type.as_ref().and_then(plum_checker::nominalName) {
5806
- Some(PlumType::TNamed(outer_enum)) if *outer_enum != enum_name => {
5709
+ Some(outer_enum) if outer_enum != enum_name => {
5807
5710
  plum_checker::tryOperatorVariants(outer_enum, &cctx)
5808
5711
  .map(|(_, _, fname, _)| fname)
5809
5712
  }
@@ -5848,6 +5751,7 @@ fn compileExpr(
5848
5751
  field_index: fld(0),
5849
5752
  }
5850
5753
  .encode(body);
5754
+ coerceVt(raw_success_vt, success_vt, body, ctx.gc_types);
5851
5755
  }
5852
5756
  Instruction::End.encode(body);
5853
5757
  }
@@ -5858,12 +5762,12 @@ fn compileExpr(
5858
5762
  // the field, same as `Try`'s own success branch.
5859
5763
  ast::Expr::Elvis(e) => {
5860
5764
  let left_ty = inferLocalType(&e.left, ctx);
5861
- let enum_name = match &left_ty {
5765
+ let enum_name = match plum_checker::nominalName(&left_ty) {
5862
- PlumType::TNamed(n) => n.clone(),
5766
+ Some(n) => n.to_string(),
5863
- other => {
5767
+ None => {
5864
5768
  return Err(format!(
5865
5769
  "codegen: '??' requires a Try value, found {}",
5866
- other
5770
+ left_ty
5867
5771
  ))
5868
5772
  }
5869
5773
  };
@@ -5900,13 +5804,14 @@ fn compileExpr(
5900
5804
  failure_name
5901
5805
  )
5902
5806
  })?;
5903
- let success_vt = plumTypeToValtype(
5807
+ let raw_success_vt = plumTypeToValtype(
5904
5808
  &success_info
5905
5809
  .field_types
5906
5810
  .first()
5907
5811
  .cloned()
5908
5812
  .unwrap_or(PlumType::TUnit),
5909
5813
  );
5814
+ let success_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
5910
5815
 
5911
5816
  let key = expr as *const ast::Expr as usize;
5912
5817
  let slot = *ctx
@@ -5919,7 +5824,7 @@ fn compileExpr(
5919
5824
  Instruction::LocalTee(scratch_local).encode(body);
5920
5825
  Instruction::RefTestNonNull(HeapType::Concrete(failure_idx)).encode(body);
5921
5826
  Instruction::If(BlockType::Result(success_vt)).encode(body);
5922
- compileExpr(&e.right, body, ctx, state)?;
5827
+ compileExprAs(&e.right, Some(success_vt), body, ctx, state)?;
5923
5828
  Instruction::Else.encode(body);
5924
5829
  Instruction::LocalGet(scratch_local).encode(body);
5925
5830
  Instruction::RefCastNonNull(HeapType::Concrete(success_idx)).encode(body);
@@ -5929,6 +5834,7 @@ fn compileExpr(
5929
5834
  field_index: fld(0),
5930
5835
  }
5931
5836
  .encode(body);
5837
+ coerceVt(raw_success_vt, success_vt, body, ctx.gc_types);
5932
5838
  }
5933
5839
  Instruction::End.encode(body);
5934
5840
  }
@@ -6030,6 +5936,23 @@ fn compileExpr(
6030
5936
  compileExpr(&c.left, body, ctx, state)?;
6031
5937
  compileExpr(&c.right, body, ctx, state)?;
6032
5938
  match left_ty {
5939
+ // Two erased values (`T == T` inside a generic body): the runtime
5940
+ // helper compares boxed primitives and `Str`s by value.
5941
+ PlumType::TParam(_) => match &c.op {
5942
+ ast::CmpOp::Eq => {
5943
+ Instruction::Call(ctx.dyn_eq_func).encode(body);
5944
+ }
5945
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => {
5946
+ Instruction::Call(ctx.dyn_eq_func).encode(body);
5947
+ Instruction::I32Eqz.encode(body);
5948
+ }
5949
+ other => {
5950
+ return Err(format!(
5951
+ "codegen: '{:?}' is not supported between generic values",
5952
+ other
5953
+ ))
5954
+ }
5955
+ },
6033
5956
  PlumType::TFloat => {
6034
5957
  match c.op {
6035
5958
  ast::CmpOp::Lt => Instruction::F64Lt,
@@ -6095,12 +6018,12 @@ fn compileExpr(
6095
6018
  pushBoolRefFromI32Flag(body, ctx);
6096
6019
  }
6097
6020
  ast::Expr::Ternary(t) => {
6098
- let result_vt = plumTypeToValtype(&inferLocalType(&t.then, ctx));
6021
+ let result_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
6099
6022
  compileBoolConditionAsI32(&t.condition, body, ctx, state)?;
6100
6023
  Instruction::If(BlockType::Result(result_vt)).encode(body);
6101
- compileExpr(&t.then, body, ctx, state)?;
6024
+ compileExprAs(&t.then, Some(result_vt), body, ctx, state)?;
6102
6025
  Instruction::Else.encode(body);
6103
- compileExpr(&t.else_, body, ctx, state)?;
6026
+ compileExprAs(&t.else_, Some(result_vt), body, ctx, state)?;
6104
6027
  Instruction::End.encode(body);
6105
6028
  }
6106
6029
  ast::Expr::FnCall(call) => {
@@ -6108,8 +6031,8 @@ fn compileExpr(
6108
6031
  // dispatched via `call_indirect` — not a direct `Call` to a named function.
6109
6032
  let is_closure_call = ctx.locals.contains_key(&call.name)
6110
6033
  && matches!(
6111
- inferLocalType(&ast::Expr::Var(call.name.clone()), ctx),
6034
+ ctx.local_decl_types.get(&call.name),
6112
- PlumType::TFun(_, _)
6035
+ Some(PlumType::TFun(_, _))
6113
6036
  );
6114
6037
  // `Int(x)`/`Float(x)`/`Byte(x)` is the primitive numeric CAST
6115
6038
  // special case just below UNLESS `call.name` is ALSO a
@@ -6125,12 +6048,7 @@ fn compileExpr(
6125
6048
  // numeric-cast call site in the stdlib (`Float(self)`,
6126
6049
  // `Int(y)`, ...) keeps working unchanged.
6127
6050
  let single_arg_ty = if call.args.len() == 1 {
6128
- let arg_expr = match &call.args[0] {
6129
- ast::Arg::Positional(e) => e,
6130
- ast::Arg::Keyword { value, .. } => value,
6131
- ast::Arg::Pair { value, .. } => value,
6132
- };
6133
- Some(inferLocalType(arg_expr, ctx))
6051
+ Some(inferLocalType(argExprOf(&call.args[0]), ctx))
6134
6052
  } else {
6135
6053
  None
6136
6054
  };
@@ -6144,11 +6062,7 @@ fn compileExpr(
6144
6062
  && !is_same_type_variant_wrap
6145
6063
  && single_arg_ty.is_some()
6146
6064
  {
6147
- let arg_expr = match &call.args[0] {
6065
+ let arg_expr = argExprOf(&call.args[0]);
6148
- ast::Arg::Positional(e) => e,
6149
- ast::Arg::Keyword { value, .. } => value,
6150
- ast::Arg::Pair { value, .. } => value,
6151
- };
6152
6066
  let arg_ty = single_arg_ty.expect("just checked is_some");
6153
6067
  compileExpr(arg_expr, body, ctx, state)?;
6154
6068
  match (call.name.as_str(), &arg_ty) {
@@ -6167,32 +6081,9 @@ fn compileExpr(
6167
6081
  _ => {}
6168
6082
  }
6169
6083
  } else if is_closure_call {
6170
- compileClosureCall(call, body, ctx, state)?;
6084
+ compileClosureCall(call, expr, body, ctx, state)?;
6171
- // `Type(1, 2, 3)` — mirrors `plum-checker`'s own `inferExpr` handling of
6172
- // the same case: sugar for `Type.init(1, 2, 3)`, desugared to that
6085
+ // `Bad(x)` / `Bad()` — an uppercase name the checker typed as a tag
6173
- // equivalent static method-call expression so the existing
6174
- // `AttrKind::Method` codegen (below) handles it without duplication.
6175
- // Checked BEFORE the ordinary positional-variant-construction case just
6176
- // below — see the matching comment in `plum-checker`'s `inferExpr` for
6177
- // why a record-shaped enum's own name needs this precedence.
6086
+ // union is a tag literal, even when a declared variant shares its name.
6178
- } else if ctx
6179
- .enum_variants
6180
- .get(&call.name)
6181
- .is_some_and(|info| info.enum_name == call.name)
6182
- && ctx
6183
- .methods
6184
- .contains_key(&(call.name.clone(), "init".to_string()))
6185
- {
6186
- let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
6187
- object: ast::Expr::TypeName(call.name.clone()),
6188
- attr: ast::AttrKind::Method(ast::FnCall {
6189
- name: "init".to_string(),
6190
- args: call.args.clone(),
6191
- }),
6192
- }));
6193
- compileExpr(&synthetic, body, ctx, state)?;
6194
- } else if let Some(info) = ctx.enum_variants.get(&call.name) {
6195
- compileVariantConstruction(info, call, expr, body, ctx, state)?;
6196
6087
  } else if !ctx.func_ids.contains_key(&call.name)
6197
6088
  && call.args.len() <= 1
6198
6089
  && call
@@ -6202,12 +6093,7 @@ fn compileExpr(
6202
6093
  .is_some_and(|c| c.is_uppercase() || c == plum_checker::TAG_MARKER)
6203
6094
  && matches!(inferLocalType(expr, ctx), PlumType::TTags(_))
6204
6095
  {
6205
- // `Bad(x)` / `Bad()` — an undeclared uppercase name is a tag literal.
6206
- let payload = call.args.first().map(|a| match a {
6096
+ let payload = call.args.first().map(argExprOf);
6207
- ast::Arg::Positional(e) => e,
6208
- ast::Arg::Keyword { value, .. } => value,
6209
- ast::Arg::Pair { value, .. } => value,
6210
- });
6211
6097
  compileTagLiteral(
6212
6098
  plum_checker::tagLiteralName(&call.name),
6213
6099
  payload,
@@ -6215,69 +6101,49 @@ fn compileExpr(
6215
6101
  ctx,
6216
6102
  state,
6217
6103
  )?;
6104
+ // `Type(1, 2, 3)` — mirrors `plum-checker`'s own `inferExpr` handling of
6105
+ // the same case: sugar for `Type.init(1, 2, 3)`, compiled as that
6106
+ // equivalent static method call (so the existing method-call codegen
6107
+ // handles it without duplication). Checked BEFORE the ordinary
6108
+ // positional-variant-construction case just below — see the matching
6109
+ // comment in `plum-checker`'s `inferExpr` for why a record-shaped
6110
+ // enum's own name needs this precedence.
6218
- } else {
6111
+ } else if ctx
6112
+ .enum_variants
6113
+ .get(&call.name)
6219
- fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
6114
+ .is_some_and(|info| info.enum_name == call.name)
6220
- match arg {
6115
+ && ctx
6221
- ast::Arg::Positional(e) => e,
6116
+ .methods
6222
- ast::Arg::Keyword { value, .. } => value,
6117
+ .contains_key(&(call.name.clone(), "init".to_string()))
6223
- ast::Arg::Pair { value, .. } => value,
6224
- }
6118
+ {
6225
- }
6226
- let callee_sig = inferLocalType(&ast::Expr::Var(call.name.clone()), ctx);
6119
+ let object = ast::Expr::TypeName(call.name.clone());
6227
- let variadic_split = match &callee_sig {
6120
+ let init_call = ast::FnCall {
6228
- PlumType::TFun(params, _) => match params.last() {
6229
- Some(PlumType::TVariadic(elem)) => {
6230
- Some(((**elem).clone(), params.len() - 1))
6121
+ name: "init".to_string(),
6231
- }
6232
- _ => None,
6122
+ args: call.args.clone(),
6233
- },
6234
- _ => None,
6235
6123
  };
6124
+ compileMethodCall(
6125
+ &object,
6126
+ &init_call,
6236
- let sig_params: Vec<ValType> = ctx
6127
+ &PlumType::TNamed(call.name.clone()),
6128
+ expr,
6129
+ body,
6130
+ ctx,
6131
+ state,
6132
+ )?;
6133
+ } else if let Some(info) = ctx.enum_variants.get(&call.name) {
6134
+ compileVariantConstruction(info, call, expr, body, ctx, state)?;
6135
+ } else {
6136
+ let sig = ctx
6237
6137
  .func_sigs
6238
6138
  .get(&call.name)
6239
- .map(|s| s.params.clone())
6240
- .unwrap_or_default();
6241
- match variadic_split {
6242
- Some((elem_ty, fixed_count)) => {
6243
- for (i, arg) in call.args.iter().take(fixed_count).enumerate() {
6139
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6244
- compileExprAs(argExprOf(arg), sig_params.get(i).copied(), body, ctx, state)?;
6140
+ compileCallArgs(&call.name, &call.args, sig, body, ctx, state)?;
6245
- }
6246
- let trailing: Vec<&ast::Expr> =
6247
- call.args.iter().skip(fixed_count).map(argExprOf).collect();
6248
- let elem_vt = plumTypeToValtype(&elem_ty);
6249
- let array_type_idx = *ctx
6250
- .gc_types
6251
- .variadic_array_type_idx
6252
- .get(&elem_vt)
6253
- .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
6254
- for arg_expr in &trailing {
6255
- compileExprAs(arg_expr, Some(elem_vt), body, ctx, state)?;
6256
- }
6257
- Instruction::ArrayNewFixed {
6258
- array_type_index: array_type_idx,
6259
- array_size: trailing.len() as u32,
6260
- }
6261
- .encode(body);
6262
-
6263
- let func_idx = ctx
6141
+ let func_idx = ctx
6264
- .func_ids
6142
+ .func_ids
6265
- .get(&call.name)
6143
+ .get(&call.name)
6266
- .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6144
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6267
- Instruction::Call(*func_idx).encode(body);
6145
+ Instruction::Call(*func_idx).encode(body);
6268
- }
6269
- None => {
6270
- for (i, arg) in call.args.iter().enumerate() {
6271
- compileExprAs(argExprOf(arg), sig_params.get(i).copied(), body, ctx, state)?;
6272
- }
6273
- compileDefaultArgs(&call.name, call.args.len(), body, ctx, state)?;
6146
+ coerceCallResult(&sig.decl_ret, expr, body, ctx);
6274
- let func_idx = ctx
6275
- .func_ids
6276
- .get(&call.name)
6277
- .ok_or_else(|| format!("unknown function '{}'", call.name))?;
6278
- Instruction::Call(*func_idx).encode(body);
6279
- }
6280
- }
6281
6147
  }
6282
6148
  }
6283
6149
  ast::Expr::Self_ => {
@@ -6290,6 +6156,11 @@ fn compileExpr(
6290
6156
  }
6291
6157
  ast::Expr::TypeName(n) => {
6292
6158
  match ctx.enum_variants.get(n) {
6159
+ // A bare uppercase name the checker typed as a tag union is a tag
6160
+ // literal, even when a declared variant shares its name.
6161
+ Some(_) if matches!(inferLocalType(expr, ctx), PlumType::TTags(_)) => {
6162
+ compileTagLiteral(plum_checker::tagLiteralName(n), None, body, ctx, state)?
6163
+ }
6293
6164
  Some(info) if info.field_types.is_empty() => {
6294
6165
  let global_idx = *ctx.singleton_globals.get(n)
6295
6166
  .unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' has no singleton global", n));
@@ -6349,14 +6220,20 @@ fn compileExpr(
6349
6220
  && call.spread.is_none()
6350
6221
  && matches!(ctx.methods.get(&(call.type_name.clone(), "init".to_string())), Some(PlumType::TFun(p, _)) if initCallableWithNoArgs(p)) =>
6351
6222
  {
6352
- let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
6353
- object: ast::Expr::TypeName(call.type_name.clone()),
6223
+ let object = ast::Expr::TypeName(call.type_name.clone());
6354
- attr: ast::AttrKind::Method(ast::FnCall {
6224
+ let init_call = ast::FnCall {
6355
- name: "init".to_string(),
6225
+ name: "init".to_string(),
6356
- args: vec![],
6226
+ args: vec![],
6357
- }),
6227
+ };
6228
+ compileMethodCall(
6229
+ &object,
6230
+ &init_call,
6231
+ &PlumType::TNamed(call.type_name.clone()),
6232
+ expr,
6233
+ body,
6234
+ ctx,
6235
+ state,
6358
- }));
6236
+ )?;
6359
- compileExpr(&synthetic, body, ctx, state)?;
6360
6237
  }
6361
6238
  // A NAMED-payload enum variant construction (`Circle(radius: 5)`, as
6362
6239
  // opposed to the positional/generic-payload form `Next(T)` — constructed
@@ -6391,13 +6268,7 @@ fn compileExpr(
6391
6268
  Instruction::I32Const(typeIdOf(ctx.gc_types, &call.type_name)).encode(body);
6392
6269
  for (field_idx, field_name) in field_names.iter().enumerate() {
6393
6270
  match call.fields.iter().find(|fa| &fa.name == field_name) {
6394
- Some(fa) => compileExprAs(
6395
- &fa.value,
6396
- info.field_types.get(field_idx).map(plumTypeToValtype),
6271
+ Some(fa) => compileExprInto(&fa.value, &info.field_types[field_idx], body, ctx, state)?,
6397
- body,
6398
- ctx,
6399
- state,
6400
- )?,
6401
6272
  // `Circle(..c, radius: 10)` — a field not overridden is read
6402
6273
  // straight off the spread source instead (checker only proved
6403
6274
  // `c` is SOME value of the owning enum, restricted to a bare
@@ -6440,305 +6311,10 @@ fn compileExpr(
6440
6311
  let obj_ty = inferLocalType(&attr.object, ctx);
6441
6312
  match &attr.attr {
6442
6313
  ast::AttrKind::Field(field_name) => {
6443
- // `Str` is an ordinary class (`data: Buffer`) with no name of
6444
- // its own carried in `PlumType::TStr` — resolve it via the
6445
- // literal name "Str", same as `TNamed` does via its own name.
6446
- let class_name = match &obj_ty {
6447
- PlumType::TNamed(n) => n.clone(),
6448
- PlumType::TStr => "Str".to_string(),
6449
- other => {
6450
- return Err(format!(
6451
- "codegen: cannot access field '{}' on non-class type {}",
6452
- field_name, other
6453
- ))
6454
- }
6455
- };
6456
- // Fall back to a discriminant enum's shared params, declared
6457
- // directly on the enum's SUPERTYPE (see `buildGcTypeRegistry`'s
6458
- // `EnumSuper` arm) — no `ref.cast` to any particular variant
6459
- // needed, since every variant has the exact same field list as
6460
- // the supertype itself. This ALSO covers a record-shaped enum's
6461
- // own field (the `type X = ...` replacement) via the unique-
6462
- // owning-variant fallback below, since its one variant is
6463
- // trivially its own unique owner of every field it declares.
6464
- match ctx.enum_params.get(&class_name) {
6465
- Some(params) => {
6466
- let field_idx = params
6467
- .iter()
6468
- .position(|(n, _)| n == field_name)
6469
- .ok_or_else(|| {
6470
- format!(
6471
- "codegen: no field '{}' on enum '{}'",
6472
- field_name, class_name
6473
- )
6474
- })?;
6475
- let super_type_idx = *ctx
6476
- .gc_types
6477
- .enum_super_type_idx
6478
- .get(&class_name)
6479
- .ok_or_else(|| {
6480
- format!(
6481
- "codegen: enum '{}' missing from the GC type registry",
6482
- class_name
6483
- )
6484
- })?;
6485
- compileExpr(&attr.object, body, ctx, state)?;
6314
+ compileFieldRead(&attr.object, field_name, &obj_ty, expr, body, ctx, state)?
6486
- Instruction::StructGet {
6487
- struct_type_index: super_type_idx,
6488
- field_index: fld(field_idx),
6489
- }
6490
- .encode(body);
6491
- }
6492
- // Not a discriminant enum either: `class_name` is an
6493
- // ordinary enum, and `plum-checker`'s `inferExpr` already
6494
- // verified exactly ONE of its variants declares a field
6495
- // with this name (ambiguity across sibling variants is a
6496
- // checker error, never reaches codegen). Compile `.field`
6497
- // as a checked downcast to that one variant's own
6498
- // concrete struct type (same `ref.cast` idiom `match`
6499
- // pattern-matching already uses, e.g. `compileCasePositions`),
6500
- // then read the field off it — traps at runtime if the
6501
- // value turns out to be a different variant, so no static
6502
- // proof the value IS that variant is required here.
6503
- None => {
6504
- let (variant_name, info) = ctx
6505
- .enum_variants
6506
- .iter()
6507
- .find(|(_, info)| {
6508
- info.enum_name == class_name
6509
- && info.field_names.iter().any(|n| n == field_name)
6510
- })
6511
- .ok_or_else(|| {
6512
- format!(
6513
- "codegen: no field '{}' on enum '{}'",
6514
- field_name, class_name
6515
- )
6516
- })?;
6517
- let field_idx = info
6518
- .field_names
6519
- .iter()
6520
- .position(|n| n == field_name)
6521
- .expect("just found by this field name");
6522
- let variant_type_idx = *ctx
6523
- .gc_types
6524
- .variant_type_idx
6525
- .get(variant_name)
6526
- .ok_or_else(|| {
6527
- format!(
6528
- "codegen: variant '{}' missing from the GC type registry",
6529
- variant_name
6530
- )
6531
- })?;
6532
- compileExpr(&attr.object, body, ctx, state)?;
6533
- Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))
6534
- .encode(body);
6535
- Instruction::StructGet {
6536
- struct_type_index: variant_type_idx,
6537
- field_index: fld(field_idx),
6538
- }
6539
- .encode(body);
6540
- }
6541
- }
6542
6315
  }
6543
6316
  ast::AttrKind::Method(call) => {
6544
- let class_name = match &obj_ty {
6545
- PlumType::TNamed(n) => n.clone(),
6546
- // Builtin primitive types (`Int`/`Float`/`Bool`/`Str`) declare
6547
- // methods the same way classes do (`type Int = fun ...` in
6548
- // `libs/std`) — they're just never `TNamed`, so map them back
6549
- // to the receiver name `ctx.func_ids`/`ctx.methods` use.
6550
- PlumType::TInt => "Int".to_string(),
6551
- PlumType::TFloat => "Float".to_string(),
6552
- PlumType::TBool => "Bool".to_string(),
6553
- PlumType::TStr => "Str".to_string(),
6554
- PlumType::TByte => "Byte".to_string(),
6555
- PlumType::TByteSlice => "ByteSlice".to_string(),
6556
- other => {
6557
- return Err(format!(
6558
- "codegen: cannot call method '{}' on non-class type {}",
6559
- call.name, other
6560
- ))
6561
- }
6562
- };
6563
- // A method call on a trait-typed value goes through the trait's
6564
- // `$dyn$<Trait>$<method>` dispatcher (reads the receiver's typeId).
6565
- let is_trait_recv = ctx.gc_types.class_type_idx.get(&class_name)
6566
- == Some(&ctx.gc_types.obj_type_idx)
6567
- && ctx.methods.contains_key(&(
6568
- class_name.clone(),
6569
- plum_checker::TRAIT_MARKER.to_string(),
6570
- ));
6571
- // A method the receiver's own trait doesn't declare (e.g. a
6572
- // `T: ToStr` bound's `toStr` on a `Shape`-typed value inside a
6573
- // specialized generic) dispatches through whichever trait does.
6574
- let dyn_owner = if is_trait_recv {
6575
- if ctx.gc_types.dyn_fns.contains_key(&(class_name.clone(), call.name.clone())) {
6576
- Some(class_name.clone())
6577
- } else {
6578
- ctx.gc_types
6579
- .dyn_fns
6580
- .keys()
6581
- .find(|(_, m)| *m == call.name)
6582
- .map(|(t, _)| t.clone())
6583
- }
6584
- } else {
6585
- None
6586
- };
6587
- if is_trait_recv && dyn_owner.is_none() {
6588
- // No implementor of this method exists at all: unreachable at
6589
- // runtime, but the call must still type-check structurally.
6590
- Instruction::Unreachable.encode(body);
6591
- return Ok(());
6592
- }
6593
- if let Some(&dyn_idx) = dyn_owner.as_ref().and_then(|t| {
6594
- ctx.gc_types
6595
- .dyn_fns
6596
- .get(&(t.clone(), call.name.clone()))
6597
- }) {
6598
- let owner = dyn_owner.clone().unwrap_or_default();
6599
- let param_tys: Vec<PlumType> =
6600
- match ctx.methods.get(&(owner, call.name.clone())) {
6601
- Some(PlumType::TFun(ps, _)) => ps.clone(),
6602
- _ => Vec::new(),
6603
- };
6604
- compileExpr(&attr.object, body, ctx, state)?;
6605
- for (i, arg) in call.args.iter().enumerate() {
6606
- let e = match arg {
6607
- ast::Arg::Positional(e) => e,
6608
- ast::Arg::Keyword { value, .. } => value,
6609
- ast::Arg::Pair { value, .. } => value,
6610
- };
6611
- compileExprAs(e, param_tys.get(i).map(plumTypeToValtype), body, ctx, state)?;
6612
- }
6613
- Instruction::Call(dyn_idx).encode(body);
6614
- return Ok(());
6615
- }
6616
- let direct_key = format!("{}::{}", class_name, call.name);
6617
- // Fallback: a primitive/class receiver's method might be
6618
- // defined once on the "union" enum that bare-wraps it
6619
- // instead (`enum Number = | Int | Float`, e.g. `abs`
6620
- // defined on `Number` rather than duplicated per
6621
- // primitive) — `class_name` (e.g. "Int") is itself a
6622
- // registered enum-variant NAME in that case (see
6623
- // `buildGlobalTables`'s bare-wrap conditions), so look up
6624
- // its owning enum and retry there. `box_variant_idx` is
6625
- // this variant's own concrete GC struct type index, used
6626
- // below to box the raw `self` value into it before the
6627
- // call (the wrap-enum's method expects a boxed `Number`,
6628
- // not a bare `i64`/`f64`).
6629
- let wrap_fallback = (!ctx.func_ids.contains_key(&direct_key)).then(|| {
6630
- ctx.enum_variants.get(class_name.as_str()).and_then(|info| {
6631
- let wrap_key = format!("{}::{}", info.enum_name, call.name);
6632
- ctx.func_ids.contains_key(&wrap_key).then(|| {
6633
- let variant_idx = *ctx.gc_types.variant_type_idx.get(class_name.as_str())
6634
- .expect("class_name is a registered enum variant, checked above");
6635
- (info.enum_name.clone(), wrap_key, variant_idx)
6636
- })
6637
- })
6638
- }).flatten();
6639
- let (dispatch_name, key, box_variant_idx) = match wrap_fallback {
6640
- Some((enum_name, wrap_key, variant_idx)) => {
6641
- (enum_name, wrap_key, Some(variant_idx))
6642
- }
6643
- None => (class_name.clone(), direct_key, None),
6644
- };
6645
- let func_idx = *ctx.func_ids.get(&key).ok_or_else(|| {
6646
- format!("codegen: unknown method '{}.{}'", class_name, call.name)
6647
- })?;
6648
- // A "static"-style call (`Bool.parse("true")`, `Float.fromStr("3.14")`)
6649
- // on a self-less method declared inside a `type`/`enum` body — its
6650
- // receiver is a bare reference to the type's own name, not a real
6651
- // value. `attr.object`'s only possible resolution to exactly
6652
- // `TNamed(class_name)` via a bare `TypeName` is this pattern (a real
6653
- // enum-variant/const reference resolves to some OTHER concrete type,
6654
- // per the checker's `inferExpr`). Every method still reserves a
6655
- // leading self slot in its wasm signature regardless of whether its
6656
- // Plum source declares a `self` param (see `fnWasmParamTypes`), so
6657
- // something of the right type must still be pushed — the body simply
6658
- // never reads it (no `self` binding exists for it to read).
6659
- let is_static_call =
6660
- matches!(&attr.object, ast::Expr::TypeName(n) if *n == class_name);
6661
- if is_static_call {
6662
- // Use `class_name`'s wasm type (via `astTypeToWasm`, exactly
6663
- // like `fnWasmParamTypes` computed the callee's actual self
6664
- // slot type), NOT `obj_ty` — for a builtin primitive receiver
6665
- // (`Int.fromStr`), `obj_ty` is `TNamed("Int")` (the checker's
6666
- // bare-`TypeName` fallback doesn't know about primitives), whose
6667
- // `plumTypeToValtype` would wrongly resolve to a GC ref instead
6668
- // of `i64`.
6669
- let vt = astTypeToWasm(&dispatch_name).unwrap_or(ValType::I32);
6670
- pushSelfPlaceholder(vt, body);
6671
- } else {
6672
- if box_variant_idx.is_some() {
6673
- // The wrap-enum variant's header typeId goes below its payload.
6674
- Instruction::I32Const(typeIdOf(ctx.gc_types, &class_name)).encode(body);
6675
- }
6676
- compileExpr(&attr.object, body, ctx, state)?; // push self
6677
- if let Some(variant_idx) = box_variant_idx {
6678
- // Dispatching to the wrap-enum's method (see
6679
- // `wrap_fallback` above) — its `self` param is
6680
- // typed as the ENUM (`Number`), a boxed GC struct,
6681
- // not the bare primitive/class value just pushed.
6682
- Instruction::StructNew(variant_idx).encode(body);
6683
- }
6684
- }
6685
-
6686
- fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
6687
- match arg {
6688
- ast::Arg::Positional(e) => e,
6689
- ast::Arg::Keyword { value, .. } => value,
6690
- ast::Arg::Pair { value, .. } => value,
6691
- }
6692
- }
6693
- // A trailing `...T` param (e.g. `add(self, values: ...T)`) packs its
6694
- // trailing args into one GC array, exactly like a plain function call
6695
- // (see the `Expr::FnCall` variadic-call arm) — `call.args` doesn't
6696
- // include `self`, matching `ctx.methods`' own param list.
6697
- let variadic_split =
6698
- match ctx.methods.get(&(dispatch_name.clone(), call.name.clone())) {
6699
- Some(PlumType::TFun(params, _)) => match params.last() {
6700
- Some(PlumType::TVariadic(elem)) => {
6701
- Some(((**elem).clone(), params.len() - 1))
6702
- }
6703
- _ => None,
6704
- },
6705
- _ => None,
6706
- };
6707
- let sig_params: Vec<ValType> = ctx
6708
- .func_sigs
6709
- .get(&key)
6710
- .map(|s| s.params.clone())
6711
- .unwrap_or_default();
6712
- match variadic_split {
6713
- Some((elem_ty, fixed_count)) => {
6714
- for (i, arg) in call.args.iter().take(fixed_count).enumerate() {
6715
- compileExprAs(argExprOf(arg), sig_params.get(i + 1).copied(), body, ctx, state)?;
6716
- }
6717
- let trailing: Vec<&ast::Expr> =
6718
- call.args.iter().skip(fixed_count).map(argExprOf).collect();
6719
- let elem_vt = plumTypeToValtype(&elem_ty);
6720
- let array_type_idx = *ctx
6721
- .gc_types
6722
- .variadic_array_type_idx
6723
- .get(&elem_vt)
6724
- .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
6725
- for arg_expr in &trailing {
6726
- compileExprAs(arg_expr, Some(elem_vt), body, ctx, state)?;
6727
- }
6728
- Instruction::ArrayNewFixed {
6729
- array_type_index: array_type_idx,
6730
- array_size: trailing.len() as u32,
6731
- }
6732
- .encode(body);
6733
- }
6734
- None => {
6735
- for (i, arg) in call.args.iter().enumerate() {
6736
- compileExprAs(argExprOf(arg), sig_params.get(i + 1).copied(), body, ctx, state)?;
6737
- }
6738
- compileDefaultArgs(&key, call.args.len(), body, ctx, state)?;
6317
+ compileMethodCall(&attr.object, call, &obj_ty, expr, body, ctx, state)?
6739
- }
6740
- }
6741
- Instruction::Call(func_idx).encode(body);
6742
6318
  }
6743
6319
  }
6744
6320
  }
@@ -6768,6 +6344,353 @@ fn compileExpr(
6768
6344
  Ok(())
6769
6345
  }
6770
6346
 
6347
+ /// After a call to a function whose DECLARED return type is `decl_ret`: converts
6348
+ /// the result from the callee's (erased) representation to the call
6349
+ /// expression's own instantiated type (unboxing a primitive that travelled
6350
+ /// through a generic `T` slot, or casting an erased reference back down).
6351
+ fn coerceCallResult(decl_ret: &PlumType, call_expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) {
6352
+ coerceCallResultTo(decl_ret, &inferLocalType(call_expr, ctx), body, ctx);
6353
+ }
6354
+
6355
+ fn coerceCallResultTo(decl_ret: &PlumType, want: &PlumType, body: &mut Vec<u8>, ctx: &LocalCtx) {
6356
+ if matches!(decl_ret, PlumType::TUnit) || matches!(want, PlumType::TUnit | PlumType::TVar(_)) {
6357
+ return;
6358
+ }
6359
+ coerceVt(
6360
+ plumTypeToValtype(decl_ret),
6361
+ plumTypeToValtype(want),
6362
+ body,
6363
+ ctx.gc_types,
6364
+ );
6365
+ }
6366
+
6367
+ /// Pushes the arguments of a call to the function described by `sig`: each fixed
6368
+ /// argument converted to its DECLARED param type (boxing into erased `T` slots,
6369
+ /// wrapping a bare `Int`/`Float` for a `Number` param), a trailing `...T` pack
6370
+ /// built into one GC array, and the declared defaults for omitted trailing params.
6371
+ fn compileCallArgs(
6372
+ func_key: &str,
6373
+ args: &[ast::Arg],
6374
+ sig: &FuncSig,
6375
+ body: &mut Vec<u8>,
6376
+ ctx: &LocalCtx,
6377
+ state: &mut ModuleState,
6378
+ ) -> Result<(), String> {
6379
+ let decl = &sig.decl_params;
6380
+ match decl.last() {
6381
+ Some(PlumType::TVariadic(elem)) => {
6382
+ let fixed = decl.len() - 1;
6383
+ for (i, arg) in args.iter().take(fixed).enumerate() {
6384
+ compileExprInto(argExprOf(arg), &decl[i], body, ctx, state)?;
6385
+ }
6386
+ let trailing: Vec<&ast::Expr> = args.iter().skip(fixed).map(argExprOf).collect();
6387
+ let elem_vt = plumTypeToValtype(elem);
6388
+ let array_type_idx = *ctx
6389
+ .gc_types
6390
+ .variadic_array_type_idx
6391
+ .get(&elem_vt)
6392
+ .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
6393
+ for arg_expr in &trailing {
6394
+ compileExprInto(arg_expr, elem, body, ctx, state)?;
6395
+ }
6396
+ Instruction::ArrayNewFixed {
6397
+ array_type_index: array_type_idx,
6398
+ array_size: trailing.len() as u32,
6399
+ }
6400
+ .encode(body);
6401
+ }
6402
+ _ => {
6403
+ for (i, arg) in args.iter().enumerate() {
6404
+ match decl.get(i) {
6405
+ Some(d) => compileExprInto(argExprOf(arg), d, body, ctx, state)?,
6406
+ None => compileExpr(argExprOf(arg), body, ctx, state)?,
6407
+ }
6408
+ }
6409
+ compileDefaultArgs(func_key, args.len(), body, ctx, state)?;
6410
+ }
6411
+ }
6412
+ Ok(())
6413
+ }
6414
+
6415
+ /// `object.field` for a class / enum value: a `struct.get` of the field at its
6416
+ /// DECLARED (possibly erased `T`) representation, converted to the access
6417
+ /// expression's own instantiated type.
6418
+ fn compileFieldRead(
6419
+ object: &ast::Expr,
6420
+ field_name: &str,
6421
+ obj_ty: &PlumType,
6422
+ expr: &ast::Expr,
6423
+ body: &mut Vec<u8>,
6424
+ ctx: &LocalCtx,
6425
+ state: &mut ModuleState,
6426
+ ) -> Result<(), String> {
6427
+ // `Str` is an ordinary class (`data: Buffer`) with no name of its own carried
6428
+ // in `PlumType::TStr` — resolve it via the literal name "Str", same as a
6429
+ // `TNamed`/`TApp` does via its own name.
6430
+ let class_name = match (plum_checker::nominalName(obj_ty), obj_ty) {
6431
+ (Some(n), _) => n.to_string(),
6432
+ (None, PlumType::TStr) => "Str".to_string(),
6433
+ _ => {
6434
+ return Err(format!(
6435
+ "codegen: cannot access field '{}' on non-class type {}",
6436
+ field_name, obj_ty
6437
+ ))
6438
+ }
6439
+ };
6440
+ let want = plumTypeToValtype(&inferLocalType(expr, ctx));
6441
+ // A discriminant enum's shared params are declared directly on the enum's
6442
+ // SUPERTYPE (see `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to
6443
+ // any one variant needed.
6444
+ if let Some(params) = ctx.enum_params.get(&class_name) {
6445
+ let field_idx = params
6446
+ .iter()
6447
+ .position(|(n, _)| n == field_name)
6448
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
6449
+ let super_type_idx = *ctx
6450
+ .gc_types
6451
+ .enum_super_type_idx
6452
+ .get(&class_name)
6453
+ .ok_or_else(|| {
6454
+ format!("codegen: enum '{}' missing from the GC type registry", class_name)
6455
+ })?;
6456
+ compileExpr(object, body, ctx, state)?;
6457
+ Instruction::StructGet {
6458
+ struct_type_index: super_type_idx,
6459
+ field_index: fld(field_idx),
6460
+ }
6461
+ .encode(body);
6462
+ coerceVt(
6463
+ plumTypeToValtype(&params[field_idx].1),
6464
+ want,
6465
+ body,
6466
+ ctx.gc_types,
6467
+ );
6468
+ return Ok(());
6469
+ }
6470
+ // Not a discriminant enum: `class_name` is an ordinary enum, and the checker
6471
+ // already verified exactly ONE of its variants declares a field with this name.
6472
+ // Compile `.field` as a checked downcast to that one variant's own struct type,
6473
+ // then read the field off it — traps at runtime if the value turns out to be a
6474
+ // different variant.
6475
+ let (variant_name, info) = ctx
6476
+ .enum_variants
6477
+ .iter()
6478
+ .find(|(_, info)| {
6479
+ info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name)
6480
+ })
6481
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
6482
+ let field_idx = info
6483
+ .field_names
6484
+ .iter()
6485
+ .position(|n| n == field_name)
6486
+ .expect("just found by this field name");
6487
+ let variant_type_idx = *ctx
6488
+ .gc_types
6489
+ .variant_type_idx
6490
+ .get(variant_name)
6491
+ .ok_or_else(|| {
6492
+ format!(
6493
+ "codegen: variant '{}' missing from the GC type registry",
6494
+ variant_name
6495
+ )
6496
+ })?;
6497
+ compileExpr(object, body, ctx, state)?;
6498
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
6499
+ Instruction::StructGet {
6500
+ struct_type_index: variant_type_idx,
6501
+ field_index: fld(field_idx),
6502
+ }
6503
+ .encode(body);
6504
+ coerceVt(
6505
+ plumTypeToValtype(&info.field_types[field_idx]),
6506
+ want,
6507
+ body,
6508
+ ctx.gc_types,
6509
+ );
6510
+ Ok(())
6511
+ }
6512
+
6513
+ /// Resolves a method call's receiver name to the function that implements it:
6514
+ /// the receiver's own `Name::method`, or — for a primitive/class receiver whose
6515
+ /// method is defined once on the "union" enum that bare-wraps it
6516
+ /// (`enum Number = | Int | Float`) — that enum's method together with the wrapping
6517
+ /// variant the raw `self` must be boxed into first.
6518
+ fn resolveMethodKey(class_name: &str, method: &str, ctx: &LocalCtx) -> (String, String, Option<String>) {
6519
+ let direct_key = format!("{}::{}", class_name, method);
6520
+ if !ctx.func_ids.contains_key(&direct_key) {
6521
+ if let Some(info) = ctx.enum_variants.get(class_name) {
6522
+ let wrap_key = format!("{}::{}", info.enum_name, method);
6523
+ if ctx.func_ids.contains_key(&wrap_key) {
6524
+ return (info.enum_name.clone(), wrap_key, Some(class_name.to_string()));
6525
+ }
6526
+ }
6527
+ }
6528
+ (class_name.to_string(), direct_key, None)
6529
+ }
6530
+
6531
+ /// The trait that declares `method` (with `arity` params) and has a `$dyn$` dispatcher,
6532
+ /// for a call on an erased (`TParam`-typed) receiver whose bound traits the codegen
6533
+ /// does not otherwise track.
6534
+ fn dynOwnerForMethod(method: &str, arity: usize, ctx: &LocalCtx) -> Option<String> {
6535
+ let mut owners: Vec<&String> = ctx
6536
+ .gc_types
6537
+ .dyn_fns
6538
+ .keys()
6539
+ .filter(|(_, m)| m == method)
6540
+ .map(|(t, _)| t)
6541
+ .filter(|t| {
6542
+ matches!(
6543
+ ctx.methods.get(&((*t).clone(), method.to_string())),
6544
+ Some(PlumType::TFun(ps, _)) if ps.len() == arity
6545
+ )
6546
+ })
6547
+ .collect();
6548
+ owners.sort();
6549
+ owners.first().map(|t| (*t).clone())
6550
+ }
6551
+
6552
+ /// Compiles `object.method(args)`. `obj_ty` is the receiver's static type and
6553
+ /// `result_expr` the expression whose recorded type is the call's instantiated
6554
+ /// result type (a desugared `Type(...)` -> `Type.init(...)` passes the original
6555
+ /// call expression).
6556
+ fn compileMethodCall(
6557
+ object: &ast::Expr,
6558
+ call: &ast::FnCall,
6559
+ obj_ty: &PlumType,
6560
+ result_expr: &ast::Expr,
6561
+ body: &mut Vec<u8>,
6562
+ ctx: &LocalCtx,
6563
+ state: &mut ModuleState,
6564
+ ) -> Result<(), String> {
6565
+ // A method on an erased generic parameter (`K: Hashable` -> `key.hash()`)
6566
+ // dispatches dynamically through the declaring trait's `$dyn$` dispatcher.
6567
+ if matches!(obj_ty, PlumType::TParam(_)) {
6568
+ let owner = dynOwnerForMethod(&call.name, call.args.len(), ctx).ok_or_else(|| {
6569
+ format!(
6570
+ "codegen: '{}' has no method '{}' (add a bound)",
6571
+ obj_ty, call.name
6572
+ )
6573
+ })?;
6574
+ return compileDynCall(&owner, object, call, &inferLocalType(result_expr, ctx), body, ctx, state);
6575
+ }
6576
+ let class_name = match plum_checker::methodReceiverName(obj_ty) {
6577
+ Some(n) => n,
6578
+ None => {
6579
+ return Err(format!(
6580
+ "codegen: cannot call method '{}' on non-class type {}",
6581
+ call.name, obj_ty
6582
+ ))
6583
+ }
6584
+ };
6585
+ // A method call on a trait-typed value goes through the trait's
6586
+ // `$dyn$<Trait>$<method>` dispatcher (reads the receiver's typeId).
6587
+ let is_trait_recv = ctx.gc_types.class_type_idx.get(&class_name)
6588
+ == Some(&ctx.gc_types.obj_type_idx)
6589
+ && ctx
6590
+ .methods
6591
+ .contains_key(&(class_name.clone(), plum_checker::TRAIT_MARKER.to_string()));
6592
+ if is_trait_recv {
6593
+ // A method the receiver's own trait doesn't declare dispatches through
6594
+ // whichever trait does.
6595
+ let owner = if ctx
6596
+ .gc_types
6597
+ .dyn_fns
6598
+ .contains_key(&(class_name.clone(), call.name.clone()))
6599
+ {
6600
+ Some(class_name.clone())
6601
+ } else {
6602
+ dynOwnerForMethod(&call.name, call.args.len(), ctx)
6603
+ };
6604
+ return match owner {
6605
+ Some(owner) => compileDynCall(&owner, object, call, &inferLocalType(result_expr, ctx), body, ctx, state),
6606
+ None => {
6607
+ // The trait declares the method (the checker rejects any other) but
6608
+ // no type implements it: unreachable at runtime, but the call must
6609
+ // still be valid wasm.
6610
+ Instruction::Unreachable.encode(body);
6611
+ Ok(())
6612
+ }
6613
+ };
6614
+ }
6615
+
6616
+ let (dispatch_name, key, box_variant) = resolveMethodKey(&class_name, &call.name, ctx);
6617
+ let func_idx = *ctx
6618
+ .func_ids
6619
+ .get(&key)
6620
+ .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, call.name))?;
6621
+ // A "static"-style call (`Bool.parse("true")`, `Float.fromStr("3.14")`) on a
6622
+ // self-less method declared inside a `type`/`enum` body — its receiver is a
6623
+ // bare reference to the type's own name, not a real value. Every method still
6624
+ // reserves a leading self slot in its wasm signature regardless of whether its
6625
+ // Plum source declares a `self` param (see `fnWasmParamTypes`), so something of
6626
+ // the right type must still be pushed — the body simply never reads it.
6627
+ let is_static_call = matches!(object, ast::Expr::TypeName(n) if *n == class_name);
6628
+ if is_static_call {
6629
+ let vt = astTypeToWasm(&dispatch_name).unwrap_or(ValType::I32);
6630
+ pushSelfPlaceholder(vt, body);
6631
+ } else {
6632
+ if let Some(variant) = &box_variant {
6633
+ // The wrap-enum variant's header typeId goes below its payload.
6634
+ Instruction::I32Const(typeIdOf(ctx.gc_types, variant)).encode(body);
6635
+ }
6636
+ compileExpr(object, body, ctx, state)?; // push self
6637
+ if let Some(variant) = &box_variant {
6638
+ // Dispatching to the wrap-enum's method — its `self` param is typed as
6639
+ // the ENUM (`Number`), a boxed GC struct, not the bare primitive just pushed.
6640
+ let variant_idx = *ctx.gc_types.variant_type_idx.get(variant).ok_or_else(|| {
6641
+ format!("codegen: variant '{}' missing from the GC type registry", variant)
6642
+ })?;
6643
+ Instruction::StructNew(variant_idx).encode(body);
6644
+ }
6645
+ }
6646
+ let sig = ctx
6647
+ .func_sigs
6648
+ .get(&key)
6649
+ .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, call.name))?;
6650
+ compileCallArgs(&key, &call.args, sig, body, ctx, state)?;
6651
+ Instruction::Call(func_idx).encode(body);
6652
+ coerceCallResult(&sig.decl_ret, result_expr, body, ctx);
6653
+ Ok(())
6654
+ }
6655
+
6656
+ /// A call through `owner`'s `$dyn$<owner>$<method>` dispatcher: the receiver (any
6657
+ /// value, converted to the erased `$Obj` representation — a primitive is boxed)
6658
+ /// followed by the arguments at the trait method's declared param types.
6659
+ fn compileDynCall(
6660
+ owner: &str,
6661
+ object: &ast::Expr,
6662
+ call: &ast::FnCall,
6663
+ want: &PlumType,
6664
+ body: &mut Vec<u8>,
6665
+ ctx: &LocalCtx,
6666
+ state: &mut ModuleState,
6667
+ ) -> Result<(), String> {
6668
+ let dyn_idx = *ctx
6669
+ .gc_types
6670
+ .dyn_fns
6671
+ .get(&(owner.to_string(), call.name.clone()))
6672
+ .ok_or_else(|| format!("codegen: no dispatcher for '{}.{}'", owner, call.name))?;
6673
+ let (param_tys, ret_ty): (Vec<PlumType>, PlumType) =
6674
+ match ctx.methods.get(&(owner.to_string(), call.name.clone())) {
6675
+ Some(PlumType::TFun(ps, r)) => (ps.clone(), (**r).clone()),
6676
+ _ => (Vec::new(), PlumType::TUnit),
6677
+ };
6678
+ let obj_vt = ValType::Ref(RefType {
6679
+ nullable: true,
6680
+ heap_type: HeapType::Concrete(ctx.gc_types.obj_type_idx),
6681
+ });
6682
+ compileExprAs(object, Some(obj_vt), body, ctx, state)?;
6683
+ for (i, arg) in call.args.iter().enumerate() {
6684
+ match param_tys.get(i) {
6685
+ Some(d) => compileExprInto(argExprOf(arg), d, body, ctx, state)?,
6686
+ None => compileExpr(argExprOf(arg), body, ctx, state)?,
6687
+ }
6688
+ }
6689
+ Instruction::Call(dyn_idx).encode(body);
6690
+ coerceCallResultTo(&ret_ty, want, body, ctx);
6691
+ Ok(())
6692
+ }
6693
+
6771
6694
  /// Compiles a variant-construction call. A payload-free variant (`End`, called as
6772
6695
  /// `End()` rather than used bare) is its pre-allocated singleton global. A payload
6773
6696
  /// variant pushes its field values in order and does `struct.new` into its own
@@ -6790,14 +6713,17 @@ fn compileDefaultArgs(
6790
6713
  let Some(sig) = ctx.func_sigs.get(func_key) else {
6791
6714
  return Ok(());
6792
6715
  };
6793
- for default in sig.param_defaults.iter().skip(supplied_count) {
6716
+ for (i, default) in sig.param_defaults.iter().enumerate().skip(supplied_count) {
6794
6717
  let default_expr = default.as_ref().ok_or_else(|| {
6795
6718
  format!(
6796
6719
  "codegen: call '{}' is missing a required argument (no default declared)",
6797
6720
  func_key
6798
6721
  )
6799
6722
  })?;
6723
+ match sig.decl_params.get(i) {
6724
+ Some(d) => compileExprInto(default_expr, d, body, ctx, state)?,
6800
- compileExpr(default_expr, body, ctx, state)?;
6725
+ None => compileExpr(default_expr, body, ctx, state)?,
6726
+ }
6801
6727
  }
6802
6728
  Ok(())
6803
6729
  }
@@ -6852,13 +6778,10 @@ fn compileVariantConstruction(
6852
6778
  ast::Arg::Keyword { value, .. } => value,
6853
6779
  ast::Arg::Pair { value, .. } => value,
6854
6780
  };
6855
- compileExprAs(
6856
- arg_expr,
6857
- info.field_types.get(i).map(plumTypeToValtype),
6781
+ match info.field_types.get(i) {
6858
- body,
6782
+ Some(d) => compileExprInto(arg_expr, d, body, ctx, state)?,
6859
- ctx,
6860
- state,
6783
+ None => compileExpr(arg_expr, body, ctx, state)?,
6861
- )?;
6784
+ }
6862
6785
  }
6863
6786
  Instruction::StructNew(variant_type_idx).encode(body);
6864
6787
  Ok(())
@@ -6944,10 +6867,35 @@ fn compileInterpolatedString(
6944
6867
  Instruction::Call(func_idx).encode(body);
6945
6868
  }
6946
6869
  other => {
6870
+ // An erased (`T`) or trait-typed value dispatches `toStr`
6871
+ // dynamically; a class/enum with its own `toStr` calls it.
6872
+ let to_str = ast::FnCall {
6873
+ name: "toStr".to_string(),
6874
+ args: vec![],
6875
+ };
6876
+ let str_ty = PlumType::TStr;
6877
+ let is_dyn = match &other {
6878
+ PlumType::TParam(_) => true,
6879
+ t => plum_checker::nominalName(t).is_some_and(|n| {
6880
+ ctx.gc_types.class_type_idx.get(n)
6881
+ == Some(&ctx.gc_types.obj_type_idx)
6882
+ }),
6883
+ };
6884
+ if is_dyn {
6885
+ let owner = dynOwnerForMethod("toStr", 0, ctx).ok_or_else(|| {
6886
+ format!("codegen: interpolating a value of type {} requires a `toStr` method", other)
6887
+ })?;
6888
+ compileDynCall(&owner, expr, &to_str, &str_ty, body, ctx, state)?;
6889
+ } else if plum_checker::nominalName(&other)
6890
+ .is_some_and(|n| ctx.func_ids.contains_key(&format!("{}::toStr", n)))
6891
+ {
6892
+ compileMethodCall(expr, &to_str, &other, expr, body, ctx, state)?;
6893
+ } else {
6947
- return Err(format!(
6894
+ return Err(format!(
6948
- "codegen: interpolating a value of type {} is not yet supported",
6895
+ "codegen: interpolating a value of type {} is not yet supported",
6949
- other
6896
+ other
6950
- ));
6897
+ ));
6898
+ }
6951
6899
  }
6952
6900
  }
6953
6901
  }
@@ -6980,7 +6928,7 @@ fn compileClosureLiteral(
6980
6928
  // env struct (snapshotting each captured free variable's current value from the
6981
6929
  // enclosing function's local), then wrap both into the shared closure struct.
6982
6930
  Instruction::I32Const(info.table_idx as i32).encode(body);
6983
- for (name, _) in &info.free_vars {
6931
+ for (name, cap_ty) in &info.free_vars {
6984
6932
  let local_idx = *ctx.locals.get(name).ok_or_else(|| {
6985
6933
  format!(
6986
6934
  "codegen: captured variable '{}' is not a local in the enclosing scope",
@@ -6988,20 +6936,30 @@ fn compileClosureLiteral(
6988
6936
  )
6989
6937
  })?;
6990
6938
  Instruction::LocalGet(local_idx).encode(body);
6939
+ let have = ctx
6940
+ .local_decl_types
6941
+ .get(name)
6942
+ .map(plumTypeToValtype)
6943
+ .unwrap_or_else(|| plumTypeToValtype(cap_ty));
6944
+ coerceVt(have, plumTypeToValtype(cap_ty), body, ctx.gc_types);
6991
6945
  }
6992
6946
  Instruction::StructNew(info.env_type_idx).encode(body);
6993
6947
  Instruction::StructNew(ctx.gc_types.closure_type_idx).encode(body);
6994
6948
  Ok(())
6995
6949
  }
6996
6950
 
6997
- /// Compiles a call to a closure-typed local via `call_indirect`. The local is
6951
+ /// Compiles a call to a closure-typed local via `call_indirect`, using the erased
6952
+ /// closure ABI (`ClosureSigKey`): every argument is converted to `anyref`
6953
+ /// (primitives boxed), the result is converted from `anyref` to the call's own
6954
+ /// instantiated type (or dropped when the closure's declared result is `Unit`).
6998
- /// statically typed `anyref` (closures share that placeholder type — see
6955
+ /// The local is statically `anyref` (closures share that placeholder type — see
6999
6956
  /// `plumTypeToValtype`), so every read of its fields `ref.cast`s down to the shared
7000
6957
  /// concrete closure struct type first. Stack order matches the closure function's
7001
6958
  /// signature `(env, ...args)`: push the env, then each argument, then the table
7002
6959
  /// index (the `call_indirect` operand).
7003
6960
  fn compileClosureCall(
7004
6961
  call: &ast::FnCall,
6962
+ call_expr: &ast::Expr,
7005
6963
  body: &mut Vec<u8>,
7006
6964
  ctx: &LocalCtx,
7007
6965
  state: &mut ModuleState,
@@ -7011,41 +6969,14 @@ fn compileClosureCall(
7011
6969
  .get(&call.name)
7012
6970
  .ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
7013
6971
  let closure_type_idx = ctx.gc_types.closure_type_idx;
7014
-
7015
- // Prefer the exact signature recorded when this local was assigned a closure
7016
- // *literal* (see `closure_local_sigs`) — it's already correct. Otherwise (e.g.
7017
- // `call.name` is a `fn(...)`-typed parameter, whose declared type is reliable on
7018
- // its own) fall back to re-deriving it from the type env.
7019
- let sig_key: ClosureSigKey = match ctx.closure_local_sigs.borrow().get(&call.name) {
7020
- Some(key) => key.clone(),
7021
- None => {
7022
- let (param_ptypes, ret_ptype) =
7023
- match inferLocalType(&ast::Expr::Var(call.name.clone()), ctx) {
7024
- PlumType::TFun(p, r) => (p, *r),
7025
- other => {
7026
- return Err(format!(
7027
- "codegen: '{}' is not callable (type {:?})",
7028
- call.name, other
7029
- ))
7030
- }
7031
- };
7032
- let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
7033
- for p in &param_ptypes {
7034
- sig_params.push(plumTypeToValtype(p));
7035
- }
7036
- let ret_vt = match ret_ptype {
7037
- PlumType::TUnit => None,
7038
- other => Some(plumTypeToValtype(&other)),
7039
- };
7040
- (sig_params, ret_vt)
7041
- }
7042
- };
7043
- let type_index = *ctx.closure_call_types.get(&sig_key).ok_or_else(|| {
6972
+ let type_index = *ctx.closure_call_types.get(&call.args.len()).ok_or_else(|| {
7044
6973
  format!(
7045
- "internal codegen error: no call_indirect type for closure '{}'",
6974
+ "internal codegen error: no call_indirect type for closure '{}' with {} args",
7046
- call.name
6975
+ call.name,
6976
+ call.args.len()
7047
6977
  )
7048
6978
  })?;
6979
+ let any = ValType::Ref(RefType::ANYREF);
7049
6980
 
7050
6981
  // env (closure struct field 1)
7051
6982
  Instruction::LocalGet(closure_local).encode(body);
@@ -7055,9 +6986,9 @@ fn compileClosureCall(
7055
6986
  field_index: 1,
7056
6987
  }
7057
6988
  .encode(body);
7058
- // real arguments
6989
+ // real arguments, boxed to the erased ABI
7059
6990
  for arg in &call.args {
7060
- compileExpr(argExprOf(arg), body, ctx, state)?;
6991
+ compileExprAs(argExprOf(arg), Some(any), body, ctx, state)?;
7061
6992
  }
7062
6993
  // table index (closure struct field 0) — the call_indirect operand
7063
6994
  Instruction::LocalGet(closure_local).encode(body);
@@ -7072,12 +7003,36 @@ fn compileClosureCall(
7072
7003
  table_index: 0,
7073
7004
  }
7074
7005
  .encode(body);
7006
+ // The result: `Unit` closures produce a null anyref that is dropped here, so
7007
+ // a Unit-typed call leaves nothing on the stack (see `exprHasResult`).
7008
+ if closureCallReturnsUnit(&call.name, ctx) {
7009
+ Instruction::Drop.encode(body);
7010
+ } else {
7011
+ let want = plumTypeToValtype(&inferLocalType(call_expr, ctx));
7012
+ coerceVt(any, want, body, ctx.gc_types);
7013
+ }
7075
7014
  Ok(())
7076
7015
  }
7077
7016
 
7017
+ /// Whether the closure-typed local `name` is declared to return `Unit`.
7018
+ fn closureCallReturnsUnit(name: &str, ctx: &LocalCtx) -> bool {
7019
+ match ctx.local_decl_types.get(name) {
7020
+ Some(PlumType::TFun(_, ret)) => matches!(**ret, PlumType::TUnit),
7021
+ _ => match inferLocalType(&ast::Expr::Var(name.to_string()), ctx) {
7022
+ PlumType::TFun(_, ret) => matches!(*ret, PlumType::TUnit),
7023
+ _ => false,
7024
+ },
7025
+ }
7026
+ }
7027
+
7078
- /// Compiles a closure literal's own body into a standalone wasm function. Local 0 is the
7028
+ /// Compiles a closure literal's own body into a standalone wasm function with the
7029
+ /// erased closure ABI `(env: anyref, a1: anyref, ..., an: anyref) -> anyref`
7030
+ /// (see `ClosureSigKey`). Local 0 is the env pointer; locals `1..=n` are the raw
7079
- /// implicit env pointer; the closure's params follow; then each captured free variable
7031
+ /// incoming params; typed locals follow — one per closure param (holding the
7032
+ /// raw param unboxed/cast to its real Plum type by the prologue), one per
7080
- /// gets a local loaded from the env struct at function entry (restoring the snapshot).
7033
+ /// captured free variable (restored from the env struct), then the body's own
7034
+ /// named locals and scratch slots. The tail value is boxed back to `anyref` (a
7035
+ /// `Unit` body yields a null `anyref`).
7081
7036
  fn compileClosureBody(
7082
7037
  cl: &ast::Closure,
7083
7038
  info: &ClosureInfo,
@@ -7086,23 +7041,21 @@ fn compileClosureBody(
7086
7041
  ) -> Result<Vec<u8>, String> {
7087
7042
  let mut body = Vec::new();
7088
7043
 
7089
- // Base type env: globals + captured free vars + closure params.
7044
+ // Fallback type env (synthesized nodes only) + declared local types.
7090
7045
  let mut base_env = ctx.global_env.clone();
7046
+ let mut decl_types: HashMap<String, PlumType> = HashMap::new();
7091
7047
  for (name, ty) in &info.free_vars {
7092
7048
  base_env.insert(name.clone(), TypeScheme::mono(ty.clone()));
7049
+ decl_types.insert(name.clone(), ty.clone());
7093
7050
  }
7094
7051
  for (name, pty) in cl.params.iter().zip(info.param_ptypes.iter()) {
7095
7052
  base_env.insert(name.clone(), TypeScheme::mono(pty.clone()));
7053
+ decl_types.insert(name.clone(), pty.clone());
7096
7054
  }
7097
7055
 
7098
7056
  let mut collector = Collector {
7099
- env: base_env.clone(),
7100
- cctx: checkCtxOf(
7101
- &ctx.methods,
7057
+ types: &ctx.types,
7102
- &ctx.enum_variants,
7058
+ enum_variants: &ctx.enum_variants,
7103
- &ctx.enum_params,
7104
- &ctx.min_required,
7105
- ),
7106
7059
  named: Vec::new(),
7107
7060
  named_set: Default::default(),
7108
7061
  match_scratch: HashMap::new(),
@@ -7115,14 +7068,18 @@ fn compileClosureBody(
7115
7068
  collector.walkBlock(&cl.body);
7116
7069
 
7117
7070
  // ---- local index layout ----
7118
- // [env_ptr][closure params][free-var locals][named...][classcall][match][closure scratch]
7071
+ // [env_ptr][raw params][typed params][free-var locals][named...][match][scratch]
7119
7072
  let mut locals: HashMap<String, u32> = HashMap::new();
7120
7073
  let mut groups: Vec<ValType> = Vec::new();
7121
7074
  let mut idx = 0u32;
7122
7075
 
7123
- idx += 1; // local 0 = env pointer (a param, so not declared below)
7076
+ idx += 1 + cl.params.len() as u32; // env + raw params (all in the signature)
7077
+ let mut param_prologue: Vec<(u32, u32, ValType)> = Vec::new(); // (raw, typed, vt)
7078
+ for (i, (name, pty)) in cl.params.iter().zip(info.param_ptypes.iter()).enumerate() {
7124
- for name in &cl.params {
7079
+ let vt = plumTypeToValtype(pty);
7125
7080
  locals.insert(name.clone(), idx);
7081
+ groups.push(vt);
7082
+ param_prologue.push((i as u32 + 1, idx, vt));
7126
7083
  idx += 1;
7127
7084
  }
7128
7085
  for (name, ty) in &info.free_vars {
@@ -7131,6 +7088,7 @@ fn compileClosureBody(
7131
7088
  idx += 1;
7132
7089
  }
7133
7090
  for (name, ty) in &collector.named {
7091
+ decl_types.entry(name.clone()).or_insert_with(|| ty.clone());
7134
7092
  if locals.contains_key(name) {
7135
7093
  continue;
7136
7094
  }
@@ -7150,10 +7108,6 @@ fn compileClosureBody(
7150
7108
  }
7151
7109
 
7152
7110
  let nested_class_scratch_base = idx;
7153
- // Each slot is declared with its OWN concrete variant ref type (not a uniform
7154
- // placeholder) — `struct.get` on a constructor-pattern match requires the local
7155
- // holding the narrowed (`ref.cast`) value to be statically typed as that exact
7156
- // variant, and different slots very likely narrow to different variants.
7157
7111
  for vname in &collector.nested_class_scratch_types {
7158
7112
  let variant_idx = withGcTypes(|r| classOrVariantTypeIdx(r, vname));
7159
7113
  groups.push(gcRef(variant_idx));
@@ -7184,6 +7138,12 @@ fn compileClosureBody(
7184
7138
  }
7185
7139
  }
7186
7140
 
7141
+ // Prologue: convert each raw `anyref` param to its real representation.
7142
+ for (raw, typed, vt) in &param_prologue {
7143
+ Instruction::LocalGet(*raw).encode(&mut body);
7144
+ coerceVt(ValType::Ref(RefType::ANYREF), *vt, &mut body, &ctx.gc_types);
7145
+ Instruction::LocalSet(*typed).encode(&mut body);
7146
+ }
7187
7147
  // Restore each captured free variable from the env struct (local 0, statically
7188
7148
  // `anyref` — `ref.cast` down to THIS closure's own concrete env type) at entry.
7189
7149
  for (i, (name, _)) in info.free_vars.iter().enumerate() {
@@ -7208,11 +7168,14 @@ fn compileClosureBody(
7208
7168
  variadic_for_scratch: collector.variadic_for_scratch,
7209
7169
  func_ids: &ctx.func_ids,
7210
7170
  func_sigs: &ctx.func_sigs,
7171
+ types: &ctx.types,
7172
+ local_decl_types: decl_types,
7211
7173
  closures: &ctx.closures,
7212
7174
  closure_call_types: &ctx.closure_call_types,
7213
7175
  named_fn_values: &ctx.named_fn_values,
7214
7176
  string_concat_func: ctx.string_concat_func,
7215
7177
  string_eq_func: ctx.string_eq_func,
7178
+ dyn_eq_func: ctx.dyn_eq_func,
7216
7179
  int_to_string_func: ctx.int_to_string_func,
7217
7180
  methods: &ctx.methods,
7218
7181
  enum_variants: &ctx.enum_variants,
@@ -7221,13 +7184,22 @@ fn compileClosureBody(
7221
7184
  gc_types: &ctx.gc_types,
7222
7185
  singleton_globals: &ctx.singleton_globals,
7223
7186
  type_env: RefCell::new(base_env),
7224
- closure_local_sigs: RefCell::new(HashMap::new()),
7225
7187
  loop_extra_depth: Cell::new(0),
7226
- ret_type: None,
7188
+ in_closure: true,
7189
+ ret_type: Some(info.ret_ptype.clone()),
7227
7190
  match_subject_types: RefCell::new(HashMap::new()),
7191
+ subject_ptypes: RefCell::new(HashMap::new()),
7228
7192
  };
7229
7193
 
7194
+ let any = ValType::Ref(RefType::ANYREF);
7195
+ if matches!(info.ret_ptype, PlumType::TUnit) {
7196
+ compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, None)?;
7197
+ Instruction::RefNull(HeapType::ANY).encode(&mut body);
7198
+ } else {
7199
+ let ret_vt = plumTypeToValtype(&info.ret_ptype);
7230
- compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, info.ret_vt)?;
7200
+ compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, Some(ret_vt))?;
7201
+ coerceVt(ret_vt, any, &mut body, &ctx.gc_types);
7202
+ }
7231
7203
 
7232
7204
  Instruction::End.encode(&mut body);
7233
7205
  Ok(body)
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -194,6 +194,9 @@ fn assertValid(src: &str) -> Vec<u8> {
194
194
  let source = parse(src);
195
195
  let bytes =
196
196
  compileSource(&source).unwrap_or_else(|e| panic!("compile failed for {:?}: {}", src, e));
197
+ if let Some(path) = std::env::var_os("PLUM_DUMP_WASM") {
198
+ let _ = std::fs::write(path, &bytes);
199
+ }
197
200
  let result = wasmparser::validate(&bytes);
198
201
  assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
199
202
  bytes
@@ -1132,9 +1135,9 @@ fn question_mark_different_ok_types() {
1132
1135
  fun inner() -> Try[Int, [A]] =
1133
1136
  {}
1134
1137
 
1135
- fun outer() -> Try[Str, [A, B]] =
1138
+ fun outer() -> Try[Float, [A, B]] =
1136
1139
  x := inner()?
1137
- Ok(\"fine\")
1140
+ Ok(1.5)
1138
1141
 
1139
1142
  fun main() -> Int =
1140
1143
  match outer()
@@ -1288,9 +1291,9 @@ fn probe_question_mark_payload_tag_and_str_payload() {
1288
1291
  fun inner() -> Try[Int, [Bad(Str)]] =
1289
1292
  {}
1290
1293
 
1291
- fun outer() -> Try[Str, [Bad(Str), Other(Int)]] =
1294
+ fun outer() -> Try[Float, [Bad(Str), Other(Int)]] =
1292
1295
  x := inner()?
1293
- Ok(\"fine\")
1296
+ Ok(1.5)
1294
1297
 
1295
1298
  fun main() -> Int =
1296
1299
  match outer()
@@ -1468,7 +1471,7 @@ fn probe_question_mark_merges_two_ok_types() {
1468
1471
  fun getInt() -> Try[Int, [A]] =
1469
1472
  {}
1470
1473
 
1471
- fun getStr() -> Try[Str, [B]] =
1474
+ fun getStr() -> Try[Float, [B]] =
1472
1475
  {}
1473
1476
 
1474
1477
  fun both() -> Try[Int, [A, B]] =
@@ -1485,8 +1488,8 @@ fun main() -> Int =
1485
1488
  a, b
1486
1489
  )
1487
1490
  };
1488
- assert_eq!(runTryMain(&prog("Ok(5)", "Ok(\"x\")")), 5);
1491
+ assert_eq!(runTryMain(&prog("Ok(5)", "Ok(2.5)")), 5);
1489
- assert_eq!(runTryMain(&prog("Err(A)", "Ok(\"x\")")), 100);
1492
+ assert_eq!(runTryMain(&prog("Err(A)", "Ok(2.5)")), 100);
1490
1493
  assert_eq!(runTryMain(&prog("Ok(5)", "Err(B)")), 200);
1491
1494
  }
1492
1495
 
@@ -1672,3 +1675,100 @@ fun main() -> Int =
1672
1675
  ";
1673
1676
  assert_eq!(runMerged::<i64>("boxed", src), 1);
1674
1677
  }
1678
+
1679
+ // ---- erased generics (T4) ----
1680
+
1681
+ #[test]
1682
+ fn boxed_i64_extremes_through_generic() {
1683
+ let src = "\
1684
+ import std/Bool
1685
+ import std/Str
1686
+ import std/Number
1687
+
1688
+ fun identity(x: T) -> T =
1689
+ x
1690
+
1691
+ fun main() -> Int =
1692
+ lo := 0 - 9223372036854775807 - 1
1693
+ big := 9007199254740993
1694
+ nz := identity(-0.0)
1695
+ ok := identity(lo) == lo && identity(big) == big && identity(big + 0) - big == 0
1696
+ neg := 1.0 / nz < 0.0
1697
+ if ok && neg && identity(0 - 5) == 0 - 5
1698
+ return 1
1699
+ return 0
1700
+ ";
1701
+ assert_eq!(runMerged::<i64>("extremes", src), 1);
1702
+ }
1703
+
1704
+ #[test]
1705
+ fn generic_closure_thunk() {
1706
+ let src = "\
1707
+ import std/Bool
1708
+ import std/Str
1709
+ import std/Number
1710
+ import std/Try
1711
+ import std/List
1712
+
1713
+ fun main() -> Int =
1714
+ xs := List(1, 2, 3).map(|n| n * 2)
1715
+ xs.reduce(0, |acc, x| acc + x)
1716
+ ";
1717
+ assert_eq!(runMerged::<i64>("thunk", src), 12);
1718
+ }
1719
+
1720
+ #[test]
1721
+ fn try_of_list_of_try() {
1722
+ let src = "\
1723
+ import std/Bool
1724
+ import std/Str
1725
+ import std/Number
1726
+ import std/Try
1727
+ import std/List
1728
+
1729
+ fun inner(n: Int) -> Try[Int, [A]] =
1730
+ if n > 1
1731
+ return Err(A)
1732
+ return Ok(n + 40)
1733
+
1734
+ fun make() -> Try[List[Try[Int, [A]]], [B]] =
1735
+ return Ok(List(inner(1), inner(2)))
1736
+
1737
+ fun main() -> Int =
1738
+ match make()
1739
+ Ok(xs) =>
1740
+ first := xs.get(0).unwrap()
1741
+ second := xs.get(1).unwrap()
1742
+ match first, second
1743
+ Ok(a), Err(_) => a
1744
+ _, _ => 0
1745
+ Err(_) => 0
1746
+ ";
1747
+ assert_eq!(runMerged::<i64>("trylist", src), 41);
1748
+ }
1749
+
1750
+ #[test]
1751
+ fn one_copy_per_generic() {
1752
+ let src = "\
1753
+ import std/Bool
1754
+ import std/Str
1755
+ import std/Number
1756
+ import std/Try
1757
+ import std/List
1758
+
1759
+ fun main() -> Int =
1760
+ a := List(1, 2).map(|n| n + 1)
1761
+ b := List(\"x\", \"y\").map(|s| s + \"!\")
1762
+ a.length() + b.length()
1763
+ ";
1764
+ let dir = std::env::temp_dir().join(format!("plum_t4_copy_{}", std::process::id()));
1765
+ std::fs::create_dir_all(&dir).unwrap();
1766
+ let path = dir.join("main.plum");
1767
+ std::fs::write(&path, src).unwrap();
1768
+ let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
1769
+ let (_, keys) = plum_wasm_codegen::compileSourceWithFunctions(&source).expect("compile");
1770
+ let _ = std::fs::remove_dir_all(&dir);
1771
+ let maps: Vec<&String> = keys.iter().filter(|k| k.as_str() == "List::map").collect();
1772
+ assert_eq!(maps.len(), 1, "got {:?}", keys);
1773
+ assert!(!keys.iter().any(|k| k.contains('$')), "mangled names remain");
1774
+ }