Skip to main content

submilli_engine/codegen/function_emitter/
cast.rs

1//! Box / unbox helpers — the unified mechanism for moving values
2//! between language types and the universal `(ref $Object)` slot
3//! type used by arrays, `unknown`, and erased generics.
4
5use wasm_encoder::{BlockType, HeapType, Ieee64, Instruction, RefType, ValType};
6
7use crate::Type;
8use crate::codegen::CodegenCtx;
9use crate::codegen::classes::VTABLE_PARENT_SLOT;
10use crate::codegen::function_emitter::{FunctionEmitter, ReturnTarget};
11use crate::codegen::symbol_table::is_nullable_ref;
12use crate::typechecker::infer::narrowing::{TruthinessClass, truthiness_class};
13
14/// Emit the nominal class-membership test for the value on top of the stack
15/// (any `(ref null $Object)`-compatible ref), leaving an i32 (0/1): walk the
16/// value's vtable parent chain, comparing each link by `ref.eq` against the
17/// target class's vtable-singleton global. Value identity, not `ref.test` —
18/// same-shape sibling classes canonicalize to one WasmGC type, so shape can't
19/// carry `instanceof`. Non-class values (strings, boxed primitives, structural
20/// objects, closures) exit false at the `$ClassVTable` test — their vtables are
21/// plain `$VTable` subtypes.
22pub fn emit_nominal_instance_test(
23    emitter: &mut FunctionEmitter<'_>,
24    ctx: &CodegenCtx<'_>,
25    target_vtable_global_idx: u32,
26) -> Result<(), crate::compiler_error::CompilerFailure> {
27    let intr = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
28        crate::codegen::internal_failure("intrinsic types declared before body emission")
29    })?;
30    let ref_null_to = |idx: u32| {
31        ValType::Ref(RefType {
32            nullable: true,
33            heap_type: HeapType::Concrete(idx),
34        })
35    };
36    let obj_local = emitter.add_anonymous_local(ref_null_to(intr.object))?;
37    let raw_vt_local = emitter.add_anonymous_local(ref_null_to(intr.vtable))?;
38    let vt_local = emitter.add_anonymous_local(ref_null_to(intr.class_vtable))?;
39
40    emitter.instruction(Instruction::LocalSet(obj_local));
41    emitter.emit_block(BlockType::Result(ValType::I32)); // $out
42    emitter.emit_block(BlockType::Empty); // $false
43    emitter.instruction(Instruction::LocalGet(obj_local));
44    emitter.instruction(Instruction::BrOnNull(0));
45    emitter.instruction(Instruction::StructGet {
46        struct_type_index: intr.object,
47        field_index: 0,
48    });
49    emitter.instruction(Instruction::LocalTee(raw_vt_local));
50    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
51        intr.class_vtable,
52    )));
53    emitter.instruction(Instruction::I32Eqz);
54    emitter.instruction(Instruction::BrIf(0));
55    emitter.instruction(Instruction::LocalGet(raw_vt_local));
56    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
57        intr.class_vtable,
58    )));
59    emitter.instruction(Instruction::LocalSet(vt_local));
60    emitter.emit_loop(BlockType::Empty); // $walk
61    emitter.instruction(Instruction::LocalGet(vt_local));
62    emitter.instruction(Instruction::GlobalGet(target_vtable_global_idx));
63    emitter.instruction(Instruction::RefEq);
64    emitter.emit_if(BlockType::Empty);
65    emitter.instruction(Instruction::I32Const(1));
66    emitter.instruction(Instruction::Br(3)); // -> $out
67    emitter.emit_end();
68    emitter.instruction(Instruction::LocalGet(vt_local));
69    emitter.instruction(Instruction::StructGet {
70        struct_type_index: intr.class_vtable,
71        field_index: VTABLE_PARENT_SLOT,
72    });
73    emitter.instruction(Instruction::BrOnNull(1)); // chain root -> $false
74    emitter.instruction(Instruction::LocalSet(vt_local));
75    emitter.instruction(Instruction::Br(0)); // continue $walk
76    emitter.emit_end(); // end $walk
77    emitter.emit_end(); // end $false
78    emitter.instruction(Instruction::I32Const(0));
79    emitter.emit_end(); // end $out
80
81    Ok(())
82}
83
84/// Emit Wasm instructions that wrap the value currently on top of
85/// the stack (of language type `ty`) into a `(ref $Object)`.
86pub fn emit_box(
87    emitter: &mut FunctionEmitter<'_>,
88    ctx: &CodegenCtx<'_>,
89    ty: &Type,
90) -> Result<(), crate::compiler_error::CompilerFailure> {
91    let ty = ty.lowered();
92    match ty {
93        Type::Number | Type::NumberLiteral(_) => {
94            let boxed_idx = ctx
95                .symbols
96                .boxed_number_type_idx()
97                .ok_or_else(|| crate::codegen::internal_failure("boxed_number type registered"))?;
98            let vtable_global = ctx
99                .symbols
100                .prelude_global_idx("boxed_number_vtable")
101                .ok_or_else(|| crate::codegen::internal_failure("boxed_number_vtable imported"))?;
102            // Stack: [n_f64]. Need [vtable_ref, n_f64] for struct.new.
103            // Stash via scratch local.
104            let scratch = emitter.add_anonymous_local(wasm_encoder::ValType::F64)?;
105            emitter.instruction(Instruction::LocalSet(scratch));
106            emitter.instruction(Instruction::GlobalGet(vtable_global));
107            emitter.instruction(Instruction::LocalGet(scratch));
108            emitter.instruction(Instruction::StructNew(boxed_idx));
109        }
110        Type::Boolean | Type::BooleanLiteral(_) => {
111            let boxed_idx = ctx
112                .symbols
113                .boxed_boolean_type_idx()
114                .ok_or_else(|| crate::codegen::internal_failure("boxed_boolean type registered"))?;
115            let vtable_global = ctx
116                .symbols
117                .prelude_global_idx("boxed_boolean_vtable")
118                .ok_or_else(|| crate::codegen::internal_failure("boxed_boolean_vtable imported"))?;
119            let scratch = emitter.add_anonymous_local(wasm_encoder::ValType::I32)?;
120            emitter.instruction(Instruction::LocalSet(scratch));
121            emitter.instruction(Instruction::GlobalGet(vtable_global));
122            emitter.instruction(Instruction::LocalGet(scratch));
123            emitter.instruction(Instruction::StructNew(boxed_idx));
124        }
125        Type::String
126        | Type::StringLiteral(_)
127        | Type::BigInt
128        | Type::BigIntLiteral(_)
129        | Type::Object { .. }
130        | Type::Array(_)
131        | Type::Tuple(_)
132        | Type::Uint8Array
133        | Type::Function { .. } => {
134            // Already an $Object subtype — upcast is implicit.
135        }
136        Type::TypeVar(_)
137        | Type::GenericParam { .. }
138        | Type::Unknown
139        | Type::NonPrimitive
140        | Type::Never => {
141            // Never: the call always throws before producing a value, but
142            // the wasm sig still has the slot — lowered same as Unknown.
143        }
144        Type::Null => {
145            // null lowers to (ref null none) — the bottom of the WasmGC ref
146            // hierarchy — a subtype of every nullable ref; no instruction needed.
147        }
148        Type::Undefined => {
149            let idx = ctx
150                .symbols
151                .intrinsic_type_indices()
152                .ok_or_else(|| {
153                    crate::codegen::internal_failure("undefined intrinsic not registered")
154                })?
155                .undefined;
156            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
157        }
158        Type::Void => {}
159        Type::Error => {
160            return Err(crate::codegen::internal_failure(
161                "invalid type representation during cast lowering",
162            ));
163        }
164        Type::NumberEnum { .. } | Type::StringEnum { .. } => {}
165        Type::Union(_) => {
166            // value_type is authoritative: literal-only unions can collapse to
167            // primitive slots (mirrors emit_cast_to's Union arm); ref-lowered
168            // unions are already $Object subtypes.
169            match ctx.symbols.value_type(ty)? {
170                ValType::F64 => emit_box(emitter, ctx, &Type::Number)?,
171                ValType::I32 => emit_box(emitter, ctx, &Type::Boolean)?,
172                ValType::Ref(_) => {}
173                _ => {
174                    return Err(crate::codegen::internal_failure(
175                        "invalid type representation during cast lowering",
176                    ));
177                }
178            }
179        }
180        // A class instance is an `$Object` subtype; the upcast is implicit.
181        Type::InterfaceRef { .. } | Type::ClassRef { .. } => {}
182        // A recursion back-edge lowers to `(ref null $Object)` — already
183        // an `$Object` subtype, so the upcast is implicit. No-op.
184        Type::AliasRef { .. } => {}
185        Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => {
186            return Err(crate::codegen::internal_failure(
187                "invalid type representation during cast lowering",
188            ));
189        }
190    };
191    Ok(())
192}
193
194/// Coerce a `source_ty`-typed value into the Wasm slot type for `target_ty`.
195/// Boxes primitives (f64/i32 → ref); WasmGC subtyping handles ref→ref cases.
196///
197/// Caller guarantees `source_ty` is assignable to `target_ty` (typechecker-proven);
198/// narrowing-direction casts use `emit_narrowing_cast` instead.
199pub fn emit_coerce_to_slot(
200    emitter: &mut FunctionEmitter<'_>,
201    ctx: &CodegenCtx<'_>,
202    source_ty: &Type,
203    target_ty: &Type,
204) -> Result<(), crate::compiler_error::CompilerFailure> {
205    let source_val = ctx.symbols.value_type(source_ty)?;
206    let target_val = ctx.symbols.value_type(target_ty)?;
207    if source_val == target_val {
208        return Ok(());
209    }
210    // Never: the call already threw, but the wasm validator still needs a
211    // well-typed cast sequence; emit_cast_to covers this. Traps here are fine.
212    if matches!(source_ty.lowered(), Type::Never) {
213        emit_cast_to(emitter, ctx, target_ty)?;
214        return Ok(());
215    }
216    emit_coerce_to_wasm_slot(emitter, ctx, source_ty, target_val)?;
217
218    Ok(())
219}
220
221/// Coerce a `source_ty`-typed value into an already-known Wasm slot type.
222///
223/// Used where the target's Wasm type was *recorded* when the signature was
224/// emitted rather than re-derived from a language type — class-method vtable
225/// slots and closure funcref result slots. A vtable signature deliberately
226/// erases class types and type variables (see `SymbolTable::slot_value_type`);
227/// re-deriving would be unsound because `value_type`'s `ClassRef` lowering
228/// changes once the class's struct type is recorded.
229pub fn emit_coerce_to_wasm_slot(
230    emitter: &mut FunctionEmitter<'_>,
231    ctx: &CodegenCtx<'_>,
232    source_ty: &Type,
233    target_val: ValType,
234) -> Result<(), crate::compiler_error::CompilerFailure> {
235    let source_val = ctx.symbols.value_type(source_ty)?;
236    if source_val == target_val {
237        return Ok(());
238    }
239    // A `never`-typed argument comes from a call that always throws, so this
240    // point is unreachable. The validator still has to see a well-typed stack
241    // and the value's slot may be a primitive it can't be cast into, so make
242    // the rest of the sequence stack-polymorphic instead.
243    if matches!(source_ty.lowered(), Type::Never) {
244        emitter.instruction(Instruction::Unreachable);
245        return Ok(());
246    }
247    if crate::codegen::closure_coercions::emit_coercion(emitter, ctx, source_ty, target_val)? {
248        return Ok(());
249    }
250    if source_ty == &Type::Unknown && target_val == ctx.symbols.value_type(&Type::String)? {
251        let function = ctx
252            .symbols
253            .prelude_func_idx("__value_to_string")
254            .ok_or_else(|| {
255                crate::codegen::internal_failure("dynamic string conversion collected")
256            })?;
257        emitter.instruction(Instruction::Call(function));
258        return Ok(());
259    }
260    match (&source_val, &target_val) {
261        (ValType::Ref(_), ValType::F64) if matches!(source_ty, Type::Unknown) => {
262            let function = ctx
263                .symbols
264                .prelude_func_idx("__value_to_number")
265                .ok_or_else(|| {
266                    crate::codegen::internal_failure("dynamic number conversion collected")
267                })?;
268            emitter.instruction(Instruction::Call(function));
269        }
270        (ValType::Ref(_), ValType::I32) if matches!(source_ty, Type::Unknown) => {
271            emit_condition_to_i32(emitter, ctx, source_ty)?;
272        }
273        (ValType::Ref(_), ValType::F64) => {
274            emit_cast_to(emitter, ctx, &Type::Number)?;
275        }
276        // Primitive into a boxed slot.
277        (ValType::F64 | ValType::I32, ValType::Ref(_)) => emit_box(emitter, ctx, source_ty)?,
278        // Ref subtypes — WasmGC subtyping carries the value; no instruction needed.
279        (ValType::Ref(source), ValType::Ref(target))
280            if ctx.symbols.ref_fits_slot(*source, *target) => {}
281        // Any other ref pairing needs the value pinned to the slot's own heap
282        // type: a nullable value into a non-null slot, or a supertype-lowered
283        // value (an `InterfaceRef` is `(ref null $Object)`) into a narrower
284        // structural slot. The typechecker proved the value fits, so neither
285        // trap is reachable.
286        (ValType::Ref(_), ValType::Ref(target)) => {
287            if target.nullable {
288                emitter.instruction(Instruction::RefCastNullable(target.heap_type));
289            } else {
290                emitter.instruction(Instruction::RefCastNonNull(target.heap_type));
291            }
292        }
293        _ => {
294            return Err(crate::codegen::internal_failure(
295                "value representation does not fit the recorded slot",
296            ));
297        }
298    };
299    Ok(())
300}
301
302/// Coerce the returned value on top of the stack (static type `source_ty`) into
303/// the enclosing body's [`ReturnTarget`].
304///
305/// A recorded slot is coerced to as a Wasm type, not through the declaration it
306/// came from: coercing `(x: number): number => x + 2` against its declaration
307/// would leave a bare f64 in the closure ABI's ref slot.
308pub fn emit_coerce_to_return_slot(
309    emitter: &mut FunctionEmitter<'_>,
310    ctx: &CodegenCtx<'_>,
311    source_ty: &Type,
312) -> Result<(), crate::compiler_error::CompilerFailure> {
313    match emitter.return_target().clone() {
314        ReturnTarget::Slot(slot) => emit_coerce_to_wasm_slot(emitter, ctx, source_ty, slot)?,
315        ReturnTarget::Declared(ret) => emit_coerce_to_slot(emitter, ctx, source_ty, &ret)?,
316        ReturnTarget::NoResult => {}
317    };
318    Ok(())
319}
320
321/// Consume the condition value (static type `cond_ty`) on the stack and leave
322/// an i32 where nonzero = truthy, per JS semantics: `null`, `""`, `0`, `-0`,
323/// `NaN`, `false`, and `0n` are falsy; everything else is truthy.
324pub fn emit_condition_to_i32(
325    emitter: &mut FunctionEmitter<'_>,
326    ctx: &CodegenCtx<'_>,
327    cond_ty: &Type,
328) -> Result<(), crate::compiler_error::CompilerFailure> {
329    let val = ctx.symbols.value_type(cond_ty)?;
330    match val {
331        // Number slot: `|x| > 0` folds `0`, `-0`, and `NaN` (abs(NaN) > 0 is
332        // false) into falsy in three instructions.
333        ValType::F64 => {
334            emitter.instruction(Instruction::F64Abs);
335            emitter.instruction(Instruction::F64Const(Ieee64::from(0.0)));
336            emitter.instruction(Instruction::F64Gt);
337        }
338        ValType::Ref(rt) => emit_ref_truthiness(emitter, ctx, cond_ty, rt.nullable)?,
339        // Boolean slot (i32) is already the condition convention.
340        _ => {}
341    };
342    Ok(())
343}
344
345/// Ref-slot truthiness. Fast paths: a type whose only falsy value is `null`
346/// needs just a null test; a bare `$string` needs just its length. Everything
347/// else (falsy-bearing unions, erased generics) takes a `ref.test` dispatch
348/// over only the falsy kinds the static type admits, defaulting to truthy —
349/// mirrors `emit_typeof_tag`.
350fn emit_ref_truthiness(
351    emitter: &mut FunctionEmitter<'_>,
352    ctx: &CodegenCtx<'_>,
353    cond_ty: &Type,
354    nullable: bool,
355) -> Result<(), crate::compiler_error::CompilerFailure> {
356    use TruthinessClass::*;
357    let lowered = cond_ty.lowered();
358    let members: Vec<&Type> = match lowered {
359        Type::Union(members) => members.iter().collect(),
360        single => vec![single],
361    };
362    let classes: Vec<TruthinessClass> = members
363        .iter()
364        .filter(|m| !matches!(m.lowered(), Type::Null))
365        .map(|m| match m.lowered() {
366            Type::Void => Dynamic,
367            Type::Undefined => AlwaysFalsy,
368            _ => truthiness_class(m),
369        })
370        .collect();
371
372    if classes.iter().all(|c| *c == AlwaysTruthy) {
373        if nullable {
374            emitter.instruction(Instruction::RefIsNull);
375            emitter.instruction(Instruction::I32Eqz);
376        } else {
377            emitter.instruction(Instruction::Drop);
378            emitter.instruction(Instruction::I32Const(1));
379        }
380        return Ok(());
381    }
382    if classes.iter().all(|c| *c == AlwaysFalsy) {
383        emitter.instruction(Instruction::Drop);
384        emitter.instruction(Instruction::I32Const(0));
385        return Ok(());
386    }
387
388    let needs = |class: TruthinessClass, literal: fn(&Type) -> bool| {
389        classes.contains(&class)
390            || classes.contains(&Dynamic)
391            || members.iter().any(|m| literal(m.lowered()))
392    };
393    let test_string = needs(StringLike, |m| matches!(m, Type::StringLiteral(_)));
394    let test_number = needs(NumberLike, |m| matches!(m, Type::NumberLiteral(_)));
395    let test_boolean = needs(BooleanLike, |m| matches!(m, Type::BooleanLiteral(_)));
396    let test_bigint = needs(BigIntLike, |_| false);
397    let test_undefined = classes.contains(&Dynamic)
398        || members
399            .iter()
400            .any(|member| matches!(member.lowered(), Type::Undefined));
401
402    let slot = ctx.symbols.value_type(cond_ty)?;
403    let tmp = emitter.add_anonymous_local(slot)?;
404    emitter.instruction(Instruction::LocalSet(tmp));
405
406    // Nested if/else chain, innermost default = truthy. Each falsy-capable
407    // kind gets a `ref.test` arm that extracts its payload's own truthiness.
408    let mut open_blocks = 0u32;
409    if nullable {
410        emitter.instruction(Instruction::LocalGet(tmp));
411        emitter.instruction(Instruction::RefIsNull);
412        emitter.emit_if(BlockType::Result(ValType::I32));
413        emitter.instruction(Instruction::I32Const(0));
414        emitter.emit_else();
415        open_blocks += 1;
416    }
417    if test_undefined {
418        emitter.instruction(Instruction::LocalGet(tmp));
419        super::expr::emit_is_undefined(emitter, ctx)?;
420        emitter.emit_if(BlockType::Result(ValType::I32));
421        emitter.instruction(Instruction::I32Const(0));
422        emitter.emit_else();
423        open_blocks += 1;
424    }
425    if test_string {
426        let idx = ctx
427            .symbols
428            .string_type_idx()
429            .ok_or_else(|| crate::codegen::internal_failure("$string intrinsic type registered"))?;
430        emitter.instruction(Instruction::LocalGet(tmp));
431        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(idx)));
432        emitter.emit_if(BlockType::Result(ValType::I32));
433        emitter.instruction(Instruction::LocalGet(tmp));
434        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
435        emitter.instruction(Instruction::StructGet {
436            struct_type_index: idx,
437            field_index: 1,
438        });
439        emitter.instruction(Instruction::ArrayLen);
440        emitter.emit_else();
441        open_blocks += 1;
442    }
443    if test_number {
444        let idx = ctx.symbols.boxed_number_type_idx().ok_or_else(|| {
445            crate::codegen::internal_failure("$BoxedNumber intrinsic type registered")
446        })?;
447        emitter.instruction(Instruction::LocalGet(tmp));
448        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(idx)));
449        emitter.emit_if(BlockType::Result(ValType::I32));
450        emitter.instruction(Instruction::LocalGet(tmp));
451        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
452        emitter.instruction(Instruction::StructGet {
453            struct_type_index: idx,
454            field_index: 1,
455        });
456        emitter.instruction(Instruction::F64Abs);
457        emitter.instruction(Instruction::F64Const(0.0.into()));
458        emitter.instruction(Instruction::F64Gt);
459        emitter.emit_else();
460        open_blocks += 1;
461    }
462    if test_boolean {
463        let idx = ctx.symbols.boxed_boolean_type_idx().ok_or_else(|| {
464            crate::codegen::internal_failure("$BoxedBoolean intrinsic type registered")
465        })?;
466        emitter.instruction(Instruction::LocalGet(tmp));
467        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(idx)));
468        emitter.emit_if(BlockType::Result(ValType::I32));
469        emitter.instruction(Instruction::LocalGet(tmp));
470        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
471        emitter.instruction(Instruction::StructGet {
472            struct_type_index: idx,
473            field_index: 1,
474        });
475        emitter.emit_else();
476        open_blocks += 1;
477    }
478    if test_bigint {
479        let idx = ctx
480            .symbols
481            .bigint_type_idx()
482            .ok_or_else(|| crate::codegen::internal_failure("$bigint intrinsic type registered"))?;
483        emitter.instruction(Instruction::LocalGet(tmp));
484        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(idx)));
485        emitter.emit_if(BlockType::Result(ValType::I32));
486        emitter.instruction(Instruction::LocalGet(tmp));
487        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
488        // Field 1 is the sign (−1/0/1) — nonzero iff the bigint is nonzero.
489        emitter.instruction(Instruction::StructGet {
490            struct_type_index: idx,
491            field_index: 1,
492        });
493        emitter.emit_else();
494        open_blocks += 1;
495    }
496    emitter.instruction(Instruction::I32Const(1));
497    for _ in 0..open_blocks {
498        emitter.emit_end();
499    }
500    Ok(())
501}
502
503/// Emit the Wasm cast from `cast_info.from_ty` to `cast_info.to_ty`.
504/// The cast is **unchecked** — codegen trusts the typechecker's narrowing
505/// decision; the Wasm-level cast traps if the predicate's runtime check was unsound.
506pub fn emit_narrowing_cast(
507    emitter: &mut FunctionEmitter<'_>,
508    ctx: &CodegenCtx<'_>,
509    cast_info: &crate::typechecker::infer::narrowing::CastInfo,
510) -> Result<(), crate::compiler_error::CompilerFailure> {
511    use crate::typechecker::infer::narrowing::CastKind;
512    if matches!(cast_info.to_ty.lowered(), Type::Never) {
513        emitter.instruction(Instruction::Drop);
514        emitter.instruction(Instruction::Unreachable);
515        return Ok(());
516    }
517    let from_val = ctx.symbols.value_type(&cast_info.from_ty)?;
518    let to_val = ctx.symbols.value_type(&cast_info.to_ty)?;
519    // Same Wasm repr → no instructions. Happens when the inferer
520    // tightened a multi-member union into another multi-member union
521    // (e.g., `number | string | null` → `string | null` after
522    // `typeof x === "number"` else-branch) — both lower to
523    // `(ref null $Object)`.
524    if from_val == to_val {
525        return Ok(());
526    }
527    // Repr changes only in the nullable bit (same heap type, source nullable,
528    // target non-null) — e.g. the false-branch of `!== null` on a union like
529    // `number | string | null` narrows to a non-null union still lowered to
530    // `(ref $Object)`. A bare `ref.as_non_null` covers the repr gap; no
531    // `ref.cast` is meaningful since the heap type is unchanged.
532    if let (ValType::Ref(s), ValType::Ref(t)) = (&from_val, &to_val)
533        && s.heap_type == t.heap_type
534        && s.nullable
535        && !t.nullable
536    {
537        emitter.instruction(Instruction::RefAsNonNull);
538        return Ok(());
539    }
540    match cast_info.cast_kind {
541        CastKind::NonNull | CastKind::Unbox => {
542            // Both reduce to emit_cast_to: source is (ref null $Object),
543            // target is the narrowed concrete type.
544            emit_cast_to(emitter, ctx, &cast_info.to_ty)?;
545        }
546        CastKind::RefSubtype => {
547            // Same-repr cases were handled above. ref.cast trusts the
548            // typechecker — traps only if the narrowing decision was unsound.
549            let ValType::Ref(target_ref) = to_val else {
550                return Err(crate::codegen::internal_failure(
551                    "invalid type representation during cast lowering",
552                ));
553            };
554            let cast = if target_ref.nullable {
555                Instruction::RefCastNullable(target_ref.heap_type)
556            } else {
557                Instruction::RefCastNonNull(target_ref.heap_type)
558            };
559            emitter.instruction(cast);
560        }
561    };
562    Ok(())
563}
564
565/// Recover `ty`'s value type from a slot that stores it erased — a box cell's
566/// payload, keyed on `slot_value_type`.
567///
568/// Skips the cast when the two lowerings already agree — which is not the same
569/// as `ty` not being erased. An erased type often lowers to exactly the
570/// `(ref null $Object)` the cell holds: a `TypeVar` or `GenericParam` does, and
571/// so does a union mixing one with anything else. Gating on erasure instead
572/// would cast a value already in its own lowering — and for an unrecorded
573/// `ClassRef` that cast is a `ref.as_non_null` plus a `ref.cast` to a class
574/// struct this module never recorded.
575pub fn emit_unerase(
576    emitter: &mut FunctionEmitter<'_>,
577    ctx: &CodegenCtx<'_>,
578    ty: &Type,
579) -> Result<(), crate::compiler_error::CompilerFailure> {
580    if ctx.symbols.slot_value_type(ty)? != ctx.symbols.value_type(ty)? {
581        emit_cast_to(emitter, ctx, ty)?;
582    };
583    Ok(())
584}
585
586/// Emit Wasm instructions that pop a `(ref null $Object)` from the
587/// stack and produce a `ty`-typed value.
588///
589/// Never leaves the stack *wider* than `value_type(ty)` — the arms that emit no
590/// `ref.cast` narrow with `ref.as_non_null` or nothing at all. Several callers
591/// gate on `ValType` equality alone and would emit an ill-typed sequence if a
592/// cast target could come back wider than its own lowering.
593pub fn emit_cast_to(
594    emitter: &mut FunctionEmitter<'_>,
595    ctx: &CodegenCtx<'_>,
596    ty: &Type,
597) -> Result<(), crate::compiler_error::CompilerFailure> {
598    // codegen casts on the lowered shape: aliases peeled (their label is
599    // purely a typechecker/display thing) and `{}` read as `unknown`.
600    let ty = ty.lowered();
601    // Every per-type cast below assumes non-null input, so lift unless the target
602    // admits a null of its own.
603    if !target_allows_null(ctx, ty)? {
604        emitter.instruction(Instruction::RefAsNonNull);
605    }
606    match ty {
607        Type::Number | Type::NumberLiteral(_) => {
608            let boxed_idx = ctx
609                .symbols
610                .boxed_number_type_idx()
611                .ok_or_else(|| crate::codegen::internal_failure("boxed_number type registered"))?;
612            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(boxed_idx)));
613            emitter.instruction(Instruction::StructGet {
614                struct_type_index: boxed_idx,
615                field_index: 1,
616            });
617        }
618        Type::Boolean | Type::BooleanLiteral(_) => {
619            let boxed_idx = ctx
620                .symbols
621                .boxed_boolean_type_idx()
622                .ok_or_else(|| crate::codegen::internal_failure("boxed_boolean type registered"))?;
623            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(boxed_idx)));
624            emitter.instruction(Instruction::StructGet {
625                struct_type_index: boxed_idx,
626                field_index: 1,
627            });
628        }
629        Type::String | Type::StringLiteral(_) => {
630            let idx = ctx
631                .symbols
632                .string_type_idx()
633                .ok_or_else(|| crate::codegen::internal_failure("string type registered"))?;
634            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
635        }
636        Type::BigInt | Type::BigIntLiteral(_) => {
637            let idx = ctx
638                .symbols
639                .bigint_type_idx()
640                .ok_or_else(|| crate::codegen::internal_failure("bigint type registered"))?;
641            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
642        }
643        Type::Object { .. } => {
644            // `Type::Object` lowers to `(ref $ObjectShape)`,
645            // not the arity-specific subtype. The static arity does
646            // not pin runtime arity once width subtyping has erased
647            // it, so casting to a per-arity subtype here would trap
648            // for any widened value. Field reads/writes go through
649            // the per-shape getter/setter funcrefs on slots 2/3.
650            let idx = ctx
651                .symbols
652                .object_shape_type_idx()
653                .ok_or_else(|| crate::codegen::internal_failure("$ObjectShape type registered"))?;
654            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
655        }
656        Type::Array(_) | Type::Tuple(_) => {
657            // Tuples and arrays share the `$Array` Wasm representation
658            // — the typechecker keeps `Type::Tuple` distinct for
659            // positional assignability + per-position `emit_cast_to`
660            // at the element level.
661            let idx = ctx
662                .symbols
663                .array_type_idx()
664                .ok_or_else(|| crate::codegen::internal_failure("array type registered"))?;
665            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
666        }
667        Type::Uint8Array => {
668            let idx = ctx
669                .symbols
670                .uint8_array_type_idx()
671                .ok_or_else(|| crate::codegen::internal_failure("uint8_array type registered"))?;
672            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
673        }
674        Type::TypeVar(_)
675        | Type::GenericParam { .. }
676        | Type::Unknown
677        | Type::NonPrimitive
678        | Type::Never => {
679            // Never: the call always throws before producing a value; reuses
680            // Unknown's (ref null $Object) lowering — see value_type.
681        }
682        Type::Function { .. } => {
683            crate::codegen::closure_coercions::emit_erased_cast(
684                emitter,
685                ctx,
686                crate::codegen::closures::classify(ty)?,
687            )?;
688        }
689        Type::Null => {
690            // Null lowers to (ref null $Object) — already a subtype of any nullable ref.
691        }
692        Type::Undefined => {
693            let idx = ctx
694                .symbols
695                .intrinsic_type_indices()
696                .ok_or_else(|| {
697                    crate::codegen::internal_failure("undefined intrinsic not registered")
698                })?
699                .undefined;
700            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
701        }
702        Type::Void => {}
703        Type::Error => {
704            return Err(crate::codegen::internal_failure(
705                "invalid type representation during cast lowering",
706            ));
707        }
708        Type::Union(_) => {
709            // value_type is authoritative: literal-only unions can collapse to
710            // primitive slots, while object/mixed unions remain references.
711            match ctx.symbols.value_type(ty)? {
712                ValType::F64 => emit_cast_to(emitter, ctx, &Type::Number)?,
713                ValType::I32 => emit_cast_to(emitter, ctx, &Type::Boolean)?,
714                ValType::Ref(target_ref) => {
715                    let cast = if target_ref.nullable {
716                        Instruction::RefCastNullable(target_ref.heap_type)
717                    } else {
718                        Instruction::RefCastNonNull(target_ref.heap_type)
719                    };
720                    emitter.instruction(cast);
721                }
722                _ => {
723                    return Err(crate::codegen::internal_failure(
724                        "invalid type representation during cast lowering",
725                    ));
726                }
727            }
728        }
729        Type::NumberEnum { .. } => {
730            // Numeric enum values are `$BoxedNumber` instances —
731            // cast the universal slot back to that subtype. Unlike
732            // `Type::Number`, do NOT unbox to f64: the static type
733            // is the boxed form.
734            let boxed_idx = ctx
735                .symbols
736                .boxed_number_type_idx()
737                .ok_or_else(|| crate::codegen::internal_failure("boxed_number type registered"))?;
738            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(boxed_idx)));
739        }
740        Type::StringEnum { .. } => {
741            let idx = ctx
742                .symbols
743                .string_type_idx()
744                .ok_or_else(|| crate::codegen::internal_failure("string type registered"))?;
745            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
746        }
747        Type::InterfaceRef { .. } => {
748            // InterfaceRef: RefAsNonNull above is enough — method dispatch uses
749            // mangled names, not ref.cast to a per-interface struct.
750        }
751        Type::ClassRef { mangled, .. } => {
752            // Body emission runs after `imported_classes::reconstruct` and the
753            // `ClassPlan` reservation, so every class a body can reference has a
754            // recorded struct type; a miss here is a dependency-usage collection
755            // gap, not the erased pre-recording representation `value_type`
756            // tolerates for env/box emission.
757            let idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
758                crate::codegen::internal_failure("class struct type is not registered")
759            })?;
760            emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(idx)));
761        }
762        Type::AliasRef { .. } => {
763            // A recursion back-edge lowers to `(ref null $Object)` and the
764            // `RefAsNonNull` above is skipped for it, so the null the alias body
765            // may list flows through. Field reads go through the shape
766            // field-name scan, same as `InterfaceRef`; no per-shape cast.
767        }
768        Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => {
769            return Err(crate::codegen::internal_failure(
770                "invalid type representation during cast lowering",
771            ));
772        }
773    };
774    Ok(())
775}
776
777/// Whether [`emit_cast_to`] lets a `null` reach the target rather than lifting to
778/// non-null first. True for the targets whose lowering is a nullable
779/// `(ref null $Object)`: bare `Null`, nullable unions, and the no-op
780/// erased/dynamic types — `unknown`, `never`, an erased generic (which can hold
781/// null after instantiation, e.g. `T = string | null`), and a recursion back-edge
782/// whose body may list `null`. `emit_cast_to`'s `Union` arm uses `RefCastNullable`
783/// for the same reason. A union's nullability is its *lowering*, not whether a
784/// member is spelled `null`: `T | number` lowers nullable because `T` does.
785///
786/// A caller that must not hand [`emit_cast_to`] a `null` has to ask this, not
787/// `is_nullable_ref`: the two disagree — an `InterfaceRef` lowers to a nullable
788/// slot and is still `ref.as_non_null`ed there, which traps.
789pub fn target_allows_null(
790    ctx: &CodegenCtx<'_>,
791    ty: &Type,
792) -> Result<bool, crate::compiler_error::CompilerFailure> {
793    let ty = ty.lowered();
794    Ok(matches!(
795        ty,
796        Type::Null
797            | Type::Void
798            | Type::Unknown
799            | Type::Never
800            | Type::TypeVar(_)
801            | Type::GenericParam { .. }
802            | Type::AliasRef { .. }
803    ) || (matches!(ty, Type::Union(_)) && is_nullable_ref(ctx.symbols.value_type(ty)?)))
804}