Skip to main content

submilli_engine/codegen/function_emitter/
stmt.rs

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