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