Skip to main content

submilli_engine/codegen/function_emitter/
stmt.rs

1use crate::codegen::CodegenCtx;
2use crate::codegen::bounds::{
3    emit_checked_index, emit_checked_index_with_length, stash_array_length, stash_index_operand,
4};
5use crate::codegen::function_emitter::FunctionEmitter;
6use crate::codegen::init_guard;
7use crate::{
8    EnumVariantPayload, ExprId, Ident, MangledName, StmtId, Type, TypedStmtKind, TypedSwitchCase,
9    TypedSwitchValue,
10};
11use wasm_encoder::{BlockType, HeapType, Instruction, RefType, ValType};
12
13use super::cast;
14use super::expr::{
15    emit_expr, emit_method_call, emit_object_property_write, stash_receiver_as_object_shape,
16};
17
18/// Does this statement contain a body some execution can skip, or re-enter?
19///
20/// That is exactly where emission order stops implying execution order, which
21/// is the one thing a narrowing shadow depends on — see
22/// [`FunctionEmitter::clear_all_narrow_shadows`]. Clearing is conservative (a
23/// missing shadow only costs the narrowed read a cast at use), so a statement
24/// kind that gains a body opts *out* of the rule rather than having to
25/// remember to opt in.
26fn branches(kind: &TypedStmtKind) -> bool {
27    matches!(
28        kind,
29        TypedStmtKind::If { .. }
30            | TypedStmtKind::While { .. }
31            | TypedStmtKind::DoWhile { .. }
32            | TypedStmtKind::For { .. }
33            | TypedStmtKind::ForOf { .. }
34            | TypedStmtKind::Switch { .. }
35            | TypedStmtKind::Try { .. }
36    )
37}
38
39pub fn emit_statement(
40    emitter: &mut FunctionEmitter,
41    ctx: &CodegenCtx,
42    id: StmtId,
43) -> Result<(), crate::compiler_error::CompilerFailure> {
44    let stmt = ctx.ta.try_stmt(id).map_err(crate::codegen::arena_failure)?;
45    emitter.record_span(stmt.span);
46    // Before as well as after: a loop's back edge re-runs the reads above its
47    // body's writes, so a shadow taken before the loop is already unsound at
48    // the loop head.
49    if branches(&stmt.kind) {
50        emitter.clear_all_narrow_shadows();
51    }
52    match &stmt.kind {
53        TypedStmtKind::Block(stmts) => {
54            // Wasm locals are function-scoped, so a source-level block only
55            // needs a fresh name-resolution scope — no Wasm `block`.
56            emitter.push_scope();
57            for &child in stmts {
58                emit_statement(emitter, ctx, child)?;
59            }
60            emitter.pop_scope()?;
61        }
62        TypedStmtKind::Return(value) => {
63            if let Some(expr_id) = value {
64                emit_expr(emitter, ctx, *expr_id)?;
65                cast::emit_coerce_to_return_slot(
66                    emitter,
67                    ctx,
68                    &ctx.ta
69                        .try_expr(*expr_id)
70                        .map_err(crate::codegen::arena_failure)?
71                        .ty,
72                )?;
73            } else if emitter.wasm_result_type(ctx)?.is_some() {
74                super::expr::emit_undefined(emitter, ctx)?;
75                cast::emit_coerce_to_return_slot(emitter, ctx, &Type::Undefined)?;
76            }
77            super::finally::emit_transfer(emitter, super::finally::Transfer::Return)?;
78        }
79        TypedStmtKind::Expr(expr_id) => {
80            emit_expr(emitter, ctx, *expr_id)?;
81            emitter.instruction(Instruction::Drop);
82        }
83        // top-level decls become global.set in _start; these are function-local
84        TypedStmtKind::Let {
85            name,
86            ty,
87            value,
88            boxed,
89            ..
90        } => {
91            emit_expr(emitter, ctx, *value)?;
92            let value_ty = ctx
93                .ta
94                .try_expr(*value)
95                .map_err(crate::codegen::arena_failure)?
96                .ty
97                .clone();
98            cast::emit_coerce_to_slot(emitter, ctx, &value_ty, ty)?;
99            if *boxed {
100                let box_idx = ctx.symbols.box_type_idx(ty)?.ok_or_else(|| {
101                    crate::codegen::internal_failure("box type registered for every boxed Let")
102                })?;
103                if ctx.ta.parameter_initializations.contains(&id) {
104                    let slot = emitter
105                        .parameter_binding_cells
106                        .get(name)
107                        .copied()
108                        .ok_or_else(|| {
109                            crate::codegen::internal_failure(
110                                "parameter pattern cell was not reserved",
111                            )
112                        })?;
113                    let value_slot = emitter.add_anonymous_local(ctx.symbols.value_type(ty)?)?;
114                    emitter.instruction(Instruction::LocalSet(value_slot));
115                    emitter.instruction(Instruction::LocalGet(slot));
116                    emitter.instruction(Instruction::LocalGet(value_slot));
117                    emitter.instruction(Instruction::StructSet {
118                        struct_type_index: box_idx,
119                        field_index: 0,
120                    });
121                    super::binding_cells::mark_initialized(emitter, slot, box_idx);
122                    return Ok(());
123                }
124                emitter.instruction(Instruction::I32Const(1));
125                emitter.instruction(Instruction::StructNew(box_idx));
126                let box_val = ValType::Ref(RefType {
127                    nullable: false,
128                    heap_type: HeapType::Concrete(box_idx),
129                });
130                let slot = emitter.define_local(name, box_val)?;
131                emitter.instruction(Instruction::LocalSet(slot));
132            } else {
133                let slot = emitter.define_local(name, ctx.symbols.value_type(ty)?)?;
134                emitter.instruction(Instruction::LocalSet(slot));
135            }
136        }
137        TypedStmtKind::ReboxLocal { ident, ty } => {
138            let slot = emitter.write_slot(&ident.name)?;
139            let box_idx = ctx.symbols.box_type_idx(ty)?.ok_or_else(|| {
140                crate::codegen::internal_failure("box type registered for every boxed Let")
141            })?;
142            super::binding_cells::read(emitter, ctx, slot, box_idx)?;
143            emitter.instruction(Instruction::I32Const(1));
144            emitter.instruction(Instruction::StructNew(box_idx));
145            emitter.instruction(Instruction::LocalSet(slot));
146        }
147        TypedStmtKind::Const {
148            name, ty, value, ..
149        } => {
150            // captured const is copied into closure env, not boxed
151            emit_expr(emitter, ctx, *value)?;
152            let value_ty = ctx
153                .ta
154                .try_expr(*value)
155                .map_err(crate::codegen::arena_failure)?
156                .ty
157                .clone();
158            cast::emit_coerce_to_slot(emitter, ctx, &value_ty, ty)?;
159            let slot = emitter.define_local(name, ctx.symbols.value_type(ty)?)?;
160            emitter.instruction(Instruction::LocalSet(slot));
161        }
162        TypedStmtKind::AssignLocal {
163            ident,
164            target_ty,
165            value,
166            boxed,
167            narrowed_shadow_ty,
168        } => {
169            let slot = emitter.write_slot(&ident.name)?;
170            if *boxed {
171                // The box's inner field is keyed on the binding's
172                // declared type (`target_ty`), not the RHS type, so
173                // a primitive RHS into a wider boxed slot lands the
174                // boxed primitive in the right box variant.
175                let box_idx = ctx.symbols.box_type_idx(target_ty)?.ok_or_else(|| {
176                    crate::codegen::internal_failure(
177                        "box type registered for every boxed AssignLocal",
178                    )
179                })?;
180                let value_ty = ctx
181                    .ta
182                    .try_expr(*value)
183                    .map_err(crate::codegen::arena_failure)?
184                    .ty
185                    .clone();
186                emitter.instruction(Instruction::LocalGet(slot));
187                emit_expr(emitter, ctx, *value)?;
188                cast::emit_coerce_to_slot(emitter, ctx, &value_ty, target_ty)?;
189                let initialization = ctx.ta.parameter_initializations.contains(&id);
190                if !initialization {
191                    super::binding_cells::check_initialized(emitter, ctx, slot, box_idx)?;
192                }
193                emitter.instruction(Instruction::StructSet {
194                    struct_type_index: box_idx,
195                    field_index: 0,
196                });
197                if initialization {
198                    super::binding_cells::mark_initialized(emitter, slot, box_idx);
199                }
200            } else {
201                emit_expr(emitter, ctx, *value)?;
202                let value_ty = ctx
203                    .ta
204                    .try_expr(*value)
205                    .map_err(crate::codegen::arena_failure)?
206                    .ty
207                    .clone();
208                // If this assignment installs a narrowing, allocate a fresh
209                // shadow local of the narrowed type. `local.tee` keeps the RHS
210                // on the stack for the subsequent coerce+set while also storing
211                // into the shadow. Installing it makes `LocalNarrowRef` reads
212                // resolve there — no per-use cast needed. The narrowed type can
213                // differ from the RHS's own: a function narrows to the declared
214                // member it fits, which may take more parameters.
215                let stored_ty = match narrowed_shadow_ty {
216                    Some(narrowed_ty) => {
217                        cast::emit_coerce_to_slot(emitter, ctx, &value_ty, narrowed_ty)?;
218                        let narrowed_val = ctx.symbols.value_type(narrowed_ty)?;
219                        let shadow_idx = emitter.add_anonymous_local(narrowed_val)?;
220                        emitter.instruction(Instruction::LocalTee(shadow_idx));
221                        emitter.install_narrow_shadow(&ident.name, shadow_idx, narrowed_val)?;
222                        narrowed_ty
223                    }
224                    None => &value_ty,
225                };
226                cast::emit_coerce_to_slot(emitter, ctx, stored_ty, target_ty)?;
227                emitter.instruction(Instruction::LocalSet(slot));
228            }
229        }
230        TypedStmtKind::AssignGlobal {
231            mangled,
232            target_ty,
233            value,
234            ..
235        } => {
236            emit_assign_global(emitter, ctx, id, mangled, target_ty, *value)?;
237        }
238        TypedStmtKind::AssignField {
239            receiver,
240            name,
241            value,
242        } => {
243            emit_assign_field(emitter, ctx, *receiver, name, *value)?;
244        }
245        TypedStmtKind::AssignIndex {
246            receiver,
247            index,
248            value,
249            elem_ty,
250        } => {
251            emit_assign_index(emitter, ctx, *receiver, *index, *value, elem_ty)?;
252        }
253        TypedStmtKind::If {
254            condition,
255            then_block,
256            else_block,
257        } => {
258            let cond_ty = ctx
259                .ta
260                .try_expr(*condition)
261                .map_err(crate::codegen::arena_failure)?
262                .ty
263                .clone();
264            emit_expr(emitter, ctx, *condition)?;
265            cast::emit_condition_to_i32(emitter, ctx, &cond_ty)?;
266            emitter.emit_if(BlockType::Empty);
267            emit_statement(emitter, ctx, *then_block)?;
268            if let Some(else_id) = else_block {
269                emitter.emit_else();
270                emit_statement(emitter, ctx, *else_id)?;
271            }
272            emitter.emit_end();
273        }
274        TypedStmtKind::While { condition, body } => {
275            // `block { loop { <cond>; i32.eqz; br_if 1; <body>; br 0; } }`
276            // — break exits the outer block, continue restarts the loop.
277            emitter.emit_while_open();
278            let cond_ty = ctx
279                .ta
280                .try_expr(*condition)
281                .map_err(crate::codegen::arena_failure)?
282                .ty
283                .clone();
284            emit_expr(emitter, ctx, *condition)?;
285            cast::emit_condition_to_i32(emitter, ctx, &cond_ty)?;
286            emitter.instruction(Instruction::I32Eqz);
287            let break_label = emitter.break_label()?;
288            emitter.instruction(Instruction::BrIf(break_label));
289            emit_statement(emitter, ctx, *body)?;
290            let continue_label = emitter.continue_label()?;
291            emitter.instruction(Instruction::Br(continue_label));
292            emitter.emit_while_close();
293        }
294        TypedStmtKind::Break => {
295            let transfer = super::finally::Transfer::Branch {
296                depth: emitter.wasm_block_depth - emitter.break_label()? - 1,
297                finally_floor: emitter.break_finally_floor(),
298            };
299            super::finally::emit_transfer(emitter, transfer)?;
300        }
301        TypedStmtKind::Continue => {
302            let transfer = super::finally::Transfer::Branch {
303                depth: emitter.wasm_block_depth - emitter.continue_label()? - 1,
304                finally_floor: emitter.continue_finally_floor(),
305            };
306            super::finally::emit_transfer(emitter, transfer)?;
307        }
308        TypedStmtKind::For { .. } | TypedStmtKind::ForOf { .. } | TypedStmtKind::DoWhile { .. } => {
309            return Err(crate::codegen::internal_failure(
310                "for / for-of / do-while must be lowered by the desugar pass before codegen",
311            ));
312        }
313        TypedStmtKind::Switch {
314            discriminant,
315            discriminant_ty,
316            cases,
317            default,
318        } => {
319            emit_switch(
320                emitter,
321                ctx,
322                *discriminant,
323                discriminant_ty,
324                cases,
325                default.as_ref().copied(),
326            )?;
327        }
328        TypedStmtKind::NarrowRegion {
329            path,
330            source,
331            binding,
332            cast_info,
333            body,
334        } => {
335            emitter.push_scope();
336            if path.chain.is_empty() {
337                let shadow_val = ctx.symbols.value_type(&cast_info.to_ty)?;
338                let shadow = emitter.add_anonymous_local(shadow_val)?;
339                emit_expr(emitter, ctx, *source)?;
340                cast::emit_narrowing_cast(emitter, ctx, cast_info)?;
341                emitter.instruction(Instruction::LocalSet(shadow));
342                emitter.install_narrow_shadow(&binding.name, shadow, shadow_val)?;
343            } else {
344                // The predicate proved a field-path value that may already have
345                // changed while evaluating the rest of the condition. Defer its
346                // first re-read and checked cast until an actual use in the body.
347                emitter.register_narrow_source(&binding.name, *source)?;
348            }
349            emit_statement(emitter, ctx, *body)?;
350            emitter.pop_scope()?;
351        }
352        TypedStmtKind::Throw { value } => {
353            // cast is unconditional: typechecker guarantees Error type; Wasm-level nullability comes from (ref null $Object) lowering
354            emit_expr(emitter, ctx, *value)?;
355            crate::codegen::throw::emit_error_throw(emitter, ctx);
356        }
357        TypedStmtKind::Try {
358            body,
359            catches,
360            finally,
361        } => {
362            emit_try(emitter, ctx, *body, catches, *finally)?;
363        }
364    }
365    let _: () = if branches(&stmt.kind) {
366        emitter.clear_all_narrow_shadows();
367    };
368    Ok(())
369}
370
371/// Evaluate `expr` into an anonymous local and register the node as already
372/// evaluated, so a compound assignment's synthesized read — which shares the
373/// write's own receiver and index nodes — reads the local instead of running
374/// the expression a second time. Leaves the stack unchanged.
375fn emit_operand_once(
376    emitter: &mut FunctionEmitter,
377    ctx: &CodegenCtx,
378    expr: ExprId,
379) -> Result<u32, crate::compiler_error::CompilerFailure> {
380    let slot = emitter.add_anonymous_local(
381        ctx.symbols.value_type(
382            &ctx.ta
383                .try_expr(expr)
384                .map_err(crate::codegen::arena_failure)?
385                .ty,
386        )?,
387    )?;
388    emit_expr(emitter, ctx, expr)?;
389    emitter.instruction(Instruction::LocalSet(slot));
390    emitter.record_single_evaluation(expr, slot)?;
391    Ok(slot)
392}
393
394fn emit_assign_global(
395    emitter: &mut FunctionEmitter,
396    ctx: &CodegenCtx,
397    stmt: StmtId,
398    mangled: &MangledName,
399    target_ty: &Type,
400    value: ExprId,
401) -> Result<(), crate::compiler_error::CompilerFailure> {
402    emit_expr(emitter, ctx, value)?;
403    let value_ty = ctx
404        .ta
405        .try_expr(value)
406        .map_err(crate::codegen::arena_failure)?
407        .ty
408        .clone();
409    cast::emit_coerce_to_slot(emitter, ctx, &value_ty, target_ty)?;
410    let idx = ctx
411        .symbols
412        .global_idx(mangled)
413        .ok_or_else(|| crate::codegen::internal_failure("Inferer guarantees the binding exists"))?;
414    // JavaScript checks the binding only once the value is computed.
415    let declares = init_guard::is_declaration(ctx, mangled, stmt);
416    if !declares {
417        init_guard::emit_check(emitter, ctx, mangled);
418    }
419    emitter.instruction(Instruction::GlobalSet(idx));
420    if declares {
421        init_guard::emit_mark_initialized(emitter, ctx, mangled);
422    }
423    Ok(())
424}
425
426fn emit_assign_field(
427    emitter: &mut FunctionEmitter,
428    ctx: &CodegenCtx,
429    receiver: ExprId,
430    name: &Ident,
431    value: ExprId,
432) -> Result<(), crate::compiler_error::CompilerFailure> {
433    let mark = emitter.single_evaluation_mark();
434    let receiver_ty = ctx
435        .ta
436        .source_type(receiver)
437        .map_err(crate::codegen::arena_failure)?
438        .clone();
439    // The receiver runs before the value in both arms, which is both the
440    // left-to-right order the language guarantees and the order
441    // `emit_operand_once` needs: a read inside `value` can only reuse a slot
442    // that is already filled.
443    let recv = emit_operand_once(emitter, ctx, receiver)?;
444    emit_operand_once(emitter, ctx, value)?;
445    emitter.instruction(Instruction::LocalGet(recv));
446    if ctx
447        .ta
448        .try_expr(receiver)
449        .map_err(crate::codegen::arena_failure)?
450        .ty
451        != receiver_ty
452    {
453        crate::codegen::cast_check::emit_operation_cast_on_stack(
454            emitter,
455            ctx,
456            &ctx.ta
457                .try_expr(receiver)
458                .map_err(crate::codegen::arena_failure)?
459                .ty,
460            &receiver_ty,
461        )?;
462    }
463    let recv = emitter.add_anonymous_local(ctx.symbols.value_type(&receiver_ty)?)?;
464    emitter.instruction(Instruction::LocalSet(recv));
465    if let Some(setter) = host_property_setter(ctx, &receiver_ty, name) {
466        // The setter's Direct wrapper takes a non-null receiver, as a method's does.
467        emitter.instruction(Instruction::LocalGet(recv));
468        emitter.instruction(Instruction::RefAsNonNull);
469        emit_expr(emitter, ctx, value)?;
470        emitter.instruction(Instruction::Call(setter));
471        emitter.end_single_evaluations(mark)?;
472        return Ok(());
473    }
474    match receiver_ty.lowered() {
475        // Class instance: nominal receiver, array-backed object payload.
476        Type::ClassRef { mangled, .. } => {
477            emit_class_field_store(emitter, ctx, receiver, recv, mangled, name, value)?;
478        }
479        _ => emit_object_field_store(emitter, ctx, &receiver_ty, recv, name, value)?,
480    }
481    emitter.end_single_evaluations(mark)?;
482    Ok(())
483}
484
485/// The host setter a prelude interface declares for a writable property it
486/// implements (`RegExp#lastIndex`), which takes the receiver and the value.
487fn host_property_setter(ctx: &CodegenCtx, receiver_ty: &Type, name: &Ident) -> Option<u32> {
488    let iface = crate::codegen::host_property_iface(receiver_ty)?;
489    let key = crate::mangle::extend(&iface, &crate::mangle::property_setter(&name.name));
490    ctx.symbols.func_idx(&key)
491}
492
493#[allow(clippy::too_many_arguments)]
494fn emit_class_field_store(
495    emitter: &mut FunctionEmitter,
496    ctx: &CodegenCtx,
497    receiver: ExprId,
498    recv: u32,
499    mangled: &crate::MangledName,
500    name: &Ident,
501    value: ExprId,
502) -> Result<(), crate::compiler_error::CompilerFailure> {
503    // Accessor property (no data slot): dispatch the synthetic setter. The
504    // call leaves its `undefined` result, which the store discards.
505    let Some(slot) = ctx.symbols.class_field_slot(mangled, &name.name) else {
506        let setter = crate::codegen::classes::accessor_setter_name(&name.name);
507        emit_method_call(
508            emitter,
509            ctx,
510            receiver,
511            mangled,
512            &setter,
513            std::slice::from_ref(&value),
514            None,
515            &[],
516            None,
517            &Type::Void,
518        )?;
519        emitter.instruction(Instruction::Drop);
520        return Ok(());
521    };
522    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
523        crate::codegen::internal_failure("class struct type recorded in classes::emit")
524    })?;
525    let intrinsics = ctx
526        .symbols
527        .intrinsic_type_indices()
528        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
529    let value_ty = ctx
530        .ta
531        .try_expr(value)
532        .map_err(crate::codegen::arena_failure)?
533        .ty
534        .clone();
535    emit_expr(emitter, ctx, value)?;
536    cast::emit_box(emitter, ctx, &value_ty)?;
537    let stored = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
538    emitter.instruction(Instruction::LocalSet(stored));
539    emitter.instruction(Instruction::LocalGet(recv));
540    emitter.instruction(Instruction::StructGet {
541        struct_type_index: struct_idx,
542        field_index: 2,
543    });
544    emitter.instruction(Instruction::I32Const(slot as i32));
545    emitter.instruction(Instruction::LocalGet(stored));
546    emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
547    let index = emitter.add_anonymous_local(ValType::I32)?;
548    emitter.instruction(Instruction::I32Const(slot as i32));
549    emitter.instruction(Instruction::LocalSet(index));
550    crate::codegen::field_names::emit_mark_present(emitter, ctx, recv, index)?;
551    Ok(())
552}
553
554fn emit_object_field_store(
555    emitter: &mut FunctionEmitter,
556    ctx: &CodegenCtx,
557    receiver_ty: &Type,
558    recv: u32,
559    name: &Ident,
560    value: ExprId,
561) -> Result<(), crate::compiler_error::CompilerFailure> {
562    let object_shape = ctx
563        .symbols
564        .intrinsic_type_indices()
565        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?
566        .object_shape;
567    let builtin = crate::codegen::may_hold_builtin_value(receiver_ty);
568    if builtin {
569        emitter.instruction(Instruction::LocalGet(recv));
570        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
571            object_shape,
572        )));
573        emitter.emit_if(BlockType::Empty);
574    }
575    emitter.instruction(Instruction::LocalGet(recv));
576    let rcv_local = stash_receiver_as_object_shape(emitter, receiver_ty, object_shape)?;
577    // Accessor-aware: a data field writes its slot; an accessor property
578    // invokes its `set <prop>` method closure.
579    emit_object_property_write(emitter, ctx, rcv_local, name, value)?;
580    if builtin {
581        emitter.emit_else();
582        emit_builtin_field_store(emitter, ctx, recv, name, value)?;
583        emitter.emit_end();
584    }
585    Ok(())
586}
587
588/// A structural view may hold a built-in value (an array behind
589/// `{ length: number }`), which has no object payload: the host writes it
590/// through the property's setter, or throws as JavaScript does for a
591/// read-only property.
592fn emit_builtin_field_store(
593    emitter: &mut FunctionEmitter,
594    ctx: &CodegenCtx,
595    recv: u32,
596    name: &Ident,
597    value: ExprId,
598) -> Result<(), crate::compiler_error::CompilerFailure> {
599    let key = ctx
600        .symbols
601        .field_name_string_global_idx(&name.name)
602        .ok_or_else(|| crate::codegen::internal_failure("written field name interned"))?;
603    let set_field = ctx
604        .symbols
605        .prelude_func_idx("ObjectConstructor##setField")
606        .ok_or_else(|| crate::codegen::internal_failure("dynamic write imported"))?;
607    emitter.instruction(Instruction::LocalGet(recv));
608    emitter.instruction(Instruction::GlobalGet(key));
609    emit_expr(emitter, ctx, value)?;
610    cast::emit_box(
611        emitter,
612        ctx,
613        &ctx.ta
614            .try_expr(value)
615            .map_err(crate::codegen::arena_failure)?
616            .ty,
617    )?;
618    emitter.instruction(Instruction::Call(set_field));
619    Ok(())
620}
621
622fn emit_assign_index(
623    emitter: &mut FunctionEmitter,
624    ctx: &CodegenCtx,
625    receiver: ExprId,
626    index: ExprId,
627    value: ExprId,
628    elem_ty: &Type,
629) -> Result<(), crate::compiler_error::CompilerFailure> {
630    let mark = emitter.single_evaluation_mark();
631    if ctx
632        .ta
633        .source_type(receiver)
634        .map_err(crate::codegen::arena_failure)?
635        .is_structural_object()
636    {
637        let object = emit_operand_once(emitter, ctx, receiver)?;
638        let key = emit_operand_once(emitter, ctx, index)?;
639        let stored = emit_operand_once(emitter, ctx, value)?;
640        emitter.instruction(Instruction::LocalGet(object));
641        cast::emit_box(
642            emitter,
643            ctx,
644            &ctx.ta
645                .try_expr(receiver)
646                .map_err(crate::codegen::arena_failure)?
647                .ty,
648        )?;
649        emitter.instruction(Instruction::LocalGet(key));
650        crate::codegen::cast_check::emit_operation_cast_on_stack(
651            emitter,
652            ctx,
653            &ctx.ta
654                .try_expr(index)
655                .map_err(crate::codegen::arena_failure)?
656                .ty,
657            &Type::String,
658        )?;
659        emitter.instruction(Instruction::LocalGet(stored));
660        cast::emit_box(
661            emitter,
662            ctx,
663            &ctx.ta
664                .try_expr(value)
665                .map_err(crate::codegen::arena_failure)?
666                .ty,
667        )?;
668        let Some(symbol) = ctx.require(
669            ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
670            "dynamic write imported",
671        ) else {
672            return Ok(());
673        };
674        emitter.instruction(Instruction::Call(symbol));
675    } else if matches!(
676        ctx.ta
677            .source_type(receiver)
678            .map_err(crate::codegen::arena_failure)?
679            .lowered(),
680        Type::Uint8Array
681    ) {
682        emit_uint8_index_store(emitter, ctx, receiver, index, value)?;
683    } else {
684        emit_array_index_store(emitter, ctx, receiver, index, value, elem_ty)?;
685    }
686    emitter.end_single_evaluations(mark)?;
687    Ok(())
688}
689
690/// `u8[i] = v`. Packed `i8` storage, so the RHS is truncated to its low 8 bits.
691fn emit_uint8_index_store(
692    emitter: &mut FunctionEmitter,
693    ctx: &CodegenCtx,
694    receiver: ExprId,
695    index: ExprId,
696    value: ExprId,
697) -> Result<(), crate::compiler_error::CompilerFailure> {
698    let raw_uint8_idx = ctx.symbols.raw_uint8_array_type_idx().ok_or_else(|| {
699        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
700    })?;
701    let uint8_idx = ctx.symbols.uint8_array_type_idx().ok_or_else(|| {
702        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
703    })?;
704    let recv_local = emit_operand_once(emitter, ctx, receiver)?;
705    let key = emit_operand_once(emitter, ctx, index)?;
706    // RHS evaluates before the bounds check throws, preserving left-to-right
707    // evaluation order (`emit_array_index_store` does the same).
708    let original_value = emit_operand_once(emitter, ctx, value)?;
709
710    // `$Uint8Array`'s backing field is immutable — no `push` exists to swap the
711    // buffer out — so this read could sit anywhere after the receiver. It goes
712    // here to keep the two index stores the same shape.
713    let raw_local = emitter.add_anonymous_local(ValType::Ref(RefType {
714        nullable: false,
715        heap_type: HeapType::Concrete(raw_uint8_idx),
716    }))?;
717    emitter.instruction(Instruction::LocalGet(recv_local));
718    crate::codegen::cast_check::emit_operation_cast_on_stack(
719        emitter,
720        ctx,
721        &ctx.ta
722            .try_expr(receiver)
723            .map_err(crate::codegen::arena_failure)?
724            .ty,
725        &Type::Uint8Array,
726    )?;
727    emitter.instruction(Instruction::LocalGet(key));
728    super::expr::emit_index_number(
729        emitter,
730        ctx,
731        &ctx.ta
732            .try_expr(index)
733            .map_err(crate::codegen::arena_failure)?
734            .ty,
735    )?;
736    let idx_f64_local = stash_index_operand(emitter)?;
737    let value_local = emitter.add_anonymous_local(ValType::I32)?;
738    emitter.instruction(Instruction::LocalGet(original_value));
739    cast::emit_coerce_to_slot(
740        emitter,
741        ctx,
742        &ctx.ta
743            .try_expr(value)
744            .map_err(crate::codegen::arena_failure)?
745            .ty,
746        &Type::Number,
747    )?;
748    emit_to_uint8(emitter)?;
749    emitter.instruction(Instruction::LocalSet(value_local));
750    emitter.instruction(Instruction::StructGet {
751        struct_type_index: uint8_idx,
752        field_index: 1,
753    });
754    emitter.instruction(Instruction::LocalSet(raw_local));
755    let idx_local = emit_checked_index(emitter, ctx, raw_local, idx_f64_local)?;
756    emitter.instruction(Instruction::LocalGet(raw_local));
757    emitter.instruction(Instruction::LocalGet(idx_local));
758    emitter.instruction(Instruction::LocalGet(value_local));
759    emitter.instruction(Instruction::ArraySet(raw_uint8_idx));
760    Ok(())
761}
762
763/// JavaScript's `ToUint8` on the `f64` on the stack, leaving an `i32` byte:
764/// truncate, then reduce modulo 256 (so `-1` is 255). Every step is exact in
765/// `f64`; `NaN` and `±Infinity` become `NaN`, which `trunc_sat` maps to 0.
766fn emit_to_uint8(
767    emitter: &mut FunctionEmitter,
768) -> Result<(), crate::compiler_error::CompilerFailure> {
769    let truncated = emitter.add_anonymous_local(ValType::F64)?;
770    emitter.instruction(Instruction::F64Trunc);
771    emitter.instruction(Instruction::LocalTee(truncated));
772    emitter.instruction(Instruction::LocalGet(truncated));
773    emitter.instruction(Instruction::F64Const(256.0.into()));
774    emitter.instruction(Instruction::F64Div);
775    emitter.instruction(Instruction::F64Floor);
776    emitter.instruction(Instruction::F64Const(256.0.into()));
777    emitter.instruction(Instruction::F64Mul);
778    emitter.instruction(Instruction::F64Sub);
779    emitter.instruction(Instruction::I32TruncSatF64U);
780    Ok(())
781}
782
783/// `a[i] = v` on a `$Array`.
784fn emit_array_index_store(
785    emitter: &mut FunctionEmitter,
786    ctx: &CodegenCtx,
787    receiver: ExprId,
788    index: ExprId,
789    value: ExprId,
790    _elem_ty: &Type,
791) -> Result<(), crate::compiler_error::CompilerFailure> {
792    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
793        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
794    })?;
795    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
796        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
797    })?;
798    let object_idx = ctx
799        .symbols
800        .intrinsic_type_indices()
801        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?
802        .object;
803    let recv_local = emit_operand_once(emitter, ctx, receiver)?;
804    let key = emit_operand_once(emitter, ctx, index)?;
805    // RHS evaluates before the store (and so before the bounds check throws),
806    // preserving left-to-right evaluation order.
807    let value_local = emitter.add_anonymous_local(ValType::Ref(RefType {
808        nullable: true,
809        heap_type: HeapType::Concrete(object_idx),
810    }))?;
811    let value_ty = ctx
812        .ta
813        .try_expr(value)
814        .map_err(crate::codegen::arena_failure)?
815        .ty
816        .clone();
817    emit_expr(emitter, ctx, value)?;
818    // Preserve the actual RHS in the erased element slot.
819    cast::emit_box(emitter, ctx, &value_ty)?;
820    emitter.instruction(Instruction::LocalSet(value_local));
821
822    // Read the backing array *after* the RHS: `push` swaps in a fresh
823    // `$rawArray`, so a RHS that grows this array leaves any buffer read
824    // earlier detached — the store would land in one nobody holds, and the
825    // bounds check would test the stale length.
826    let raw_arr_local = emitter.add_anonymous_local(ValType::Ref(RefType {
827        nullable: false,
828        heap_type: HeapType::Concrete(raw_array_idx),
829    }))?;
830    emitter.instruction(Instruction::LocalGet(recv_local));
831    crate::codegen::cast_check::emit_operation_cast_on_stack(
832        emitter,
833        ctx,
834        &ctx.ta
835            .try_expr(receiver)
836            .map_err(crate::codegen::arena_failure)?
837            .ty,
838        &Type::Array(Box::new(Type::Unknown)),
839    )?;
840    emitter.instruction(Instruction::LocalGet(key));
841    super::expr::emit_index_number(
842        emitter,
843        ctx,
844        &ctx.ta
845            .try_expr(index)
846            .map_err(crate::codegen::arena_failure)?
847            .ty,
848    )?;
849    let idx_f64_local = stash_index_operand(emitter)?;
850    let length = stash_array_length(emitter, array_idx)?;
851    emitter.instruction(Instruction::StructGet {
852        struct_type_index: array_idx,
853        field_index: 1,
854    });
855    emitter.instruction(Instruction::LocalSet(raw_arr_local));
856    let idx_local = emit_checked_index_with_length(emitter, ctx, length, idx_f64_local)?;
857    emitter.instruction(Instruction::LocalGet(raw_arr_local));
858    emitter.instruction(Instruction::LocalGet(idx_local));
859    emitter.instruction(Instruction::LocalGet(value_local));
860    emitter.instruction(Instruction::ArraySet(raw_array_idx));
861    Ok(())
862}
863
864/// Catch handlers surround only the try body. A finally gets an outer
865/// completion target shared by normal, exceptional, and early exits.
866pub(super) fn emit_try(
867    emitter: &mut FunctionEmitter,
868    ctx: &CodegenCtx,
869    body: StmtId,
870    catches: &[crate::TypedCatchClause],
871    finally: Option<StmtId>,
872) -> Result<(), crate::compiler_error::CompilerFailure> {
873    if let Some(finally) = finally {
874        super::finally::emit_try_finally(emitter, ctx, body, catches, finally)?;
875        return Ok(());
876    }
877    if catches.is_empty() {
878        emit_statement(emitter, ctx, body)?;
879        return Ok(());
880    }
881    let error_idx = ctx
882        .symbols
883        .intrinsic_type_indices()
884        .ok_or_else(|| crate::codegen::internal_failure("error intrinsic registered"))?
885        .error;
886    let tag_idx = ctx
887        .symbols
888        .error_tag_idx()
889        .ok_or_else(|| crate::codegen::internal_failure("error tag registered"))?;
890    emitter.emit_block(BlockType::Empty);
891    emitter.emit_block(BlockType::Result(ValType::Ref(RefType {
892        nullable: false,
893        heap_type: HeapType::Concrete(error_idx),
894    })));
895    emitter.instruction(Instruction::TryTable(
896        BlockType::Empty,
897        std::borrow::Cow::Owned(vec![wasm_encoder::Catch::One {
898            tag: tag_idx,
899            label: 0,
900        }]),
901    ));
902    emitter.bump_block_depth();
903    emit_statement(emitter, ctx, body)?;
904    emitter.emit_end();
905    emitter.instruction(Instruction::Br(1));
906    emitter.emit_end();
907    let err_stash = emitter.add_anonymous_local(ValType::Ref(RefType {
908        nullable: true,
909        heap_type: HeapType::Concrete(error_idx),
910    }))?;
911    emitter.instruction(Instruction::LocalSet(err_stash));
912    emit_catch_dispatch(emitter, ctx, catches, err_stash)?;
913    emitter.emit_end();
914    Ok(())
915}
916
917/// A `catch (e: MyError)` clause annotated with a proper `Error` subclass
918/// filters by nominal identity — `(vtable global, struct type)` of the
919/// annotation class. The root `Error` annotation (or none) binds everything.
920fn catch_filter_class(
921    ctx: &CodegenCtx,
922    clause_ty: &Type,
923) -> Result<Option<(u32, u32)>, crate::compiler_error::CompilerFailure> {
924    let Type::ClassRef { mangled, .. } = clause_ty.lowered() else {
925        return Ok(None);
926    };
927    if *mangled == crate::mangle::prelude("Error") {
928        return Ok(None);
929    }
930    let vtable_global = ctx
931        .symbols
932        .class_vtable_global_idx(mangled)
933        .ok_or_else(|| {
934            crate::codegen::internal_failure("catch-annotation class vtable global recorded")
935        })?;
936    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
937        crate::codegen::internal_failure("catch-annotation class struct type recorded")
938    })?;
939    Ok(Some((vtable_global, struct_idx)))
940}
941
942/// Ordered dispatch over the catch arms for the caught error stashed in
943/// `err_stash` (a nullable `(ref null $Error)` local, already set). Each
944/// subclass arm runs the nominal brand test; the first match binds, runs its
945/// body, and branches out. A root-`Error`/untyped arm is a catch-all and ends
946/// the chain; without one, the chain ends by re-raising with `throw` (not
947/// `throw_ref`) so an enclosing `catch_all_ref` wrapper still observes it and
948/// runs `finally` first.
949///
950/// At the emission site, relative label 0 is `$end_try`, the matched-arm exit.
951/// When cleanup is present, the whole try/catch sits inside its wrapper.
952fn emit_catch_dispatch(
953    emitter: &mut FunctionEmitter,
954    ctx: &CodegenCtx,
955    catches: &[crate::TypedCatchClause],
956    err_stash: u32,
957) -> Result<(), crate::compiler_error::CompilerFailure> {
958    for clause in catches {
959        let Some((vtable_global, struct_idx)) = catch_filter_class(ctx, &clause.ty)? else {
960            emitter.push_scope();
961            emitter.instruction(Instruction::LocalGet(err_stash));
962            emitter.instruction(Instruction::RefAsNonNull);
963            bind_catch_local(emitter, ctx, clause)?;
964            emit_statement(emitter, ctx, clause.body)?;
965            emitter.pop_scope()?;
966            // Falling through reaches the matched-arm exit in both topologies.
967            return Ok(());
968        };
969        emitter.instruction(Instruction::LocalGet(err_stash));
970        super::cast::emit_nominal_instance_test(emitter, ctx, vtable_global)?;
971        emitter.emit_if(BlockType::Empty);
972        emitter.push_scope();
973        emitter.instruction(Instruction::LocalGet(err_stash));
974        // Shape-only cast is sound here: the brand matched.
975        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(struct_idx)));
976        bind_catch_local(emitter, ctx, clause)?;
977        emit_statement(emitter, ctx, clause.body)?;
978        emitter.pop_scope()?;
979        // Matched-arm exit is label 0 at chain level; +1 for the `if` frame.
980        emitter.instruction(Instruction::Br(1));
981        emitter.emit_end();
982    }
983    let tag_idx = ctx
984        .symbols
985        .error_tag_idx()
986        .ok_or_else(|| crate::codegen::internal_failure("error tag registered"))?;
987    emitter.instruction(Instruction::LocalGet(err_stash));
988    emitter.instruction(Instruction::RefAsNonNull);
989    emitter.instruction(Instruction::Throw(tag_idx));
990    Ok(())
991}
992
993/// Store the caught error on top of the stack in the clause's binding, boxed
994/// when a closure captures the binding and may reassign it.
995fn bind_catch_local(
996    emitter: &mut FunctionEmitter,
997    ctx: &CodegenCtx,
998    clause: &crate::TypedCatchClause,
999) -> Result<(), crate::compiler_error::CompilerFailure> {
1000    let slot_ty = if clause.boxed {
1001        let box_idx = ctx.symbols.box_type_idx(&clause.ty)?.ok_or_else(|| {
1002            crate::codegen::internal_failure("box type registered for every boxed catch binding")
1003        })?;
1004        // The caught error initializes the cell.
1005        emitter.instruction(Instruction::I32Const(1));
1006        emitter.instruction(Instruction::StructNew(box_idx));
1007        ValType::Ref(RefType {
1008            nullable: false,
1009            heap_type: HeapType::Concrete(box_idx),
1010        })
1011    } else {
1012        ctx.symbols.value_type(&clause.ty)?
1013    };
1014    let slot = emitter.define_local(&clause.binding, slot_ty)?;
1015    emitter.instruction(Instruction::LocalSet(slot));
1016    Ok(())
1017}
1018
1019/// Emit a `switch` statement as a block-stack of per-case targets.
1020///
1021/// Layout (with N cases and a `default`):
1022///
1023/// ```text
1024/// (block $end
1025///   (block $default
1026///     (block $case_{N-1})
1027///     …
1028///     (block $case_0
1029///       <dispatcher>            ;; br_if $case_i for each value-test
1030///       br $default             ;; (or $end when no default)
1031///     )                          ;; close $case_0
1032///     <case_0 body>; br $end
1033///     …                          ;; close + body for each case
1034///     <case_{N-1} body>; br $end
1035///   )                            ;; close $default
1036///   <default body>               ;; falls through to $end naturally
1037/// )
1038/// ```
1039///
1040/// The discriminant is evaluated once into a fresh anonymous local
1041/// so each per-case test reads it without re-running side effects.
1042/// `break` from any case body targets `$end` via the loop-contexts
1043/// stack (`emit_switch_open` pushed `is_switch: true`); `continue`
1044/// inside a case body walks past this frame to the enclosing loop.
1045fn emit_switch(
1046    emitter: &mut FunctionEmitter,
1047    ctx: &CodegenCtx,
1048    discriminant: crate::ExprId,
1049    discriminant_ty: &Type,
1050    cases: &[TypedSwitchCase],
1051    default: Option<StmtId>,
1052) -> Result<(), crate::compiler_error::CompilerFailure> {
1053    // Stash discriminant in an anonymous local; numeric enums unboxed to f64
1054    // so per-case tests use f64.eq instead of a structural-equality call.
1055    emit_expr(emitter, ctx, discriminant)?;
1056    let disc_val = switch_disc_local_type(ctx, discriminant_ty)?;
1057    if matches!(discriminant_ty.lowered(), Type::NumberEnum { .. }) {
1058        let boxed_idx = ctx.symbols.boxed_number_type_idx().ok_or_else(|| {
1059            crate::codegen::internal_failure("boxed_number type registered with intrinsics")
1060        })?;
1061        emitter.instruction(Instruction::StructGet {
1062            struct_type_index: boxed_idx,
1063            field_index: 1,
1064        });
1065    }
1066    let disc_local = emitter.add_anonymous_local(disc_val)?;
1067    emitter.instruction(Instruction::LocalSet(disc_local));
1068
1069    emitter.emit_switch_open();
1070    // Open $default first (outermost), then $case_{N-1} … $case_0 (innermost),
1071    // so the dispatcher's br_if(source_idx) directly addresses each case block.
1072    if default.is_some() {
1073        emitter.emit_block(BlockType::Empty);
1074    }
1075    for _ in 0..cases.len() {
1076        emitter.emit_block(BlockType::Empty);
1077    }
1078
1079    // Dispatcher inside $case_0: one br_if per case value, then br to $default (or $end).
1080    for (source_idx, case) in cases.iter().enumerate() {
1081        emitter.record_span(case.span);
1082        for value in &case.values {
1083            emit_case_comparison(emitter, ctx, disc_local, discriminant_ty, value)?;
1084            emitter.instruction(Instruction::BrIf(crate::codegen::wasm_u32(source_idx)?));
1085        }
1086    }
1087    // Both $default and $end sit at depth cases.len() from inside the dispatcher.
1088    emitter.instruction(Instruction::Br(crate::codegen::wasm_u32(cases.len())?));
1089
1090    // Close each $case_i and emit its body. The trailing br $end is dead when
1091    // the body terminates via break/return, but Wasm's stack-polymorphic typing
1092    // validates unreachable code, so it's harmless.
1093    for case in cases {
1094        emitter.emit_end();
1095        emitter.push_scope();
1096        emit_statement(emitter, ctx, case.body)?;
1097        emitter.pop_scope()?;
1098        let end_label = emitter.break_label()?;
1099        emitter.instruction(Instruction::Br(end_label));
1100    }
1101
1102    if let Some(default_body) = default {
1103        emitter.emit_end();
1104        emitter.push_scope();
1105        emit_statement(emitter, ctx, default_body)?;
1106        emitter.pop_scope()?;
1107        // Falls through to `$end` naturally — no explicit `br`.
1108    }
1109
1110    emitter.emit_switch_close();
1111    Ok(())
1112}
1113
1114/// Numeric-enum discriminants are unboxed to `f64` here so per-case tests use `f64.eq`
1115/// instead of a structural-equality call on every comparison.
1116fn switch_disc_local_type(
1117    ctx: &CodegenCtx,
1118    ty: &Type,
1119) -> Result<ValType, crate::compiler_error::CompilerFailure> {
1120    Ok(if matches!(ty.lowered(), Type::NumberEnum { .. }) {
1121        ValType::F64
1122    } else {
1123        ctx.symbols.value_type(ty)?
1124    })
1125}
1126
1127/// Internal `if/else` uses `BlockType::Result(ValType::I32)` so the i32 result
1128/// flows out without adding a `br_if`-visible block depth.
1129fn emit_case_comparison(
1130    emitter: &mut FunctionEmitter,
1131    ctx: &CodegenCtx,
1132    disc_local: u32,
1133    disc_ty: &Type,
1134    value: &TypedSwitchValue,
1135) -> Result<(), crate::compiler_error::CompilerFailure> {
1136    let disc_val = switch_disc_local_type(ctx, disc_ty)?;
1137    if let Some(case_ty) = switch_case_primitive_type(value)
1138        && matches!(disc_val, ValType::Ref(_))
1139        && disc_val != ctx.symbols.value_type(&case_ty)?
1140    {
1141        emit_boxed_case_comparison(emitter, ctx, disc_local, &case_ty, value)?;
1142        return Ok(());
1143    }
1144    match value {
1145        TypedSwitchValue::Expr { comparison, .. } => {
1146            emit_expr(emitter, ctx, *comparison)?;
1147        }
1148        TypedSwitchValue::Undefined { .. } => {
1149            emitter.instruction(Instruction::LocalGet(disc_local));
1150            super::expr::emit_is_undefined(emitter, ctx)?;
1151        }
1152        TypedSwitchValue::Null { .. } => {
1153            // case null: accepted only when discriminant can hold null (nullable ref)
1154            emitter.instruction(Instruction::LocalGet(disc_local));
1155            emitter.instruction(Instruction::RefIsNull);
1156        }
1157        TypedSwitchValue::Number { value, .. } => {
1158            emitter.instruction(Instruction::LocalGet(disc_local));
1159            emitter.instruction(Instruction::F64Const((*value).into()));
1160            emitter.instruction(Instruction::F64Eq);
1161        }
1162        TypedSwitchValue::Boolean { value, .. } => {
1163            emitter.instruction(Instruction::LocalGet(disc_local));
1164            emitter.instruction(Instruction::I32Const(if *value { 1 } else { 0 }));
1165            emitter.instruction(Instruction::I32Eq);
1166        }
1167        TypedSwitchValue::String { value, .. } => {
1168            emit_string_compare(emitter, ctx, disc_local, disc_val, value)?;
1169        }
1170        TypedSwitchValue::Enum { value: payload, .. } => match payload {
1171            EnumVariantPayload::Number(n) => {
1172                emitter.instruction(Instruction::LocalGet(disc_local));
1173                emitter.instruction(Instruction::F64Const((*n).into()));
1174                emitter.instruction(Instruction::F64Eq);
1175            }
1176            EnumVariantPayload::String(s) => {
1177                emit_string_compare(emitter, ctx, disc_local, disc_val, s)?;
1178            }
1179        },
1180    };
1181    Ok(())
1182}
1183
1184/// A union discriminant may carry a different primitive kind or null. Test
1185/// its boxed shape before unboxing; a nonmatching kind simply skips this case.
1186fn emit_boxed_case_comparison(
1187    emitter: &mut FunctionEmitter,
1188    ctx: &CodegenCtx,
1189    disc_local: u32,
1190    case_ty: &Type,
1191    value: &TypedSwitchValue,
1192) -> Result<(), crate::compiler_error::CompilerFailure> {
1193    let heap_index = match case_ty {
1194        Type::Number => ctx.symbols.boxed_number_type_idx(),
1195        Type::Boolean => ctx.symbols.boxed_boolean_type_idx(),
1196        Type::String => ctx.symbols.string_type_idx(),
1197        _ => {
1198            return Err(crate::codegen::internal_failure(
1199                "switch case primitive classified above",
1200            ));
1201        }
1202    }
1203    .ok_or_else(|| {
1204        crate::codegen::internal_failure("primitive types registered before switch emission")
1205    })?;
1206    emitter.instruction(Instruction::LocalGet(disc_local));
1207    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(heap_index)));
1208    emitter.emit_if(BlockType::Result(ValType::I32));
1209    emitter.instruction(Instruction::LocalGet(disc_local));
1210    cast::emit_cast_to(emitter, ctx, case_ty)?;
1211    let unboxed = emitter.add_anonymous_local(ctx.symbols.value_type(case_ty)?)?;
1212    emitter.instruction(Instruction::LocalSet(unboxed));
1213    emit_case_comparison(emitter, ctx, unboxed, case_ty, value)?;
1214    emitter.emit_else();
1215    emitter.instruction(Instruction::I32Const(0));
1216    emitter.emit_end();
1217
1218    Ok(())
1219}
1220
1221fn switch_case_primitive_type(value: &TypedSwitchValue) -> Option<Type> {
1222    match value {
1223        TypedSwitchValue::Number { .. }
1224        | TypedSwitchValue::Enum {
1225            value: EnumVariantPayload::Number(_),
1226            ..
1227        } => Some(Type::Number),
1228        TypedSwitchValue::String { .. }
1229        | TypedSwitchValue::Enum {
1230            value: EnumVariantPayload::String(_),
1231            ..
1232        } => Some(Type::String),
1233        TypedSwitchValue::Boolean { .. } => Some(Type::Boolean),
1234        TypedSwitchValue::Null { .. }
1235        | TypedSwitchValue::Undefined { .. }
1236        | TypedSwitchValue::Expr { .. } => None,
1237    }
1238}
1239
1240/// Wraps in a `ref.is_null` guard when discriminant is nullable so a null
1241/// discriminant compares as not-equal. The `if/else` uses explicit i32 result
1242/// so no extra block depth is added to the dispatcher.
1243fn emit_string_compare(
1244    emitter: &mut FunctionEmitter,
1245    ctx: &CodegenCtx,
1246    disc_local: u32,
1247    disc_val: ValType,
1248    literal: &str,
1249) -> Result<(), crate::compiler_error::CompilerFailure> {
1250    let string_eq_idx = ctx.symbols.prelude_func_idx("string_eq").ok_or_else(|| {
1251        crate::codegen::internal_failure("string_eq is not imported from the prelude")
1252    })?;
1253
1254    let disc_is_nullable = matches!(
1255        disc_val,
1256        ValType::Ref(RefType {
1257            nullable: true,
1258            heap_type: HeapType::Concrete(_),
1259        })
1260    );
1261    if disc_is_nullable {
1262        emitter.instruction(Instruction::LocalGet(disc_local));
1263        emitter.instruction(Instruction::RefIsNull);
1264        emitter.emit_if(BlockType::Result(ValType::I32));
1265        emitter.instruction(Instruction::I32Const(0));
1266        emitter.emit_else();
1267        emitter.instruction(Instruction::LocalGet(disc_local));
1268        emitter.instruction(Instruction::RefAsNonNull);
1269        super::emit_const_string_by_text(emitter, ctx, literal)?;
1270        emitter.instruction(Instruction::Call(string_eq_idx));
1271        emitter.emit_end();
1272    } else {
1273        emitter.instruction(Instruction::LocalGet(disc_local));
1274        super::emit_const_string_by_text(emitter, ctx, literal)?;
1275        emitter.instruction(Instruction::Call(string_eq_idx));
1276    }
1277    Ok(())
1278}