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            None,
516            &Type::Void,
517        )?;
518        emitter.instruction(Instruction::Drop);
519        return Ok(());
520    };
521    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
522        crate::codegen::internal_failure("class struct type recorded in classes::emit")
523    })?;
524    let intrinsics = ctx
525        .symbols
526        .intrinsic_type_indices()
527        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
528    let value_ty = ctx
529        .ta
530        .try_expr(value)
531        .map_err(crate::codegen::arena_failure)?
532        .ty
533        .clone();
534    emit_expr(emitter, ctx, value)?;
535    cast::emit_box(emitter, ctx, &value_ty)?;
536    let stored = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
537    emitter.instruction(Instruction::LocalSet(stored));
538    emitter.instruction(Instruction::LocalGet(recv));
539    emitter.instruction(Instruction::StructGet {
540        struct_type_index: struct_idx,
541        field_index: 2,
542    });
543    emitter.instruction(Instruction::I32Const(slot as i32));
544    emitter.instruction(Instruction::LocalGet(stored));
545    emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
546    let index = emitter.add_anonymous_local(ValType::I32)?;
547    emitter.instruction(Instruction::I32Const(slot as i32));
548    emitter.instruction(Instruction::LocalSet(index));
549    crate::codegen::field_names::emit_mark_present(emitter, ctx, recv, index)?;
550    Ok(())
551}
552
553fn emit_object_field_store(
554    emitter: &mut FunctionEmitter,
555    ctx: &CodegenCtx,
556    receiver_ty: &Type,
557    recv: u32,
558    name: &Ident,
559    value: ExprId,
560) -> Result<(), crate::compiler_error::CompilerFailure> {
561    let object_shape = ctx
562        .symbols
563        .intrinsic_type_indices()
564        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?
565        .object_shape;
566    let builtin = crate::codegen::may_hold_builtin_value(receiver_ty);
567    if builtin {
568        emitter.instruction(Instruction::LocalGet(recv));
569        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
570            object_shape,
571        )));
572        emitter.emit_if(BlockType::Empty);
573    }
574    emitter.instruction(Instruction::LocalGet(recv));
575    let rcv_local = stash_receiver_as_object_shape(emitter, receiver_ty, object_shape)?;
576    // Accessor-aware: a data field writes its slot; an accessor property
577    // invokes its `set <prop>` method closure.
578    emit_object_property_write(emitter, ctx, rcv_local, name, value)?;
579    if builtin {
580        emitter.emit_else();
581        emit_builtin_field_store(emitter, ctx, recv, name, value)?;
582        emitter.emit_end();
583    }
584    Ok(())
585}
586
587/// A structural view may hold a built-in value (an array behind
588/// `{ length: number }`), which has no object payload: the host writes it
589/// through the property's setter, or throws as JavaScript does for a
590/// read-only property.
591fn emit_builtin_field_store(
592    emitter: &mut FunctionEmitter,
593    ctx: &CodegenCtx,
594    recv: u32,
595    name: &Ident,
596    value: ExprId,
597) -> Result<(), crate::compiler_error::CompilerFailure> {
598    let key = ctx
599        .symbols
600        .field_name_string_global_idx(&name.name)
601        .ok_or_else(|| crate::codegen::internal_failure("written field name interned"))?;
602    let set_field = ctx
603        .symbols
604        .prelude_func_idx("ObjectConstructor##setField")
605        .ok_or_else(|| crate::codegen::internal_failure("dynamic write imported"))?;
606    emitter.instruction(Instruction::LocalGet(recv));
607    emitter.instruction(Instruction::GlobalGet(key));
608    emit_expr(emitter, ctx, value)?;
609    cast::emit_box(
610        emitter,
611        ctx,
612        &ctx.ta
613            .try_expr(value)
614            .map_err(crate::codegen::arena_failure)?
615            .ty,
616    )?;
617    emitter.instruction(Instruction::Call(set_field));
618    Ok(())
619}
620
621fn emit_assign_index(
622    emitter: &mut FunctionEmitter,
623    ctx: &CodegenCtx,
624    receiver: ExprId,
625    index: ExprId,
626    value: ExprId,
627    elem_ty: &Type,
628) -> Result<(), crate::compiler_error::CompilerFailure> {
629    let mark = emitter.single_evaluation_mark();
630    if ctx
631        .ta
632        .source_type(receiver)
633        .map_err(crate::codegen::arena_failure)?
634        .is_structural_object()
635    {
636        let object = emit_operand_once(emitter, ctx, receiver)?;
637        let key = emit_operand_once(emitter, ctx, index)?;
638        let stored = emit_operand_once(emitter, ctx, value)?;
639        emitter.instruction(Instruction::LocalGet(object));
640        cast::emit_box(
641            emitter,
642            ctx,
643            &ctx.ta
644                .try_expr(receiver)
645                .map_err(crate::codegen::arena_failure)?
646                .ty,
647        )?;
648        emitter.instruction(Instruction::LocalGet(key));
649        crate::codegen::cast_check::emit_operation_cast_on_stack(
650            emitter,
651            ctx,
652            &ctx.ta
653                .try_expr(index)
654                .map_err(crate::codegen::arena_failure)?
655                .ty,
656            &Type::String,
657        )?;
658        emitter.instruction(Instruction::LocalGet(stored));
659        cast::emit_box(
660            emitter,
661            ctx,
662            &ctx.ta
663                .try_expr(value)
664                .map_err(crate::codegen::arena_failure)?
665                .ty,
666        )?;
667        let Some(symbol) = ctx.require(
668            ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
669            "dynamic write imported",
670        ) else {
671            return Ok(());
672        };
673        emitter.instruction(Instruction::Call(symbol));
674    } else if matches!(
675        ctx.ta
676            .source_type(receiver)
677            .map_err(crate::codegen::arena_failure)?
678            .lowered(),
679        Type::Uint8Array
680    ) {
681        emit_uint8_index_store(emitter, ctx, receiver, index, value)?;
682    } else {
683        emit_array_index_store(emitter, ctx, receiver, index, value, elem_ty)?;
684    }
685    emitter.end_single_evaluations(mark)?;
686    Ok(())
687}
688
689/// `u8[i] = v`. Packed `i8` storage, so the RHS is truncated to its low 8 bits.
690fn emit_uint8_index_store(
691    emitter: &mut FunctionEmitter,
692    ctx: &CodegenCtx,
693    receiver: ExprId,
694    index: ExprId,
695    value: ExprId,
696) -> Result<(), crate::compiler_error::CompilerFailure> {
697    let raw_uint8_idx = ctx.symbols.raw_uint8_array_type_idx().ok_or_else(|| {
698        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
699    })?;
700    let uint8_idx = ctx.symbols.uint8_array_type_idx().ok_or_else(|| {
701        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
702    })?;
703    let recv_local = emit_operand_once(emitter, ctx, receiver)?;
704    let key = emit_operand_once(emitter, ctx, index)?;
705    // RHS evaluates before the bounds check throws, preserving left-to-right
706    // evaluation order (`emit_array_index_store` does the same).
707    let original_value = emit_operand_once(emitter, ctx, value)?;
708
709    // `$Uint8Array`'s backing field is immutable — no `push` exists to swap the
710    // buffer out — so this read could sit anywhere after the receiver. It goes
711    // here to keep the two index stores the same shape.
712    let raw_local = emitter.add_anonymous_local(ValType::Ref(RefType {
713        nullable: false,
714        heap_type: HeapType::Concrete(raw_uint8_idx),
715    }))?;
716    emitter.instruction(Instruction::LocalGet(recv_local));
717    crate::codegen::cast_check::emit_operation_cast_on_stack(
718        emitter,
719        ctx,
720        &ctx.ta
721            .try_expr(receiver)
722            .map_err(crate::codegen::arena_failure)?
723            .ty,
724        &Type::Uint8Array,
725    )?;
726    emitter.instruction(Instruction::LocalGet(key));
727    super::expr::emit_index_number(
728        emitter,
729        ctx,
730        &ctx.ta
731            .try_expr(index)
732            .map_err(crate::codegen::arena_failure)?
733            .ty,
734    )?;
735    let idx_f64_local = stash_index_operand(emitter)?;
736    let value_local = emitter.add_anonymous_local(ValType::I32)?;
737    emitter.instruction(Instruction::LocalGet(original_value));
738    cast::emit_coerce_to_slot(
739        emitter,
740        ctx,
741        &ctx.ta
742            .try_expr(value)
743            .map_err(crate::codegen::arena_failure)?
744            .ty,
745        &Type::Number,
746    )?;
747    emit_to_uint8(emitter)?;
748    emitter.instruction(Instruction::LocalSet(value_local));
749    emitter.instruction(Instruction::StructGet {
750        struct_type_index: uint8_idx,
751        field_index: 1,
752    });
753    emitter.instruction(Instruction::LocalSet(raw_local));
754    let idx_local = emit_checked_index(emitter, ctx, raw_local, idx_f64_local)?;
755    emitter.instruction(Instruction::LocalGet(raw_local));
756    emitter.instruction(Instruction::LocalGet(idx_local));
757    emitter.instruction(Instruction::LocalGet(value_local));
758    emitter.instruction(Instruction::ArraySet(raw_uint8_idx));
759    Ok(())
760}
761
762/// JavaScript's `ToUint8` on the `f64` on the stack, leaving an `i32` byte:
763/// truncate, then reduce modulo 256 (so `-1` is 255). Every step is exact in
764/// `f64`; `NaN` and `±Infinity` become `NaN`, which `trunc_sat` maps to 0.
765fn emit_to_uint8(
766    emitter: &mut FunctionEmitter,
767) -> Result<(), crate::compiler_error::CompilerFailure> {
768    let truncated = emitter.add_anonymous_local(ValType::F64)?;
769    emitter.instruction(Instruction::F64Trunc);
770    emitter.instruction(Instruction::LocalTee(truncated));
771    emitter.instruction(Instruction::LocalGet(truncated));
772    emitter.instruction(Instruction::F64Const(256.0.into()));
773    emitter.instruction(Instruction::F64Div);
774    emitter.instruction(Instruction::F64Floor);
775    emitter.instruction(Instruction::F64Const(256.0.into()));
776    emitter.instruction(Instruction::F64Mul);
777    emitter.instruction(Instruction::F64Sub);
778    emitter.instruction(Instruction::I32TruncSatF64U);
779    Ok(())
780}
781
782/// `a[i] = v` on a `$Array`.
783fn emit_array_index_store(
784    emitter: &mut FunctionEmitter,
785    ctx: &CodegenCtx,
786    receiver: ExprId,
787    index: ExprId,
788    value: ExprId,
789    _elem_ty: &Type,
790) -> Result<(), crate::compiler_error::CompilerFailure> {
791    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
792        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
793    })?;
794    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
795        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
796    })?;
797    let object_idx = ctx
798        .symbols
799        .intrinsic_type_indices()
800        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?
801        .object;
802    let recv_local = emit_operand_once(emitter, ctx, receiver)?;
803    let key = emit_operand_once(emitter, ctx, index)?;
804    // RHS evaluates before the store (and so before the bounds check throws),
805    // preserving left-to-right evaluation order.
806    let value_local = emitter.add_anonymous_local(ValType::Ref(RefType {
807        nullable: true,
808        heap_type: HeapType::Concrete(object_idx),
809    }))?;
810    let value_ty = ctx
811        .ta
812        .try_expr(value)
813        .map_err(crate::codegen::arena_failure)?
814        .ty
815        .clone();
816    emit_expr(emitter, ctx, value)?;
817    // Preserve the actual RHS in the erased element slot.
818    cast::emit_box(emitter, ctx, &value_ty)?;
819    emitter.instruction(Instruction::LocalSet(value_local));
820
821    // Read the backing array *after* the RHS: `push` swaps in a fresh
822    // `$rawArray`, so a RHS that grows this array leaves any buffer read
823    // earlier detached — the store would land in one nobody holds, and the
824    // bounds check would test the stale length.
825    let raw_arr_local = emitter.add_anonymous_local(ValType::Ref(RefType {
826        nullable: false,
827        heap_type: HeapType::Concrete(raw_array_idx),
828    }))?;
829    emitter.instruction(Instruction::LocalGet(recv_local));
830    crate::codegen::cast_check::emit_operation_cast_on_stack(
831        emitter,
832        ctx,
833        &ctx.ta
834            .try_expr(receiver)
835            .map_err(crate::codegen::arena_failure)?
836            .ty,
837        &Type::Array(Box::new(Type::Unknown)),
838    )?;
839    emitter.instruction(Instruction::LocalGet(key));
840    super::expr::emit_index_number(
841        emitter,
842        ctx,
843        &ctx.ta
844            .try_expr(index)
845            .map_err(crate::codegen::arena_failure)?
846            .ty,
847    )?;
848    let idx_f64_local = stash_index_operand(emitter)?;
849    let length = stash_array_length(emitter, array_idx)?;
850    emitter.instruction(Instruction::StructGet {
851        struct_type_index: array_idx,
852        field_index: 1,
853    });
854    emitter.instruction(Instruction::LocalSet(raw_arr_local));
855    let idx_local = emit_checked_index_with_length(emitter, ctx, length, idx_f64_local)?;
856    emitter.instruction(Instruction::LocalGet(raw_arr_local));
857    emitter.instruction(Instruction::LocalGet(idx_local));
858    emitter.instruction(Instruction::LocalGet(value_local));
859    emitter.instruction(Instruction::ArraySet(raw_array_idx));
860    Ok(())
861}
862
863/// Catch handlers surround only the try body. A finally gets an outer
864/// completion target shared by normal, exceptional, and early exits.
865pub(super) fn emit_try(
866    emitter: &mut FunctionEmitter,
867    ctx: &CodegenCtx,
868    body: StmtId,
869    catches: &[crate::TypedCatchClause],
870    finally: Option<StmtId>,
871) -> Result<(), crate::compiler_error::CompilerFailure> {
872    if let Some(finally) = finally {
873        super::finally::emit_try_finally(emitter, ctx, body, catches, finally)?;
874        return Ok(());
875    }
876    if catches.is_empty() {
877        emit_statement(emitter, ctx, body)?;
878        return Ok(());
879    }
880    let error_idx = ctx
881        .symbols
882        .intrinsic_type_indices()
883        .ok_or_else(|| crate::codegen::internal_failure("error intrinsic registered"))?
884        .error;
885    let tag_idx = ctx
886        .symbols
887        .error_tag_idx()
888        .ok_or_else(|| crate::codegen::internal_failure("error tag registered"))?;
889    emitter.emit_block(BlockType::Empty);
890    emitter.emit_block(BlockType::Result(ValType::Ref(RefType {
891        nullable: false,
892        heap_type: HeapType::Concrete(error_idx),
893    })));
894    emitter.instruction(Instruction::TryTable(
895        BlockType::Empty,
896        std::borrow::Cow::Owned(vec![wasm_encoder::Catch::One {
897            tag: tag_idx,
898            label: 0,
899        }]),
900    ));
901    emitter.bump_block_depth();
902    emit_statement(emitter, ctx, body)?;
903    emitter.emit_end();
904    emitter.instruction(Instruction::Br(1));
905    emitter.emit_end();
906    let err_stash = emitter.add_anonymous_local(ValType::Ref(RefType {
907        nullable: true,
908        heap_type: HeapType::Concrete(error_idx),
909    }))?;
910    emitter.instruction(Instruction::LocalSet(err_stash));
911    emit_catch_dispatch(emitter, ctx, catches, err_stash)?;
912    emitter.emit_end();
913    Ok(())
914}
915
916/// A `catch (e: MyError)` clause annotated with a proper `Error` subclass
917/// filters by nominal identity — `(vtable global, struct type)` of the
918/// annotation class. The root `Error` annotation (or none) binds everything.
919fn catch_filter_class(
920    ctx: &CodegenCtx,
921    clause_ty: &Type,
922) -> Result<Option<(u32, u32)>, crate::compiler_error::CompilerFailure> {
923    let Type::ClassRef { mangled, .. } = clause_ty.lowered() else {
924        return Ok(None);
925    };
926    if *mangled == crate::mangle::prelude("Error") {
927        return Ok(None);
928    }
929    let vtable_global = ctx
930        .symbols
931        .class_vtable_global_idx(mangled)
932        .ok_or_else(|| {
933            crate::codegen::internal_failure("catch-annotation class vtable global recorded")
934        })?;
935    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
936        crate::codegen::internal_failure("catch-annotation class struct type recorded")
937    })?;
938    Ok(Some((vtable_global, struct_idx)))
939}
940
941/// Ordered dispatch over the catch arms for the caught error stashed in
942/// `err_stash` (a nullable `(ref null $Error)` local, already set). Each
943/// subclass arm runs the nominal brand test; the first match binds, runs its
944/// body, and branches out. A root-`Error`/untyped arm is a catch-all and ends
945/// the chain; without one, the chain ends by re-raising with `throw` (not
946/// `throw_ref`) so an enclosing `catch_all_ref` wrapper still observes it and
947/// runs `finally` first.
948///
949/// At the emission site, relative label 0 is `$end_try`, the matched-arm exit.
950/// When cleanup is present, the whole try/catch sits inside its wrapper.
951fn emit_catch_dispatch(
952    emitter: &mut FunctionEmitter,
953    ctx: &CodegenCtx,
954    catches: &[crate::TypedCatchClause],
955    err_stash: u32,
956) -> Result<(), crate::compiler_error::CompilerFailure> {
957    for clause in catches {
958        let Some((vtable_global, struct_idx)) = catch_filter_class(ctx, &clause.ty)? else {
959            emitter.push_scope();
960            emitter.instruction(Instruction::LocalGet(err_stash));
961            emitter.instruction(Instruction::RefAsNonNull);
962            bind_catch_local(emitter, ctx, clause)?;
963            emit_statement(emitter, ctx, clause.body)?;
964            emitter.pop_scope()?;
965            // Falling through reaches the matched-arm exit in both topologies.
966            return Ok(());
967        };
968        emitter.instruction(Instruction::LocalGet(err_stash));
969        super::cast::emit_nominal_instance_test(emitter, ctx, vtable_global)?;
970        emitter.emit_if(BlockType::Empty);
971        emitter.push_scope();
972        emitter.instruction(Instruction::LocalGet(err_stash));
973        // Shape-only cast is sound here: the brand matched.
974        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(struct_idx)));
975        bind_catch_local(emitter, ctx, clause)?;
976        emit_statement(emitter, ctx, clause.body)?;
977        emitter.pop_scope()?;
978        // Matched-arm exit is label 0 at chain level; +1 for the `if` frame.
979        emitter.instruction(Instruction::Br(1));
980        emitter.emit_end();
981    }
982    let tag_idx = ctx
983        .symbols
984        .error_tag_idx()
985        .ok_or_else(|| crate::codegen::internal_failure("error tag registered"))?;
986    emitter.instruction(Instruction::LocalGet(err_stash));
987    emitter.instruction(Instruction::RefAsNonNull);
988    emitter.instruction(Instruction::Throw(tag_idx));
989    Ok(())
990}
991
992/// Store the caught error on top of the stack in the clause's binding, boxed
993/// when a closure captures the binding and may reassign it.
994fn bind_catch_local(
995    emitter: &mut FunctionEmitter,
996    ctx: &CodegenCtx,
997    clause: &crate::TypedCatchClause,
998) -> Result<(), crate::compiler_error::CompilerFailure> {
999    let slot_ty = if clause.boxed {
1000        let box_idx = ctx.symbols.box_type_idx(&clause.ty)?.ok_or_else(|| {
1001            crate::codegen::internal_failure("box type registered for every boxed catch binding")
1002        })?;
1003        // The caught error initializes the cell.
1004        emitter.instruction(Instruction::I32Const(1));
1005        emitter.instruction(Instruction::StructNew(box_idx));
1006        ValType::Ref(RefType {
1007            nullable: false,
1008            heap_type: HeapType::Concrete(box_idx),
1009        })
1010    } else {
1011        ctx.symbols.value_type(&clause.ty)?
1012    };
1013    let slot = emitter.define_local(&clause.binding, slot_ty)?;
1014    emitter.instruction(Instruction::LocalSet(slot));
1015    Ok(())
1016}
1017
1018/// Emit a `switch` statement as a block-stack of per-case targets.
1019///
1020/// Layout (with N cases and a `default`):
1021///
1022/// ```text
1023/// (block $end
1024///   (block $default
1025///     (block $case_{N-1})
1026///     …
1027///     (block $case_0
1028///       <dispatcher>            ;; br_if $case_i for each value-test
1029///       br $default             ;; (or $end when no default)
1030///     )                          ;; close $case_0
1031///     <case_0 body>; br $end
1032///     …                          ;; close + body for each case
1033///     <case_{N-1} body>; br $end
1034///   )                            ;; close $default
1035///   <default body>               ;; falls through to $end naturally
1036/// )
1037/// ```
1038///
1039/// The discriminant is evaluated once into a fresh anonymous local
1040/// so each per-case test reads it without re-running side effects.
1041/// `break` from any case body targets `$end` via the loop-contexts
1042/// stack (`emit_switch_open` pushed `is_switch: true`); `continue`
1043/// inside a case body walks past this frame to the enclosing loop.
1044fn emit_switch(
1045    emitter: &mut FunctionEmitter,
1046    ctx: &CodegenCtx,
1047    discriminant: crate::ExprId,
1048    discriminant_ty: &Type,
1049    cases: &[TypedSwitchCase],
1050    default: Option<StmtId>,
1051) -> Result<(), crate::compiler_error::CompilerFailure> {
1052    // Stash discriminant in an anonymous local; numeric enums unboxed to f64
1053    // so per-case tests use f64.eq instead of a structural-equality call.
1054    emit_expr(emitter, ctx, discriminant)?;
1055    let disc_val = switch_disc_local_type(ctx, discriminant_ty)?;
1056    if matches!(discriminant_ty.lowered(), Type::NumberEnum { .. }) {
1057        let boxed_idx = ctx.symbols.boxed_number_type_idx().ok_or_else(|| {
1058            crate::codegen::internal_failure("boxed_number type registered with intrinsics")
1059        })?;
1060        emitter.instruction(Instruction::StructGet {
1061            struct_type_index: boxed_idx,
1062            field_index: 1,
1063        });
1064    }
1065    let disc_local = emitter.add_anonymous_local(disc_val)?;
1066    emitter.instruction(Instruction::LocalSet(disc_local));
1067
1068    emitter.emit_switch_open();
1069    // Open $default first (outermost), then $case_{N-1} … $case_0 (innermost),
1070    // so the dispatcher's br_if(source_idx) directly addresses each case block.
1071    if default.is_some() {
1072        emitter.emit_block(BlockType::Empty);
1073    }
1074    for _ in 0..cases.len() {
1075        emitter.emit_block(BlockType::Empty);
1076    }
1077
1078    // Dispatcher inside $case_0: one br_if per case value, then br to $default (or $end).
1079    for (source_idx, case) in cases.iter().enumerate() {
1080        emitter.record_span(case.span);
1081        for value in &case.values {
1082            emit_case_comparison(emitter, ctx, disc_local, discriminant_ty, value)?;
1083            emitter.instruction(Instruction::BrIf(crate::codegen::wasm_u32(source_idx)?));
1084        }
1085    }
1086    // Both $default and $end sit at depth cases.len() from inside the dispatcher.
1087    emitter.instruction(Instruction::Br(crate::codegen::wasm_u32(cases.len())?));
1088
1089    // Close each $case_i and emit its body. The trailing br $end is dead when
1090    // the body terminates via break/return, but Wasm's stack-polymorphic typing
1091    // validates unreachable code, so it's harmless.
1092    for case in cases {
1093        emitter.emit_end();
1094        emitter.push_scope();
1095        emit_statement(emitter, ctx, case.body)?;
1096        emitter.pop_scope()?;
1097        let end_label = emitter.break_label()?;
1098        emitter.instruction(Instruction::Br(end_label));
1099    }
1100
1101    if let Some(default_body) = default {
1102        emitter.emit_end();
1103        emitter.push_scope();
1104        emit_statement(emitter, ctx, default_body)?;
1105        emitter.pop_scope()?;
1106        // Falls through to `$end` naturally — no explicit `br`.
1107    }
1108
1109    emitter.emit_switch_close();
1110    Ok(())
1111}
1112
1113/// Numeric-enum discriminants are unboxed to `f64` here so per-case tests use `f64.eq`
1114/// instead of a structural-equality call on every comparison.
1115fn switch_disc_local_type(
1116    ctx: &CodegenCtx,
1117    ty: &Type,
1118) -> Result<ValType, crate::compiler_error::CompilerFailure> {
1119    Ok(if matches!(ty.lowered(), Type::NumberEnum { .. }) {
1120        ValType::F64
1121    } else {
1122        ctx.symbols.value_type(ty)?
1123    })
1124}
1125
1126/// Internal `if/else` uses `BlockType::Result(ValType::I32)` so the i32 result
1127/// flows out without adding a `br_if`-visible block depth.
1128fn emit_case_comparison(
1129    emitter: &mut FunctionEmitter,
1130    ctx: &CodegenCtx,
1131    disc_local: u32,
1132    disc_ty: &Type,
1133    value: &TypedSwitchValue,
1134) -> Result<(), crate::compiler_error::CompilerFailure> {
1135    let disc_val = switch_disc_local_type(ctx, disc_ty)?;
1136    if let Some(case_ty) = switch_case_primitive_type(value)
1137        && matches!(disc_val, ValType::Ref(_))
1138        && disc_val != ctx.symbols.value_type(&case_ty)?
1139    {
1140        emit_boxed_case_comparison(emitter, ctx, disc_local, &case_ty, value)?;
1141        return Ok(());
1142    }
1143    match value {
1144        TypedSwitchValue::Expr { comparison, .. } => {
1145            emit_expr(emitter, ctx, *comparison)?;
1146        }
1147        TypedSwitchValue::Undefined { .. } => {
1148            emitter.instruction(Instruction::LocalGet(disc_local));
1149            super::expr::emit_is_undefined(emitter, ctx)?;
1150        }
1151        TypedSwitchValue::Null { .. } => {
1152            // case null: accepted only when discriminant can hold null (nullable ref)
1153            emitter.instruction(Instruction::LocalGet(disc_local));
1154            emitter.instruction(Instruction::RefIsNull);
1155        }
1156        TypedSwitchValue::Number { value, .. } => {
1157            emitter.instruction(Instruction::LocalGet(disc_local));
1158            emitter.instruction(Instruction::F64Const((*value).into()));
1159            emitter.instruction(Instruction::F64Eq);
1160        }
1161        TypedSwitchValue::Boolean { value, .. } => {
1162            emitter.instruction(Instruction::LocalGet(disc_local));
1163            emitter.instruction(Instruction::I32Const(if *value { 1 } else { 0 }));
1164            emitter.instruction(Instruction::I32Eq);
1165        }
1166        TypedSwitchValue::String { value, .. } => {
1167            emit_string_compare(emitter, ctx, disc_local, disc_val, value)?;
1168        }
1169        TypedSwitchValue::Enum { value: payload, .. } => match payload {
1170            EnumVariantPayload::Number(n) => {
1171                emitter.instruction(Instruction::LocalGet(disc_local));
1172                emitter.instruction(Instruction::F64Const((*n).into()));
1173                emitter.instruction(Instruction::F64Eq);
1174            }
1175            EnumVariantPayload::String(s) => {
1176                emit_string_compare(emitter, ctx, disc_local, disc_val, s)?;
1177            }
1178        },
1179    };
1180    Ok(())
1181}
1182
1183/// A union discriminant may carry a different primitive kind or null. Test
1184/// its boxed shape before unboxing; a nonmatching kind simply skips this case.
1185fn emit_boxed_case_comparison(
1186    emitter: &mut FunctionEmitter,
1187    ctx: &CodegenCtx,
1188    disc_local: u32,
1189    case_ty: &Type,
1190    value: &TypedSwitchValue,
1191) -> Result<(), crate::compiler_error::CompilerFailure> {
1192    let heap_index = match case_ty {
1193        Type::Number => ctx.symbols.boxed_number_type_idx(),
1194        Type::Boolean => ctx.symbols.boxed_boolean_type_idx(),
1195        Type::String => ctx.symbols.string_type_idx(),
1196        _ => {
1197            return Err(crate::codegen::internal_failure(
1198                "switch case primitive classified above",
1199            ));
1200        }
1201    }
1202    .ok_or_else(|| {
1203        crate::codegen::internal_failure("primitive types registered before switch emission")
1204    })?;
1205    emitter.instruction(Instruction::LocalGet(disc_local));
1206    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(heap_index)));
1207    emitter.emit_if(BlockType::Result(ValType::I32));
1208    emitter.instruction(Instruction::LocalGet(disc_local));
1209    cast::emit_cast_to(emitter, ctx, case_ty)?;
1210    let unboxed = emitter.add_anonymous_local(ctx.symbols.value_type(case_ty)?)?;
1211    emitter.instruction(Instruction::LocalSet(unboxed));
1212    emit_case_comparison(emitter, ctx, unboxed, case_ty, value)?;
1213    emitter.emit_else();
1214    emitter.instruction(Instruction::I32Const(0));
1215    emitter.emit_end();
1216
1217    Ok(())
1218}
1219
1220fn switch_case_primitive_type(value: &TypedSwitchValue) -> Option<Type> {
1221    match value {
1222        TypedSwitchValue::Number { .. }
1223        | TypedSwitchValue::Enum {
1224            value: EnumVariantPayload::Number(_),
1225            ..
1226        } => Some(Type::Number),
1227        TypedSwitchValue::String { .. }
1228        | TypedSwitchValue::Enum {
1229            value: EnumVariantPayload::String(_),
1230            ..
1231        } => Some(Type::String),
1232        TypedSwitchValue::Boolean { .. } => Some(Type::Boolean),
1233        TypedSwitchValue::Null { .. }
1234        | TypedSwitchValue::Undefined { .. }
1235        | TypedSwitchValue::Expr { .. } => None,
1236    }
1237}
1238
1239/// Wraps in a `ref.is_null` guard when discriminant is nullable so a null
1240/// discriminant compares as not-equal. The `if/else` uses explicit i32 result
1241/// so no extra block depth is added to the dispatcher.
1242fn emit_string_compare(
1243    emitter: &mut FunctionEmitter,
1244    ctx: &CodegenCtx,
1245    disc_local: u32,
1246    disc_val: ValType,
1247    literal: &str,
1248) -> Result<(), crate::compiler_error::CompilerFailure> {
1249    let string_eq_idx = ctx.symbols.prelude_func_idx("string_eq").ok_or_else(|| {
1250        crate::codegen::internal_failure("string_eq is not imported from the prelude")
1251    })?;
1252
1253    let disc_is_nullable = matches!(
1254        disc_val,
1255        ValType::Ref(RefType {
1256            nullable: true,
1257            heap_type: HeapType::Concrete(_),
1258        })
1259    );
1260    if disc_is_nullable {
1261        emitter.instruction(Instruction::LocalGet(disc_local));
1262        emitter.instruction(Instruction::RefIsNull);
1263        emitter.emit_if(BlockType::Result(ValType::I32));
1264        emitter.instruction(Instruction::I32Const(0));
1265        emitter.emit_else();
1266        emitter.instruction(Instruction::LocalGet(disc_local));
1267        emitter.instruction(Instruction::RefAsNonNull);
1268        super::emit_const_string_by_text(emitter, ctx, literal)?;
1269        emitter.instruction(Instruction::Call(string_eq_idx));
1270        emitter.emit_end();
1271    } else {
1272        emitter.instruction(Instruction::LocalGet(disc_local));
1273        super::emit_const_string_by_text(emitter, ctx, literal)?;
1274        emitter.instruction(Instruction::Call(string_eq_idx));
1275    }
1276    Ok(())
1277}