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