Skip to main content

submilli_engine/codegen/function_emitter/
expr.rs

1//! Recursive emitter for typed expressions.
2//!
3//! Walks a [`TypedExprKind`] tree and pushes Wasm instructions onto a
4//! [`FunctionEmitter`]. Stateless — pure recursion over `&CodegenCtx` plus
5//! mutation of the supplied emitter; both `_start`'s initializer code and
6//! function bodies share this single dispatch.
7//!
8//! Missing emission metadata returns a compiler failure; the caller discards
9//! the owned emitter and module before publishing Wasm.
10
11use crate::codegen::CodegenCtx;
12use crate::codegen::bounds::receiver_name;
13use crate::codegen::bounds::{
14    emit_checked_index, emit_checked_index_with_length, stash_array_length, stash_index_operand,
15};
16use crate::codegen::cast_check::emit_structural_test;
17use crate::codegen::function_emitter::FunctionEmitter;
18use crate::codegen::function_emitter::cast;
19use crate::codegen::symbol_table::{MethodSlotAbi, may_hold_null};
20use crate::runtime::prelude::index::IndexAccess;
21use crate::typechecker::infer::narrowing::{
22    BindingId, ReferencePath, cast_info_for, falsy_part, truthy_part,
23};
24use crate::typed_ast::field_runtime_type_is_testable;
25use crate::{
26    BinOp, ExprId, Ident, Intrinsic, Type, TypedExprKind, TypedObjectFieldSource,
27    TypedObjectMember, UnOp,
28};
29use std::collections::HashMap;
30use wasm_encoder::{BlockType, HeapType, Ieee64, Instruction, RefType, ValType};
31
32/// Emit code for the expression at `id`, leaving its result on the Wasm
33/// stack. Records the expression's source span against the emitter so the
34/// DWARF wiring task can recover instruction-offset → source-position
35/// mappings later.
36pub fn emit_expr(
37    emitter: &mut FunctionEmitter,
38    ctx: &CodegenCtx,
39    id: ExprId,
40) -> Result<(), crate::compiler_error::CompilerFailure> {
41    // `Sub.hello()` reads `Sub` before it reaches `Base`'s static method.
42    if let Some(class) = ctx.ta.inherited_static_receivers.get(&id) {
43        crate::codegen::init_guard::emit_check(emitter, ctx, class);
44    }
45    let kind = &ctx
46        .ta
47        .try_expr(id)
48        .map_err(crate::codegen::arena_failure)?
49        .kind;
50    let call_boundary = matches!(
51        kind,
52        TypedExprKind::Call { .. }
53            | TypedExprKind::CallClosure { .. }
54            | TypedExprKind::VirtualCall { .. }
55            | TypedExprKind::GenericCall { .. }
56            | TypedExprKind::MethodCall { .. }
57            | TypedExprKind::GenericMethodCall { .. }
58            | TypedExprKind::IntrinsicCall { .. }
59            | TypedExprKind::SuperCtorCall { .. }
60            | TypedExprKind::SuperMethodCall { .. }
61            | TypedExprKind::OptionalChain { .. }
62    );
63    let mark = call_boundary.then(|| emitter.single_evaluation_mark());
64    let cache_tuple_source =
65        ctx.ta.tuple_call_spread_sources.contains(&id) && emitter.evaluated_slot(id).is_none();
66    emit_expr_value(emitter, ctx, id)?;
67    if let Some(mark) = mark {
68        emitter.end_single_evaluations(mark)?;
69    }
70    if cache_tuple_source {
71        let ty = &ctx
72            .ta
73            .try_expr(id)
74            .map_err(crate::codegen::arena_failure)?
75            .ty;
76        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(ty)?)?;
77        emitter.instruction(Instruction::LocalTee(slot));
78        emitter.record_single_evaluation(id, slot)?;
79    }
80    // Preserve concrete field validators while the expression still carries
81    // its class arguments, before a surrounding cast or slot erases them.
82    crate::codegen::field_guards::attach(
83        emitter,
84        ctx,
85        &ctx.ta
86            .try_expr(id)
87            .map_err(crate::codegen::arena_failure)?
88            .ty,
89    )?;
90    Ok(())
91}
92
93fn emit_receiver(
94    emitter: &mut FunctionEmitter,
95    ctx: &CodegenCtx,
96    id: ExprId,
97) -> Result<(), crate::compiler_error::CompilerFailure> {
98    emit_expr(emitter, ctx, id)?;
99    let source = &ctx
100        .ta
101        .try_expr(id)
102        .map_err(crate::codegen::arena_failure)?
103        .ty;
104    let target = ctx
105        .ta
106        .source_type(id)
107        .map_err(crate::codegen::arena_failure)?;
108    let _: () = if source != target {
109        crate::codegen::cast_check::emit_operation_cast_on_stack(emitter, ctx, source, target)?;
110    };
111    Ok(())
112}
113
114fn emit_expr_value(
115    emitter: &mut FunctionEmitter,
116    ctx: &CodegenCtx,
117    id: ExprId,
118) -> Result<(), crate::compiler_error::CompilerFailure> {
119    let expr = ctx.ta.try_expr(id).map_err(crate::codegen::arena_failure)?;
120    emitter.record_span(expr.span);
121    // A node the enclosing statement already evaluated into a local — see
122    // `FunctionEmitter::single_evaluations`.
123    if let Some(slot) = emitter.evaluated_slot(id) {
124        emitter.instruction(Instruction::LocalGet(slot));
125        return Ok(());
126    }
127    let _: () = match &expr.kind {
128        TypedExprKind::Number(v) => {
129            emitter.instruction(Instruction::F64Const(Ieee64::from(*v)));
130        }
131        TypedExprKind::BigInt(digits) => emit_bigint_literal(emitter, ctx, digits)?,
132        TypedExprKind::Boolean(b) => {
133            emitter.instruction(Instruction::I32Const(if *b { 1 } else { 0 }));
134        }
135        // String literals materialize via the prelude's vtable global +
136        // `array.new_data` + `struct.new $string`, reading from the
137        // per-literal passive data segment that StringPool assigned.
138        TypedExprKind::String(_) => {
139            let pool_idx = ctx.strings.locations.get(&id).copied().ok_or_else(|| {
140                crate::codegen::internal_failure("a string literal was not interned")
141            })?;
142            super::emit_pooled_string(emitter, ctx, pool_idx)?;
143        }
144        TypedExprKind::Regex { source, flags } => {
145            emit_regex_literal(emitter, ctx, source, flags)?;
146        }
147        TypedExprKind::Sequence { stmts, result } => {
148            for &stmt in stmts {
149                super::stmt::emit_statement(emitter, ctx, stmt)?;
150            }
151            emit_expr(emitter, ctx, *result)?;
152        }
153        TypedExprKind::EffectThen { effect, result } => {
154            emit_expr(emitter, ctx, *effect)?;
155            emitter.instruction(Instruction::Drop);
156            emit_expr(emitter, ctx, *result)?;
157        }
158        TypedExprKind::Binary { op, lhs, rhs } => {
159            emit_binary(emitter, ctx, *op, *lhs, *rhs, &expr.ty)?;
160        }
161        TypedExprKind::Unary { op, operand } => {
162            emit_unary(emitter, ctx, *op, *operand, &expr.ty)?;
163        }
164        TypedExprKind::LocalRef { ident, boxed } => {
165            if ctx.ta.parameter_inputs.contains(&id) {
166                let (slot, source) = emitter
167                    .parameter_input_slots
168                    .get(&ident.name)
169                    .cloned()
170                    .ok_or_else(|| {
171                        crate::codegen::internal_failure("parameter input slot is missing")
172                    })?;
173                emitter.instruction(Instruction::LocalGet(slot));
174                cast::emit_coerce_to_slot(emitter, ctx, &source, &expr.ty)?;
175                return Ok(());
176            }
177            // Function-scope binding (parameter, function-local
178            // `let`/`const`, or captured-by-closure — captures are
179            // materialized as ordinary locals at the closure
180            // prologue, see `emit_closure_function`). Boxed reads
181            // load the `(ref $box_T)` slot, `struct.get` the value
182            // field, then recover the declared type from the cell's
183            // erased payload. Non-boxed reads are a plain `local.get`.
184            let slot = emitter.require_local_slot(&ident.name)?;
185            // A local holding a read that may have found no value is stored,
186            // and boxed, as `T | undefined`.
187            let maybe_missing = emitter.maybe_missing_slots.get(&slot).cloned();
188            let stored_ty = maybe_missing
189                .as_ref()
190                .map_or(&expr.ty, |local| &local.storage);
191            if *boxed {
192                let box_idx = ctx.symbols.box_type_idx(stored_ty)?.ok_or_else(|| {
193                    crate::codegen::internal_failure("box type registered for every boxed LocalRef")
194                })?;
195                super::binding_cells::read(emitter, ctx, slot, box_idx)?;
196                cast::emit_unerase(emitter, ctx, stored_ty)?;
197            } else {
198                emitter.instruction(Instruction::LocalGet(slot));
199            }
200            if let Some(local) = maybe_missing.as_ref() {
201                // A use the typechecker left at `T` tests the value: a missing
202                // one throws here. A check takes it as it is, at
203                // `T | undefined`, which the typechecker records. A use typed
204                // with room for `undefined` also takes it as it is when the
205                // local has no present flag: a local declared `T | undefined`
206                // may hold a real `undefined` (never assigned, `v = undefined`),
207                // which only a flag could tell from a missing read. A local
208                // with a flag decides by the record alone: its narrowed type
209                // `T & ({} | undefined)` holds `undefined` too.
210                let checked = ctx.ta.widened_missing_reads.contains_key(&id)
211                    || (local.present_flag.is_none()
212                        && ctx
213                            .ta
214                            .source_type(id)
215                            .map_err(crate::codegen::arena_failure)?
216                            .holds_undefined());
217                if checked {
218                    cast::emit_coerce_to_slot(emitter, ctx, &local.storage, &expr.ty)?;
219                } else {
220                    let stack = ctx.symbols.value_type(stored_ty)?;
221                    emit_missing_local_check(emitter, ctx, slot, stack)?;
222                    cast::emit_cast_to(emitter, ctx, &expr.ty)?;
223                }
224            }
225        }
226        // A guard that rules out every value leaves no shadow to read, and
227        // no value ever reaches the read.
228        TypedExprKind::LocalNarrowRef { .. } if inferred_never(ctx, id)? => {
229            emitter.instruction(Instruction::Unreachable);
230        }
231        TypedExprKind::LocalNarrowRef { binding, path, .. } => {
232            emit_local_narrow_ref(emitter, ctx, binding, path, &expr.ty)?;
233        }
234        TypedExprKind::GlobalRef { mangled, .. } => {
235            emit_global_ref(emitter, ctx, mangled, &expr.ty)?;
236        }
237        TypedExprKind::FunctionRef { mangled, .. } => {
238            crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
239            emit_function_ref(emitter, ctx, mangled, &expr.ty)?;
240        }
241        TypedExprKind::Call { mangled, args, .. } => {
242            crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
243            emit_symbol_call(emitter, ctx, mangled, args, &expr.ty)?;
244        }
245        TypedExprKind::SuperCtorCall { parent, args } => {
246            // Direct call of the parent's constructor *init* fn on the current
247            // `this` (self-first ABI), initializing the parent-declared fields.
248            // A generic parent's declared ctor params erase to boxed slots.
249            let this = emitter.this_local().ok_or_else(|| {
250                crate::codegen::internal_failure("super(...) only inside a constructor body")
251            })?;
252            emitter.instruction(Instruction::LocalGet(this));
253            let ctor_abi = ctx.symbols.class_ctor_abi(parent).map(<[ValType]>::to_vec);
254            let init = ctx
255                .symbols
256                .class_ctor_init_func_idx(parent)
257                .ok_or_else(|| crate::codegen::internal_failure("parent ctor init fn allocated"))?;
258            // A host parent's declared defaults replace an `undefined` argument.
259            let defaults = ctx.symbols.host_call_defaults(init, args.len()).to_vec();
260            emit_args_into_slots_with_defaults(emitter, ctx, args, ctor_abi.as_deref(), &defaults)?;
261            emitter.instruction(Instruction::Call(init));
262            // Own field initializers / parameter-property copies run right after
263            // the parent is initialized, before the rest of the constructor body.
264            if let Some(mangled) = emitter.ctor_class().cloned() {
265                emit_class_field_setup(emitter, ctx, &mangled)?;
266            }
267            emit_undefined(emitter, ctx)?;
268        }
269        TypedExprKind::SuperMethodCall { owner, name, args } => {
270            // Direct call of the parent body that declares the method (skips
271            // vtable dispatch, which would re-resolve to the override). The
272            // owner's physical sig is the slot sig — same erasure handling as
273            // the vtable path.
274            let this = emitter.this_local().ok_or_else(|| {
275                crate::codegen::internal_failure("super.method() only inside a method body")
276            })?;
277            emitter.instruction(Instruction::LocalGet(this));
278            let abi = ctx.symbols.class_method_abi(owner, &name.name).cloned();
279            emit_args_into_slots(
280                emitter,
281                ctx,
282                args,
283                abi.as_ref().map(|a| a.params.as_slice()),
284            )?;
285            let func = ctx
286                .symbols
287                .class_method_func_idx(owner, &name.name)
288                .ok_or_else(|| {
289                    crate::codegen::internal_failure("parent method body fn allocated")
290                })?;
291            emitter.instruction(Instruction::Call(func));
292            emit_slot_return_cast(emitter, ctx, &expr.ty.clone(), abi.as_ref())?;
293        }
294        TypedExprKind::VirtualCall { target, tool, args } => {
295            // No per-tool import: serialize the args to JSON, dispatch the package's
296            // single host fn, which returns guest objects as `unknown`. Known-return
297            // tools are wrapped in a normal `Cast` by typecheck.
298            super::virtual_call::emit_virtual_call(emitter, ctx, target, tool, args)?;
299        }
300        TypedExprKind::CallClosure { callee, args } => {
301            // Indirect dispatch via `call_ref` through a closure
302            // struct: `LocalRef` to a function-typed binding, an
303            // inline `Closure` expression, a function-typed field,
304            // etc. The typechecker decided this isn't a static
305            // top-level call.
306            emit_indirect_closure_call(emitter, ctx, *callee, args, &expr.ty)?;
307        }
308        TypedExprKind::GenericCall {
309            mangled,
310            type_args,
311            args,
312            return_cast,
313            type_predicate: _,
314        } => {
315            crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
316            emit_generic_call(
317                emitter,
318                ctx,
319                &expr.ty,
320                mangled,
321                type_args,
322                args,
323                return_cast,
324            )?;
325        }
326        TypedExprKind::IntrinsicCall { kind, args } => {
327            let source_ty = ctx
328                .ta
329                .source_type(id)
330                .map_err(crate::codegen::arena_failure)?;
331            emit_intrinsic_call(emitter, ctx, *kind, args, &expr.ty, source_ty)?;
332        }
333        TypedExprKind::MethodCall {
334            receiver,
335            iface,
336            name,
337            args,
338            type_predicate: _,
339        } => {
340            emit_method_call(
341                emitter,
342                ctx,
343                *receiver,
344                iface,
345                &name.name,
346                args,
347                None,
348                &[],
349                None,
350                &expr.ty,
351            )?;
352        }
353        TypedExprKind::GenericMethodCall {
354            receiver,
355            iface,
356            name,
357            type_args,
358            args,
359            return_cast,
360            type_predicate: _,
361        } => {
362            let plain_args: Vec<ExprId> = args.iter().map(|a| a.expr).collect();
363            emit_method_call(
364                emitter,
365                ctx,
366                *receiver,
367                iface,
368                &name.name,
369                &plain_args,
370                Some(args),
371                type_args,
372                return_cast.as_ref(),
373                &expr.ty,
374            )?;
375        }
376        TypedExprKind::ObjectLiteral { members, fields } => {
377            emit_object_literal(emitter, ctx, &expr.ty, members, fields)?;
378        }
379        TypedExprKind::FieldAccess { receiver, name } => {
380            emit_field_access(emitter, ctx, id, &expr.ty, receiver, name)?;
381        }
382        TypedExprKind::InterfacePropertyAccess {
383            receiver,
384            iface,
385            name,
386        } => emit_interface_property(emitter, ctx, receiver, iface, name)?,
387        TypedExprKind::ArrayLiteral { elements, .. } => {
388            emit_array_literal(emitter, ctx, elements)?;
389        }
390        TypedExprKind::TupleLiteral { elements, .. } => {
391            emit_tuple_literal(emitter, ctx, elements)?;
392        }
393        TypedExprKind::IndexAccess { receiver, index } => {
394            emit_index_access(emitter, ctx, id, receiver, index, &expr.ty)?;
395        }
396        TypedExprKind::Undefined => emit_undefined(emitter, ctx)?,
397        TypedExprKind::Null => {
398            // Plan 75.8: `null` lowers to `ref.null none`.
399            // The `none` heap type is the bottom of the GC reference
400            // hierarchy, so the resulting `(ref null none)` is
401            // assignable to any nullable struct ref — `(ref null
402            // $Object)`, `(ref null $ObjectShape)`, `(ref null
403            // $string)`, etc. Using a specific heap type would only
404            // work when the surrounding slot is exactly that type;
405            // made `Type::Object | null` lower to `(ref null
406            // $ObjectShape)` rather than `(ref null $Object)`, so a
407            // hard-coded `$Object` null no longer flows.
408            let _ = ctx; // intrinsics unused after the heap-type switch
409            emitter.instruction(Instruction::RefNull(HeapType::Abstract {
410                shared: false,
411                ty: wasm_encoder::AbstractHeapType::None,
412            }));
413        }
414        TypedExprKind::This => {
415            let slot = emitter.this_local().ok_or_else(|| {
416                crate::codegen::internal_failure("`this` has a local receiver or captured binding")
417            })?;
418            if emitter.dynamic_this {
419                emitter.instruction(Instruction::LocalGet(slot));
420                emitter.instruction(Instruction::RefIsNull);
421                emitter.emit_if(BlockType::Empty);
422                crate::codegen::throw::emit_type_error_throw(
423                    emitter,
424                    ctx,
425                    "Unbound function has no this receiver",
426                );
427                emitter.emit_end();
428            }
429            emitter.instruction(Instruction::LocalGet(slot));
430            if emitter.dynamic_this {
431                crate::codegen::cast_check::emit_operation_cast_on_stack(
432                    emitter,
433                    ctx,
434                    &Type::Unknown,
435                    &expr.ty,
436                )?;
437            }
438        }
439        TypedExprKind::NumberEnumMember { value, .. } => {
440            // Numeric enum values are full `$Object` subtypes — same
441            // Wasm shape as boxed `Number`. Materialize a fresh
442            // `$BoxedNumber` here: vtable in slot 0, f64 value in
443            // slot 1. Mirror of the `Type::Number` arm in
444            // `cast::emit_box`, but inline because the f64 is a
445            // compile-time constant.
446            let boxed_idx = ctx
447                .symbols
448                .boxed_number_type_idx()
449                .ok_or_else(|| crate::codegen::internal_failure("boxed_number type registered"))?;
450            let vtable_global = ctx
451                .symbols
452                .prelude_global_idx("boxed_number_vtable")
453                .ok_or_else(|| {
454                    crate::codegen::internal_failure("boxed_number_vtable imported from prelude")
455                })?;
456            emitter.instruction(Instruction::GlobalGet(vtable_global));
457            emitter.instruction(Instruction::F64Const(Ieee64::from(*value)));
458            emitter.instruction(Instruction::StructNew(boxed_idx));
459        }
460        TypedExprKind::StringEnumMember { .. } => {
461            // String enum values share the `$string` representation.
462            // The StringPool collector records each `StringEnumMember`
463            // by `ExprId` (see string_pool.rs), so the materialisation
464            // path is identical to a regular string literal.
465            let pool_idx = ctx.strings.locations.get(&id).copied().ok_or_else(|| {
466                crate::codegen::internal_failure("a string enum value was not interned")
467            })?;
468            super::emit_pooled_string(emitter, ctx, pool_idx)?;
469        }
470        TypedExprKind::TypeofTag { value, tag } => {
471            emit_typeof_tag(emitter, ctx, *value, *tag)?;
472        }
473        TypedExprKind::InstanceOf { value, class } => match class.lowered() {
474            Type::ClassRef { mangled, .. } => {
475                let vtable_global =
476                    ctx.symbols
477                        .class_vtable_global_idx(mangled)
478                        .ok_or_else(|| {
479                            crate::codegen::internal_failure("class vtable global recorded")
480                        })?;
481                emit_expr(emitter, ctx, *value)?;
482                crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
483                cast::emit_nominal_instance_test(emitter, ctx, vtable_global)?;
484            }
485            // Not a class, so there is no vtable to walk. `$Uint8Array` is
486            // canonically unique, which makes the structural test the same
487            // answer the nominal walk would give.
488            Type::Uint8Array => {
489                let idx = ctx.symbols.uint8_array_type_idx().ok_or_else(|| {
490                    crate::codegen::internal_failure("$Uint8Array intrinsic registered")
491                })?;
492                emit_expr(emitter, ctx, *value)?;
493                emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(idx)));
494            }
495            _ => {
496                return Err(crate::codegen::internal_failure(format!(
497                    "instanceof codegen with a non-class RHS: {class:?}"
498                )));
499            }
500        },
501        TypedExprKind::Narrowed {
502            path,
503            source,
504            binding,
505            cast_info,
506            inner,
507        } => {
508            // Expression-scoped narrowing mirrors `NarrowRegion`: stable root
509            // bindings snapshot once, while field/index paths register their
510            // source for checked live reads. This scope lasts only for `inner`.
511            emitter.push_scope();
512            if !path.chain.is_empty() {
513                emitter.register_narrow_source(&binding.name, *source)?;
514            } else if !reads_maybe_missing_local(emitter, ctx, *source)? {
515                let shadow_val = ctx.symbols.value_type(&cast_info.to_ty)?;
516                let shadow = emitter.define_local(binding, shadow_val)?;
517                emit_expr(emitter, ctx, *source)?;
518                crate::codegen::function_emitter::cast::emit_narrowing_cast(
519                    emitter, ctx, cast_info,
520                )?;
521                emitter.instruction(Instruction::LocalSet(shadow));
522            }
523            emit_expr(emitter, ctx, *inner)?;
524            emitter.pop_scope()?;
525        }
526        TypedExprKind::Closure {
527            params,
528            captured,
529            runtime_generics,
530            ..
531        } => {
532            emit_closure_value(
533                emitter,
534                ctx,
535                id,
536                &expr.ty,
537                params,
538                captured,
539                runtime_generics,
540            )?;
541        }
542        // `cond ? then_: else_`. Wasm `if`-with-result lifts
543        // the branch values onto the parent stack. Each branch's value
544        // is coerced to the chain's result Wasm slot so both arms
545        // agree on the stack type, including erased void completions.
546        TypedExprKind::Ternary { cond, then_, else_ } => {
547            let result_ty = expr.ty.clone();
548            let block_ty = conditional_block_type(ctx, &result_ty)?;
549            let cond_ty = ctx
550                .ta
551                .try_expr(*cond)
552                .map_err(crate::codegen::arena_failure)?
553                .ty
554                .clone();
555            emit_expr(emitter, ctx, *cond)?;
556            crate::codegen::function_emitter::cast::emit_condition_to_i32(emitter, ctx, &cond_ty)?;
557            emitter.emit_if(block_ty);
558            emit_conditional_operand(emitter, ctx, *then_, &result_ty)?;
559            emitter.emit_else();
560            emit_conditional_operand(emitter, ctx, *else_, &result_ty)?;
561            emitter.emit_end();
562        }
563        // `a ?? b` evaluates its left side once and selects the right side for
564        // null or undefined. Unboxed scalar slots cannot be nullish.
565        TypedExprKind::NullishCoalesce { lhs, rhs } => {
566            emit_nullish_coalesce(emitter, ctx, lhs, rhs, &expr.ty)?;
567        }
568        // optional chain. Each `?.` step opens a fresh
569        // `if`-with-result that short-circuits to undefined on a nullish
570        // receiver and recurses into the rest of the chain otherwise.
571        // Per-part access codegen is factored into
572        // `emit_chain_access` (Field + Index for v1; Call/MethodCall/
573        // InterfaceProperty land alongside their non-chain siblings'
574        // refactor).
575        TypedExprKind::OptionalChain { base, parts } => {
576            let present_flag = take_present_flag(emitter, id);
577            if let Some(flag) = present_flag {
578                set_present_flag(emitter, flag, true);
579            }
580            let result_ty = expr.ty.clone();
581            let base_ty = ctx
582                .ta
583                .try_expr(*base)
584                .map_err(crate::codegen::arena_failure)?
585                .ty
586                .clone();
587            let receiver = if matches!(parts.first(), Some(crate::TypedChainPart::Call { .. })) {
588                emit_callee(emitter, ctx, *base)?
589            } else {
590                emit_expr(emitter, ctx, *base)?;
591                None
592            };
593            emitter.call_receiver = receiver;
594            emit_chain_parts(
595                emitter,
596                ctx,
597                *base,
598                parts,
599                0,
600                &base_ty,
601                &result_ty,
602                ctx.ta.runtime_chain_types.get(&id).map(Vec::as_slice),
603                present_flag,
604            )?;
605        }
606        TypedExprKind::PostfixUnary { op, target } => {
607            if let crate::PostfixTarget::Index {
608                receiver, index, ..
609            } = target
610                && ctx
611                    .ta
612                    .source_type(*receiver)
613                    .map_err(crate::codegen::arena_failure)?
614                    .indexes_by_name()
615            {
616                emit_object_index_postfix(
617                    emitter,
618                    ctx,
619                    *receiver,
620                    *index,
621                    *op,
622                    ctx.ta
623                        .source_type(id)
624                        .map_err(crate::codegen::arena_failure)?,
625                )?;
626                cast::emit_coerce_to_slot(emitter, ctx, &Type::Unknown, &expr.ty)?;
627            } else {
628                emit_postfix_unary(emitter, ctx, *op, target, &expr.ty)?;
629            }
630        }
631        TypedExprKind::NonNullAssert { value } => {
632            crate::codegen::cast_check::emit_non_null_assert(emitter, ctx, *value, &expr.ty)?;
633        }
634        TypedExprKind::Cast {
635            value,
636            target_ty,
637            check,
638        } => {
639            crate::codegen::cast_check::emit_cast(
640                emitter,
641                ctx,
642                *value,
643                target_ty,
644                check.as_deref(),
645                expr.span,
646            )?;
647        }
648    };
649    // A `never` expression doesn't complete, so what an enclosing expression
650    // would do with its value is unreachable: `"a" + fail()` never concatenates.
651    // A read does complete: a `never[]` an alias filled holds elements, as
652    // TypeScript's types allow, so reading one yields what it holds.
653    if matches!(expr.ty, Type::Never) && !completes_when_never(&expr.kind) {
654        emitter.instruction(Instruction::Unreachable);
655    }
656    Ok(())
657}
658
659/// Whether a `never`-typed `kind` still yields a value or settles its own
660/// reachability. A field or element read yields what the object or array
661/// holds; a narrowed reference emits its own `unreachable` when it is `never`.
662/// A `never` binding stays unreachable: its initializer or the call that bound
663/// it diverged.
664fn completes_when_never(kind: &TypedExprKind) -> bool {
665    matches!(
666        kind,
667        TypedExprKind::LocalNarrowRef { .. }
668            | TypedExprKind::FieldAccess { .. }
669            | TypedExprKind::IndexAccess { .. }
670    )
671}
672
673/// Materialize the store's canonical undefined value, including synthetic omissions.
674pub(crate) fn emit_undefined(
675    emitter: &mut FunctionEmitter<'_>,
676    ctx: &CodegenCtx<'_>,
677) -> Result<(), crate::compiler_error::CompilerFailure> {
678    let global = ctx
679        .symbols
680        .prelude_global_idx(crate::runtime::prelude::undefined::GLOBAL_NAME)
681        .ok_or_else(|| crate::codegen::internal_failure("undefined global was not imported"))?;
682    emitter.instruction(Instruction::GlobalGet(global));
683    Ok(())
684}
685
686/// Consume an erased reference and test both language nullish values.
687pub(crate) fn emit_is_nullish(
688    emitter: &mut FunctionEmitter<'_>,
689    ctx: &CodegenCtx<'_>,
690) -> Result<(), crate::compiler_error::CompilerFailure> {
691    let intrinsic = ctx
692        .symbols
693        .intrinsic_type_indices()
694        .ok_or_else(|| crate::codegen::internal_failure("undefined intrinsic was not declared"))?;
695    let value = emitter.add_anonymous_local(ValType::Ref(RefType {
696        nullable: true,
697        heap_type: HeapType::Concrete(intrinsic.object),
698    }))?;
699    emitter.instruction(Instruction::LocalSet(value));
700    emitter.instructions(crate::codegen::nullish::is_nullish(
701        &[Instruction::LocalGet(value)],
702        intrinsic,
703    ));
704    Ok(())
705}
706
707pub(crate) fn emit_is_undefined(
708    emitter: &mut FunctionEmitter<'_>,
709    ctx: &CodegenCtx<'_>,
710) -> Result<(), crate::compiler_error::CompilerFailure> {
711    let intrinsic = ctx
712        .symbols
713        .intrinsic_type_indices()
714        .ok_or_else(|| crate::codegen::internal_failure("undefined intrinsic was not declared"))?;
715    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
716        intrinsic.undefined,
717    )));
718    Ok(())
719}
720
721fn emit_global_ref(
722    emitter: &mut FunctionEmitter,
723    ctx: &CodegenCtx,
724    mangled: &crate::MangledName,
725    result_ty: &Type,
726) -> Result<(), crate::compiler_error::CompilerFailure> {
727    // Read a top-level `let` / `const` binding. The typed AST
728    // split means we no longer dispatch on
729    // ValueKind here — `FunctionRef` handles the
730    // function-as-value path separately.
731    //
732    // Static-interface bindings (`console`, `Map`, `Temporal.Instant`) have
733    // no global behind them (see `static_interface_of` in codegen::mod). As
734    // a value, one reads as the host object that prints like the binding.
735    if let Some(iface) = static_interface(ctx, result_ty) {
736        emit_static_value(emitter, ctx, iface)?;
737        if let ValType::Ref(RefType {
738            nullable: false, ..
739        }) = ctx.symbols.value_type(result_ty)?
740        {
741            emitter.instruction(Instruction::RefAsNonNull);
742        }
743        return Ok(());
744    }
745    crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
746    let idx = ctx.symbols.global_idx(mangled).ok_or_else(|| {
747        crate::codegen::internal_failure("top-level let/const recorded during codegen")
748    })?;
749    emitter.instruction(Instruction::GlobalGet(idx));
750    if let ValType::Ref(RefType {
751        nullable: false, ..
752    }) = ctx.symbols.value_type(result_ty)?
753    {
754        emitter.instruction(Instruction::RefAsNonNull);
755    }
756
757    Ok(())
758}
759
760/// The static-dispatch interface `ty` names, if any.
761fn static_interface<'a>(ctx: &CodegenCtx, ty: &'a Type) -> Option<&'a crate::MangledName> {
762    match ty.lowered() {
763        Type::InterfaceRef { mangled, .. }
764            if ctx.symbols.iface_dispatch(mangled) == Some(crate::Dispatch::Static) =>
765        {
766            Some(mangled)
767        }
768        _ => None,
769    }
770}
771
772fn emit_static_value(
773    emitter: &mut FunctionEmitter,
774    ctx: &CodegenCtx,
775    iface: &crate::MangledName,
776) -> Result<(), crate::compiler_error::CompilerFailure> {
777    let tag = ctx.symbols.static_value_tag(iface).ok_or_else(|| {
778        crate::codegen::internal_failure("a static binding read as a value has a recorded tag")
779    })?;
780    super::emit_inline_string_literal(emitter, ctx, tag)?;
781    let helper = ctx
782        .symbols
783        .prelude_func_idx("ObjectConstructor##staticValue")
784        .ok_or_else(|| crate::codegen::internal_failure("the static value helper is imported"))?;
785    emitter.instruction(Instruction::Call(helper));
786    Ok(())
787}
788
789fn emit_function_ref(
790    emitter: &mut FunctionEmitter,
791    ctx: &CodegenCtx,
792    mangled: &crate::MangledName,
793    result_ty: &Type,
794) -> Result<(), crate::compiler_error::CompilerFailure> {
795    // top-level function used as a value. Wrap it in
796    // a closure struct whose funcref points at a per-function
797    // adapter (slot 0 = env, slot 1 = adapter funcref, slot 2
798    // = env sentinel). The shared closure vtable reuses for
799    // the env slot — adapter bodies ignore it, and the slot
800    // just needs a non-null `(ref any)`. The closure is built
801    // once and cached in a global, so every read is the same value.
802    // A `FunctionRef` is always typed by its declared signature, so
803    // `result_ty` classifies like the adapter's signature and the
804    // cached struct type matches the global's.
805    let closure_struct_idx = ctx
806        .symbols
807        .closure_struct_type_idx(crate::codegen::closures::classify(result_ty)?)
808        .ok_or_else(|| {
809            crate::codegen::internal_failure(
810                "closure struct type registered for every function-as-value",
811            )
812        })?;
813    let adapter_idx = ctx.symbols.adapter_func_idx(mangled).ok_or_else(|| {
814        crate::codegen::internal_failure("adapter func recorded for every function-as-value")
815    })?;
816    let closure_global_idx = ctx
817        .symbols
818        .adapter_closure_global_idx(mangled)
819        .ok_or_else(|| {
820            crate::codegen::internal_failure("closure global recorded for every function-as-value")
821        })?;
822    let vtable_idx = ctx.symbols.closure_vtable_global_idx().ok_or_else(|| {
823        crate::codegen::internal_failure(
824            "closure vtable global emitted whenever closures or adapters exist",
825        )
826    })?;
827    emitter.instruction(Instruction::GlobalGet(closure_global_idx));
828    emitter.instruction(Instruction::RefIsNull);
829    emitter.emit_if(BlockType::Empty);
830    emitter.instruction(Instruction::GlobalGet(vtable_idx));
831    emitter.instruction(Instruction::RefFunc(adapter_idx));
832    emitter.instruction(Instruction::GlobalGet(vtable_idx));
833    if let Some(metadata) = ctx.symbols.function_argument_metadata.get(mangled) {
834        crate::codegen::call_arguments::wrap(emitter, ctx, metadata)?;
835    }
836    emitter.instruction(Instruction::I64Const(0));
837    emitter.instruction(Instruction::StructNew(closure_struct_idx));
838    emitter.instruction(Instruction::GlobalSet(closure_global_idx));
839    emitter.emit_end();
840    emitter.instruction(Instruction::GlobalGet(closure_global_idx));
841    if ctx.symbols.is_shared_closure_global(mangled) {
842        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
843            closure_struct_idx,
844        )));
845    } else {
846        emitter.instruction(Instruction::RefAsNonNull);
847    }
848
849    Ok(())
850}
851
852fn emit_symbol_call(
853    emitter: &mut FunctionEmitter,
854    ctx: &CodegenCtx,
855    mangled: &crate::MangledName,
856    args: &[ExprId],
857    result_ty: &Type,
858) -> Result<(), crate::compiler_error::CompilerFailure> {
859    // Static dispatch — the typechecker already resolved this to
860    // a known top-level symbol (`mangled`), so it must be in the
861    // top-level fn registry. Imported (prelude / host) and local
862    // user functions take slightly different emission paths.
863    // No call-boundary box/cast logic here — generic calls route
864    // through [`GenericCall`](TypedExprKind::GenericCall) instead.
865    //
866    // note: variadic callees see one synthesized
867    // `ArrayLiteral` in the rest slot — the typechecker
868    // pre-packs the trailing args — so `args.len()` always
869    // matches `target.params.len()` and no codegen branching
870    // is needed.
871    let target = ctx.symbols.top_level_fn(mangled).ok_or_else(|| {
872        crate::codegen::internal_failure(format!(
873            "Call references unknown top-level fn `{}`",
874            mangled.as_str()
875        ))
876    })?;
877    if crate::codegen::field_guards::guarded_constructor(ctx, mangled, result_ty)? {
878        let Type::ClassRef { mangled: class, .. } = result_ty.lowered() else {
879            return Err(crate::codegen::internal_failure("constructor class"));
880        };
881        emit_args_into_slots(emitter, ctx, args, ctx.symbols.class_ctor_abi(class))?;
882        crate::codegen::field_guards::constructor_argument(emitter, ctx, result_ty)?;
883        emitter.instruction(Instruction::Call(target.wasm_idx));
884    } else {
885        emit_direct_call(
886            emitter,
887            ctx,
888            target.is_host,
889            target.wasm_idx,
890            &target.params,
891            &target.ret,
892            args,
893        )?;
894    }
895    if result_ty.is_void() {
896        cast::emit_box(emitter, ctx, &target.ret)?;
897    }
898
899    Ok(())
900}
901
902fn emit_interface_property(
903    emitter: &mut FunctionEmitter,
904    ctx: &CodegenCtx,
905    receiver: &ExprId,
906    iface: &crate::MangledName,
907    name: &Ident,
908) -> Result<(), crate::compiler_error::CompilerFailure> {
909    // property dispatch. The typechecker resolved this
910    // to an interface property at infer time and stamped the
911    // interface's mangled name on the node, so codegen looks
912    // up the getter under `<iface>#<name>` directly — no
913    // receiver-type inspection needed.
914    let key = crate::mangle::extend(iface, &name.name);
915    // Static-interface properties (`Number.EPSILON`) import as
916    // constant globals — no receiver, no getter call.
917    if ctx.symbols.iface_dispatch(iface) == Some(crate::Dispatch::Static) {
918        let global_idx = ctx.symbols.global_idx(&key).ok_or_else(|| {
919            crate::codegen::internal_failure(
920                "static interface property recorded as a global import",
921            )
922        })?;
923        emitter.instruction(Instruction::GlobalGet(global_idx));
924        return Ok(());
925    }
926    if let Some(struct_idx) = inline_length_struct_idx(ctx, iface, &name.name)? {
927        emit_receiver(emitter, ctx, *receiver)?;
928        emit_inline_length(emitter, ctx, struct_idx);
929        return Ok(());
930    }
931    let func_idx = ctx.symbols.func_idx(&key).ok_or_else(|| {
932        crate::codegen::internal_failure(
933            "interface property getter recorded during the dependency-import pass",
934        )
935    })?;
936    emit_receiver(emitter, ctx, *receiver)?;
937    // InterfaceRef-typed receivers (`Map<K, V>`,
938    // `Set<T>` — Direct dispatch) lower to
939    // `(ref null $Object)`, but the property getter wrapper
940    // takes `(ref $Object)` non-null. Coerce the same way
941    // `emit_method_call` does for the method path. Primitive
942    // Direct receivers (Array, Uint8Array, etc.) already have
943    // non-null Wasm types so no coercion needed.
944    //
945    // `peel` so an aliased InterfaceRef
946    // (`type Query = Map<string, string>` etc.) still hits
947    // this branch — `matches!` on the raw type would miss
948    // the wrapper.
949    if matches!(
950        ctx.ta
951            .source_type(*receiver)
952            .map_err(crate::codegen::arena_failure)?
953            .lowered(),
954        Type::InterfaceRef { .. }
955    ) {
956        emitter.instruction(Instruction::RefAsNonNull);
957    }
958    emitter.instruction(Instruction::Call(func_idx));
959
960    Ok(())
961}
962
963fn emit_index_access(
964    emitter: &mut FunctionEmitter,
965    ctx: &CodegenCtx,
966    id: ExprId,
967    receiver: &ExprId,
968    index: &ExprId,
969    result_ty: &Type,
970) -> Result<(), crate::compiler_error::CompilerFailure> {
971    // Uint8Array uses a different storage shape than
972    // Array (packed i8 vs boxed anyref slots), so the index
973    // recipe forks on the static receiver type. Tuples lower
974    // to `$Array` and fall through to the default arm.
975    //
976    // Only an array or record read sets a present flag (see
977    // `emit_value_with_present_flag`); a string or `Uint8Array` element is
978    // never `undefined`, so its value tells. Taken before anything nested is
979    // emitted, so no nested read can take it.
980    let mut present_flag = take_present_flag(emitter, id);
981    let missing_reads_undefined = ctx.ta.missing_reads_undefined.contains(&id);
982    // Such a read always finds a value (see `checked_ty` below): a local
983    // holding it is present.
984    if missing_reads_undefined && let Some(flag) = present_flag.take() {
985        set_present_flag(emitter, flag, true);
986    }
987    let recv_ty = ctx
988        .ta
989        .source_type(*receiver)
990        .map_err(crate::codegen::arena_failure)?
991        .clone();
992    // The typechecker's type for the read, which a wide slot may have
993    // erased from `result_ty`: a read typed with room for `undefined` gives
994    // it for a missing element, and any other throws. A generic read whose
995    // missing key reads `undefined` is checked as one typed `T[K] |
996    // undefined`.
997    let source_ty = ctx
998        .ta
999        .source_type(id)
1000        .map_err(crate::codegen::arena_failure)?;
1001    let checked_ty = if missing_reads_undefined {
1002        Type::union(vec![source_ty.clone(), Type::Undefined])
1003    } else {
1004        source_ty.clone()
1005    };
1006    let missing_gives_undefined = checked_ty.holds_undefined();
1007    if recv_ty.indexes_by_name() {
1008        emit_expr(emitter, ctx, *receiver)?;
1009        cast::emit_box(
1010            emitter,
1011            ctx,
1012            &ctx.ta
1013                .try_expr(*receiver)
1014                .map_err(crate::codegen::arena_failure)?
1015                .ty,
1016        )?;
1017        let probe = stash_probed_record(emitter, ctx, &checked_ty, present_flag)?;
1018        emit_expr(emitter, ctx, *index)?;
1019        crate::codegen::cast_check::emit_operation_cast_on_stack(
1020            emitter,
1021            ctx,
1022            &ctx.ta
1023                .try_expr(*index)
1024                .map_err(crate::codegen::arena_failure)?
1025                .ty,
1026            &Type::String,
1027        )?;
1028        let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
1029        emitter.instruction(Instruction::LocalTee(key));
1030        let Some(symbol) = ctx.require(
1031            ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
1032            "dynamic read imported",
1033        ) else {
1034            return Ok(());
1035        };
1036        emitter.instruction(Instruction::Call(symbol));
1037        emit_checked_record_read(
1038            emitter,
1039            ctx,
1040            &checked_ty,
1041            KeyOperand::Local(key),
1042            probe,
1043            &receiver_name(ctx.ta, *receiver)?,
1044        )?;
1045        cast::emit_coerce_to_slot(emitter, ctx, &checked_ty, result_ty)?;
1046    } else if recv_ty.lowered() == &Type::Uint8Array {
1047        emit_expr(emitter, ctx, *receiver)?;
1048        emit_uint8_index_with_receiver_on_stack(
1049            emitter,
1050            ctx,
1051            *index,
1052            missing_gives_undefined.then_some(result_ty),
1053            &receiver_name(ctx.ta, *receiver)?,
1054        )?;
1055        if let Some(flag) = present_flag {
1056            set_present_flag_from_value(emitter, ctx, flag, result_ty)?;
1057        }
1058    } else if recv_ty.is_string_shaped() {
1059        emit_expr(emitter, ctx, *receiver)?;
1060        emit_string_index_with_receiver_on_stack(
1061            emitter,
1062            ctx,
1063            *index,
1064            missing_gives_undefined.then_some(result_ty),
1065            &receiver_name(ctx.ta, *receiver)?,
1066        )?;
1067        if let Some(flag) = present_flag {
1068            set_present_flag_from_value(emitter, ctx, flag, result_ty)?;
1069        }
1070    } else {
1071        emit_expr(emitter, ctx, *receiver)?;
1072        emit_bounds_checked_index_with_receiver_on_stack(
1073            emitter,
1074            ctx,
1075            *index,
1076            result_ty,
1077            MissingElement {
1078                gives_undefined: ctx.ta.call_spread_index_reads.contains(&id)
1079                    || missing_gives_undefined
1080                    || is_optional_tuple_position(ctx, &recv_ty, *index)?,
1081                throws_on_absent_group: recv_ty.is_regexp_match_array() && !missing_gives_undefined,
1082                present_flag,
1083            },
1084            &receiver_name(ctx.ta, *receiver)?,
1085        )?;
1086    }
1087
1088    Ok(())
1089}
1090
1091/// `s[index]`, with `s` on the stack: the host helper reads the code unit,
1092/// throwing `MissingValueError` out of range as an array read does. With
1093/// `undefined_into`, the read was typed with room for `undefined`, and an
1094/// index out of range gives it in that slot instead.
1095fn emit_string_index_with_receiver_on_stack(
1096    emitter: &mut FunctionEmitter,
1097    ctx: &CodegenCtx,
1098    index: ExprId,
1099    undefined_into: Option<&Type>,
1100    receiver_name: &str,
1101) -> Result<(), crate::compiler_error::CompilerFailure> {
1102    if let Some(result_ty) = undefined_into {
1103        return emit_string_index_or_undefined(emitter, ctx, index, result_ty, receiver_name);
1104    }
1105    let Some(helper) = ctx.require(
1106        ctx.symbols.prelude_func_idx("string_index"),
1107        "string index helper imported",
1108    ) else {
1109        return Ok(());
1110    };
1111    // A chain step's receiver may sit on the stack in a wider slot, such as
1112    // `string | undefined`'s, so it is cast to `$string` after the index runs.
1113    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
1114    emitter.instruction(Instruction::LocalSet(receiver));
1115    // Put the label and index below the receiver argument. This keeps the
1116    // receiver cast after index evaluation without an index temporary, so the
1117    // pooled label adds no work to an in-range read.
1118    let label = ctx
1119        .symbols
1120        .field_name_string_global_idx(receiver_name)
1121        .ok_or_else(|| crate::codegen::internal_failure("string index label global collected"))?;
1122    emitter.instruction(Instruction::GlobalGet(label));
1123    emit_expr(emitter, ctx, index)?;
1124    emit_index_number(
1125        emitter,
1126        ctx,
1127        &ctx.ta
1128            .try_expr(index)
1129            .map_err(crate::codegen::arena_failure)?
1130            .ty,
1131    )?;
1132    emitter.instruction(Instruction::LocalGet(receiver));
1133    crate::codegen::cast_check::emit_operation_cast_on_stack(
1134        emitter,
1135        ctx,
1136        &Type::Unknown,
1137        &Type::String,
1138    )?;
1139    emitter.instruction(Instruction::Call(helper));
1140    Ok(())
1141}
1142
1143/// [`emit_string_index_with_receiver_on_stack`] for a read whose index may be
1144/// out of range, which gives `undefined`.
1145fn emit_string_index_or_undefined(
1146    emitter: &mut FunctionEmitter,
1147    ctx: &CodegenCtx,
1148    index: ExprId,
1149    result_ty: &Type,
1150    receiver_name: &str,
1151) -> Result<(), crate::compiler_error::CompilerFailure> {
1152    let Some(helper) = ctx.require(
1153        ctx.symbols.prelude_func_idx("string_index"),
1154        "string index helper imported",
1155    ) else {
1156        return Ok(());
1157    };
1158    let string_idx = ctx.symbols.string_type_idx().ok_or_else(|| {
1159        crate::codegen::internal_failure("the $string intrinsic is not registered")
1160    })?;
1161    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
1162    emitter.instruction(Instruction::LocalSet(receiver));
1163    emit_expr(emitter, ctx, index)?;
1164    emit_index_number(
1165        emitter,
1166        ctx,
1167        &ctx.ta
1168            .try_expr(index)
1169            .map_err(crate::codegen::arena_failure)?
1170            .ty,
1171    )?;
1172    let idx_f64_local = stash_index_operand(emitter)?;
1173    emitter.instruction(Instruction::LocalGet(receiver));
1174    crate::codegen::cast_check::emit_operation_cast_on_stack(
1175        emitter,
1176        ctx,
1177        &Type::Unknown,
1178        &Type::String,
1179    )?;
1180    let string = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
1181    emitter.instruction(Instruction::LocalTee(string));
1182    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(string_idx)));
1183    emitter.instruction(Instruction::StructGet {
1184        struct_type_index: string_idx,
1185        field_index: 1,
1186    });
1187    emitter.instruction(Instruction::ArrayLen);
1188    let length = emitter.add_anonymous_local(ValType::I32)?;
1189    emitter.instruction(Instruction::LocalSet(length));
1190    crate::codegen::bounds::emit_index_in_range(emitter, length, idx_f64_local);
1191    emitter.emit_if(BlockType::Result(ctx.symbols.value_type(result_ty)?));
1192    let label = ctx
1193        .symbols
1194        .field_name_string_global_idx(receiver_name)
1195        .ok_or_else(|| crate::codegen::internal_failure("string index label global collected"))?;
1196    emitter.instruction(Instruction::GlobalGet(label));
1197    emitter.instruction(Instruction::LocalGet(idx_f64_local));
1198    emitter.instruction(Instruction::LocalGet(string));
1199    emitter.instruction(Instruction::Call(helper));
1200    cast::emit_coerce_to_slot(emitter, ctx, &Type::String, result_ty)?;
1201    emitter.emit_else();
1202    emit_undefined(emitter, ctx)?;
1203    cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, result_ty)?;
1204    emitter.emit_end();
1205    Ok(())
1206}
1207
1208/// Where a record read pushes its key again for a missing-key message.
1209#[derive(Clone, Copy)]
1210enum KeyOperand {
1211    Local(u32),
1212    Global(u32),
1213}
1214
1215/// With a record read's `getField` result (`unknown`) on the stack, leaves it
1216/// checked against `read_ty`, the type the read was given.
1217///
1218/// `getField` answers `undefined` for a key the record does not have. Where
1219/// `read_ty` has no room for `undefined`, that throws `MissingValueError`
1220/// naming the key. Where the typechecker gave the read room for it (a check,
1221/// or a place that holds `undefined`), `undefined` passes, and only a
1222/// present value is checked, against `read_ty` without `undefined`: a
1223/// representation test alone would let any object through a
1224/// `number | undefined` slot. With `record`, the record kept by
1225/// `stash_probed_record`, an `undefined` answer is a missing key only if
1226/// `hasField` says the record has no such key.
1227fn emit_checked_record_read(
1228    emitter: &mut FunctionEmitter,
1229    ctx: &CodegenCtx,
1230    read_ty: &Type,
1231    key: KeyOperand,
1232    probe: RecordProbe,
1233    receiver_label: &str,
1234) -> Result<(), crate::compiler_error::CompilerFailure> {
1235    let present_ty = crate::typechecker::infer::narrowing::strip_undefined(read_ty);
1236    let value = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
1237    emitter.instruction(Instruction::LocalTee(value));
1238    emit_is_undefined(emitter, ctx)?;
1239    if read_ty.holds_undefined() && !present_ty.holds_undefined() {
1240        emitter.emit_if(BlockType::Result(ctx.symbols.value_type(read_ty)?));
1241        // A present flag says whether the key exists, which `undefined`
1242        // alone doesn't for a type parameter's value.
1243        if let Some(flag) = probe.present_flag {
1244            match probe.record {
1245                Some(record) => {
1246                    let Some(has_field) = ctx.require(
1247                        ctx.symbols.prelude_func_idx("ObjectConstructor##hasField"),
1248                        "record key probe imported",
1249                    ) else {
1250                        return Ok(());
1251                    };
1252                    emitter.instruction(Instruction::LocalGet(record));
1253                    push_record_key(emitter, key);
1254                    emitter.instruction(Instruction::Call(has_field));
1255                    emitter.instruction(Instruction::LocalSet(flag));
1256                }
1257                None => set_present_flag(emitter, flag, false),
1258            }
1259        }
1260        emit_undefined(emitter, ctx)?;
1261        cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, read_ty)?;
1262        emitter.emit_else();
1263        if let Some(flag) = probe.present_flag {
1264            set_present_flag(emitter, flag, true);
1265        }
1266        emitter.instruction(Instruction::LocalGet(value));
1267        crate::codegen::cast_check::emit_checked_cast_on_stack(
1268            emitter,
1269            ctx,
1270            &Type::Unknown,
1271            &present_ty,
1272        )?;
1273        cast::emit_coerce_to_slot(emitter, ctx, &present_ty, read_ty)?;
1274        emitter.emit_end();
1275        return Ok(());
1276    }
1277    // Only a read typed with room for `undefined` is absorbed, so here the
1278    // value, when there is one, is present.
1279    if let Some(flag) = probe.present_flag {
1280        set_present_flag(emitter, flag, true);
1281    }
1282    emitter.emit_if(BlockType::Empty);
1283    if !read_ty.holds_undefined() && !matches!(read_ty.peel(), Type::Error | Type::Never) {
1284        let Some(helper) = ctx.require(
1285            ctx.symbols.prelude_func_idx("missing_key_error"),
1286            "missing key helper imported",
1287        ) else {
1288            return Ok(());
1289        };
1290        // A type parameter's instantiation may hold `undefined`: only a key
1291        // the record doesn't have is missing there.
1292        if let Some(record) = probe.record {
1293            let Some(has_field) = ctx.require(
1294                ctx.symbols.prelude_func_idx("ObjectConstructor##hasField"),
1295                "record key probe imported",
1296            ) else {
1297                return Ok(());
1298            };
1299            emitter.instruction(Instruction::LocalGet(record));
1300            push_record_key(emitter, key);
1301            emitter.instruction(Instruction::Call(has_field));
1302            emitter.instruction(Instruction::I32Eqz);
1303            emitter.emit_if(BlockType::Empty);
1304        }
1305        push_record_key(emitter, key);
1306        // A label the analysis didn't pool falls back to the unnamed message.
1307        let label = if ctx.strings.lookup_text(receiver_label).is_some() {
1308            receiver_label
1309        } else {
1310            ""
1311        };
1312        crate::codegen::function_emitter::emit_const_string_by_text(emitter, ctx, label)?;
1313        emitter.instruction(Instruction::Call(helper));
1314        emitter.instruction(Instruction::Unreachable);
1315        if probe.record.is_some() {
1316            emitter.emit_end();
1317        }
1318    }
1319    emitter.emit_end();
1320    emitter.instruction(Instruction::LocalGet(value));
1321    crate::codegen::cast_check::emit_checked_cast_on_stack(emitter, ctx, &Type::Unknown, read_ty)?;
1322    Ok(())
1323}
1324
1325/// Pushes a record read's key again.
1326fn push_record_key(emitter: &mut FunctionEmitter, key: KeyOperand) {
1327    match key {
1328        KeyOperand::Local(local) => emitter.instruction(Instruction::LocalGet(local)),
1329        KeyOperand::Global(global) => emitter.instruction(Instruction::GlobalGet(global)),
1330    }
1331}
1332
1333/// How a record read at a type parameter tells a missing key from a present
1334/// `undefined` value (see `emit_checked_record_read`).
1335#[derive(Clone, Copy)]
1336struct RecordProbe {
1337    /// The record, kept so a `getField` answer of `undefined` can be probed
1338    /// with `hasField`.
1339    record: Option<u32>,
1340    /// The present flag the read sets, for a local holding it.
1341    present_flag: Option<u32>,
1342}
1343
1344/// With a record on the stack about to be read at `read_ty`: keeps the
1345/// record in a local when that type is a type parameter with no room for
1346/// `undefined` of its own, or when the read sets `present_flag`.
1347fn stash_probed_record(
1348    emitter: &mut FunctionEmitter,
1349    ctx: &CodegenCtx,
1350    read_ty: &Type,
1351    present_flag: Option<u32>,
1352) -> Result<RecordProbe, crate::compiler_error::CompilerFailure> {
1353    // A read that sets a present flag probes its key even where its type has
1354    // room for `undefined`.
1355    if !read_ty.has_type_parameter_member() || (read_ty.holds_undefined() && present_flag.is_none())
1356    {
1357        return Ok(RecordProbe {
1358            record: None,
1359            present_flag,
1360        });
1361    }
1362    let record = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
1363    emitter.instruction(Instruction::LocalTee(record));
1364    Ok(RecordProbe {
1365        record: Some(record),
1366        present_flag,
1367    })
1368}
1369
1370fn emit_nullish_coalesce(
1371    emitter: &mut FunctionEmitter,
1372    ctx: &CodegenCtx,
1373    lhs: &ExprId,
1374    rhs: &ExprId,
1375    result_ty: &Type,
1376) -> Result<(), crate::compiler_error::CompilerFailure> {
1377    let result_ty = result_ty.clone();
1378
1379    let result_val = ctx.symbols.value_type(&result_ty)?;
1380    let lhs_ty = ctx
1381        .ta
1382        .try_expr(*lhs)
1383        .map_err(crate::codegen::arena_failure)?
1384        .ty
1385        .clone();
1386    let lhs_val = ctx.symbols.value_type(&lhs_ty)?;
1387    let lhs_is_ref = matches!(lhs_val, ValType::Ref(_));
1388    if lhs_is_ref {
1389        let tmp = emitter.add_anonymous_local(lhs_val)?;
1390        emit_expr(emitter, ctx, *lhs)?;
1391        emitter.instruction(Instruction::LocalTee(tmp));
1392        emit_is_nullish(emitter, ctx)?;
1393        emitter.emit_if(BlockType::Result(result_val));
1394        emit_conditional_operand(emitter, ctx, *rhs, &result_ty)?;
1395        emitter.emit_else();
1396        emitter.instruction(Instruction::LocalGet(tmp));
1397        // The else branch knows the value isn't null; cast
1398        // from `lhs_ty`'s Wasm form to the result slot. For a
1399        // mixed-typed union (`string | null` → `(ref null
1400        // $Object)`) this is a ref-cast down to the result's
1401        // concrete heap type, which traps if the value's
1402        // runtime tag disagrees (it shouldn't — infer
1403        // enforced assignability). `emit_cast_to` handles
1404        // both the `ref.as_non_null` lift and the per-type
1405        // narrowing cast.
1406        crate::codegen::function_emitter::cast::emit_cast_to(emitter, ctx, &result_ty)?;
1407        emitter.emit_end();
1408    } else {
1409        // Non-nullable, primitive-typed lhs — just emit it.
1410        emit_expr(emitter, ctx, *lhs)?;
1411        crate::codegen::function_emitter::cast::emit_coerce_to_slot(
1412            emitter, ctx, &lhs_ty, &result_ty,
1413        )?;
1414    }
1415
1416    Ok(())
1417}
1418
1419/// The Wasm block type of a `?:` or `??` typed `result_ty`: none when the
1420/// expression is `void`, else the result's value slot.
1421fn conditional_block_type(
1422    ctx: &CodegenCtx,
1423    result_ty: &Type,
1424) -> Result<BlockType, crate::compiler_error::CompilerFailure> {
1425    Ok(BlockType::Result(ctx.symbols.value_type(result_ty)?))
1426}
1427
1428/// Emits one operand of a `?:` or `??` typed `result_ty`, leaving what the
1429/// enclosing block expects: the operand coerced to the result slot, or nothing
1430/// when the expression is `void`.
1431fn emit_conditional_operand(
1432    emitter: &mut FunctionEmitter,
1433    ctx: &CodegenCtx,
1434    operand: ExprId,
1435    result_ty: &Type,
1436) -> Result<(), crate::compiler_error::CompilerFailure> {
1437    let operand_ty = ctx
1438        .ta
1439        .try_expr(operand)
1440        .map_err(crate::codegen::arena_failure)?
1441        .ty
1442        .clone();
1443    emit_expr(emitter, ctx, operand)?;
1444    cast::emit_coerce_to_slot(emitter, ctx, &operand_ty, result_ty)
1445}
1446
1447/// Read a refinement that the runtime-value pass proved representation-stable.
1448/// Mutable global, captured, and property reads whose values can outlive their
1449/// refinements have already been replaced with boxed reads. Remaining shadows
1450/// bridge assignment and branch-local narrowing slots to their inferred types.
1451// Keep allocation and member lowering out of the recursive expression dispatcher
1452// so unrelated branches do not enlarge every debug-build recursion frame.
1453fn emit_generic_call(
1454    emitter: &mut FunctionEmitter,
1455    ctx: &CodegenCtx,
1456    result_ty: &Type,
1457    mangled: &crate::MangledName,
1458    type_args: &[Type],
1459    args: &[crate::GenericArgument],
1460    return_cast: &Option<Type>,
1461) -> Result<(), crate::compiler_error::CompilerFailure> {
1462    // Direct call to a user-declared top-level generic
1463    // function. Args whose unsubstituted slot is a bare
1464    // `Type::TypeVar` (`is_generic == true`) box at the
1465    // call boundary; ref-typed values and concrete-typed
1466    // args pass through unchanged. If `return_cast` is
1467    // `Some`, the callee returns `(ref $Object)` and we
1468    // emit `cast::emit_cast_to` to materialize the
1469    // call-site return type.
1470    //
1471    // note: variadic generic callees have their
1472    // trailing args pre-packed by the typechecker into a
1473    // synthesized `ArrayLiteral` filling the rest slot,
1474    // marked `is_generic: false` (the array's outer type is
1475    // concrete). No call-site branching is needed here.
1476    let (wasm_idx, target_params, target_return) = match ctx.symbols.top_level_fn(mangled) {
1477        Some(target) => (target.wasm_idx, target.params.clone(), target.ret.clone()),
1478        None => {
1479            return Err(crate::codegen::internal_failure(
1480                "generic call target is not registered",
1481            ));
1482        }
1483    };
1484    for (i, arg) in args.iter().enumerate() {
1485        emit_expr(emitter, ctx, arg.expr)?;
1486        if arg.is_generic {
1487            let arg_ty = ctx
1488                .ta
1489                .try_expr(arg.expr)
1490                .map_err(crate::codegen::arena_failure)?
1491                .ty
1492                .clone();
1493            crate::codegen::function_emitter::cast::emit_box(emitter, ctx, &arg_ty)?;
1494        } else if let Some(param_ty) = target_params.get(i) {
1495            // A non-generic arg can still land in a wider erased slot
1496            // (`count: number | null`, a `T | null` param): coerce a
1497            // primitive into the ref-typed param exactly like the
1498            // plain-call path. No-op when the Wasm types line up.
1499            let arg_ty = ctx
1500                .ta
1501                .try_expr(arg.expr)
1502                .map_err(crate::codegen::arena_failure)?
1503                .ty
1504                .clone();
1505            cast::emit_coerce_to_slot(emitter, ctx, &arg_ty, param_ty)?;
1506        }
1507    }
1508    if crate::codegen::field_guards::guarded_constructor(ctx, mangled, result_ty)? {
1509        crate::codegen::field_guards::constructor_argument(emitter, ctx, result_ty)?;
1510    }
1511    if ctx.symbols.runtime_generic_functions.contains(mangled) {
1512        crate::codegen::runtime_descriptors::require_direct_schema(ctx, mangled, type_args)?;
1513        crate::codegen::runtime_descriptors::environment(emitter, ctx, type_args)?;
1514    }
1515    emitter.instruction(Instruction::Call(wasm_idx));
1516    if ctx.symbols.resultless_functions.contains(&wasm_idx) {
1517        emit_undefined(emitter, ctx)?;
1518    }
1519    if let Some(ty) = return_cast {
1520        cast::emit_cast_to(emitter, ctx, ty)?;
1521    } else {
1522        cast::emit_coerce_to_slot(emitter, ctx, &target_return, result_ty)?;
1523    }
1524
1525    Ok(())
1526}
1527
1528fn emit_object_literal(
1529    emitter: &mut FunctionEmitter,
1530    ctx: &CodegenCtx,
1531    result_ty: &Type,
1532    members: &[TypedObjectMember],
1533    fields: &[crate::TypedObjectFieldOrigin],
1534) -> Result<(), crate::compiler_error::CompilerFailure> {
1535    if members
1536        .iter()
1537        .any(|member| matches!(member, TypedObjectMember::Computed { .. }))
1538    {
1539        emit_computed_object(emitter, ctx, members)?;
1540        return Ok(());
1541    }
1542    if !matches!(result_ty, Type::Object { .. } | Type::InterfaceRef { .. }) {
1543        return Err(crate::codegen::internal_failure(
1544            "object literal requires an object representation",
1545        ));
1546    }
1547    let structural_ty = crate::typed_ast::object_literal_layout(result_ty, fields);
1548    if members
1549        .iter()
1550        .any(|member| matches!(member, TypedObjectMember::Spread { .. }))
1551    {
1552        emit_object_spread(emitter, ctx, members, &structural_ty)?;
1553        return Ok(());
1554    }
1555    let vtable_idx = ctx
1556        .symbols
1557        .vtable_global_idx(&structural_ty)
1558        .ok_or_else(|| {
1559            crate::codegen::internal_failure("vtable global recorded during object-emission pass")
1560        })?;
1561    let object_shape_idx = ctx
1562        .symbols
1563        .object_subtype_idx(&structural_ty)
1564        .ok_or_else(|| {
1565            crate::codegen::internal_failure(
1566                "object shape type recorded during object-emission pass",
1567            )
1568        })?;
1569    let Type::Object {
1570        fields: declared_fields,
1571        ..
1572    } = &structural_ty
1573    else {
1574        return Err(crate::codegen::internal_failure(
1575            "object literal has no structural representation",
1576        ));
1577    };
1578    let shape_key: Vec<_> = declared_fields
1579        .iter()
1580        .map(|(name, field)| crate::codegen::field_names::FieldName {
1581            name: name.clone(),
1582            optional: field.optional,
1583            is_accessor: false,
1584            is_method: false,
1585            is_private: false,
1586        })
1587        .collect();
1588    let intrinsics = ctx
1589        .symbols
1590        .intrinsic_type_indices()
1591        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
1592
1593    // Plain literals still evaluate overwritten values in source order.
1594    let evaluated = evaluate_object_members(emitter, ctx, members)?;
1595
1596    emitter.instruction(Instruction::GlobalGet(vtable_idx));
1597    crate::codegen::field_names::emit_instance_names(emitter, ctx, &shape_key, |name| {
1598        fields.iter().any(|field| {
1599            field.name.name == name && !matches!(field.source, TypedObjectFieldSource::Absent(_))
1600        })
1601    })?;
1602
1603    // Walk the declared shape's field set in BTreeMap order
1604    // and emit one value per slot. The typed-AST `fields`
1605    // list is already in the same BTreeMap order — pair them
1606    // by name to stay robust against ordering drift.
1607    let by_name: std::collections::BTreeMap<&str, &crate::TypedObjectFieldOrigin> =
1608        fields.iter().map(|f| (f.name.name.as_str(), f)).collect();
1609    for (name, field) in declared_fields {
1610        if let Some(origin) = by_name.get(name.as_str()) {
1611            let (TypedObjectFieldSource::Literal(value) | TypedObjectFieldSource::Absent(value)) =
1612                &origin.source
1613            else {
1614                return Err(crate::codegen::internal_failure(
1615                    "spread field reached plain object lowering",
1616                ));
1617            };
1618            emitter.instruction(Instruction::LocalGet(*evaluated.get(value).ok_or_else(
1619                || crate::codegen::internal_failure("object field value was not evaluated"),
1620            )?));
1621        } else {
1622            if !field.optional {
1623                return Err(crate::codegen::internal_failure(
1624                    "required object field is missing from the literal",
1625                ));
1626            }
1627            emit_undefined(emitter, ctx)?;
1628        }
1629    }
1630    emitter.instruction(Instruction::ArrayNewFixed {
1631        array_type_index: intrinsics.object_fields,
1632        array_size: crate::codegen::wasm_u32(declared_fields.len())?,
1633    });
1634    emitter.instruction(Instruction::RefNull(HeapType::ANY));
1635    emitter.instruction(Instruction::StructNew(object_shape_idx));
1636
1637    Ok(())
1638}
1639
1640fn emit_field_access(
1641    emitter: &mut FunctionEmitter,
1642    ctx: &CodegenCtx,
1643    id: ExprId,
1644    result_ty: &Type,
1645    receiver: &ExprId,
1646    name: &Ident,
1647) -> Result<(), crate::compiler_error::CompilerFailure> {
1648    let receiver_ty = ctx
1649        .ta
1650        .source_type(*receiver)
1651        .map_err(crate::codegen::arena_failure)?
1652        .clone();
1653    if ctx.ta.has_string_index(&receiver_ty) {
1654        // See `emit_index_access`.
1655        let present_flag = take_present_flag(emitter, id);
1656        emit_expr(emitter, ctx, *receiver)?;
1657        cast::emit_box(
1658            emitter,
1659            ctx,
1660            &ctx.ta
1661                .try_expr(*receiver)
1662                .map_err(crate::codegen::arena_failure)?
1663                .ty,
1664        )?;
1665        let checked_ty = ctx
1666            .ta
1667            .source_type(id)
1668            .map_err(crate::codegen::arena_failure)?;
1669        let probe = stash_probed_record(emitter, ctx, checked_ty, present_flag)?;
1670        let Some(symbol) = ctx.require(
1671            ctx.symbols.field_name_string_global_idx(&name.name),
1672            "property name collected",
1673        ) else {
1674            return Ok(());
1675        };
1676        emitter.instruction(Instruction::GlobalGet(symbol));
1677        let key = KeyOperand::Global(symbol);
1678        let Some(symbol) = ctx.require(
1679            ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
1680            "record read imported",
1681        ) else {
1682            return Ok(());
1683        };
1684        emitter.instruction(Instruction::Call(symbol));
1685        emit_checked_record_read(
1686            emitter,
1687            ctx,
1688            checked_ty,
1689            key,
1690            probe,
1691            &receiver_name(ctx.ta, *receiver)?,
1692        )?;
1693        cast::emit_coerce_to_slot(emitter, ctx, checked_ty, result_ty)?;
1694        return Ok(());
1695    }
1696    // Class instance: nominal receiver, array-backed object payload.
1697    if let Type::ClassRef { mangled, .. } = receiver_ty.lowered() {
1698        if ctx.symbols.is_class_payload_method(mangled, &name.name) {
1699            emit_receiver(emitter, ctx, *receiver)?;
1700            emit_class_method_value(
1701                emitter,
1702                ctx,
1703                &receiver_ty,
1704                &name.name,
1705                result_ty,
1706                ctx.ta
1707                    .source_type(id)
1708                    .map_err(crate::codegen::arena_failure)?,
1709            )?;
1710            return Ok(());
1711        }
1712        // Accessor property (no data slot): dispatch the synthetic getter.
1713        if ctx.symbols.class_field_slot(mangled, &name.name).is_none() {
1714            let getter = crate::codegen::classes::accessor_getter_name(&name.name);
1715            emit_method_call(
1716                emitter,
1717                ctx,
1718                *receiver,
1719                mangled,
1720                &getter,
1721                &[],
1722                None,
1723                &[],
1724                None,
1725                &ctx.ta
1726                    .try_expr(id)
1727                    .map_err(crate::codegen::arena_failure)?
1728                    .ty,
1729            )?;
1730            return Ok(());
1731        }
1732        let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
1733            crate::codegen::internal_failure("class struct type recorded in classes::emit")
1734        })?;
1735        let slot = ctx
1736            .symbols
1737            .class_field_slot(mangled, &name.name)
1738            .ok_or_else(|| {
1739                crate::codegen::internal_failure("class field slot recorded in classes::emit")
1740            })?;
1741        let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
1742            crate::codegen::internal_failure("intrinsics declared by codegen entry")
1743        })?;
1744        emit_receiver(emitter, ctx, *receiver)?;
1745        let object = emitter.add_anonymous_local(ctx.symbols.value_type(&receiver_ty)?)?;
1746        emitter.instruction(Instruction::LocalTee(object));
1747        emitter.instruction(Instruction::StructGet {
1748            struct_type_index: struct_idx,
1749            field_index: 2,
1750        });
1751        emitter.instruction(Instruction::I32Const(slot as i32));
1752        emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
1753        if ctx
1754            .symbols
1755            .class_field_narrowing_check(mangled, &name.name)
1756            .is_some()
1757        {
1758            crate::codegen::field_guards::check(emitter, ctx, object, mangled, &name.name)?;
1759        }
1760        emit_class_field_slot_cast_as(
1761            emitter,
1762            ctx,
1763            mangled,
1764            &name.name,
1765            &ctx.ta
1766                .try_expr(id)
1767                .map_err(crate::codegen::arena_failure)?
1768                .ty,
1769            ctx.ta
1770                .source_type(id)
1771                .map_err(crate::codegen::arena_failure)?,
1772        )?;
1773        return Ok(());
1774    }
1775    let intrinsics = ctx
1776        .symbols
1777        .intrinsic_type_indices()
1778        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
1779    // Use the expression's physical type so a field read preserves
1780    // unexpected values after its source refinement becomes stale.
1781    let field_ty = ctx
1782        .ta
1783        .try_expr(id)
1784        .map_err(crate::codegen::arena_failure)?
1785        .ty
1786        .clone();
1787    // Stashed so we can re-push it (as the call's self argument) and
1788    // then read its slot 2 to get the getter funcref.
1789    emit_receiver(emitter, ctx, *receiver)?;
1790    let rcv_local = stash_receiver_as_object_shape(emitter, &receiver_ty, intrinsics.object_shape)?;
1791    emit_object_property_read_as(
1792        emitter,
1793        ctx,
1794        rcv_local,
1795        &name.name,
1796        &field_ty,
1797        ctx.ta
1798            .source_type(id)
1799            .map_err(crate::codegen::arena_failure)?,
1800    )?;
1801
1802    Ok(())
1803}
1804
1805fn emit_closure_value(
1806    emitter: &mut FunctionEmitter,
1807    ctx: &CodegenCtx,
1808    id: ExprId,
1809    result_ty: &Type,
1810    params: &[crate::TypedParam],
1811    captured: &[crate::CapturedVar],
1812    runtime_generics: &[String],
1813) -> Result<(), crate::compiler_error::CompilerFailure> {
1814    // emit the closure value at the creation site.
1815    // `$closure_<sig>` is `(sub $Object (struct (ref $VTable)
1816    // (ref $fn_<sig>) (ref any)))` — three fields, consumed
1817    // by `struct.new` in source order:
1818    //
1819    //   global.get $closure_vtable   ;; field 0: (ref $VTable)
1820    //   ref.func <closure_body>      ;; field 1: (ref $fn_<sig>)
1821    //   <captured-value loads>       ;; → env-field types
1822    //   struct.new $env_N            ;; field 2: (ref $env_N) →
1823    //                                 ;;   subtypes (ref any)
1824    //   struct.new $closure_<sig>
1825    //
1826    // Per-signature `$closure_<sig>` is registered from the
1827    // closure expression's `result_ty` (a `Type::Function`);
1828    // per-arrow `$env_N` and the body's func index are keyed
1829    // on `id` (the closure's `ExprId`). The closure vtable
1830    // global is shared across every closure.
1831    let closure_struct_idx = ctx
1832        .symbols
1833        .closure_struct_type_idx(crate::codegen::closures::classify(result_ty)?)
1834        .ok_or_else(|| {
1835            crate::codegen::internal_failure(
1836                "closure struct type registered for every closure-typed expression",
1837            )
1838        })?;
1839    let env_type_idx = ctx.symbols.env_type_idx(id).ok_or_else(|| {
1840        crate::codegen::internal_failure("env type registered for every closure expression")
1841    })?;
1842    let closure_func_idx = ctx
1843        .symbols
1844        .closure_func_idx(id)
1845        .ok_or_else(|| crate::codegen::internal_failure("closure body function index allocated"))?;
1846    let closure_vtable_idx = ctx.symbols.closure_vtable_global_idx().ok_or_else(|| {
1847        crate::codegen::internal_failure("closure vtable global emitted whenever closures exist")
1848    })?;
1849
1850    // Field 0: shared closure vtable (inherited from $Object).
1851    emitter.instruction(Instruction::GlobalGet(closure_vtable_idx));
1852
1853    // Field 1: typed funcref to the closure body.
1854    emitter.instruction(Instruction::RefFunc(closure_func_idx));
1855
1856    // Field 2: env. Build it inline by pushing each captured
1857    // value in order, then `struct.new`.
1858    for c in captured {
1859        emit_captured_load(emitter, ctx, c)?;
1860    }
1861    if !runtime_generics.is_empty() {
1862        let types: Vec<_> = runtime_generics
1863            .iter()
1864            .cloned()
1865            .map(Type::TypeVar)
1866            .collect();
1867        crate::codegen::runtime_descriptors::environment(emitter, ctx, &types)?;
1868    }
1869    let self_environment = if ctx.ta.closure_names.contains_key(&id) {
1870        emitter.instruction(Instruction::RefNull(HeapType::Concrete(
1871            ctx.symbols
1872                .intrinsic_type_indices()
1873                .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?
1874                .object,
1875        )));
1876        Some(emitter.add_anonymous_local(ValType::Ref(RefType {
1877            nullable: false,
1878            heap_type: HeapType::Concrete(env_type_idx),
1879        }))?)
1880    } else {
1881        None
1882    };
1883    emitter.instruction(Instruction::StructNew(env_type_idx));
1884    if let Some(local) = self_environment {
1885        emitter.instruction(Instruction::LocalTee(local));
1886    }
1887    if let Some(metadata) = crate::codegen::call_arguments::typed_metadata(params) {
1888        crate::codegen::call_arguments::wrap(emitter, ctx, &metadata)?;
1889    }
1890
1891    if ctx.ta.closure_this.contains_key(&id) {
1892        crate::codegen::this_binding::wrap(emitter, ctx)?;
1893    }
1894    // Stack: vtable, funcref, env — `(ref $env_N)` subtypes
1895    // `(ref any)` so the env flows into field 2 implicitly.
1896    // Closure struct allocation consumes all four fields.
1897    emitter.instruction(Instruction::I64Const(0));
1898    emitter.instruction(Instruction::StructNew(closure_struct_idx));
1899    if let Some(environment) = self_environment {
1900        let closure = emitter.add_anonymous_local(ctx.symbols.value_type(result_ty)?)?;
1901        emitter.instruction(Instruction::LocalSet(closure));
1902        emitter.instruction(Instruction::LocalGet(environment));
1903        emitter.instruction(Instruction::LocalGet(closure));
1904        emitter.instruction(Instruction::StructSet {
1905            struct_type_index: env_type_idx,
1906            field_index: crate::codegen::wasm_u32(
1907                captured
1908                    .len()
1909                    .checked_add(usize::from(!runtime_generics.is_empty()))
1910                    .ok_or_else(|| {
1911                        crate::codegen::internal_failure("closure capture count overflow")
1912                    })?,
1913            )?,
1914        });
1915        emitter.instruction(Instruction::LocalGet(closure));
1916    }
1917
1918    Ok(())
1919}
1920
1921/// Whether inference typed `id` as `never`. Runtime-value lowering may widen
1922/// an expression's type afterwards, so `expr.ty` alone can't tell.
1923fn inferred_never(
1924    ctx: &CodegenCtx,
1925    id: ExprId,
1926) -> Result<bool, crate::compiler_error::CompilerFailure> {
1927    let ty = ctx
1928        .ta
1929        .source_type(id)
1930        .map_err(crate::codegen::arena_failure)?;
1931    Ok(matches!(ty, Type::Never))
1932}
1933
1934/// Emits `value`, stored next in a local holding a maybe-missing read, and
1935/// with `present_flag`, that local's present flag, sets the flag. A value the
1936/// typechecker let hold a missing read (`widened_missing_reads`) sets it as
1937/// that read finds: an array or record read reports it, another local's flag
1938/// is copied, and anything else is present unless it is `undefined`. Any
1939/// other value is present, even an `undefined` one.
1940pub(super) fn emit_value_with_present_flag(
1941    emitter: &mut FunctionEmitter,
1942    ctx: &CodegenCtx,
1943    value: ExprId,
1944    present_flag: Option<u32>,
1945) -> Result<(), crate::compiler_error::CompilerFailure> {
1946    let Some(flag) = present_flag else {
1947        return emit_expr(emitter, ctx, value);
1948    };
1949    let expr = ctx
1950        .ta
1951        .try_expr(value)
1952        .map_err(crate::codegen::arena_failure)?;
1953    if !ctx.ta.widened_missing_reads.contains_key(&value) {
1954        emit_expr(emitter, ctx, value)?;
1955        set_present_flag(emitter, flag, true);
1956        return Ok(());
1957    }
1958    match &expr.kind {
1959        TypedExprKind::LocalRef { ident, .. } => {
1960            let source = emitter
1961                .local_slot(&ident.name)
1962                .and_then(|slot| emitter.present_flag(slot));
1963            emit_expr(emitter, ctx, value)?;
1964            match source {
1965                Some(source) => {
1966                    emitter.instruction(Instruction::LocalGet(source));
1967                    emitter.instruction(Instruction::LocalSet(flag));
1968                }
1969                // A local without a flag has a type with no room for a
1970                // present `undefined`.
1971                None => set_present_flag_from_value(emitter, ctx, flag, &expr.ty)?,
1972            }
1973        }
1974        TypedExprKind::IndexAccess { .. }
1975        | TypedExprKind::FieldAccess { .. }
1976        | TypedExprKind::OptionalChain { .. } => {
1977            emitter.present_flag_request = Some((value, flag));
1978            emit_expr(emitter, ctx, value)?;
1979            // No read known to reach here leaves the request (string and
1980            // `Uint8Array` reads take it); if one did, its value would tell,
1981            // as for a local without a flag.
1982            if emitter.present_flag_request.take().is_some() {
1983                set_present_flag_from_value(emitter, ctx, flag, &expr.ty)?;
1984            }
1985        }
1986        _ => {
1987            emit_expr(emitter, ctx, value)?;
1988            set_present_flag_from_value(emitter, ctx, flag, &expr.ty)?;
1989        }
1990    }
1991    Ok(())
1992}
1993
1994/// The present flag `emit_value_with_present_flag` asked the read `id` to
1995/// set, taken so no read nested inside it sets it.
1996fn take_present_flag(emitter: &mut FunctionEmitter, id: ExprId) -> Option<u32> {
1997    match emitter.present_flag_request {
1998        Some((owner, flag)) if owner == id => {
1999            emitter.present_flag_request = None;
2000            Some(flag)
2001        }
2002        _ => None,
2003    }
2004}
2005
2006fn set_present_flag(emitter: &mut FunctionEmitter, flag: u32, found: bool) {
2007    emitter.instruction(Instruction::I32Const(i32::from(found)));
2008    emitter.instruction(Instruction::LocalSet(flag));
2009}
2010
2011/// With a value of type `ty` on the stack, whose type has no room for a
2012/// present `undefined`: sets `flag` to whether the value isn't `undefined`.
2013fn set_present_flag_from_value(
2014    emitter: &mut FunctionEmitter,
2015    ctx: &CodegenCtx,
2016    flag: u32,
2017    ty: &Type,
2018) -> Result<(), crate::compiler_error::CompilerFailure> {
2019    let value_ty = ctx.symbols.value_type(ty)?;
2020    if !matches!(value_ty, ValType::Ref(_)) {
2021        set_present_flag(emitter, flag, true);
2022        return Ok(());
2023    }
2024    let value = emitter.add_anonymous_local(value_ty)?;
2025    emitter.instruction(Instruction::LocalTee(value));
2026    emit_is_undefined(emitter, ctx)?;
2027    emitter.instruction(Instruction::I32Eqz);
2028    emitter.instruction(Instruction::LocalSet(flag));
2029    emitter.instruction(Instruction::LocalGet(value));
2030    Ok(())
2031}
2032
2033/// Whether a narrowing's `source` reads a local that may hold a missing read.
2034/// Such a narrowing takes no shadow: a narrowing never proves the value
2035/// present, so the typechecker reads the local itself at the narrowed type,
2036/// checking it, and a narrowed read with no shadow falls back to the local
2037/// (`emit_local_narrow_ref`).
2038pub(super) fn reads_maybe_missing_local(
2039    emitter: &FunctionEmitter,
2040    ctx: &CodegenCtx,
2041    source: ExprId,
2042) -> Result<bool, crate::compiler_error::CompilerFailure> {
2043    let expr = ctx
2044        .ta
2045        .try_expr(source)
2046        .map_err(crate::codegen::arena_failure)?;
2047    let TypedExprKind::LocalRef { ident, .. } = &expr.kind else {
2048        return Ok(false);
2049    };
2050    Ok(emitter
2051        .local_slot(&ident.name)
2052        .is_some_and(|slot| emitter.maybe_missing_slots.contains_key(&slot)))
2053}
2054
2055/// With the value of the local in `slot` on the stack as a `stack`: when the
2056/// local holds a read that may have found no value, throws
2057/// `MissingValueError` with its message if it found none, by the local's
2058/// present flag when it has one, else by the value being `undefined`. Leaves
2059/// the value on the stack, and emits nothing for any other local.
2060fn emit_missing_local_check(
2061    emitter: &mut FunctionEmitter,
2062    ctx: &CodegenCtx,
2063    slot: u32,
2064    stack: ValType,
2065) -> Result<(), crate::compiler_error::CompilerFailure> {
2066    let Some((message, present_flag)) = emitter
2067        .maybe_missing_slots
2068        .get(&slot)
2069        .map(|local| (local.message.clone(), local.present_flag))
2070    else {
2071        return Ok(());
2072    };
2073    // A local typed with a type parameter keeps a present flag: its value
2074    // may be a present `undefined`.
2075    if let Some(flag) = present_flag {
2076        emitter.instruction(Instruction::LocalGet(flag));
2077        emitter.instruction(Instruction::I32Eqz);
2078        emitter.emit_if(BlockType::Empty);
2079        crate::codegen::throw::emit_missing_value_error_throw(emitter, ctx, &message);
2080        emitter.emit_end();
2081        return Ok(());
2082    }
2083    let value = emitter.add_anonymous_local(stack)?;
2084    emitter.instruction(Instruction::LocalTee(value));
2085    emit_is_undefined(emitter, ctx)?;
2086    emitter.emit_if(BlockType::Empty);
2087    crate::codegen::throw::emit_missing_value_error_throw(emitter, ctx, &message);
2088    emitter.emit_end();
2089    emitter.instruction(Instruction::LocalGet(value));
2090    Ok(())
2091}
2092
2093fn emit_local_narrow_ref(
2094    emitter: &mut FunctionEmitter<'_>,
2095    ctx: &CodegenCtx<'_>,
2096    binding: &Ident,
2097    path: &ReferencePath,
2098    narrowed_ty: &Type,
2099) -> Result<(), crate::compiler_error::CompilerFailure> {
2100    if !path.chain.is_empty()
2101        && let Some(source) = emitter.narrow_source(&binding.name)
2102    {
2103        // Calls preserve this typecheck-time fact but may mutate the referenced
2104        // slot, so re-read and validate every use instead of caching a snapshot.
2105        emit_expr(emitter, ctx, source)?;
2106        crate::codegen::cast_check::emit_checked_cast_on_stack(
2107            emitter,
2108            ctx,
2109            &ctx.ta
2110                .try_expr(source)
2111                .map_err(crate::codegen::arena_failure)?
2112                .ty,
2113            narrowed_ty,
2114        )?;
2115        return Ok(());
2116    }
2117    let Some((slot, slot_ty)) = emitter.narrowed_read_slot(&binding.name) else {
2118        // A rematerialized root view can outlive the synthetic shadow named by
2119        // its source (for example, an identifier narrowing carried into a
2120        // nested block across a call). Read the real binding that the path
2121        // identifies instead of recursively trying to emit the closed shadow.
2122        if let BindingId::Local { name, .. } = &path.root
2123            && let Some((slot, slot_ty)) = emitter.narrowed_read_slot(name)
2124        {
2125            emitter.instruction(Instruction::LocalGet(slot));
2126            let stack_ty = unbox_if_boxed(emitter, ctx, slot_ty)?;
2127            // The binding may hold a read that found no value; a narrowing
2128            // never proves one present (see `missing_reads`).
2129            emit_missing_local_check(emitter, ctx, slot, stack_ty)?;
2130            if stack_ty != ctx.symbols.value_type(narrowed_ty)? {
2131                cast::emit_cast_to(emitter, ctx, narrowed_ty)?;
2132            }
2133            return Ok(());
2134        }
2135        if let BindingId::Global(mangled) = &path.root {
2136            let idx = ctx.symbols.global_idx(mangled).ok_or_else(|| {
2137                crate::codegen::internal_failure("Inferer guarantees the binding exists")
2138            })?;
2139            emitter.instruction(Instruction::GlobalGet(idx));
2140            let source_ty = ctx.symbols.global_type(mangled).ok_or_else(|| {
2141                crate::codegen::internal_failure("language globals retain their declared type")
2142            })?;
2143            crate::codegen::cast_check::emit_checked_cast_on_stack(
2144                emitter,
2145                ctx,
2146                source_ty,
2147                narrowed_ty,
2148            )?;
2149            return Ok(());
2150        }
2151        if let Some(source) = emitter.narrow_source(&binding.name) {
2152            emit_expr(emitter, ctx, source)?;
2153            let source_ty = &ctx
2154                .ta
2155                .try_expr(source)
2156                .map_err(crate::codegen::arena_failure)?
2157                .ty;
2158            let cast_info = cast_info_for(source_ty.clone(), narrowed_ty.clone());
2159            cast::emit_narrowing_cast(emitter, ctx, &cast_info)?;
2160            return Ok(());
2161        }
2162        return Err(crate::codegen::internal_failure(format!(
2163            "narrow binding `{}` (path {path:?}) not registered in codegen scope — \
2164             the region's source names a shadow from a scope that already closed",
2165            binding.name,
2166        )));
2167    };
2168    emitter.instruction(Instruction::LocalGet(slot));
2169    let stack_ty = unbox_if_boxed(emitter, ctx, slot_ty)?;
2170    let _: () = if stack_ty != ctx.symbols.value_type(narrowed_ty)? {
2171        cast::emit_cast_to(emitter, ctx, narrowed_ty)?;
2172    };
2173    Ok(())
2174}
2175
2176/// If `slot_ty` is a `(ref $box)` over a registered box struct, emit the
2177/// `struct.get` that reads its payload; return the type now on the stack.
2178///
2179/// Post-`if` join narrowing rebinds to the *original* ident. When that binding
2180/// is a captured-and-mutated `let` (e.g. a nullable `let` assigned inside a
2181/// `for` body — the loop body is a capture frame), its slot holds the
2182/// `(ref $box)`, not the value, and a cast applied to the box would trap.
2183fn unbox_if_boxed(
2184    emitter: &mut FunctionEmitter<'_>,
2185    ctx: &CodegenCtx<'_>,
2186    slot_ty: ValType,
2187) -> Result<ValType, crate::compiler_error::CompilerFailure> {
2188    let ValType::Ref(RefType {
2189        heap_type: HeapType::Concrete(idx),
2190        ..
2191    }) = slot_ty
2192    else {
2193        return Ok(slot_ty);
2194    };
2195    let Some(payload) = ctx.symbols.box_payload_type(idx) else {
2196        return Ok(slot_ty);
2197    };
2198    let local = emitter.add_anonymous_local(slot_ty)?;
2199    emitter.instruction(Instruction::LocalSet(local));
2200    super::binding_cells::read(emitter, ctx, local, idx)?;
2201    Ok(payload)
2202}
2203
2204// Keep literal construction outside the recursive expression dispatcher's
2205// debug stack frame, including its fallible lookup/conversion temporaries.
2206fn emit_array_literal(
2207    emitter: &mut FunctionEmitter,
2208    ctx: &CodegenCtx,
2209    elements: &[crate::TypedArrayElement],
2210) -> Result<(), crate::compiler_error::CompilerFailure> {
2211    // two codegen paths depending on whether any
2212    // element is a spread. With no spreads, the fast path
2213    // mirrors the original recipe:
2214    //   global.get $array_vtable
2215    //   <each element pushed + emit_box(elem.ty)>
2216    //   array.new_fixed $rawArray N
2217    //   struct.new $Array
2218    // With spreads, mirror the `Array#concat` two-pass
2219    // shape inline: measure total length across fixed + spread
2220    // sources, allocate via `array.new_default`, then walk in
2221    // order copying spread chunks with `array.copy` and
2222    // setting fixed elements with `array.set`.
2223    //
2224    // Box per *each element's actual type*, not the container's
2225    // `element_ty`. When the literal flows into a generic-arg
2226    // position with hint `T[]`, `element_ty` is `Type::Var("T")`
2227    // (the unresolved hint propagated through
2228    // `infer_array_literal`); each element is still a concrete
2229    // primitive on the stack — boxing has to happen by the
2230    // actual type or the slot type mismatch trips the validator.
2231    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
2232        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
2233    })?;
2234    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
2235        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
2236    })?;
2237    let array_vtable_global = ctx
2238        .symbols
2239        .prelude_global_idx("array_vtable")
2240        .ok_or_else(|| crate::codegen::internal_failure("array_vtable imported from prelude"))?;
2241
2242    let has_spread = elements
2243        .iter()
2244        .any(|e| matches!(e, crate::TypedArrayElement::Spread(_)));
2245
2246    if has_spread {
2247        emit_spread_array_literal(emitter, ctx, elements)?;
2248    } else {
2249        emitter.instruction(Instruction::GlobalGet(array_vtable_global));
2250        for el in elements {
2251            let elem_id = el.expr_id();
2252            emit_expr(emitter, ctx, elem_id)?;
2253            let elem_ty = ctx
2254                .ta
2255                .try_expr(elem_id)
2256                .map_err(crate::codegen::arena_failure)?
2257                .ty
2258                .clone();
2259            crate::codegen::function_emitter::cast::emit_box(emitter, ctx, &elem_ty)?;
2260        }
2261        emitter.instruction(Instruction::ArrayNewFixed {
2262            array_type_index: raw_array_idx,
2263            array_size: crate::codegen::wasm_u32(elements.len())?,
2264        });
2265        emitter.instruction(Instruction::I32Const(
2266            i32::try_from(elements.len())
2267                .map_err(|_| crate::codegen::internal_failure("array literal too large"))?,
2268        ));
2269        emitter.instruction(Instruction::StructNew(array_idx));
2270    }
2271    Ok(())
2272}
2273
2274fn emit_tuple_literal(
2275    emitter: &mut FunctionEmitter,
2276    ctx: &CodegenCtx,
2277    elements: &[ExprId],
2278) -> Result<(), crate::compiler_error::CompilerFailure> {
2279    // Tuple storage is erased, so box the value actually on the stack.
2280    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
2281        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
2282    })?;
2283    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
2284        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
2285    })?;
2286    let array_vtable_global = ctx
2287        .symbols
2288        .prelude_global_idx("array_vtable")
2289        .ok_or_else(|| crate::codegen::internal_failure("array_vtable imported from prelude"))?;
2290    emitter.instruction(Instruction::GlobalGet(array_vtable_global));
2291    for &elem_id in elements {
2292        emit_expr(emitter, ctx, elem_id)?;
2293        crate::codegen::function_emitter::cast::emit_box(
2294            emitter,
2295            ctx,
2296            &ctx.ta
2297                .try_expr(elem_id)
2298                .map_err(crate::codegen::arena_failure)?
2299                .ty,
2300        )?;
2301    }
2302    emitter.instruction(Instruction::ArrayNewFixed {
2303        array_type_index: raw_array_idx,
2304        array_size: crate::codegen::wasm_u32(elements.len())?,
2305    });
2306    emitter.instruction(Instruction::I32Const(
2307        i32::try_from(elements.len())
2308            .map_err(|_| crate::codegen::internal_failure("array literal too large"))?,
2309    ));
2310    emitter.instruction(Instruction::StructNew(array_idx));
2311    Ok(())
2312}
2313
2314fn emit_spread_array_literal(
2315    emitter: &mut FunctionEmitter,
2316    ctx: &CodegenCtx,
2317    elements: &[crate::TypedArrayElement],
2318) -> Result<(), crate::compiler_error::CompilerFailure> {
2319    let array = ctx
2320        .symbols
2321        .array_type_idx()
2322        .ok_or_else(|| crate::codegen::internal_failure("Array declared"))?;
2323    let raw = ctx
2324        .symbols
2325        .raw_array_type_idx()
2326        .ok_or_else(|| crate::codegen::internal_failure("raw Array declared"))?;
2327    let raw_type = ValType::Ref(RefType {
2328        nullable: false,
2329        heap_type: HeapType::Concrete(raw),
2330    });
2331    let total = emitter.add_anonymous_local(ValType::I32)?;
2332    let offset = emitter.add_anonymous_local(ValType::I32)?;
2333    let destination = emitter.add_anonymous_local(raw_type)?;
2334    emitter.instruction(Instruction::I32Const(0));
2335    emitter.instruction(Instruction::LocalSet(total));
2336    // Snapshot every chunk in source order before later expressions can mutate
2337    // a spread's backing storage. A plain element is a one-element chunk.
2338    let mut chunks = Vec::with_capacity(elements.len());
2339    for element in elements {
2340        emit_literal_chunk(emitter, ctx, element, array, raw)?;
2341        let chunk = emitter.add_anonymous_local(raw_type)?;
2342        emitter.instruction(Instruction::LocalTee(chunk));
2343        emitter.instruction(Instruction::ArrayLen);
2344        emitter.instruction(Instruction::LocalGet(total));
2345        emitter.instruction(Instruction::I32Add);
2346        emitter.instruction(Instruction::LocalSet(total));
2347        chunks.push(chunk);
2348    }
2349    emitter.instruction(Instruction::LocalGet(total));
2350    emitter.instruction(Instruction::ArrayNewDefault(raw));
2351    emitter.instruction(Instruction::LocalSet(destination));
2352    emitter.instruction(Instruction::I32Const(0));
2353    emitter.instruction(Instruction::LocalSet(offset));
2354    for chunk in chunks {
2355        emitter.instruction(Instruction::LocalGet(destination));
2356        emitter.instruction(Instruction::LocalGet(offset));
2357        emitter.instruction(Instruction::LocalGet(chunk));
2358        emitter.instruction(Instruction::I32Const(0));
2359        emitter.instruction(Instruction::LocalGet(chunk));
2360        emitter.instruction(Instruction::ArrayLen);
2361        emitter.instruction(Instruction::ArrayCopy {
2362            array_type_index_dst: raw,
2363            array_type_index_src: raw,
2364        });
2365        emitter.instruction(Instruction::LocalGet(offset));
2366        emitter.instruction(Instruction::LocalGet(chunk));
2367        emitter.instruction(Instruction::ArrayLen);
2368        emitter.instruction(Instruction::I32Add);
2369        emitter.instruction(Instruction::LocalSet(offset));
2370    }
2371    emitter.instruction(Instruction::GlobalGet(
2372        ctx.symbols
2373            .prelude_global_idx("array_vtable")
2374            .ok_or_else(|| crate::codegen::internal_failure("array vtable declared"))?,
2375    ));
2376    emitter.instruction(Instruction::LocalGet(destination));
2377    emitter.instruction(Instruction::LocalGet(total));
2378    emitter.instruction(Instruction::StructNew(array));
2379    Ok(())
2380}
2381
2382fn emit_literal_chunk(
2383    emitter: &mut FunctionEmitter,
2384    ctx: &CodegenCtx,
2385    element: &crate::TypedArrayElement,
2386    array: u32,
2387    raw: u32,
2388) -> Result<(), crate::compiler_error::CompilerFailure> {
2389    if let crate::TypedArrayElement::Value(value) = element {
2390        emit_expr(emitter, ctx, *value)?;
2391        cast::emit_box(
2392            emitter,
2393            ctx,
2394            &ctx.ta
2395                .try_expr(*value)
2396                .map_err(crate::codegen::arena_failure)?
2397                .ty,
2398        )?;
2399        emitter.instruction(Instruction::ArrayNewFixed {
2400            array_type_index: raw,
2401            array_size: 1,
2402        });
2403        return Ok(());
2404    }
2405    // A source `runtime_values` widened may no longer hold the array its
2406    // narrowed type said; `emit_receiver` checks it as a field read's receiver.
2407    emit_receiver(emitter, ctx, element.expr_id())?;
2408    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(array)));
2409
2410    let raw_type = ValType::Ref(RefType {
2411        nullable: false,
2412        heap_type: HeapType::Concrete(raw),
2413    });
2414    let source = emitter.add_anonymous_local(raw_type)?;
2415    let snapshot = emitter.add_anonymous_local(raw_type)?;
2416    let length = stash_array_length(emitter, array)?;
2417    emitter.instruction(Instruction::StructGet {
2418        struct_type_index: array,
2419        field_index: 1,
2420    });
2421    emitter.instruction(Instruction::LocalSet(source));
2422    emitter.instruction(Instruction::LocalGet(length));
2423    emitter.instruction(Instruction::ArrayNewDefault(raw));
2424    emitter.instruction(Instruction::LocalTee(snapshot));
2425    emitter.instruction(Instruction::I32Const(0));
2426    emitter.instruction(Instruction::LocalGet(source));
2427    emitter.instruction(Instruction::I32Const(0));
2428    emitter.instruction(Instruction::LocalGet(length));
2429    emitter.instruction(Instruction::ArrayCopy {
2430        array_type_index_dst: raw,
2431        array_type_index_src: raw,
2432    });
2433    emitter.instruction(Instruction::LocalGet(snapshot));
2434    Ok(())
2435}
2436
2437/// Read the original value, write its increment/decrement, and return the
2438/// original. Binding operands are read at the narrowed result type but written
2439/// back through their declared slot, including nullable and captured bindings.
2440fn emit_postfix_unary(
2441    emitter: &mut FunctionEmitter<'_>,
2442    ctx: &CodegenCtx<'_>,
2443    op: crate::PostfixOp,
2444    target: &crate::PostfixTarget,
2445    result_ty: &Type,
2446) -> Result<(), crate::compiler_error::CompilerFailure> {
2447    let _: () = match target {
2448        crate::PostfixTarget::Local {
2449            ident,
2450            boxed,
2451            target_ty,
2452        } => {
2453            // A read-modify-write, so the write decides the slot.
2454            let slot = emitter.write_slot(&ident.name)?;
2455            let old = emitter.add_anonymous_local(ctx.symbols.value_type(result_ty)?)?;
2456            let box_idx = if *boxed {
2457                Some(ctx.symbols.box_type_idx(target_ty)?.ok_or_else(|| {
2458                    crate::codegen::internal_failure("box type registered for postfix operand")
2459                })?)
2460            } else {
2461                None
2462            };
2463            if let Some(box_idx) = box_idx {
2464                super::binding_cells::read(emitter, ctx, slot, box_idx)?;
2465            } else {
2466                emitter.instruction(Instruction::LocalGet(slot));
2467            }
2468            // `v++` reads `v`: a missing value throws.
2469            let stack = ctx.symbols.value_type(target_ty)?;
2470            emit_missing_local_check(emitter, ctx, slot, stack)?;
2471            let cast_info = cast_info_for(target_ty.clone(), result_ty.clone());
2472            cast::emit_narrowing_cast(emitter, ctx, &cast_info)?;
2473            emit_postfix_numeric(emitter, ctx, result_ty);
2474            emitter.instruction(Instruction::LocalSet(old));
2475            if box_idx.is_some() {
2476                emitter.instruction(Instruction::LocalGet(slot));
2477            }
2478            emitter.instruction(Instruction::LocalGet(old));
2479            emit_postfix_delta(emitter, ctx, op, result_ty);
2480            cast::emit_coerce_to_slot(emitter, ctx, result_ty, target_ty)?;
2481            if let Some(box_idx) = box_idx {
2482                emitter.instruction(Instruction::StructSet {
2483                    struct_type_index: box_idx,
2484                    field_index: 0,
2485                });
2486            } else {
2487                emitter.instruction(Instruction::LocalSet(slot));
2488            }
2489            emitter.instruction(Instruction::LocalGet(old));
2490        }
2491        crate::PostfixTarget::Global {
2492            mangled, target_ty, ..
2493        } => {
2494            let idx = ctx.symbols.global_idx(mangled).ok_or_else(|| {
2495                crate::codegen::internal_failure("Inferer guarantees the binding exists")
2496            })?;
2497            let old = emitter.add_anonymous_local(ctx.symbols.value_type(result_ty)?)?;
2498            crate::codegen::init_guard::emit_check(emitter, ctx, mangled);
2499            emitter.instruction(Instruction::GlobalGet(idx));
2500            // Reference globals start as null before module initialization.
2501            if let ValType::Ref(RefType {
2502                nullable: false, ..
2503            }) = ctx.symbols.value_type(target_ty)?
2504            {
2505                emitter.instruction(Instruction::RefAsNonNull);
2506            }
2507            let cast_info = cast_info_for(target_ty.clone(), result_ty.clone());
2508            cast::emit_narrowing_cast(emitter, ctx, &cast_info)?;
2509            emit_postfix_numeric(emitter, ctx, result_ty);
2510            emitter.instruction(Instruction::LocalSet(old));
2511            emitter.instruction(Instruction::LocalGet(old));
2512            emit_postfix_delta(emitter, ctx, op, result_ty);
2513            cast::emit_coerce_to_slot(emitter, ctx, result_ty, target_ty)?;
2514            emitter.instruction(Instruction::GlobalSet(idx));
2515            emitter.instruction(Instruction::LocalGet(old));
2516        }
2517        crate::PostfixTarget::Field {
2518            receiver,
2519            name,
2520            target_ty,
2521        } => {
2522            // A class instance resolves its payload slot statically, like every
2523            // other class field path. The by-name scan below would be wrong here:
2524            // a subclass that redeclares a parent field has two entries of that
2525            // name in the field-names array, and the scan finds the parent's.
2526            let receiver_ty = ctx
2527                .ta
2528                .source_type(*receiver)
2529                .map_err(crate::codegen::arena_failure)?
2530                .clone();
2531            if let Type::ClassRef { mangled, .. } = receiver_ty.lowered() {
2532                emit_class_field_postfix(
2533                    emitter, ctx, *receiver, mangled, &name.name, op, target_ty,
2534                )?;
2535                return Ok(());
2536            }
2537            if let Some((getter, setter)) = host_property_accessors(ctx, &receiver_ty, name)? {
2538                emit_host_property_postfix(
2539                    emitter,
2540                    ctx,
2541                    *receiver,
2542                    &receiver_ty,
2543                    (getter, setter),
2544                    op,
2545                    target_ty,
2546                )?;
2547                return Ok(());
2548            }
2549            if ctx.ta.has_string_index(&receiver_ty) {
2550                emit_expr(emitter, ctx, *receiver)?;
2551                cast::emit_box(emitter, ctx, &receiver_ty)?;
2552                let object =
2553                    emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
2554                emitter.instruction(Instruction::LocalSet(object));
2555                super::emit_const_string_by_text(emitter, ctx, &name.name)?;
2556                emit_record_postfix_on_stack(emitter, ctx, *receiver, object, op, target_ty)?;
2557                cast::emit_cast_to(emitter, ctx, result_ty)?;
2558                return Ok(());
2559            }
2560            // Mirror AssignField + FieldAccess's vtable-dispatched calls.
2561            let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
2562                crate::codegen::internal_failure("intrinsics declared by codegen entry")
2563            })?;
2564            let tmp = emitter.add_anonymous_local(ctx.symbols.value_type(target_ty)?)?;
2565            emit_receiver(emitter, ctx, *receiver)?;
2566            let rcv_local =
2567                stash_receiver_as_object_shape(emitter, &receiver_ty, intrinsics.object_shape)?;
2568            emit_object_property_read(emitter, ctx, rcv_local, &name.name, target_ty)?;
2569            emit_postfix_numeric(emitter, ctx, target_ty);
2570            emitter.instruction(Instruction::LocalSet(tmp));
2571            emitter.instruction(Instruction::LocalGet(tmp));
2572            emit_postfix_delta(emitter, ctx, op, target_ty);
2573            let updated = emitter.add_anonymous_local(ctx.symbols.value_type(target_ty)?)?;
2574            emitter.instruction(Instruction::LocalSet(updated));
2575            emit_object_property_write_value(
2576                emitter,
2577                ctx,
2578                rcv_local,
2579                name,
2580                &ShapeArgument::Local {
2581                    slot: updated,
2582                    ty: target_ty.clone(),
2583                },
2584            )?;
2585            emitter.instruction(Instruction::LocalGet(tmp));
2586        }
2587        crate::PostfixTarget::Index {
2588            receiver, index, ..
2589        } => {
2590            emit_index_postfix(emitter, ctx, *receiver, *index, op)?;
2591        }
2592    };
2593    Ok(())
2594}
2595
2596/// How a writable prelude property with a host setter is read.
2597enum HostPropertyGetter {
2598    Func(u32),
2599    /// `Array#length`, read inline from the array struct.
2600    InlineLength(u32),
2601}
2602
2603/// The getter and setter of a writable prelude interface property backed by
2604/// host functions (`RegExp.lastIndex`, `Array.length`).
2605fn host_property_accessors(
2606    ctx: &CodegenCtx,
2607    receiver_ty: &Type,
2608    name: &Ident,
2609) -> Result<Option<(HostPropertyGetter, u32)>, crate::compiler_error::CompilerFailure> {
2610    let Some(iface) = crate::codegen::host_property_iface(receiver_ty) else {
2611        return Ok(None);
2612    };
2613    let setter_key = crate::mangle::extend(&iface, &crate::mangle::property_setter(&name.name));
2614    let Some(setter) = ctx.symbols.func_idx(&setter_key) else {
2615        return Ok(None);
2616    };
2617    if let Some(struct_idx) = inline_length_struct_idx(ctx, &iface, &name.name)? {
2618        return Ok(Some((HostPropertyGetter::InlineLength(struct_idx), setter)));
2619    }
2620    let getter = ctx
2621        .symbols
2622        .func_idx(&crate::mangle::extend(&iface, &name.name))
2623        .ok_or_else(|| {
2624            crate::codegen::internal_failure("host property getter recorded beside its setter")
2625        })?;
2626    Ok(Some((HostPropertyGetter::Func(getter), setter)))
2627}
2628
2629fn emit_host_property_postfix(
2630    emitter: &mut FunctionEmitter,
2631    ctx: &CodegenCtx,
2632    receiver: ExprId,
2633    receiver_ty: &Type,
2634    (getter, setter): (HostPropertyGetter, u32),
2635    op: crate::PostfixOp,
2636    target_ty: &Type,
2637) -> Result<(), crate::compiler_error::CompilerFailure> {
2638    // The host accessors trade `f64`; a widened target boxes the old value.
2639    let recv = emitter.add_anonymous_local(ctx.symbols.value_type(receiver_ty)?)?;
2640    let old = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Number)?)?;
2641    emit_receiver(emitter, ctx, receiver)?;
2642    emitter.instruction(Instruction::LocalSet(recv));
2643    // Setter receiver first, then the updated value computed from the getter.
2644    emitter.instruction(Instruction::LocalGet(recv));
2645    emitter.instruction(Instruction::RefAsNonNull);
2646    emitter.instruction(Instruction::LocalGet(recv));
2647    emitter.instruction(Instruction::RefAsNonNull);
2648    match getter {
2649        HostPropertyGetter::Func(getter) => {
2650            emitter.instruction(Instruction::Call(getter));
2651        }
2652        HostPropertyGetter::InlineLength(struct_idx) => {
2653            emit_inline_length(emitter, ctx, struct_idx);
2654        }
2655    }
2656    emitter.instruction(Instruction::LocalTee(old));
2657    emit_postfix_delta(emitter, ctx, op, &Type::Number);
2658    emitter.instruction(Instruction::Call(setter));
2659    emitter.instruction(Instruction::LocalGet(old));
2660    if target_ty != &Type::Number {
2661        cast::emit_box(emitter, ctx, &Type::Number)?;
2662    }
2663    Ok(())
2664}
2665
2666fn emit_index_postfix(
2667    emitter: &mut FunctionEmitter,
2668    ctx: &CodegenCtx,
2669    receiver: ExprId,
2670    index: ExprId,
2671    op: crate::PostfixOp,
2672) -> Result<(), crate::compiler_error::CompilerFailure> {
2673    let is_uint8 = ctx
2674        .ta
2675        .source_type(receiver)
2676        .map_err(crate::codegen::arena_failure)?
2677        .lowered()
2678        == &Type::Uint8Array;
2679    let result_ty = if is_uint8 {
2680        Type::Number
2681    } else {
2682        Type::Unknown
2683    };
2684    emit_expr(emitter, ctx, receiver)?;
2685    cast::emit_box(
2686        emitter,
2687        ctx,
2688        &ctx.ta
2689            .try_expr(receiver)
2690            .map_err(crate::codegen::arena_failure)?
2691            .ty,
2692    )?;
2693    let receiver_local = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
2694    emitter.instruction(Instruction::LocalSet(receiver_local));
2695    emit_expr(emitter, ctx, index)?;
2696    let key = emitter.add_anonymous_local(
2697        ctx.symbols.value_type(
2698            &ctx.ta
2699                .try_expr(index)
2700                .map_err(crate::codegen::arena_failure)?
2701                .ty,
2702        )?,
2703    )?;
2704    emitter.instruction(Instruction::LocalSet(key));
2705    let (backing, position, raw_type) = emit_index_location(
2706        emitter,
2707        ctx,
2708        receiver_local,
2709        key,
2710        &ctx.ta
2711            .try_expr(index)
2712            .map_err(crate::codegen::arena_failure)?
2713            .ty,
2714        is_uint8,
2715        &receiver_name(ctx.ta, receiver)?,
2716        IndexAccess::Read,
2717    )?;
2718    emitter.instruction(Instruction::LocalGet(backing));
2719    emitter.instruction(Instruction::LocalGet(position));
2720    if is_uint8 {
2721        emitter.instruction(Instruction::ArrayGetU(raw_type));
2722        emitter.instruction(Instruction::F64ConvertI32U);
2723    } else {
2724        emitter.instruction(Instruction::ArrayGet(raw_type));
2725    }
2726    emit_postfix_numeric(emitter, ctx, &result_ty);
2727    let old = emitter.add_anonymous_local(ctx.symbols.value_type(&result_ty)?)?;
2728    emitter.instruction(Instruction::LocalTee(old));
2729    emit_postfix_delta(emitter, ctx, op, &result_ty);
2730    let updated = emitter.add_anonymous_local(ctx.symbols.value_type(&result_ty)?)?;
2731    emitter.instruction(Instruction::LocalSet(updated));
2732    // GetValue and PutValue each convert the original property key. Conversion
2733    // can mutate the array, so reload the backing storage for the write.
2734    let (backing, position, _) = emit_index_location(
2735        emitter,
2736        ctx,
2737        receiver_local,
2738        key,
2739        &ctx.ta
2740            .try_expr(index)
2741            .map_err(crate::codegen::arena_failure)?
2742            .ty,
2743        is_uint8,
2744        &receiver_name(ctx.ta, receiver)?,
2745        if is_uint8 {
2746            IndexAccess::FixedWrite
2747        } else {
2748            IndexAccess::Write
2749        },
2750    )?;
2751    emitter.instruction(Instruction::LocalGet(backing));
2752    emitter.instruction(Instruction::LocalGet(position));
2753    emitter.instruction(Instruction::LocalGet(updated));
2754    if is_uint8 {
2755        emitter.instruction(Instruction::I32TruncSatF64S);
2756    }
2757    emitter.instruction(Instruction::ArraySet(raw_type));
2758    emitter.instruction(Instruction::LocalGet(old));
2759    Ok(())
2760}
2761
2762#[allow(clippy::too_many_arguments)]
2763fn emit_index_location(
2764    emitter: &mut FunctionEmitter,
2765    ctx: &CodegenCtx,
2766    receiver: u32,
2767    key: u32,
2768    key_ty: &Type,
2769    is_uint8: bool,
2770    receiver_name: &str,
2771    access: IndexAccess,
2772) -> Result<(u32, u32, u32), crate::compiler_error::CompilerFailure> {
2773    let (container, raw, ty) = if is_uint8 {
2774        (
2775            ctx.symbols
2776                .uint8_array_type_idx()
2777                .ok_or_else(|| crate::codegen::internal_failure("Uint8Array declared"))?,
2778            ctx.symbols
2779                .raw_uint8_array_type_idx()
2780                .ok_or_else(|| crate::codegen::internal_failure("raw Uint8Array declared"))?,
2781            Type::Uint8Array,
2782        )
2783    } else {
2784        (
2785            ctx.symbols
2786                .array_type_idx()
2787                .ok_or_else(|| crate::codegen::internal_failure("Array declared"))?,
2788            ctx.symbols
2789                .raw_array_type_idx()
2790                .ok_or_else(|| crate::codegen::internal_failure("raw Array declared"))?,
2791            Type::Array(Box::new(Type::Unknown)),
2792        )
2793    };
2794    emitter.instruction(Instruction::LocalGet(receiver));
2795    crate::codegen::cast_check::emit_operation_cast_on_stack(emitter, ctx, &Type::Unknown, &ty)?;
2796    emitter.instruction(Instruction::LocalGet(key));
2797    emit_index_number(emitter, ctx, key_ty)?;
2798    let operand = stash_index_operand(emitter)?;
2799    let length = (!is_uint8)
2800        .then(|| stash_array_length(emitter, container))
2801        .transpose()?;
2802    emitter.instruction(Instruction::StructGet {
2803        struct_type_index: container,
2804        field_index: 1,
2805    });
2806    let backing = emitter.add_anonymous_local(ValType::Ref(RefType {
2807        nullable: false,
2808        heap_type: HeapType::Concrete(raw),
2809    }))?;
2810    emitter.instruction(Instruction::LocalSet(backing));
2811    let position = match length {
2812        Some(length) => {
2813            emit_checked_index_with_length(emitter, ctx, length, operand, receiver_name, access)?
2814        }
2815        None => emit_checked_index(emitter, ctx, backing, operand, receiver_name, access)?,
2816    };
2817    Ok((backing, position, raw))
2818}
2819
2820fn emit_postfix_numeric(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, ty: &Type) {
2821    ctx.latch(emit_postfix_numeric_checked(emitter, ctx, ty));
2822}
2823
2824fn emit_postfix_numeric_checked(
2825    emitter: &mut FunctionEmitter,
2826    ctx: &CodegenCtx,
2827    ty: &Type,
2828) -> Result<(), crate::compiler_error::CompilerFailure> {
2829    if matches!(ty, Type::Unknown) {
2830        let function = ctx
2831            .symbols
2832            .prelude_func_idx("__value_numeric")
2833            .ok_or_else(|| crate::codegen::internal_failure("dynamic update helper collected"))?;
2834        emitter.instruction(Instruction::Call(function));
2835    }
2836
2837    Ok(())
2838}
2839
2840fn emit_postfix_delta(
2841    emitter: &mut FunctionEmitter<'_>,
2842    ctx: &CodegenCtx<'_>,
2843    op: crate::PostfixOp,
2844    ty: &Type,
2845) {
2846    ctx.latch(emit_postfix_delta_checked(emitter, ctx, op, ty));
2847}
2848
2849fn emit_postfix_delta_checked(
2850    emitter: &mut FunctionEmitter<'_>,
2851    ctx: &CodegenCtx<'_>,
2852    op: crate::PostfixOp,
2853    ty: &Type,
2854) -> Result<(), crate::compiler_error::CompilerFailure> {
2855    if matches!(ty, Type::Unknown) {
2856        let name = if matches!(op, crate::PostfixOp::Inc) {
2857            "inc"
2858        } else {
2859            "dec"
2860        };
2861        let function = ctx
2862            .symbols
2863            .prelude_func_idx(&format!("__value_{name}"))
2864            .ok_or_else(|| crate::codegen::internal_failure("dynamic update helper collected"))?;
2865        emitter.instruction(Instruction::Call(function));
2866        return Ok(());
2867    }
2868    if ty.is_bigint() {
2869        emit_bigint_pm_one(emitter, ctx, op);
2870        return Ok(());
2871    }
2872    emitter.instruction(Instruction::F64Const(Ieee64::from(1.0)));
2873    emitter.instruction(match op {
2874        crate::PostfixOp::Inc => Instruction::F64Add,
2875        crate::PostfixOp::Dec => Instruction::F64Sub,
2876        crate::PostfixOp::NonNullAssert => {
2877            return Err(crate::codegen::internal_failure(
2878                "non-null assertion is not PostfixUnary",
2879            ));
2880        }
2881    });
2882
2883    Ok(())
2884}
2885
2886/// `receiver.field++` on a class instance, leaving the *old* value on the stack.
2887/// Reads and writes the statically resolved payload slot, the same slot
2888/// `FieldAccess` and `AssignField` use. Accessor properties never reach here —
2889/// the typechecker rejects postfix on them, so there is no setter to dispatch.
2890#[allow(clippy::too_many_arguments)]
2891fn emit_class_field_postfix(
2892    emitter: &mut FunctionEmitter<'_>,
2893    ctx: &CodegenCtx<'_>,
2894    receiver: ExprId,
2895    mangled: &crate::MangledName,
2896    field: &str,
2897    op: crate::PostfixOp,
2898    target_ty: &Type,
2899) -> Result<(), crate::compiler_error::CompilerFailure> {
2900    let intrinsics = ctx
2901        .symbols
2902        .intrinsic_type_indices()
2903        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
2904    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
2905        crate::codegen::internal_failure("class struct type recorded in classes::emit")
2906    })?;
2907    // A property with no slot is an accessor, which has no payload to
2908    // read-modify-write; `class_postfix_target` rejects those before codegen.
2909    let slot = ctx
2910        .symbols
2911        .class_field_slot(mangled, field)
2912        .ok_or_else(|| {
2913            crate::codegen::internal_failure(
2914                "data-field slot recorded in classes::emit — accessors are rejected in infer",
2915            )
2916        })?;
2917    let fields_local = emitter.add_anonymous_local(ValType::Ref(RefType {
2918        nullable: false,
2919        heap_type: HeapType::Concrete(intrinsics.object_fields),
2920    }))?;
2921    let old = emitter.add_anonymous_local(ctx.symbols.value_type(target_ty)?)?;
2922    let object = emitter.add_anonymous_local(
2923        ctx.symbols.value_type(
2924            ctx.ta
2925                .source_type(receiver)
2926                .map_err(crate::codegen::arena_failure)?,
2927        )?,
2928    )?;
2929    emit_receiver(emitter, ctx, receiver)?;
2930    emitter.instruction(Instruction::LocalTee(object));
2931    emitter.instruction(Instruction::StructGet {
2932        struct_type_index: struct_idx,
2933        field_index: 2,
2934    });
2935    emitter.instruction(Instruction::LocalSet(fields_local));
2936    emitter.instruction(Instruction::LocalGet(fields_local));
2937    emitter.instruction(Instruction::I32Const(slot as i32));
2938    emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
2939    if ctx
2940        .symbols
2941        .class_field_narrowing_check(mangled, field)
2942        .is_some()
2943    {
2944        crate::codegen::field_guards::check(emitter, ctx, object, mangled, field)?;
2945    }
2946    emit_class_field_slot_cast(emitter, ctx, mangled, field, target_ty)?;
2947    emit_postfix_numeric(emitter, ctx, target_ty);
2948    emitter.instruction(Instruction::LocalSet(old));
2949    emitter.instruction(Instruction::LocalGet(fields_local));
2950    emitter.instruction(Instruction::I32Const(slot as i32));
2951    emitter.instruction(Instruction::LocalGet(old));
2952    emit_postfix_delta(emitter, ctx, op, target_ty);
2953    crate::codegen::function_emitter::cast::emit_box(emitter, ctx, target_ty)?;
2954    emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
2955    emitter.instruction(Instruction::LocalGet(old));
2956    Ok(())
2957}
2958
2959/// walk the typed `parts` list from `idx` to the end, with
2960/// the current receiver value already on the stack and `receiver_ty`
2961/// describing its static type. Each `optional: true` step opens an
2962/// `if (result … T | null)` that emits `null` on the short-circuit
2963/// branch and recurses on the non-null branch. The final receiver is
2964/// cast to `result_ty` so the chain's result Wasm type lines up at
2965/// the outer expression's slot.
2966#[allow(clippy::too_many_arguments)]
2967fn emit_chain_parts(
2968    emitter: &mut FunctionEmitter<'_>,
2969    ctx: &CodegenCtx<'_>,
2970    base: ExprId,
2971    parts: &[crate::TypedChainPart],
2972    idx: usize,
2973    receiver_ty: &Type,
2974    result_ty: &Type,
2975    source_types: Option<&[Type]>,
2976    present_flag: Option<u32>,
2977) -> Result<(), crate::compiler_error::CompilerFailure> {
2978    if source_types.is_some_and(|types| parts.len().checked_add(1) != Some(types.len())) {
2979        return Err(crate::codegen::internal_failure(
2980            "optional-chain source type count mismatch",
2981        ));
2982    }
2983    if idx == parts.len() {
2984        // Receiver IS the chain's tail value; widen / box / re-cast
2985        // to the chain's outer `result_ty` slot. `emit_coerce_to_slot`
2986        // handles both directions (primitive → boxed for the widen
2987        // case, ref → ref for the same-shape case).
2988        crate::codegen::function_emitter::cast::emit_coerce_to_slot(
2989            emitter,
2990            ctx,
2991            receiver_ty,
2992            result_ty,
2993        )?;
2994        return Ok(());
2995    }
2996    let part = parts.get(idx).ok_or_else(|| {
2997        crate::codegen::internal_failure("optional-chain part index is out of bounds")
2998    })?;
2999    let receiver_name = if matches!(part, crate::TypedChainPart::Index { .. }) {
3000        crate::codegen::bounds::chain_receiver_name(ctx.ta, base, &parts[..idx])?
3001    } else {
3002        String::new()
3003    };
3004    let next = idx
3005        .checked_add(1)
3006        .ok_or_else(|| crate::codegen::internal_failure("optional-chain index overflow"))?;
3007    let source_ty = source_types.and_then(|types| types.get(idx));
3008    let check_ty = source_types.and_then(|types| types.get(next));
3009    let saved_receiver = if matches!(
3010        part,
3011        crate::TypedChainPart::Field { .. }
3012            | crate::TypedChainPart::Index { .. }
3013            | crate::TypedChainPart::InterfaceProperty { .. }
3014    ) {
3015        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(receiver_ty)?)?;
3016        emitter.instruction(Instruction::LocalTee(slot));
3017        Some(slot)
3018    } else if matches!(
3019        part,
3020        crate::TypedChainPart::NonNull { .. } | crate::TypedChainPart::Constraint { .. }
3021    ) {
3022        emitter.call_receiver
3023    } else {
3024        None
3025    };
3026    let part_result_ty = part.result_ty().clone();
3027    // Only an optional step needs the receiver's slot, so don't ask for a
3028    // lowering a straight-line step never uses.
3029    //
3030    // A `?.` that checks a base which may be missing (SUB-2148) and that no
3031    // check around the chain reached has a receiver that can't be nullish:
3032    // the base throws when missing, so this step is straight-line too.
3033    // A method's `?.()` keeps its test: the typechecker keeps it only where
3034    // the method may be absent although its type isn't nullish.
3035    let receiver_may_be_nullish = {
3036        let ty = source_ty.unwrap_or(receiver_ty);
3037        crate::codegen::symbol_table::may_hold_null(ty)
3038            || ty.holds_undefined()
3039            || matches!(
3040                part,
3041                crate::TypedChainPart::Call { .. } | crate::TypedChainPart::MethodCall { .. }
3042            )
3043    };
3044    let recv_val = (part.is_optional() && receiver_may_be_nullish)
3045        .then(|| ctx.symbols.value_type(receiver_ty))
3046        .transpose()?;
3047    // A receiver in a primitive slot cannot hold null, and `ref.is_null` does
3048    // not accept an f64/i32, so a `?.` on a non-nullable `number`/`boolean`
3049    // lowers as the straight-line access. The redundant-`?.` warning does not
3050    // gate this: it only fires on the chain's base, so the same shape mid-chain
3051    // arrives here undiagnosed.
3052    let _: () = if let Some(recv_val @ ValType::Ref(_)) = recv_val {
3053        let block_ty = BlockType::Result(ctx.symbols.value_type(result_ty)?);
3054        let tmp = emitter.add_anonymous_local(recv_val)?;
3055        emitter.instruction(Instruction::LocalTee(tmp));
3056        emit_is_nullish(emitter, ctx)?;
3057        emitter.emit_if(block_ty);
3058        if let Some(flag) = present_flag {
3059            set_present_flag(emitter, flag, false);
3060        }
3061        emit_undefined_for_chain_result(emitter, ctx, result_ty)?;
3062        emitter.emit_else();
3063        emitter.instruction(Instruction::LocalGet(tmp));
3064        emitter.instruction(Instruction::RefAsNonNull);
3065        emit_chain_operation(
3066            emitter,
3067            ctx,
3068            part,
3069            receiver_ty,
3070            source_ty,
3071            check_ty,
3072            &receiver_name,
3073            present_flag.filter(|_| next == parts.len()),
3074        )?;
3075        emitter.call_receiver = saved_receiver;
3076        emit_chain_parts(
3077            emitter,
3078            ctx,
3079            base,
3080            parts,
3081            next,
3082            &part_result_ty,
3083            result_ty,
3084            source_types,
3085            present_flag,
3086        )?;
3087        emitter.emit_end();
3088    } else {
3089        emit_chain_operation(
3090            emitter,
3091            ctx,
3092            part,
3093            receiver_ty,
3094            source_ty,
3095            check_ty,
3096            &receiver_name,
3097            present_flag.filter(|_| next == parts.len()),
3098        )?;
3099        emitter.call_receiver = saved_receiver;
3100        emit_chain_parts(
3101            emitter,
3102            ctx,
3103            base,
3104            parts,
3105            next,
3106            &part_result_ty,
3107            result_ty,
3108            source_types,
3109            present_flag,
3110        )?;
3111    };
3112    Ok(())
3113}
3114
3115#[allow(clippy::too_many_arguments)]
3116fn emit_chain_operation(
3117    emitter: &mut FunctionEmitter,
3118    ctx: &CodegenCtx,
3119    part: &crate::TypedChainPart,
3120    receiver_ty: &Type,
3121    source_ty: Option<&Type>,
3122    check_ty: Option<&Type>,
3123    receiver_name: &str,
3124    present_flag: Option<u32>,
3125) -> Result<(), crate::compiler_error::CompilerFailure> {
3126    if let crate::TypedChainPart::Constraint { result_ty, .. } = part {
3127        return cast::emit_coerce_to_slot(emitter, ctx, receiver_ty, result_ty);
3128    }
3129    if let crate::TypedChainPart::MethodCall {
3130        iface,
3131        name,
3132        result_ty,
3133        span,
3134        ..
3135    } = part
3136        && !crate::codegen::is_host_package(iface.as_str())
3137        && let Some(args) = ctx.ta.authored_call_arguments(*span)
3138    {
3139        cast::emit_box(emitter, ctx, receiver_ty)?;
3140        emit_live_member_on_stack(emitter, ctx, iface, &name.name, Some(args))?;
3141        cast::emit_cast_to(emitter, ctx, result_ty)?;
3142        return Ok(());
3143    }
3144    if receiver_ty == &Type::Unknown {
3145        match part {
3146            crate::TypedChainPart::MethodCall {
3147                iface,
3148                name,
3149                args,
3150                result_ty,
3151                span,
3152                ..
3153            } if crate::codegen::runtime_values::dynamic_member_interface(iface) => {
3154                let args = ctx.ta.authored_call_arguments(*span).unwrap_or(args);
3155                emit_live_member_on_stack(emitter, ctx, iface, &name.name, Some(args))?;
3156                return Ok(());
3157            }
3158            crate::TypedChainPart::InterfaceProperty { iface, name, .. }
3159                if crate::codegen::runtime_values::dynamic_member_interface(iface) =>
3160            {
3161                emit_live_member_on_stack(emitter, ctx, iface, &name.name, None)?;
3162                return Ok(());
3163            }
3164            _ => {}
3165        }
3166    }
3167    let operation_ty =
3168        crate::typechecker::infer::narrowing::strip_nullish(source_ty.unwrap_or(receiver_ty));
3169    if !matches!(
3170        part,
3171        crate::TypedChainPart::Index { .. }
3172            | crate::TypedChainPart::Call { .. }
3173            | crate::TypedChainPart::NonNull { .. }
3174    ) {
3175        let receiver_slot = ctx.symbols.value_type(receiver_ty)?;
3176        if receiver_slot == ctx.symbols.value_type(&operation_ty)? {
3177            if matches!(receiver_slot, ValType::Ref(_)) {
3178                cast::emit_cast_to(emitter, ctx, &operation_ty)?;
3179            }
3180        } else {
3181            crate::codegen::cast_check::emit_operation_cast_on_stack(
3182                emitter,
3183                ctx,
3184                receiver_ty,
3185                &operation_ty,
3186            )?;
3187        }
3188    }
3189    emit_chain_access(
3190        emitter,
3191        ctx,
3192        part,
3193        &operation_ty,
3194        check_ty.unwrap_or(part.result_ty()),
3195        receiver_name,
3196        present_flag,
3197    )?;
3198    Ok(())
3199}
3200
3201fn emit_live_member_on_stack(
3202    emitter: &mut FunctionEmitter,
3203    ctx: &CodegenCtx,
3204    iface: &crate::MangledName,
3205    name: &str,
3206    args: Option<&[ExprId]>,
3207) -> Result<(), crate::compiler_error::CompilerFailure> {
3208    super::emit_const_string_by_text(emitter, ctx, name)?;
3209    super::emit_const_string_by_text(emitter, ctx, iface.as_str())?;
3210    let helper = if args.is_some() {
3211        "__value_member"
3212    } else {
3213        "__value_property"
3214    };
3215    emitter.instruction(Instruction::Call(
3216        ctx.symbols
3217            .prelude_func_idx(helper)
3218            .ok_or_else(|| crate::codegen::internal_failure("live member helper collected"))?,
3219    ));
3220    let Some(args) = args else {
3221        return Ok(());
3222    };
3223    let intr = ctx
3224        .symbols
3225        .intrinsic_type_indices()
3226        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
3227    emitter.instruction(Instruction::GlobalGet(
3228        ctx.symbols
3229            .prelude_global_idx("array_vtable")
3230            .ok_or_else(|| crate::codegen::internal_failure("array vtable"))?,
3231    ));
3232    for &arg in args {
3233        emit_expr(emitter, ctx, arg)?;
3234        cast::emit_box(
3235            emitter,
3236            ctx,
3237            &ctx.ta
3238                .try_expr(arg)
3239                .map_err(crate::codegen::arena_failure)?
3240                .ty,
3241        )?;
3242    }
3243    emitter.instruction(Instruction::ArrayNewFixed {
3244        array_type_index: intr.raw_array,
3245        array_size: crate::codegen::wasm_u32(args.len())?,
3246    });
3247    emitter.instruction(Instruction::I32Const(i32::try_from(args.len()).map_err(
3248        |_| crate::codegen::internal_failure("argument array too large"),
3249    )?));
3250    emitter.instruction(Instruction::StructNew(intr.array));
3251    emitter.instruction(Instruction::Call(
3252        ctx.symbols
3253            .prelude_func_idx("__value_invoke")
3254            .ok_or_else(|| crate::codegen::internal_failure("live invocation collected"))?,
3255    ));
3256    Ok(())
3257}
3258
3259/// Intrinsic length properties have no host getter. Arrays store their logical
3260/// length; strings derive it from their packed code-unit backing.
3261fn inline_length_struct_idx(
3262    ctx: &CodegenCtx<'_>,
3263    iface: &crate::MangledName,
3264    prop: &str,
3265) -> Result<Option<u32>, crate::compiler_error::CompilerFailure> {
3266    let key = crate::mangle::extend(iface, prop);
3267    if !ctx.symbols.is_intrinsic_member(&key) {
3268        return Ok(None);
3269    }
3270    let index = if *iface == crate::mangle::prelude("String") && prop == "length" {
3271        ctx.symbols.string_type_idx()
3272    } else if *iface == crate::mangle::prelude("Array") && prop == "length" {
3273        ctx.symbols.array_type_idx()
3274    } else {
3275        return Err(crate::codegen::internal_failure(format!(
3276            "no inline lowering for intrinsic member `{}`",
3277            key.as_str()
3278        )));
3279    };
3280    index
3281        .map(Some)
3282        .ok_or_else(|| crate::codegen::internal_failure("length receiver intrinsic missing"))
3283}
3284
3285/// Receiver (concrete `$string`/`$Array`) on stack → its element count as f64.
3286fn emit_inline_length(emitter: &mut FunctionEmitter<'_>, ctx: &CodegenCtx<'_>, struct_idx: u32) {
3287    let is_array = ctx.symbols.array_type_idx() == Some(struct_idx);
3288    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(struct_idx)));
3289    emitter.instruction(Instruction::StructGet {
3290        struct_type_index: struct_idx,
3291        field_index: if is_array { 2 } else { 1 },
3292    });
3293    if !is_array {
3294        emitter.instruction(Instruction::ArrayLen);
3295    }
3296    emitter.instruction(Instruction::F64ConvertI32U);
3297}
3298
3299/// Emit the undefined completion of a short-circuited optional chain.
3300fn emit_undefined_for_chain_result(
3301    emitter: &mut FunctionEmitter<'_>,
3302    ctx: &CodegenCtx<'_>,
3303    result_ty: &Type,
3304) -> Result<(), crate::compiler_error::CompilerFailure> {
3305    emit_undefined(emitter, ctx)?;
3306    cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, result_ty)
3307}
3308
3309/// Narrows a class field's raw payload slot value, on the stack, to the type the
3310/// read is typed at. Ordinarily that is the plain representation cast; a field
3311/// carrying a [`crate::FieldNarrowingCheck`] is read through its guard instead.
3312fn emit_class_field_slot_cast(
3313    emitter: &mut FunctionEmitter,
3314    ctx: &CodegenCtx,
3315    class: &crate::MangledName,
3316    field: &str,
3317    result_ty: &Type,
3318) -> Result<(), crate::compiler_error::CompilerFailure> {
3319    emit_class_field_slot_cast_as(emitter, ctx, class, field, result_ty, result_ty)?;
3320    Ok(())
3321}
3322
3323fn emit_class_field_slot_cast_as(
3324    emitter: &mut FunctionEmitter<'_>,
3325    ctx: &CodegenCtx<'_>,
3326    class: &crate::MangledName,
3327    field: &str,
3328    result_ty: &Type,
3329    check_ty: &Type,
3330) -> Result<(), crate::compiler_error::CompilerFailure> {
3331    let _: () = match ctx.symbols.class_field_narrowing_check(class, field) {
3332        Some(check) => {
3333            crate::codegen::cast_check::emit_narrowed_field_read_as(
3334                emitter, ctx, check, result_ty, check_ty,
3335            )?;
3336        }
3337        None => crate::codegen::function_emitter::cast::emit_cast_to(emitter, ctx, result_ty)?,
3338    };
3339    Ok(())
3340}
3341
3342fn emit_object_spread(
3343    emitter: &mut FunctionEmitter,
3344    ctx: &CodegenCtx,
3345    sources: &[TypedObjectMember],
3346    shape: &Type,
3347) -> Result<(), crate::compiler_error::CompilerFailure> {
3348    let intrinsics = ctx
3349        .symbols
3350        .intrinsic_type_indices()
3351        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
3352    let merge = ctx
3353        .symbols
3354        .prelude_func_idx("ObjectConstructor##spread")
3355        .ok_or_else(|| crate::codegen::internal_failure("spread helper collected"))?;
3356    let null_object = Instruction::RefNull(HeapType::Concrete(intrinsics.object));
3357    emitter.instruction(null_object.clone());
3358    for (index, source) in sources.iter().enumerate() {
3359        let final_shape = (index + 1 == sources.len()).then_some(shape);
3360        // A source `runtime_values` widened may no longer hold what its
3361        // narrowed type said; `emit_receiver` checks it against that type,
3362        // which the stash and the mask then read in place of `unknown`.
3363        emit_receiver(emitter, ctx, source.expr_id())?;
3364        let narrowed_ty = ctx
3365            .ta
3366            .source_type(source.expr_id())
3367            .map_err(crate::codegen::arena_failure)?;
3368        // A source that may hold something other than an object shape branches
3369        // on its backing: nullish and non-string primitive values copy nothing,
3370        // while an array or string held in an opaque type copies its indices. Either way the
3371        // merge still applies the final shape.
3372        let non_shape_source = if may_hold_non_shape(narrowed_ty) {
3373            Some(emit_object_source_test(
3374                emitter,
3375                ctx,
3376                narrowed_ty,
3377                intrinsics.object,
3378                intrinsics.object_shape,
3379            )?)
3380        } else {
3381            None
3382        };
3383        let source_local =
3384            stash_receiver_as_object_shape(emitter, narrowed_ty, intrinsics.object_shape)?;
3385        emitter.instruction(Instruction::LocalGet(source_local));
3386        emit_spread_shape_argument(emitter, ctx, final_shape, &null_object);
3387        if matches!(source, TypedObjectMember::Spread { by_name: true, .. }) {
3388            emit_spread_mask(emitter, ctx, source_local, source.expr_id(), shape)?;
3389        } else {
3390            emitter.instruction(null_object.clone());
3391        }
3392        emitter.instruction(Instruction::Call(merge));
3393        if let Some(NonShapeSource {
3394            accumulator,
3395            opaque,
3396        }) = non_shape_source
3397        {
3398            emitter.emit_else();
3399            emitter.instruction(Instruction::LocalGet(accumulator));
3400            // Strings and arrays copy their indices; other built-ins copy any
3401            // enumerable fields they expose. Nullish values copy nothing.
3402            match opaque {
3403                Some(source) => emitter.instruction(Instruction::LocalGet(source)),
3404                None => emitter.instruction(null_object.clone()),
3405            }
3406            emit_spread_shape_argument(emitter, ctx, final_shape, &null_object);
3407            emitter.instruction(null_object.clone());
3408            emitter.instruction(Instruction::Call(merge));
3409            emitter.emit_end();
3410        }
3411    }
3412    Ok(())
3413}
3414
3415/// The merge's shape argument: the result shape on the last merge, which
3416/// restores its optional markers, and null before it.
3417fn emit_spread_shape_argument(
3418    emitter: &mut FunctionEmitter,
3419    ctx: &CodegenCtx,
3420    final_shape: Option<&Type>,
3421    null_object: &Instruction<'static>,
3422) {
3423    match final_shape {
3424        Some(shape) => emit_spread_shape(emitter, ctx, shape),
3425        None => emitter.instruction(null_object.clone()),
3426    }
3427}
3428
3429/// Whether a spread source of type `ty` may hold a value without an object
3430/// shape: nullish or primitive values, or a built-in behind an interface.
3431fn may_hold_non_shape(ty: &Type) -> bool {
3432    match ty.lowered() {
3433        Type::Union(members) => members.iter().any(may_hold_non_shape),
3434        Type::Null
3435        | Type::Undefined
3436        | Type::Boolean
3437        | Type::BooleanLiteral(_)
3438        | Type::Number
3439        | Type::NumberLiteral(_)
3440        | Type::String
3441        | Type::StringLiteral(_)
3442        | Type::InterfaceRef { .. }
3443        // `{}`, which holds primitives too.
3444        | Type::Unknown => true,
3445        _ => false,
3446    }
3447}
3448
3449/// The locals the `else` arm of [`emit_object_source_test`] reads.
3450struct NonShapeSource {
3451    accumulator: u32,
3452    /// The source, when held as an opaque `$Object`, may be a string, array,
3453    /// or another built-in backing rather than an `$ObjectShape`.
3454    opaque: Option<u32>,
3455}
3456
3457/// With the accumulator and then the source on the stack, opens an `if` on
3458/// whether the source has an object shape, whose result is the merged object. Inside
3459/// it, the accumulator and the source, as an `$ObjectShape`, are on the stack.
3460/// Returns the locals the `else` arm reads.
3461fn emit_object_source_test(
3462    emitter: &mut FunctionEmitter,
3463    ctx: &CodegenCtx,
3464    source_ty: &Type,
3465    object: u32,
3466    object_shape: u32,
3467) -> Result<NonShapeSource, crate::compiler_error::CompilerFailure> {
3468    let source_type = match ctx.symbols.value_type(source_ty)? {
3469        ValType::Ref(reference) => ValType::Ref(RefType {
3470            nullable: true,
3471            ..reference
3472        }),
3473        other => other,
3474    };
3475    let opaque = source_type == object_ref(object);
3476    let source = emitter.add_anonymous_local(source_type)?;
3477    emitter.instruction(Instruction::LocalSet(source));
3478    let accumulator = emitter.add_anonymous_local(object_ref(object))?;
3479    emitter.instruction(Instruction::LocalSet(accumulator));
3480    emitter.instruction(Instruction::LocalGet(source));
3481    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
3482        object_shape,
3483    )));
3484    emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
3485        nullable: false,
3486        heap_type: HeapType::Concrete(object_shape),
3487    })));
3488    emitter.instruction(Instruction::LocalGet(accumulator));
3489    emitter.instruction(Instruction::LocalGet(source));
3490    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
3491        object_shape,
3492    )));
3493    Ok(NonShapeSource {
3494        accumulator,
3495        opaque: opaque.then_some(source),
3496    })
3497}
3498
3499/// Union sources may carry a known field with an incompatible hidden value.
3500/// Preserve the checked-field contract for those names without discarding
3501/// runtime fields outside the source's static view.
3502fn emit_spread_mask(
3503    emitter: &mut FunctionEmitter,
3504    ctx: &CodegenCtx,
3505    source: u32,
3506    source_expr: crate::ExprId,
3507    shape: &Type,
3508) -> Result<(), crate::compiler_error::CompilerFailure> {
3509    let checked = ctx
3510        .ta
3511        .spread_mask_fields
3512        .get(&source_expr)
3513        .ok_or_else(|| crate::codegen::internal_failure("by-name spread fields recorded"))?;
3514    // A name an object rest leaves out is masked whatever its value; any other
3515    // is masked when its value isn't of the field's type.
3516    let omitted = ctx.ta.spread_omitted_fields.get(&source_expr);
3517    let fields: Vec<(&String, Option<&Type>)> = checked
3518        .iter()
3519        .map(|(name, ty)| (name, Some(ty)))
3520        .chain(omitted.into_iter().flatten().map(|name| (name, None)))
3521        .collect();
3522    let intrinsics = ctx
3523        .symbols
3524        .intrinsic_type_indices()
3525        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
3526    let vtable = ctx
3527        .symbols
3528        .vtable_global_idx(shape)
3529        .ok_or_else(|| crate::codegen::internal_failure("spread shape vtable collected"))?;
3530    emitter.instruction(Instruction::GlobalGet(vtable));
3531    for (name, _) in &fields {
3532        let global = ctx
3533            .symbols
3534            .field_name_string_global_idx(name)
3535            .ok_or_else(|| crate::codegen::internal_failure("spread name collected"))?;
3536        emitter.instruction(Instruction::GlobalGet(global));
3537    }
3538    emitter.instruction(Instruction::ArrayNewFixed {
3539        array_type_index: intrinsics.field_names,
3540        array_size: crate::codegen::wasm_u32(fields.len())?,
3541    });
3542    for (name, ty) in fields.iter().copied() {
3543        let global = ctx
3544            .symbols
3545            .field_name_string_global_idx(name)
3546            .ok_or_else(|| crate::codegen::internal_failure("spread name collected"))?;
3547        let Some(ty) = ty else {
3548            emitter.instruction(Instruction::GlobalGet(global));
3549            continue;
3550        };
3551        let index = emitter.add_anonymous_local(ValType::I32)?;
3552        let value = emitter.add_anonymous_local(object_ref(intrinsics.object))?;
3553        emit_object_field_index_by_name(emitter, ctx, source, global);
3554        emitter.instruction(Instruction::LocalTee(index));
3555        emitter.instruction(Instruction::I32Const(0));
3556        emitter.instruction(Instruction::I32GeS);
3557        emitter.emit_if(BlockType::Result(ValType::I32));
3558        emit_field_slot_get(emitter, intrinsics, source, index);
3559        emitter.instruction(Instruction::LocalSet(value));
3560        emit_field_holds_value_of(emitter, ctx, source, index, value, ty)?;
3561        emitter.emit_else();
3562        emitter.instruction(Instruction::I32Const(0));
3563        emitter.emit_end();
3564        emitter.emit_if(BlockType::Result(object_ref(intrinsics.object)));
3565        emitter.instruction(Instruction::RefNull(HeapType::Concrete(intrinsics.object)));
3566        emitter.emit_else();
3567        emitter.instruction(Instruction::GlobalGet(global));
3568        emitter.emit_end();
3569    }
3570    emitter.instruction(Instruction::ArrayNewFixed {
3571        array_type_index: intrinsics.object_fields,
3572        array_size: crate::codegen::wasm_u32(fields.len())?,
3573    });
3574    emitter.instruction(Instruction::RefNull(HeapType::ANY));
3575    emitter.instruction(Instruction::StructNew(intrinsics.object_shape));
3576    Ok(())
3577}
3578
3579/// The final merge restores optional markers and writable absent slots from
3580/// the inferred result shape, while retaining unnamed runtime fields.
3581fn emit_spread_shape(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, shape: &Type) {
3582    ctx.latch(emit_spread_shape_checked(emitter, ctx, shape));
3583}
3584
3585fn emit_spread_shape_checked(
3586    emitter: &mut FunctionEmitter,
3587    ctx: &CodegenCtx,
3588    shape: &Type,
3589) -> Result<(), crate::compiler_error::CompilerFailure> {
3590    let Type::Object { fields, .. } = shape else {
3591        return Err(crate::codegen::internal_failure("structural spread shape"));
3592    };
3593    let intrinsics = ctx
3594        .symbols
3595        .intrinsic_type_indices()
3596        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
3597    let names: Vec<_> = fields
3598        .iter()
3599        .map(|(name, field)| crate::codegen::field_names::FieldName {
3600            name: name.clone(),
3601            optional: field.optional,
3602            is_accessor: false,
3603            is_method: false,
3604            is_private: false,
3605        })
3606        .collect();
3607    let names_global = ctx
3608        .symbols
3609        .field_names_global_idx(&names)
3610        .ok_or_else(|| crate::codegen::internal_failure("spread shape names collected"))?;
3611    let vtable = ctx
3612        .symbols
3613        .vtable_global_idx(shape)
3614        .ok_or_else(|| crate::codegen::internal_failure("spread shape vtable collected"))?;
3615    emitter.instruction(Instruction::GlobalGet(vtable));
3616    emitter.instruction(Instruction::GlobalGet(names_global));
3617    emitter.instruction(Instruction::I32Const(
3618        crate::codegen::wasm_u32(fields.len())? as i32,
3619    ));
3620    emitter.instruction(Instruction::ArrayNewDefault(intrinsics.object_fields));
3621    emitter.instruction(Instruction::RefNull(HeapType::ANY));
3622    emitter.instruction(Instruction::StructNew(intrinsics.object_shape));
3623
3624    Ok(())
3625}
3626
3627/// Coerces a field-read receiver already on the stack to `(ref $ObjectShape)`,
3628/// which is what the field-name scan takes. Only receivers that lower to the
3629/// universal `$Object` slot need it: an `InterfaceRef`, and any union carrying a
3630/// nominal member (`value_type`'s mixed-union arm). A union of object shapes
3631/// already lowers to an `$ObjectShape` subtype, and so does a class instance —
3632/// its struct's header slots 0-2 are the `$ObjectShape` prefix.
3633/// Coerce the receiver on the stack into an anonymous non-null
3634/// `(ref $ObjectShape)` local, and return the slot.
3635///
3636/// Every property path — read, write, and read-modify-write — needs the
3637/// receiver in a local it can push more than once, and every one of them needs
3638/// the same coercion first: the helpers take `(ref $ObjectShape)` non-null,
3639/// while an interface-typed receiver (or a union with a non-object member)
3640/// lowers to `(ref null $Object)`. A receiver that is already a narrower
3641/// `$ObjectShape` subtype needs nothing — Wasm subtyping accepts it.
3642pub(super) fn stash_receiver_as_object_shape(
3643    emitter: &mut FunctionEmitter<'_>,
3644    receiver_ty: &Type,
3645    object_shape: u32,
3646) -> Result<u32, crate::compiler_error::CompilerFailure> {
3647    coerce_field_receiver_to_object_shape(emitter, receiver_ty, object_shape);
3648    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
3649        nullable: false,
3650        heap_type: HeapType::Concrete(object_shape),
3651    }))?;
3652    emitter.instruction(Instruction::LocalSet(rcv_local));
3653    Ok(rcv_local)
3654}
3655
3656fn coerce_field_receiver_to_object_shape(
3657    emitter: &mut FunctionEmitter<'_>,
3658    receiver_ty: &Type,
3659    object_shape: u32,
3660) {
3661    if crate::codegen::may_hold_builtin_value(receiver_ty) {
3662        emitter.instruction(Instruction::RefAsNonNull);
3663        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
3664            object_shape,
3665        )));
3666    }
3667}
3668
3669/// Emit one chain step's access on a receiver already on the stack.
3670/// Mirrors the non-chain `TypedExprKind::FieldAccess` / `IndexAccess`
3671/// codegen at lines 391-457 / 560+ above, with the leading
3672/// `emit_expr(receiver)` elided. After this returns, the part's
3673/// result is on the stack with `part.result_ty`'s Wasm shape.
3674fn emit_chain_access(
3675    emitter: &mut FunctionEmitter<'_>,
3676    ctx: &CodegenCtx<'_>,
3677    part: &crate::TypedChainPart,
3678    receiver_ty: &Type,
3679    check_ty: &Type,
3680    receiver_name: &str,
3681    present_flag: Option<u32>,
3682) -> Result<(), crate::compiler_error::CompilerFailure> {
3683    let _: () = match part {
3684        crate::TypedChainPart::Constraint { result_ty, .. } => {
3685            cast::emit_coerce_to_slot(emitter, ctx, receiver_ty, result_ty)?;
3686        }
3687        crate::TypedChainPart::Field {
3688            name, result_ty, ..
3689        } => {
3690            if ctx.ta.has_string_index(receiver_ty) {
3691                let probe = stash_probed_record(emitter, ctx, check_ty, present_flag)?;
3692                let Some(symbol) = ctx.require(
3693                    ctx.symbols.field_name_string_global_idx(&name.name),
3694                    "property name collected",
3695                ) else {
3696                    return Ok(());
3697                };
3698                emitter.instruction(Instruction::GlobalGet(symbol));
3699                let key = KeyOperand::Global(symbol);
3700                let Some(symbol) = ctx.require(
3701                    ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
3702                    "record read imported",
3703                ) else {
3704                    return Ok(());
3705                };
3706                emitter.instruction(Instruction::Call(symbol));
3707                emit_checked_record_read(emitter, ctx, check_ty, key, probe, receiver_name)?;
3708                cast::emit_coerce_to_slot(emitter, ctx, check_ty, result_ty)?;
3709                return Ok(());
3710            }
3711            let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
3712                crate::codegen::internal_failure("intrinsics declared by codegen entry")
3713            })?;
3714            // Class receiver: resolve the slot statically, mirroring the
3715            // non-optional `FieldAccess` path. The name scan below would read a
3716            // null for an accessor property, which backs no payload slot.
3717            if let Type::ClassRef { mangled, .. } = receiver_ty.lowered() {
3718                if ctx.symbols.is_class_payload_method(mangled, &name.name) {
3719                    emit_class_method_value(
3720                        emitter,
3721                        ctx,
3722                        receiver_ty,
3723                        &name.name,
3724                        result_ty,
3725                        check_ty,
3726                    )?;
3727                    return Ok(());
3728                }
3729                let Some(slot) = ctx.symbols.class_field_slot(mangled, &name.name) else {
3730                    let getter = crate::codegen::classes::accessor_getter_name(&name.name);
3731                    emit_class_vtable_dispatch(emitter, ctx, mangled, &getter, &[], result_ty)?;
3732                    return Ok(());
3733                };
3734                let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
3735                    crate::codegen::internal_failure("class struct type recorded in classes::emit")
3736                })?;
3737                let object = emitter.add_anonymous_local(ValType::Ref(RefType {
3738                    nullable: false,
3739                    heap_type: HeapType::Concrete(struct_idx),
3740                }))?;
3741                emitter.instruction(Instruction::LocalTee(object));
3742                emitter.instruction(Instruction::StructGet {
3743                    struct_type_index: struct_idx,
3744                    field_index: 2,
3745                });
3746                emitter.instruction(Instruction::I32Const(slot as i32));
3747                emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
3748                if ctx
3749                    .symbols
3750                    .class_field_narrowing_check(mangled, &name.name)
3751                    .is_some()
3752                {
3753                    crate::codegen::field_guards::check(emitter, ctx, object, mangled, &name.name)?;
3754                }
3755                emit_class_field_slot_cast_as(
3756                    emitter, ctx, mangled, &name.name, result_ty, check_ty,
3757                )?;
3758                return Ok(());
3759            }
3760            let rcv_local =
3761                stash_receiver_as_object_shape(emitter, receiver_ty, intrinsics.object_shape)?;
3762            emit_object_property_read_as(emitter, ctx, rcv_local, &name.name, result_ty, check_ty)?;
3763        }
3764        crate::TypedChainPart::Index { idx, result_ty, .. } => {
3765            if receiver_ty.indexes_by_name() {
3766                let probe = stash_probed_record(emitter, ctx, check_ty, present_flag)?;
3767                emit_expr(emitter, ctx, *idx)?;
3768                crate::codegen::cast_check::emit_operation_cast_on_stack(
3769                    emitter,
3770                    ctx,
3771                    &ctx.ta
3772                        .try_expr(*idx)
3773                        .map_err(crate::codegen::arena_failure)?
3774                        .ty,
3775                    &Type::String,
3776                )?;
3777                let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
3778                emitter.instruction(Instruction::LocalTee(key));
3779                let Some(symbol) = ctx.require(
3780                    ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
3781                    "record read imported",
3782                ) else {
3783                    return Ok(());
3784                };
3785                emitter.instruction(Instruction::Call(symbol));
3786                emit_checked_record_read(
3787                    emitter,
3788                    ctx,
3789                    check_ty,
3790                    KeyOperand::Local(key),
3791                    probe,
3792                    receiver_name,
3793                )?;
3794                cast::emit_coerce_to_slot(emitter, ctx, check_ty, result_ty)?;
3795            } else if receiver_ty.lowered() == &Type::Uint8Array {
3796                emit_uint8_index_with_receiver_on_stack(
3797                    emitter,
3798                    ctx,
3799                    *idx,
3800                    check_ty.holds_undefined().then_some(result_ty),
3801                    receiver_name,
3802                )?;
3803                if let Some(flag) = present_flag {
3804                    set_present_flag_from_value(emitter, ctx, flag, result_ty)?;
3805                }
3806            } else if receiver_ty.is_string_shaped() {
3807                emit_string_index_with_receiver_on_stack(
3808                    emitter,
3809                    ctx,
3810                    *idx,
3811                    check_ty.holds_undefined().then_some(result_ty),
3812                    receiver_name,
3813                )?;
3814                if let Some(flag) = present_flag {
3815                    set_present_flag_from_value(emitter, ctx, flag, result_ty)?;
3816                }
3817            } else {
3818                let source_ty = match receiver_ty.lowered() {
3819                    Type::Array(element) => element.as_ref(),
3820                    Type::Tuple(elements) => match &ctx
3821                        .ta
3822                        .try_expr(*idx)
3823                        .map_err(crate::codegen::arena_failure)?
3824                        .kind
3825                    {
3826                        TypedExprKind::Number(index) => {
3827                            elements.get(*index as usize).unwrap_or(result_ty)
3828                        }
3829                        _ => check_ty,
3830                    },
3831                    _ => check_ty,
3832                };
3833                // A step the typechecker gave room for `undefined` reads a
3834                // missing element as `undefined`; `check_ty` is its type
3835                // before any wide slot erased it.
3836                let missing_gives_undefined =
3837                    check_ty.holds_undefined() && !source_ty.holds_undefined();
3838                let source_ty = if missing_gives_undefined {
3839                    result_ty
3840                } else {
3841                    source_ty
3842                };
3843                emit_bounds_checked_index_with_receiver_on_stack(
3844                    emitter,
3845                    ctx,
3846                    *idx,
3847                    source_ty,
3848                    MissingElement {
3849                        gives_undefined: missing_gives_undefined
3850                            || source_ty.holds_undefined()
3851                            || is_optional_tuple_position(ctx, receiver_ty, *idx)?,
3852                        // The receiver of a `?.` step may still be typed nullable.
3853                        throws_on_absent_group: crate::typechecker::infer::narrowing::strip_nullish(
3854                            receiver_ty,
3855                        )
3856                        .is_regexp_match_array()
3857                            && !check_ty.holds_undefined(),
3858                        present_flag,
3859                    },
3860                    receiver_name,
3861                )?;
3862                if source_ty != result_ty {
3863                    crate::codegen::cast_check::emit_checked_cast_on_stack(
3864                        emitter, ctx, source_ty, result_ty,
3865                    )?;
3866                }
3867            }
3868        }
3869        crate::TypedChainPart::InterfaceProperty { iface, name, .. } => {
3870            // same lookup the non-chain
3871            // `InterfacePropertyAccess` arm uses. The chain has the
3872            // receiver on stack already; the wrapper takes
3873            // `(ref $Object)` non-null, so coerce only when the static
3874            // receiver type is `InterfaceRef`.
3875            //
3876            // The getter returns the property's declared type, which is
3877            // exactly this part's `result_ty` — `?.` adds `| null` once,
3878            // to the whole chain, never to a step's own result. So the
3879            // value already sits in the slot the next step expects.
3880            if let Some(struct_idx) = inline_length_struct_idx(ctx, iface, &name.name)? {
3881                emit_inline_length(emitter, ctx, struct_idx);
3882                return Ok(());
3883            }
3884            let key = crate::mangle::extend(iface, &name.name);
3885            let func_idx = ctx.symbols.func_idx(&key).ok_or_else(|| {
3886                crate::codegen::internal_failure(
3887                    "interface property getter recorded during the dependency-import pass",
3888                )
3889            })?;
3890            if matches!(receiver_ty.lowered(), Type::InterfaceRef { .. }) {
3891                emitter.instruction(Instruction::RefAsNonNull);
3892            }
3893            emitter.instruction(Instruction::Call(func_idx));
3894        }
3895        crate::TypedChainPart::Call {
3896            args,
3897            union_signature,
3898            ..
3899        } => {
3900            // receiver on stack is the closure value
3901            // (`(ref $closure_N)` after `emit_chain_parts`'s
3902            // strip-null cast). Helper extracts env + funcref and
3903            // call_refs, unboxing the erased return to the function's
3904            // declared `ret` — which is this part's `result_ty`, so the
3905            // value already sits in the slot the next step expects.
3906            if let Some(signature) = union_signature
3907                && crate::codegen::closure_coercions::is_union_call_cast(receiver_ty, signature)
3908            {
3909                crate::codegen::closure_coercions::emit_union_call_cast(emitter, ctx, signature)?;
3910            }
3911            emit_indirect_closure_call_with_receiver_on_stack(
3912                emitter,
3913                ctx,
3914                union_signature.as_ref().unwrap_or(receiver_ty),
3915                args,
3916                part.result_ty(),
3917            )?;
3918        }
3919        crate::TypedChainPart::MethodCall {
3920            iface,
3921            name,
3922            args,
3923            result_ty,
3924            ..
3925        } => {
3926            // dispatch through the same three paths the
3927            // non-chain `emit_method_call` uses (Direct/Static wrapper,
3928            // built-in vtable slot, shape-based fallback) — the helper
3929            // assumes the receiver is on the stack. `result_ty` is the
3930            // method's substituted return type, not the chain's
3931            // short-circuit union (`infer_optional_chain` adds `| null`
3932            // once, to the whole chain), so it is `call_ret_ty` verbatim:
3933            // a nullable `T | null` from `at` is the wrapper's real
3934            // return and must reach the next step still nullable.
3935            emit_method_call_with_receiver_on_stack(
3936                emitter,
3937                ctx,
3938                receiver_ty,
3939                iface,
3940                &name.name,
3941                args,
3942                None,
3943                &[],
3944                None,
3945                result_ty,
3946            )?;
3947        }
3948        crate::TypedChainPart::NonNull { result_ty, .. } => {
3949            crate::codegen::cast_check::emit_non_null_assert_on_stack(
3950                emitter,
3951                ctx,
3952                receiver_ty,
3953                result_ty,
3954            )?;
3955        }
3956    };
3957    Ok(())
3958}
3959
3960/// Emit the load for one captured-binding value at the closure-creation
3961/// site. The captured slot in the *outer* scope already has the right
3962/// representation for the env field — boxed captures store
3963/// `(ref $box_T)`, unboxed (`const`) captures store `T` — so we just
3964/// `local.get` the slot.
3965///
3966/// Transitive captures (the outer scope is itself a closure body)
3967/// work uniformly: the outer closure's prologue copied its captures
3968/// into ordinary Wasm locals, so the lookup here resolves to a
3969/// FunctionLocal slot with the right type either way.
3970fn emit_captured_load(
3971    emitter: &mut FunctionEmitter,
3972    _ctx: &CodegenCtx,
3973    c: &crate::CapturedVar,
3974) -> Result<(), crate::compiler_error::CompilerFailure> {
3975    let slot = emitter.local_slot(&c.name.name).ok_or_else(|| {
3976        crate::codegen::internal_failure("captured binding is missing from the outer scope")
3977    })?;
3978    emitter.instruction(Instruction::LocalGet(slot));
3979    Ok(())
3980}
3981
3982fn emit_binary(
3983    emitter: &mut FunctionEmitter,
3984    ctx: &CodegenCtx,
3985    op: BinOp,
3986    lhs: ExprId,
3987    rhs: ExprId,
3988    result_ty: &Type,
3989) -> Result<(), crate::compiler_error::CompilerFailure> {
3990    let admits_never_operand = matches!(
3991        op,
3992        BinOp::Add
3993            | BinOp::Sub
3994            | BinOp::Mul
3995            | BinOp::Div
3996            | BinOp::Rem
3997            | BinOp::Pow
3998            | BinOp::Lt
3999            | BinOp::Gt
4000            | BinOp::Le
4001            | BinOp::Ge
4002    );
4003    if admits_never_operand && try_emit_unreachable_for_never_operand(emitter, ctx, &[lhs, rhs])? {
4004        return Ok(());
4005    }
4006    let _: () = match op {
4007        // `+` dispatches on the result type the typechecker chose: numeric
4008        // operands → `f64.add`, string operands → `string_concat` from the
4009        // prelude. Mixed-type or other operand combinations were rejected
4010        // by the inferer.
4011        BinOp::BitAnd
4012        | BinOp::BitOr
4013        | BinOp::BitXor
4014        | BinOp::Shl
4015        | BinOp::Shr
4016        | BinOp::UnsignedShr => {
4017            if has_numeric_operands(ctx, &[lhs, rhs], result_ty)? {
4018                emit_number_bitwise(emitter, ctx, op, lhs, rhs)?;
4019            } else {
4020                let name = op.bitwise_name().ok_or_else(|| {
4021                    crate::codegen::internal_failure("missing bitwise host operation")
4022                })?;
4023                emit_bitwise_host(emitter, ctx, name, &[lhs, rhs], result_ty)?;
4024            }
4025        }
4026        // A template of constants has a string literal type: concatenate it as
4027        // a `string`.
4028        BinOp::Add => match &result_ty.widen_literal() {
4029            Type::Number => {
4030                emit_primitive_operand(emitter, ctx, lhs)?;
4031                emit_primitive_operand(emitter, ctx, rhs)?;
4032                emitter.instruction(Instruction::F64Add);
4033            }
4034            Type::String => {
4035                emit_primitive_operand(emitter, ctx, lhs)?;
4036                emit_primitive_operand(emitter, ctx, rhs)?;
4037                let idx = ctx
4038                    .symbols
4039                    .prelude_func_idx("string_concat")
4040                    .ok_or_else(|| {
4041                        crate::codegen::internal_failure("string_concat imported from prelude")
4042                    })?;
4043                emitter.instruction(Instruction::Call(idx));
4044            }
4045            // `bigint + bigint` → inline host call.
4046            Type::BigInt => emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "add")?,
4047            other => {
4048                return Err(crate::codegen::internal_failure(format!(
4049                    "typechecker rejects `+` for {other:?}"
4050                )));
4051            }
4052        },
4053        BinOp::Sub | BinOp::Mul | BinOp::Div => {
4054            // bigint operands route to inline host calls.
4055            if result_ty.is_bigint() {
4056                let name = match op {
4057                    BinOp::Sub => "sub",
4058                    BinOp::Mul => "mul",
4059                    BinOp::Div => "div",
4060                    _ => {
4061                        return Err(crate::codegen::internal_failure(
4062                            "invalid compiler emission dispatch",
4063                        ));
4064                    }
4065                };
4066                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, name)?;
4067                return Ok(());
4068            }
4069            if !matches!(result_ty, Type::Number) {
4070                return Err(crate::codegen::internal_failure(
4071                    "numeric operator has a non-number result type",
4072                ));
4073            }
4074            emit_primitive_operand(emitter, ctx, lhs)?;
4075            emit_primitive_operand(emitter, ctx, rhs)?;
4076            let inst = match op {
4077                BinOp::Sub => Instruction::F64Sub,
4078                BinOp::Mul => Instruction::F64Mul,
4079                BinOp::Div => Instruction::F64Div,
4080                _ => {
4081                    return Err(crate::codegen::internal_failure(
4082                        "invalid compiler emission dispatch",
4083                    ));
4084                }
4085            };
4086            emitter.instruction(inst);
4087        }
4088        BinOp::Pow => {
4089            // exponentiation. BigInt routes to the
4090            // `submilli:bigint.pow` host call (which validates the
4091            // exponent is non-negative and fits in u32, else traps).
4092            // Number routes to the prelude-host `Math#pow` function.
4093            if result_ty.is_bigint() {
4094                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "pow")?;
4095                return Ok(());
4096            }
4097            if !matches!(result_ty, Type::Number) {
4098                return Err(crate::codegen::internal_failure(
4099                    "numeric operator has a non-number result type",
4100                ));
4101            }
4102            emit_primitive_operand(emitter, ctx, lhs)?;
4103            emit_primitive_operand(emitter, ctx, rhs)?;
4104            let idx = ctx
4105                .symbols
4106                .func_idx(&crate::runtime::prelude::math::math_key("pow"))
4107                .ok_or_else(|| crate::codegen::internal_failure("Math#pow host import recorded"))?;
4108            emitter.instruction(Instruction::Call(idx));
4109        }
4110        BinOp::Rem => {
4111            // `bigint % bigint` → inline host call.
4112            if result_ty.is_bigint() {
4113                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "mod")?;
4114                return Ok(());
4115            }
4116            // Wasm has no `f64.rem`; lower to JS-truncation modulo using
4117            // `a - trunc(a / b) * b`. Two scratch f64 locals so each operand
4118            // is evaluated exactly once (preserves side-effect order).
4119            let a = emitter.add_anonymous_local(ValType::F64)?;
4120            let b = emitter.add_anonymous_local(ValType::F64)?;
4121            emit_primitive_operand(emitter, ctx, lhs)?;
4122            emitter.instruction(Instruction::LocalSet(a));
4123            emit_primitive_operand(emitter, ctx, rhs)?;
4124            emitter.instruction(Instruction::LocalSet(b));
4125            // Build `a - trunc(a/b)*b` on the stack.
4126            emitter.instruction(Instruction::LocalGet(a));
4127            emitter.instruction(Instruction::LocalGet(a));
4128            emitter.instruction(Instruction::LocalGet(b));
4129            emitter.instruction(Instruction::F64Div);
4130            emitter.instruction(Instruction::F64Trunc);
4131            emitter.instruction(Instruction::LocalGet(b));
4132            emitter.instruction(Instruction::F64Mul);
4133            emitter.instruction(Instruction::F64Sub);
4134        }
4135        BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
4136            // bigint → inline `bigint.cmp` host call; number → f64 ops;
4137            // string → `string_cmp` (signed lexicographic diff) vs 0.
4138            let operand_ty = ctx
4139                .ta
4140                .try_expr(lhs)
4141                .map_err(crate::codegen::arena_failure)?
4142                .ty
4143                .primitive_behavior()
4144                .clone();
4145            if operand_ty.is_bigint() {
4146                emit_bigint_cmp_inline(emitter, ctx, lhs, rhs, op)?;
4147                return Ok(());
4148            }
4149            emit_primitive_operand(emitter, ctx, lhs)?;
4150            emit_primitive_operand(emitter, ctx, rhs)?;
4151            if matches!(operand_ty, Type::String | Type::StringLiteral(_)) {
4152                let idx = ctx.symbols.prelude_func_idx("string_cmp").ok_or_else(|| {
4153                    crate::codegen::internal_failure("string_cmp imported from prelude")
4154                })?;
4155                emitter.instruction(Instruction::Call(idx));
4156                emitter.instruction(Instruction::I32Const(0));
4157                emitter.instruction(match op {
4158                    BinOp::Lt => Instruction::I32LtS,
4159                    BinOp::Gt => Instruction::I32GtS,
4160                    BinOp::Le => Instruction::I32LeS,
4161                    BinOp::Ge => Instruction::I32GeS,
4162                    _ => {
4163                        return Err(crate::codegen::internal_failure(
4164                            "invalid compiler emission dispatch",
4165                        ));
4166                    }
4167                });
4168            } else {
4169                emitter.instruction(match op {
4170                    BinOp::Lt => Instruction::F64Lt,
4171                    BinOp::Gt => Instruction::F64Gt,
4172                    BinOp::Le => Instruction::F64Le,
4173                    BinOp::Ge => Instruction::F64Ge,
4174                    _ => {
4175                        return Err(crate::codegen::internal_failure(
4176                            "invalid compiler emission dispatch",
4177                        ));
4178                    }
4179                });
4180            }
4181        }
4182        BinOp::Eq | BinOp::NotEq => emit_equality(emitter, ctx, op, lhs, rhs)?,
4183        BinOp::And | BinOp::Or => emit_logical(emitter, ctx, op, lhs, rhs, result_ty)?,
4184        BinOp::In => emit_in_operator(emitter, ctx, lhs, rhs)?,
4185        // infer lifts `Binary { op: NullishCoalesce,.. }`
4186        // into the dedicated `TypedExprKind::NullishCoalesce` node, so
4187        // this arm is unreachable in well-typed input.
4188        BinOp::NullishCoalesce => {
4189            return Err(crate::codegen::internal_failure(
4190                "BinOp::NullishCoalesce should be lifted into TypedExprKind::NullishCoalesce by infer",
4191            ));
4192        }
4193    };
4194    Ok(())
4195}
4196
4197// Substantial recursive arms live outside the dispatcher so their temporaries
4198// do not enlarge every arithmetic expression's stack frame.
4199fn emit_equality(
4200    emitter: &mut FunctionEmitter,
4201    ctx: &CodegenCtx,
4202    op: BinOp,
4203    lhs: ExprId,
4204    rhs: ExprId,
4205) -> Result<(), crate::compiler_error::CompilerFailure> {
4206    // Plan 75.8: when either operand is the `null` literal,
4207    // emit the non-null side followed by `ref.is_null`
4208    // (negated for `NotEq`). This covers `x === null` /
4209    // `x !== null` against any nullable value, the primary
4210    // null-narrowing predicate. Both-null collapses to a
4211    // constant.
4212    let lhs_is_null_lit = matches!(
4213        ctx.ta
4214            .try_expr(lhs)
4215            .map_err(crate::codegen::arena_failure)?
4216            .kind,
4217        TypedExprKind::Null
4218    );
4219    let rhs_is_null_lit = matches!(
4220        ctx.ta
4221            .try_expr(rhs)
4222            .map_err(crate::codegen::arena_failure)?
4223            .kind,
4224        TypedExprKind::Null
4225    );
4226    if lhs_is_null_lit && rhs_is_null_lit {
4227        let v: i32 = if matches!(op, BinOp::Eq) { 1 } else { 0 };
4228        emitter.instruction(Instruction::I32Const(v));
4229        return Ok(());
4230    }
4231    if lhs_is_null_lit || rhs_is_null_lit {
4232        let non_null_side = if lhs_is_null_lit { rhs } else { lhs };
4233        let non_null_ty = ctx
4234            .ta
4235            .try_expr(non_null_side)
4236            .map_err(crate::codegen::arena_failure)?
4237            .ty
4238            .clone();
4239        let non_null_val = ctx.symbols.value_type(&non_null_ty)?;
4240        // Plan 75.10 PR 2: when the non-null side has a
4241        // primitive Wasm type (f64 / i32 — e.g., a
4242        // narrowed-to-`Number` after assignment), `ref.is_null`
4243        // would be a validation error. The answer is
4244        // statically known — a primitive can never be null —
4245        // so emit the operand for side effects, drop, then
4246        // push the constant result.
4247        if matches!(non_null_val, ValType::F64 | ValType::I32) {
4248            emit_expr(emitter, ctx, non_null_side)?;
4249            emitter.instruction(Instruction::Drop);
4250            let v: i32 = if matches!(op, BinOp::NotEq) { 1 } else { 0 };
4251            emitter.instruction(Instruction::I32Const(v));
4252            return Ok(());
4253        }
4254        emit_expr(emitter, ctx, non_null_side)?;
4255        emitter.instruction(Instruction::RefIsNull);
4256        if matches!(op, BinOp::NotEq) {
4257            emitter.instruction(Instruction::I32Eqz);
4258        }
4259        return Ok(());
4260    }
4261    let lhs_ty = ctx
4262        .ta
4263        .try_expr(lhs)
4264        .map_err(crate::codegen::arena_failure)?
4265        .ty
4266        .clone();
4267    let rhs_ty = ctx
4268        .ta
4269        .try_expr(rhs)
4270        .map_err(crate::codegen::arena_failure)?
4271        .ty
4272        .clone();
4273    let operand_val = ctx.symbols.value_type(lhs_ty.primitive_behavior())?;
4274    // The typed dispatch below needs both operands in one Wasm shape. One that
4275    // admits null is a `(ref null $Object)` beside the other's f64 or
4276    // `(ref $string)`, and comparable operands need not share a representation
4277    // at all (`t === 1` with `t: T`, `n === e` with `e: {}`). Those pairs box both
4278    // sides to `(ref [null] $Object)` and dispatch through `vtable.equals`.
4279    if may_hold_null(&lhs_ty)
4280        || may_hold_null(&rhs_ty)
4281        || operand_val != ctx.symbols.value_type(rhs_ty.primitive_behavior())?
4282    {
4283        emit_boxed_eq(emitter, ctx, lhs, rhs, op)?;
4284        return Ok(());
4285    }
4286    // bigint equality short-circuits to a direct
4287    // `submilli:bigint.cmp == 0` call — a faster path than the
4288    // generic `ValType::Ref(_)` arm's vtable `equals` dispatch.
4289    if lhs_ty.is_bigint() {
4290        emit_bigint_cmp_eq_inline(emitter, ctx, lhs, rhs, op)?;
4291        return Ok(());
4292    }
4293    let string_idx = ctx.symbols.string_type_idx().ok_or_else(|| {
4294        crate::codegen::internal_failure("string type registered with intrinsics")
4295    })?;
4296    // Operands that share a Wasm `value_type` dispatch off it. The language
4297    // `Type` might be a literal-refined primitive or a literal-only union; at
4298    // the Wasm level those collapse to the same compare path as their base
4299    // primitive (equality is value-level; the literal refinement is a
4300    // typecheck-time restriction, not a runtime distinction).
4301    match operand_val {
4302        ValType::F64 => {
4303            emit_primitive_operand(emitter, ctx, lhs)?;
4304            emit_primitive_operand(emitter, ctx, rhs)?;
4305            emitter.instruction(if matches!(op, BinOp::Eq) {
4306                Instruction::F64Eq
4307            } else {
4308                Instruction::F64Ne
4309            });
4310        }
4311        ValType::I32 => {
4312            emit_expr(emitter, ctx, lhs)?;
4313            emit_expr(emitter, ctx, rhs)?;
4314            emitter.instruction(if matches!(op, BinOp::Eq) {
4315                Instruction::I32Eq
4316            } else {
4317                Instruction::I32Ne
4318            });
4319        }
4320        ValType::Ref(RefType {
4321            heap_type: HeapType::Concrete(idx),
4322            ..
4323        }) if idx == string_idx => {
4324            emit_expr(emitter, ctx, lhs)?;
4325            emit_expr(emitter, ctx, rhs)?;
4326            let func = ctx.symbols.prelude_func_idx("string_eq").ok_or_else(|| {
4327                crate::codegen::internal_failure("string_eq imported from prelude")
4328            })?;
4329            emitter.instruction(Instruction::Call(func));
4330            if matches!(op, BinOp::NotEq) {
4331                emitter.instruction(Instruction::I32Eqz);
4332            }
4333        }
4334        ValType::Ref(_) => {
4335            // Object / array / closure / generic-erased (all
4336            // subtypes of `$Object`): load lhs.vtable.equals
4337            // and call it. Each subtype's body runs the
4338            // structural compare for its own shape; nested
4339            // objects / arrays recurse through their own
4340            // vtable.equals slots.
4341            //
4342            // Enum values fall here too — they share the
4343            // `$BoxedNumber` / `$string` shapes whose
4344            // vtable `equals` slot is wired by the prelude.
4345            emit_expr(emitter, ctx, lhs)?;
4346            cast::emit_box(emitter, ctx, &lhs_ty)?;
4347            emit_expr(emitter, ctx, rhs)?;
4348            cast::emit_box(emitter, ctx, &rhs_ty)?;
4349            emit_vtable_equality(emitter, ctx, op);
4350        }
4351        other => {
4352            return Err(crate::codegen::internal_failure(format!(
4353                "typechecker rejects equality on Wasm value-type `{other:?}`"
4354            )));
4355        }
4356    }
4357    Ok(())
4358}
4359
4360fn emit_logical(
4361    emitter: &mut FunctionEmitter,
4362    ctx: &CodegenCtx,
4363    op: BinOp,
4364    lhs: ExprId,
4365    rhs: ExprId,
4366    result_ty: &Type,
4367) -> Result<(), crate::compiler_error::CompilerFailure> {
4368    // JS value-returning short-circuit, mirroring the `??` lowering:
4369    // stash the LHS in an anonymous local, truthiness-test the teed
4370    // copy, then either evaluate the RHS or replay the kept LHS —
4371    // each branch coerced into the result union's slot. `&&` keeps
4372    // the LHS when falsy, `||` when truthy.
4373    let result_val = ctx.symbols.value_type(result_ty)?;
4374    let lhs_ty = ctx
4375        .ta
4376        .try_expr(lhs)
4377        .map_err(crate::codegen::arena_failure)?
4378        .ty
4379        .clone();
4380    let lhs_val = ctx.symbols.value_type(&lhs_ty)?;
4381    let lhs_is_ref = matches!(lhs_val, ValType::Ref(_));
4382    let tmp = emitter.add_anonymous_local(lhs_val)?;
4383    emit_expr(emitter, ctx, lhs)?;
4384    emitter.instruction(Instruction::LocalTee(tmp));
4385    crate::codegen::function_emitter::cast::emit_condition_to_i32(emitter, ctx, &lhs_ty)?;
4386    let emit_rhs_branch =
4387        |emitter: &mut FunctionEmitter| -> Result<(), crate::compiler_error::CompilerFailure> {
4388            let rhs_ty = ctx
4389                .ta
4390                .try_expr(rhs)
4391                .map_err(crate::codegen::arena_failure)?
4392                .ty
4393                .clone();
4394            emit_expr(emitter, ctx, rhs)?;
4395            crate::codegen::function_emitter::cast::emit_coerce_to_slot(
4396                emitter, ctx, &rhs_ty, result_ty,
4397            )?;
4398            Ok(())
4399        };
4400    // An LHS narrowed to always-truthy (`&&`) or always-falsy (`||`)
4401    // is never the result, and the result slot has no room for it:
4402    // `c && n()` under `c === true` is a bare f64.
4403    let kept_lhs_ty = match op {
4404        BinOp::And => falsy_part(&lhs_ty),
4405        _ => truthy_part(&lhs_ty),
4406    };
4407    let lhs_is_never_kept = matches!(kept_lhs_ty.lowered(), Type::Never);
4408    let emit_kept_lhs_branch = |emitter: &mut FunctionEmitter| {
4409        if lhs_is_never_kept {
4410            emitter.instruction(Instruction::Unreachable);
4411            return Ok(());
4412        }
4413        emitter.instruction(Instruction::LocalGet(tmp));
4414        if lhs_is_ref {
4415            // Ref-repr LHS: cast into the result slot's form —
4416            // `ref.as_non_null` lift, downcast, or unbox as needed
4417            // (same contract as the `??` kept branch).
4418            crate::codegen::function_emitter::cast::emit_cast_to(emitter, ctx, result_ty)?;
4419        } else {
4420            crate::codegen::function_emitter::cast::emit_coerce_to_slot(
4421                emitter, ctx, &lhs_ty, result_ty,
4422            )?;
4423        }
4424        Ok::<(), crate::compiler_error::CompilerFailure>(())
4425    };
4426    emitter.emit_if(BlockType::Result(result_val));
4427    match op {
4428        BinOp::And => emit_rhs_branch(emitter)?,
4429        _ => emit_kept_lhs_branch(emitter)?,
4430    }
4431    emitter.emit_else();
4432    match op {
4433        BinOp::And => emit_kept_lhs_branch(emitter)?,
4434        _ => emit_rhs_branch(emitter)?,
4435    }
4436    emitter.emit_end();
4437    Ok(())
4438}
4439
4440/// Evaluates `operands` in order and ends in `unreachable` when any of them is
4441/// `never`, returning whether it did. The typechecker accepts arithmetic on a
4442/// `never` operand because no value of it exists, but that operand's slot is a
4443/// reference no numeric instruction takes, so the operator itself isn't emitted.
4444fn try_emit_unreachable_for_never_operand(
4445    emitter: &mut FunctionEmitter,
4446    ctx: &CodegenCtx,
4447    operands: &[ExprId],
4448) -> Result<bool, crate::compiler_error::CompilerFailure> {
4449    let mut has_never_operand = false;
4450    for &operand in operands {
4451        let ty = &ctx
4452            .ta
4453            .try_expr(operand)
4454            .map_err(crate::codegen::arena_failure)?
4455            .ty;
4456        has_never_operand |= matches!(ty.lowered(), Type::Never);
4457    }
4458    if !has_never_operand {
4459        return Ok(false);
4460    }
4461    for &operand in operands {
4462        emit_expr(emitter, ctx, operand)?;
4463        emitter.instruction(Instruction::Drop);
4464    }
4465    emitter.instruction(Instruction::Unreachable);
4466    Ok(true)
4467}
4468
4469fn emit_primitive_operand(
4470    emitter: &mut FunctionEmitter,
4471    ctx: &CodegenCtx,
4472    expr: ExprId,
4473) -> Result<(), crate::compiler_error::CompilerFailure> {
4474    emit_expr(emitter, ctx, expr)?;
4475    let ty = &ctx
4476        .ta
4477        .try_expr(expr)
4478        .map_err(crate::codegen::arena_failure)?
4479        .ty;
4480    let _: () = if matches!(ty.primitive_behavior(), Type::Number)
4481        && ctx.symbols.value_type(ty)? != ValType::F64
4482    {
4483        super::cast::emit_cast_to(emitter, ctx, &Type::Number)?;
4484    };
4485    Ok(())
4486}
4487
4488/// `"field" in obj` codegen. Walks the receiver's
4489/// `$ObjectShape.field_names` array twice — once with `ref.eq` (the
4490/// same-module fast path, sharing the per-name string global) and
4491/// once with the prelude `$string_eq` (the cross-module slow path).
4492/// Returns an i32 boolean (0/1) on the stack.
4493///
4494/// Presence uses the same slot scan as reads, without invoking accessors.
4495fn emit_in_operator(
4496    emitter: &mut FunctionEmitter,
4497    ctx: &CodegenCtx,
4498    lhs: ExprId,
4499    rhs: ExprId,
4500) -> Result<(), crate::compiler_error::CompilerFailure> {
4501    emit_expr(emitter, ctx, lhs)?;
4502    crate::codegen::cast_check::emit_operation_cast_on_stack(
4503        emitter,
4504        ctx,
4505        &ctx.ta
4506            .try_expr(lhs)
4507            .map_err(crate::codegen::arena_failure)?
4508            .ty,
4509        &Type::String,
4510    )?;
4511    let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
4512    emitter.instruction(Instruction::LocalSet(key));
4513    emit_expr(emitter, ctx, rhs)?;
4514    cast::emit_box(
4515        emitter,
4516        ctx,
4517        &ctx.ta
4518            .try_expr(rhs)
4519            .map_err(crate::codegen::arena_failure)?
4520            .ty,
4521    )?;
4522    emitter.instruction(Instruction::LocalGet(key));
4523    let Some(symbol) = ctx.require(
4524        ctx.symbols.prelude_func_idx("ObjectConstructor##hasField"),
4525        "presence helper imported",
4526    ) else {
4527        return Ok(());
4528    };
4529    emitter.instruction(Instruction::Call(symbol));
4530    Ok(())
4531}
4532
4533/// Emit the constructor field-setup sequence for `mangled`: each own-field
4534/// initializer (and, later, parameter-property copy) as `this.field = value`
4535/// into the object-fields payload, in recorded order. Runs after `super(...)`
4536/// (or at the top of a base class's init fn), so `this`/parent fields are live.
4537pub(crate) fn emit_class_field_setup(
4538    emitter: &mut FunctionEmitter,
4539    ctx: &CodegenCtx,
4540    mangled: &crate::MangledName,
4541) -> Result<(), crate::compiler_error::CompilerFailure> {
4542    emit_class_field_setup_phase(emitter, ctx, mangled, FieldSetupPhase::All)
4543}
4544
4545#[derive(Clone, Copy)]
4546pub(crate) enum FieldSetupPhase {
4547    All,
4548    Initializers,
4549    ParameterProperties,
4550}
4551
4552pub(crate) fn emit_class_field_setup_phase(
4553    emitter: &mut FunctionEmitter,
4554    ctx: &CodegenCtx,
4555    mangled: &crate::MangledName,
4556    phase: FieldSetupPhase,
4557) -> Result<(), crate::compiler_error::CompilerFailure> {
4558    use crate::codegen::symbol_table::FieldSetup;
4559    let setup: Vec<FieldSetup> = match ctx.symbols.class_field_setup(mangled) {
4560        Some(s) if !s.is_empty() => s.to_vec(),
4561        _ => return Ok(()),
4562    };
4563    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
4564        crate::codegen::internal_failure("class struct type recorded in classes::emit")
4565    })?;
4566    let intrinsics = ctx
4567        .symbols
4568        .intrinsic_type_indices()
4569        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4570    let this = emitter.this_local().ok_or_else(|| {
4571        crate::codegen::internal_failure("field setup runs inside a constructor init fn")
4572    })?;
4573    for step in &setup {
4574        let parameter_property = matches!(step, FieldSetup::ParamCopy { .. });
4575        if matches!(phase, FieldSetupPhase::Initializers) && parameter_property
4576            || matches!(phase, FieldSetupPhase::ParameterProperties) && !parameter_property
4577        {
4578            continue;
4579        }
4580        let field = match step {
4581            FieldSetup::Init { field, .. }
4582            | FieldSetup::ParamCopy { field, .. }
4583            | FieldSetup::Reset { field } => field,
4584        };
4585        let slot = ctx
4586            .symbols
4587            .class_field_slot(mangled, field)
4588            .ok_or_else(|| {
4589                crate::codegen::internal_failure("class field slot recorded in classes::emit")
4590            })?;
4591        match step {
4592            FieldSetup::Init { value, .. } => {
4593                let value_ty = ctx
4594                    .ta
4595                    .try_expr(*value)
4596                    .map_err(crate::codegen::arena_failure)?
4597                    .ty
4598                    .clone();
4599                emit_expr(emitter, ctx, *value)?;
4600                crate::codegen::function_emitter::cast::emit_box(emitter, ctx, &value_ty)?;
4601            }
4602            FieldSetup::ParamCopy { param, .. } => {
4603                let local = emitter.require_local_slot(&param.name.name)?;
4604                if param.boxed {
4605                    let box_type = ctx.symbols.box_type_idx(&param.ty)?.ok_or_else(|| {
4606                        crate::codegen::internal_failure(
4607                            "constructor parameter cell is not registered",
4608                        )
4609                    })?;
4610                    super::binding_cells::read(emitter, ctx, local, box_type)?;
4611                    cast::emit_unerase(emitter, ctx, &param.ty)?;
4612                } else {
4613                    emitter.instruction(Instruction::LocalGet(local));
4614                }
4615                cast::emit_box(emitter, ctx, &param.ty)?;
4616            }
4617            FieldSetup::Reset { .. } => {
4618                emit_undefined(emitter, ctx)?;
4619            }
4620        }
4621        let stored = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
4622        emitter.instruction(Instruction::LocalSet(stored));
4623        emitter.instruction(Instruction::LocalGet(this));
4624        emitter.instruction(Instruction::StructGet {
4625            struct_type_index: struct_idx,
4626            field_index: 2,
4627        });
4628        emitter.instruction(Instruction::I32Const(slot as i32));
4629        emitter.instruction(Instruction::LocalGet(stored));
4630        emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
4631        let index = emitter.add_anonymous_local(ValType::I32)?;
4632        emitter.instruction(Instruction::I32Const(slot as i32));
4633        emitter.instruction(Instruction::LocalSet(index));
4634        crate::codegen::field_names::emit_mark_present(emitter, ctx, this, index)?;
4635    }
4636    Ok(())
4637}
4638
4639/// Push the payload slot at `index_local` from an `$ObjectShape`-typed
4640/// receiver. Slot 2 of the shape header is the fields array; the caller owns
4641/// proving the index is in range.
4642pub(crate) fn emit_field_slot_get(
4643    emitter: &mut FunctionEmitter,
4644    intrinsics: crate::codegen::intrinsics::IntrinsicTypeIndices,
4645    object_local: u32,
4646    index_local: u32,
4647) {
4648    emitter.instruction(Instruction::LocalGet(object_local));
4649    emitter.instruction(Instruction::StructGet {
4650        struct_type_index: intrinsics.object_shape,
4651        field_index: 2,
4652    });
4653    emitter.instruction(Instruction::LocalGet(index_local));
4654    emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
4655}
4656
4657pub(crate) fn emit_object_field_read_by_name(
4658    emitter: &mut FunctionEmitter,
4659    ctx: &CodegenCtx,
4660    object_local: u32,
4661    name_global: u32,
4662) {
4663    ctx.latch(emit_object_field_read_by_name_checked(
4664        emitter,
4665        ctx,
4666        object_local,
4667        name_global,
4668    ));
4669}
4670
4671fn emit_object_field_read_by_name_checked(
4672    emitter: &mut FunctionEmitter,
4673    ctx: &CodegenCtx,
4674    object_local: u32,
4675    name_global: u32,
4676) -> Result<(), crate::compiler_error::CompilerFailure> {
4677    let intrinsics = ctx
4678        .symbols
4679        .intrinsic_type_indices()
4680        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4681    let Some(index_local) = ctx.latch(emitter.add_anonymous_local(ValType::I32)) else {
4682        return Ok(());
4683    };
4684    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
4685    emitter.instruction(Instruction::LocalSet(index_local));
4686    emitter.emit_block(BlockType::Result(ValType::Ref(RefType {
4687        nullable: true,
4688        heap_type: HeapType::Concrete(intrinsics.object),
4689    })));
4690    emitter.instruction(Instruction::LocalGet(index_local));
4691    emitter.instruction(Instruction::I32Const(0));
4692    emitter.instruction(Instruction::I32LtS);
4693    emitter.emit_if(BlockType::Empty);
4694    emit_undefined(emitter, ctx)?;
4695    emitter.instruction(Instruction::Br(1));
4696    emitter.emit_end();
4697    emitter.instruction(Instruction::LocalGet(object_local));
4698    emitter.instruction(Instruction::StructGet {
4699        struct_type_index: intrinsics.object_shape,
4700        field_index: 2,
4701    });
4702    emitter.instruction(Instruction::LocalGet(index_local));
4703    emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
4704    emitter.emit_end();
4705
4706    Ok(())
4707}
4708
4709pub(crate) fn emit_object_field_write_by_name(
4710    emitter: &mut FunctionEmitter,
4711    ctx: &CodegenCtx,
4712    object_local: u32,
4713    name_global: u32,
4714) -> Result<(), crate::compiler_error::CompilerFailure> {
4715    let intrinsics = ctx
4716        .symbols
4717        .intrinsic_type_indices()
4718        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4719    let value_local = emitter.add_anonymous_local(ValType::Ref(RefType {
4720        nullable: true,
4721        heap_type: HeapType::Concrete(intrinsics.object),
4722    }))?;
4723    let index_local = emitter.add_anonymous_local(ValType::I32)?;
4724    emitter.instruction(Instruction::LocalSet(value_local));
4725    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
4726    emitter.instruction(Instruction::LocalTee(index_local));
4727    emitter.instruction(Instruction::I32Const(0));
4728    emitter.instruction(Instruction::I32GeS);
4729    emitter.emit_if(BlockType::Empty);
4730    emitter.instruction(Instruction::LocalGet(object_local));
4731    emitter.instruction(Instruction::StructGet {
4732        struct_type_index: intrinsics.object_shape,
4733        field_index: 2,
4734    });
4735    emitter.instruction(Instruction::LocalGet(index_local));
4736    emitter.instruction(Instruction::LocalGet(value_local));
4737    emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
4738    crate::codegen::field_names::emit_mark_present(emitter, ctx, object_local, index_local)?;
4739    emitter.emit_else();
4740    emitter.instruction(Instruction::LocalGet(object_local));
4741    emitter.instruction(Instruction::GlobalGet(name_global));
4742    emitter.instruction(Instruction::LocalGet(value_local));
4743    let insert = ctx
4744        .symbols
4745        .prelude_func_idx("ObjectConstructor##insertField")
4746        .ok_or_else(|| {
4747            crate::codegen::internal_failure("field insertion helper is not imported")
4748        })?;
4749    emitter.instruction(Instruction::Call(insert));
4750    emitter.emit_end();
4751    Ok(())
4752}
4753
4754/// Whether a shaped property read may emit its accessor branch, which scans for
4755/// `getter_name` and so needs its `$string` global. Always true today —
4756/// `analysis::note_shaped_property_access` interns the name for every access it
4757/// classifies as shaped, which is exactly the set reaching here. It stays as a
4758/// fail-soft guard against an unclassified receiver, which would otherwise panic
4759/// on the missing lookup.
4760pub(crate) fn accessor_branch_emittable(ctx: &CodegenCtx, getter_name: &str) -> bool {
4761    ctx.symbols
4762        .field_name_string_global_idx(getter_name)
4763        .is_some()
4764}
4765
4766/// Pushes i32 1 if `object_local` is backed by the accessor `accessor_name`, 0
4767/// otherwise: the name scan finds a slot, the receiver's vtable is a
4768/// `$ClassVTable` (only a class declares accessors), and that slot holds a
4769/// closure of the accessor's shape. Callers must have checked the name has a
4770/// string global.
4771///
4772/// All three, because `get <prop>` is an ordinary string and a value can carry a
4773/// *data* field spelled exactly that — `JSON.parse('{"get x": 5}') as Foo` is
4774/// enough, and an object literal can even store a matching closure under it —
4775/// while the branch this gates goes straight on to `ref.cast` the slot to the
4776/// accessor's closure type. Anything else falls through to the data / absent
4777/// arms.
4778///
4779/// Returns the local holding the accessor slot's index, valid on the branch
4780/// where this pushed 1 — so the dispatch that follows can read the slot directly
4781/// instead of scanning the same name a second time.
4782pub(crate) fn emit_is_accessor_backed(
4783    emitter: &mut FunctionEmitter,
4784    ctx: &CodegenCtx,
4785    object_local: u32,
4786    accessor_name: &str,
4787    kind: crate::AccessorKind,
4788) -> Result<u32, crate::compiler_error::CompilerFailure> {
4789    let intrinsics = ctx
4790        .symbols
4791        .intrinsic_type_indices()
4792        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4793    let accessor_global = ctx
4794        .symbols
4795        .field_name_string_global_idx(accessor_name)
4796        .ok_or_else(|| {
4797            crate::codegen::internal_failure("caller checked the accessor name has a string global")
4798        })?;
4799    let closure_struct_idx = ctx
4800        .symbols
4801        .closure_struct_type_idx(crate::codegen::classes::accessor_closure_sig(kind))
4802        .ok_or_else(|| {
4803            crate::codegen::internal_failure(
4804                "accessor closure struct collected by analysis::note_shaped_property_access",
4805            )
4806        })?;
4807    let index_local = emitter.add_anonymous_local(ValType::I32)?;
4808    emit_field_index_by_name(emitter, ctx, object_local, accessor_global, true);
4809    emitter.instruction(Instruction::LocalTee(index_local));
4810    emitter.instruction(Instruction::I32Const(0));
4811    emitter.instruction(Instruction::I32GeS);
4812    emitter.instruction(Instruction::LocalGet(object_local));
4813    emitter.instruction(Instruction::StructGet {
4814        struct_type_index: intrinsics.object,
4815        field_index: 0,
4816    });
4817    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
4818        intrinsics.class_vtable,
4819    )));
4820    emitter.instruction(Instruction::I32And);
4821    emitter.emit_if(BlockType::Result(ValType::I32));
4822    emitter.instruction(Instruction::LocalGet(object_local));
4823    emitter.instruction(Instruction::StructGet {
4824        struct_type_index: intrinsics.object_shape,
4825        field_index: 1,
4826    });
4827    emitter.instruction(Instruction::LocalGet(index_local));
4828    emitter.instruction(Instruction::ArrayGet(intrinsics.field_names));
4829    crate::codegen::field_names::emit_name_is_accessor(emitter, ctx)?;
4830    emit_field_slot_get(emitter, intrinsics, object_local, index_local);
4831    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
4832        closure_struct_idx,
4833    )));
4834    emitter.instruction(Instruction::I32And);
4835    emitter.emit_else();
4836    emitter.instruction(Instruction::I32Const(0));
4837    emitter.emit_end();
4838    Ok(index_local)
4839}
4840
4841/// Read property `prop_name` from an `$ObjectShape`-typed receiver, accessor-aware:
4842/// a data field reads its payload slot; an accessor property invokes its
4843/// `get <prop>` method closure; a property with neither slot is absent and reads
4844/// `null`. The choice is made at runtime, since an interface property may be
4845/// backed by a field on one impl, an accessor on another, and nothing at all on a
4846/// third that reached the interface type through an optional member. Deciding it
4847/// on the data-slot miss alone cannot separate the last two, and traps on
4848/// whichever it does not pick. Leaves the value on the stack at `field_ty`'s
4849/// value type.
4850pub(crate) fn emit_object_property_read(
4851    emitter: &mut FunctionEmitter,
4852    ctx: &CodegenCtx,
4853    object_local: u32,
4854    prop_name: &str,
4855    field_ty: &Type,
4856) -> Result<(), crate::compiler_error::CompilerFailure> {
4857    emit_object_property_read_as(emitter, ctx, object_local, prop_name, field_ty, field_ty)?;
4858    Ok(())
4859}
4860
4861fn emit_object_property_read_as(
4862    emitter: &mut FunctionEmitter,
4863    ctx: &CodegenCtx,
4864    object_local: u32,
4865    prop_name: &str,
4866    field_ty: &Type,
4867    check_ty: &Type,
4868) -> Result<(), crate::compiler_error::CompilerFailure> {
4869    let getter = crate::codegen::classes::accessor_getter_name(prop_name);
4870    let name_global = ctx
4871        .symbols
4872        .field_name_string_global_idx(prop_name)
4873        .ok_or_else(|| {
4874            crate::codegen::internal_failure(
4875                "per-name string global recorded during field-name-strings emission",
4876            )
4877        })?;
4878    if !accessor_branch_emittable(ctx, &getter) {
4879        emit_object_field_read_by_name(emitter, ctx, object_local, name_global);
4880        emit_dynamic_narrowed_property_read(
4881            emitter,
4882            ctx,
4883            object_local,
4884            prop_name,
4885            field_ty,
4886            check_ty,
4887        )?;
4888        return Ok(());
4889    }
4890    let intrinsics = ctx
4891        .symbols
4892        .intrinsic_type_indices()
4893        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4894    let result_vt = ctx.symbols.value_type(field_ty)?;
4895    let index_local = emitter.add_anonymous_local(ValType::I32)?;
4896    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
4897    emitter.instruction(Instruction::LocalSet(index_local));
4898    emitter.instruction(Instruction::LocalGet(index_local));
4899    emitter.instruction(Instruction::I32Const(0));
4900    emitter.instruction(Instruction::I32GeS);
4901    emitter.emit_if(BlockType::Result(result_vt));
4902    // data slot
4903    emit_field_slot_get(emitter, intrinsics, object_local, index_local);
4904    emit_dynamic_narrowed_property_read(emitter, ctx, object_local, prop_name, field_ty, check_ty)?;
4905    emitter.emit_else();
4906    let getter_slot_local = emit_is_accessor_backed(
4907        emitter,
4908        ctx,
4909        object_local,
4910        &getter,
4911        crate::AccessorKind::Get,
4912    )?;
4913    emitter.emit_if(BlockType::Result(result_vt));
4914    // accessor slot
4915    emitter.instruction(Instruction::LocalGet(object_local));
4916    emit_interface_method_via_shape_with_receiver_on_stack(
4917        emitter,
4918        ctx,
4919        &getter,
4920        &[],
4921        field_ty,
4922        Some(getter_slot_local),
4923    )?;
4924    emitter.emit_else();
4925    // neither: absent — an optional member the value never materialised, so
4926    // `field_ty` admits null and the cast is a widen. An accessor-backed value
4927    // does not reach here even when the accessor is declared in another module:
4928    // the `get <prop>` name is interned off the *access*, not off any accessor
4929    // declaration this module can see (`analysis::note_shaped_property_access`).
4930    emit_undefined(emitter, ctx)?;
4931    cast::emit_cast_to(emitter, ctx, field_ty)?;
4932    emitter.emit_end();
4933    emitter.emit_end();
4934    Ok(())
4935}
4936
4937fn emit_dynamic_narrowed_property_read(
4938    emitter: &mut FunctionEmitter,
4939    ctx: &CodegenCtx,
4940    object_local: u32,
4941    prop_name: &str,
4942    field_ty: &Type,
4943    check_ty: &Type,
4944) -> Result<(), crate::compiler_error::CompilerFailure> {
4945    let checks: Vec<_> = ctx
4946        .symbols
4947        .class_field_narrowing_checks(prop_name)
4948        .filter_map(|(class, check)| {
4949            ctx.symbols
4950                .class_vtable_global_idx(class)
4951                .map(|vtable| (vtable, check))
4952        })
4953        .collect();
4954    if checks.is_empty() {
4955        cast::emit_cast_to(emitter, ctx, field_ty)?;
4956        return Ok(());
4957    }
4958
4959    let intrinsics = ctx
4960        .symbols
4961        .intrinsic_type_indices()
4962        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4963    let raw = emitter.add_anonymous_local(ValType::Ref(RefType {
4964        nullable: true,
4965        heap_type: HeapType::Concrete(intrinsics.object),
4966    }))?;
4967    emitter.instruction(Instruction::LocalSet(raw));
4968    emit_dynamic_narrowing_check(
4969        emitter,
4970        ctx,
4971        object_local,
4972        raw,
4973        field_ty,
4974        check_ty,
4975        &checks,
4976        0,
4977    )?;
4978    Ok(())
4979}
4980
4981#[allow(clippy::too_many_arguments)]
4982fn emit_dynamic_narrowing_check(
4983    emitter: &mut FunctionEmitter,
4984    ctx: &CodegenCtx,
4985    object_local: u32,
4986    raw: u32,
4987    field_ty: &Type,
4988    check_ty: &Type,
4989    checks: &[(u32, &crate::FieldNarrowingCheck)],
4990    index: usize,
4991) -> Result<(), crate::compiler_error::CompilerFailure> {
4992    let Some((vtable, check)) = checks.get(index) else {
4993        emitter.instruction(Instruction::LocalGet(raw));
4994        cast::emit_cast_to(emitter, ctx, field_ty)?;
4995        return Ok(());
4996    };
4997    emitter.instruction(Instruction::LocalGet(object_local));
4998    crate::codegen::function_emitter::cast::emit_nominal_instance_test(emitter, ctx, *vtable)?;
4999    emitter.emit_if(BlockType::Result(ctx.symbols.value_type(field_ty)?));
5000    emitter.instruction(Instruction::LocalGet(raw));
5001    crate::codegen::cast_check::emit_narrowed_field_read_as(
5002        emitter, ctx, check, field_ty, check_ty,
5003    )?;
5004    emitter.emit_else();
5005    emit_dynamic_narrowing_check(
5006        emitter,
5007        ctx,
5008        object_local,
5009        raw,
5010        field_ty,
5011        check_ty,
5012        checks,
5013        index + 1,
5014    )?;
5015    emitter.emit_end();
5016    Ok(())
5017}
5018
5019/// Write `value` to property `prop` on an `$ObjectShape`-typed receiver,
5020/// accessor-aware: a data field writes its payload slot; an accessor property
5021/// invokes its `set <prop>` method closure; a property backed by a getter with no
5022/// setter throws, since the target exists but is read-only; a property with none
5023/// of the three gets a new data slot. `value` is evaluated exactly once.
5024pub(crate) fn emit_object_property_write(
5025    emitter: &mut FunctionEmitter,
5026    ctx: &CodegenCtx,
5027    object_local: u32,
5028    prop: &Ident,
5029    value: ExprId,
5030) -> Result<(), crate::compiler_error::CompilerFailure> {
5031    emit_object_property_write_value(
5032        emitter,
5033        ctx,
5034        object_local,
5035        prop,
5036        &ShapeArgument::Expression(value),
5037    )?;
5038    Ok(())
5039}
5040
5041fn emit_object_property_write_value(
5042    emitter: &mut FunctionEmitter,
5043    ctx: &CodegenCtx,
5044    object_local: u32,
5045    prop: &Ident,
5046    value: &ShapeArgument,
5047) -> Result<(), crate::compiler_error::CompilerFailure> {
5048    let prop_name = prop.name.as_str();
5049    let setter = crate::codegen::classes::accessor_setter_name(prop_name);
5050    let getter = crate::codegen::classes::accessor_getter_name(prop_name);
5051    let has_setter_name = ctx.symbols.field_name_string_global_idx(&setter).is_some();
5052    let has_getter_name = ctx.symbols.field_name_string_global_idx(&getter).is_some();
5053    let name_global = ctx
5054        .symbols
5055        .field_name_string_global_idx(prop_name)
5056        .ok_or_else(|| {
5057            crate::codegen::internal_failure(
5058                "per-name string global recorded during field-name-strings emission",
5059            )
5060        })?;
5061    // Fail-soft twin of [`accessor_branch_emittable`], asked of both halves:
5062    // reaching here means analysis classified the access, which interns both
5063    // accessor names — so neither name can actually be missing.
5064    if !has_setter_name && !has_getter_name {
5065        let value_ty = value.ty(ctx)?;
5066        value.emit(emitter, ctx)?;
5067        cast::emit_box(emitter, ctx, &value_ty)?;
5068        emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
5069        return Ok(());
5070    }
5071    let index_local = emitter.add_anonymous_local(ValType::I32)?;
5072    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
5073    emitter.instruction(Instruction::LocalSet(index_local));
5074    emitter.instruction(Instruction::LocalGet(index_local));
5075    emitter.instruction(Instruction::I32Const(0));
5076    emitter.instruction(Instruction::I32GeS);
5077    emitter.emit_if(BlockType::Empty);
5078    // Evaluate the RHS before loading the payload: it may insert another
5079    // field and replace the receiver's backing arrays.
5080    let value_ty = value.ty(ctx)?;
5081    value.emit(emitter, ctx)?;
5082    cast::emit_box(emitter, ctx, &value_ty)?;
5083    emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
5084    emitter.emit_else();
5085    // One nested `if` per accessor the program declares, innermost `else` being
5086    // the absent case — so the arm list *is* the nesting depth. An accessor the
5087    // program never declares is skipped: its name has no string global to scan
5088    // for, and no value can be backed by it.
5089    let arms: Vec<(&str, AccessorWrite)> = [
5090        has_setter_name.then_some((setter.as_str(), AccessorWrite::Setter)),
5091        has_getter_name.then_some((getter.as_str(), AccessorWrite::ReadOnly)),
5092    ]
5093    .into_iter()
5094    .flatten()
5095    .collect();
5096    for (accessor, arm) in &arms {
5097        let slot_index_local =
5098            emit_is_accessor_backed(emitter, ctx, object_local, accessor, arm.scanned_kind())?;
5099        emitter.emit_if(BlockType::Empty);
5100        match arm {
5101            AccessorWrite::Setter => {
5102                emitter.instruction(Instruction::LocalGet(object_local));
5103                emit_shape_method_with_arguments(
5104                    emitter,
5105                    ctx,
5106                    accessor,
5107                    std::slice::from_ref(value),
5108                    &Type::Void,
5109                    Some(slot_index_local),
5110                )?;
5111                emitter.instruction(Instruction::Drop);
5112            }
5113            // A getter with no setter: the property is there and is read-only.
5114            // Discarding the write here would lose it silently.
5115            AccessorWrite::ReadOnly => {
5116                value.emit(emitter, ctx)?;
5117                emitter.instruction(Instruction::Drop);
5118                // The value's own span was recorded last; point the backtrace at
5119                // the property instead, which is what the write failed on.
5120                emitter.record_span(prop.span);
5121                crate::codegen::throw::emit_type_error_throw(
5122                    emitter,
5123                    ctx,
5124                    crate::codegen::throw::READ_ONLY_PROPERTY_MESSAGE,
5125                );
5126            }
5127        }
5128        emitter.emit_else();
5129    }
5130    let value_ty = value.ty(ctx)?;
5131    value.emit(emitter, ctx)?;
5132    cast::emit_box(emitter, ctx, &value_ty)?;
5133    emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
5134    for _ in &arms {
5135        emitter.emit_end();
5136    }
5137    emitter.emit_end();
5138    Ok(())
5139}
5140
5141/// What an array read does when the element is missing.
5142#[derive(Clone, Copy)]
5143struct MissingElement {
5144    /// It gives `undefined`, where the read was typed with room for it.
5145    gives_undefined: bool,
5146    /// The receiver is a regex match read at a type with no room for
5147    /// `undefined`, so a group that did not take part throws too.
5148    throws_on_absent_group: bool,
5149    /// A present flag to set: 1 when the element is found.
5150    present_flag: Option<u32>,
5151}
5152
5153/// `<receiver>[idx]` with the `(ref $Array)` receiver already on the stack:
5154///
5155/// ```text
5156///   struct.get $Array 1; local.set $raw
5157///   <idx>; i32.trunc_sat_f64_u; local.set $i
5158///   bounds-check $i against len($raw)   ;; throws MissingValueError on a miss
5159///   local.get $raw; local.get $i; array.get $rawArray
5160///   emit_cast_to(result_ty)             ;; unboxes if primitive
5161/// ```
5162///
5163/// Shared by the plain `IndexAccess` lowering and the optional chain's `Index`
5164/// step, which differ only in how the receiver gets onto the stack. Uint8Array
5165/// has its own packed-storage path and does not come through here.
5166fn emit_bounds_checked_index_with_receiver_on_stack(
5167    emitter: &mut FunctionEmitter,
5168    ctx: &CodegenCtx,
5169    idx: ExprId,
5170    result_ty: &Type,
5171    missing: MissingElement,
5172    receiver_name: &str,
5173) -> Result<(), crate::compiler_error::CompilerFailure> {
5174    let MissingElement {
5175        gives_undefined: allow_missing,
5176        throws_on_absent_group: match_groups,
5177        present_flag,
5178    } = missing;
5179    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
5180        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
5181    })?;
5182    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
5183        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
5184    })?;
5185    let raw_arr_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5186        nullable: false,
5187        heap_type: HeapType::Concrete(raw_array_idx),
5188    }))?;
5189    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5190    emitter.instruction(Instruction::LocalSet(receiver));
5191    emit_expr(emitter, ctx, idx)?;
5192    let key = emitter.add_anonymous_local(
5193        ctx.symbols.value_type(
5194            &ctx.ta
5195                .try_expr(idx)
5196                .map_err(crate::codegen::arena_failure)?
5197                .ty,
5198        )?,
5199    )?;
5200    emitter.instruction(Instruction::LocalSet(key));
5201    emitter.instruction(Instruction::LocalGet(receiver));
5202    crate::codegen::cast_check::emit_operation_cast_on_stack(
5203        emitter,
5204        ctx,
5205        &Type::Unknown,
5206        &Type::Array(Box::new(Type::Unknown)),
5207    )?;
5208    emitter.instruction(Instruction::LocalGet(key));
5209    emit_index_number(
5210        emitter,
5211        ctx,
5212        &ctx.ta
5213            .try_expr(idx)
5214            .map_err(crate::codegen::arena_failure)?
5215            .ty,
5216    )?;
5217    let idx_f64_local = stash_index_operand(emitter)?;
5218    let length = stash_array_length(emitter, array_idx)?;
5219    emitter.instruction(Instruction::StructGet {
5220        struct_type_index: array_idx,
5221        field_index: 1,
5222    });
5223    emitter.instruction(Instruction::LocalSet(raw_arr_local));
5224    // A read whose type has room for `undefined` gives it for an index that
5225    // finds no element: negative, fractional and `NaN` indices included, as
5226    // in JavaScript.
5227    if allow_missing {
5228        crate::codegen::bounds::emit_index_in_range(emitter, length, idx_f64_local);
5229        emitter.instruction(Instruction::I32Eqz);
5230        emitter.emit_if(BlockType::Result(ctx.symbols.value_type(result_ty)?));
5231        if let Some(flag) = present_flag {
5232            set_present_flag(emitter, flag, false);
5233        }
5234        emit_undefined(emitter, ctx)?;
5235        cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, result_ty)?;
5236        emitter.emit_else();
5237    }
5238    if let Some(flag) = present_flag {
5239        set_present_flag(emitter, flag, true);
5240    }
5241    let idx_local = emit_checked_index_with_length(
5242        emitter,
5243        ctx,
5244        length,
5245        idx_f64_local,
5246        receiver_name,
5247        IndexAccess::Read,
5248    )?;
5249    emitter.instruction(Instruction::LocalGet(raw_arr_local));
5250    emitter.instruction(Instruction::LocalGet(idx_local));
5251    emitter.instruction(Instruction::ArrayGet(raw_array_idx));
5252    // A regex group that did not take part holds `undefined`; a read typed
5253    // without room for it finds no value there.
5254    if match_groups {
5255        crate::codegen::bounds::emit_group_participation_check(
5256            emitter,
5257            ctx,
5258            length,
5259            idx_f64_local,
5260            receiver_name,
5261        )?;
5262    }
5263    cast::emit_cast_to(emitter, ctx, result_ty)?;
5264    if allow_missing {
5265        emitter.emit_end();
5266    }
5267    Ok(())
5268}
5269
5270fn is_optional_tuple_position(
5271    ctx: &CodegenCtx<'_>,
5272    receiver: &Type,
5273    index: ExprId,
5274) -> Result<bool, crate::compiler_error::CompilerFailure> {
5275    let expression = ctx
5276        .ta
5277        .try_expr(index)
5278        .map_err(crate::codegen::arena_failure)?;
5279    let TypedExprKind::Number(index) = expression.kind else {
5280        return Ok(false);
5281    };
5282    Ok(index.fract() == 0.0 && tuple_permits_missing_index(receiver, index))
5283}
5284
5285/// Whether a read at `index` may find no element: an optional position, or,
5286/// in a union a destructuring pattern reads, one past a shorter tuple's end.
5287/// Inference rejects any other read past a tuple's end.
5288fn tuple_permits_missing_index(receiver: &Type, index: f64) -> bool {
5289    match receiver.lowered() {
5290        Type::Tuple(elements) => index >= elements.required_len() as f64,
5291        Type::Union(members) => members
5292            .iter()
5293            .any(|member| tuple_permits_missing_index(member, index)),
5294        _ => false,
5295    }
5296}
5297
5298fn emit_uint8_index_with_receiver_on_stack(
5299    emitter: &mut FunctionEmitter,
5300    ctx: &CodegenCtx,
5301    index: ExprId,
5302    undefined_into: Option<&Type>,
5303    receiver_name: &str,
5304) -> Result<(), crate::compiler_error::CompilerFailure> {
5305    let uint8_idx = ctx.symbols.uint8_array_type_idx().ok_or_else(|| {
5306        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
5307    })?;
5308    let raw_uint8_idx = ctx.symbols.raw_uint8_array_type_idx().ok_or_else(|| {
5309        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
5310    })?;
5311    let raw_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5312        nullable: false,
5313        heap_type: HeapType::Concrete(raw_uint8_idx),
5314    }))?;
5315    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5316    emitter.instruction(Instruction::LocalSet(receiver));
5317    emit_expr(emitter, ctx, index)?;
5318    let key = emitter.add_anonymous_local(
5319        ctx.symbols.value_type(
5320            &ctx.ta
5321                .try_expr(index)
5322                .map_err(crate::codegen::arena_failure)?
5323                .ty,
5324        )?,
5325    )?;
5326    emitter.instruction(Instruction::LocalSet(key));
5327    emitter.instruction(Instruction::LocalGet(receiver));
5328    crate::codegen::cast_check::emit_operation_cast_on_stack(
5329        emitter,
5330        ctx,
5331        &Type::Unknown,
5332        &Type::Uint8Array,
5333    )?;
5334    emitter.instruction(Instruction::LocalGet(key));
5335    emit_index_number(
5336        emitter,
5337        ctx,
5338        &ctx.ta
5339            .try_expr(index)
5340            .map_err(crate::codegen::arena_failure)?
5341            .ty,
5342    )?;
5343    let idx_f64_local = stash_index_operand(emitter)?;
5344    emitter.instruction(Instruction::StructGet {
5345        struct_type_index: uint8_idx,
5346        field_index: 1,
5347    });
5348    emitter.instruction(Instruction::LocalSet(raw_local));
5349    // A read typed with room for `undefined` gives it out of range.
5350    if let Some(result_ty) = undefined_into {
5351        let length = emitter.add_anonymous_local(ValType::I32)?;
5352        emitter.instruction(Instruction::LocalGet(raw_local));
5353        emitter.instruction(Instruction::ArrayLen);
5354        emitter.instruction(Instruction::LocalSet(length));
5355        crate::codegen::bounds::emit_index_in_range(emitter, length, idx_f64_local);
5356        emitter.instruction(Instruction::I32Eqz);
5357        emitter.emit_if(BlockType::Result(ctx.symbols.value_type(result_ty)?));
5358        emit_undefined(emitter, ctx)?;
5359        cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, result_ty)?;
5360        emitter.emit_else();
5361    }
5362    let idx_local = emit_checked_index(
5363        emitter,
5364        ctx,
5365        raw_local,
5366        idx_f64_local,
5367        receiver_name,
5368        IndexAccess::Read,
5369    )?;
5370    emitter.instruction(Instruction::LocalGet(raw_local));
5371    emitter.instruction(Instruction::LocalGet(idx_local));
5372    emitter.instruction(Instruction::ArrayGetU(raw_uint8_idx));
5373    emitter.instruction(Instruction::F64ConvertI32U);
5374    if let Some(result_ty) = undefined_into {
5375        cast::emit_coerce_to_slot(emitter, ctx, &Type::Number, result_ty)?;
5376        emitter.emit_end();
5377    }
5378    Ok(())
5379}
5380
5381/// Which accessor a write dispatches to when the data slot is missing.
5382#[derive(Clone, Copy)]
5383enum AccessorWrite {
5384    Setter,
5385    ReadOnly,
5386}
5387
5388impl AccessorWrite {
5389    /// Which accessor slot this arm scans for. The read-only arm has no setter
5390    /// to find, so it scans the *getter* as proof the property is there at all.
5391    fn scanned_kind(self) -> crate::AccessorKind {
5392        match self {
5393            AccessorWrite::Setter => crate::AccessorKind::Set,
5394            AccessorWrite::ReadOnly => crate::AccessorKind::Get,
5395        }
5396    }
5397}
5398
5399pub(crate) fn emit_object_field_index_by_name(
5400    emitter: &mut FunctionEmitter,
5401    ctx: &CodegenCtx,
5402    object_local: u32,
5403    name_global: u32,
5404) {
5405    emit_field_index_by_name(emitter, ctx, object_local, name_global, false);
5406}
5407
5408fn emit_field_index_by_name(
5409    emitter: &mut FunctionEmitter,
5410    ctx: &CodegenCtx,
5411    object_local: u32,
5412    name_global: u32,
5413    accessor: bool,
5414) {
5415    ctx.latch(emit_field_index_by_name_checked(
5416        emitter,
5417        ctx,
5418        object_local,
5419        name_global,
5420        accessor,
5421    ));
5422}
5423
5424fn emit_field_index_by_name_checked(
5425    emitter: &mut FunctionEmitter,
5426    ctx: &CodegenCtx,
5427    object_local: u32,
5428    name_global: u32,
5429    accessor: bool,
5430) -> Result<(), crate::compiler_error::CompilerFailure> {
5431    emitter.instruction(Instruction::LocalGet(object_local));
5432    emitter.instruction(Instruction::GlobalGet(name_global));
5433    emitter.instruction(Instruction::I32Const(i32::from(accessor)));
5434    emitter.instruction(Instruction::Call(
5435        ctx.symbols
5436            .field_lookup_function
5437            .ok_or_else(|| crate::codegen::internal_failure("field lookup allocated"))?,
5438    ));
5439
5440    Ok(())
5441}
5442
5443/// Emit a direct call to an imported function. Host imports get the
5444/// raw-string round-trip applied around the call (extract `(ref
5445/// $rawString)` on `Type::String` args, wrap on a `Type::String`
5446/// return). Non-host imports (prelude `string_concat`, etc.) skip
5447/// both conversions.
5448fn emit_direct_call(
5449    emitter: &mut FunctionEmitter,
5450    ctx: &CodegenCtx,
5451    is_host: bool,
5452    wasm_idx: u32,
5453    params: &[Type],
5454    ret: &Type,
5455    args: &[ExprId],
5456) -> Result<(), crate::compiler_error::CompilerFailure> {
5457    if params.len() != args.len() {
5458        return Err(crate::codegen::internal_failure(
5459            "direct-call argument count mismatch",
5460        ));
5461    }
5462    let string_type_idx = ctx.symbols.string_type_idx().ok_or_else(|| {
5463        crate::codegen::internal_failure("$string type registered before call codegen")
5464    })?;
5465    let raw_string_type_idx = ctx.symbols.raw_string_type_idx().ok_or_else(|| {
5466        crate::codegen::internal_failure("$rawString type registered before call codegen")
5467    })?;
5468    let mut evaluated = Vec::with_capacity(args.len());
5469    for &arg in args {
5470        emit_expr(emitter, ctx, arg)?;
5471        let slot = emitter.add_anonymous_local(
5472            ctx.symbols.value_type(
5473                &ctx.ta
5474                    .try_expr(arg)
5475                    .map_err(crate::codegen::arena_failure)?
5476                    .ty,
5477            )?,
5478        )?;
5479        emitter.instruction(Instruction::LocalSet(slot));
5480        evaluated.push(slot);
5481    }
5482    for (i, (&arg_id, local)) in args.iter().zip(evaluated).enumerate() {
5483        emitter.instruction(Instruction::LocalGet(local));
5484        // Plan 75.8: coerce a primitive arg into a wider ref-typed
5485        // param slot (e.g., calling `f(x: number | null)` with `5`
5486        // requires boxing the f64 before the call). No-op when the
5487        // value's Wasm type already satisfies the param slot via
5488        // subtyping.
5489        if let Some(param_ty) = params.get(i) {
5490            let arg_ty = ctx
5491                .ta
5492                .try_expr(arg_id)
5493                .map_err(crate::codegen::arena_failure)?
5494                .ty
5495                .clone();
5496            let default = ctx
5497                .symbols
5498                .host_call_defaults(wasm_idx, args.len())
5499                .get(i)
5500                .and_then(Option::as_ref);
5501            crate::codegen::argument_defaults::emit_argument(
5502                emitter,
5503                ctx,
5504                &arg_ty,
5505                ctx.symbols.value_type(param_ty)?,
5506                default,
5507            )?;
5508        }
5509        // follow-up: peel param / return so aliased strings
5510        // (`type S = string`) still trigger the raw-string unwrap/
5511        // rewrap that matches the host import's `$rawString`
5512        // signature.
5513        if is_host
5514            && params
5515                .get(i)
5516                .is_some_and(|p| matches!(p.lowered(), Type::String))
5517        {
5518            emitter.instruction(Instruction::StructGet {
5519                struct_type_index: string_type_idx,
5520                field_index: 1,
5521            });
5522        }
5523    }
5524    emitter.instruction(Instruction::Call(wasm_idx));
5525    if ctx.symbols.resultless_functions.contains(&wasm_idx) {
5526        emit_undefined(emitter, ctx)?;
5527    }
5528    let _: () = if is_host && matches!(ret.lowered(), Type::String) {
5529        let scratch = emitter.add_anonymous_local(ValType::Ref(RefType {
5530            nullable: false,
5531            heap_type: HeapType::Concrete(raw_string_type_idx),
5532        }))?;
5533        emitter.instruction(Instruction::LocalSet(scratch));
5534        let vtable_global = ctx
5535            .symbols
5536            .prelude_global_idx("string_vtable")
5537            .ok_or_else(|| {
5538                crate::codegen::internal_failure("string_vtable imported from prelude")
5539            })?;
5540        emitter.instruction(Instruction::GlobalGet(vtable_global));
5541        emitter.instruction(Instruction::LocalGet(scratch));
5542        emitter.instruction(Instruction::I64Const(0));
5543        emitter.instruction(Instruction::StructNew(string_type_idx));
5544    };
5545    Ok(())
5546}
5547
5548/// Emit an indirect call via `call_ref` through a closure value
5549/// (`LocalRef` / `GlobalRef` to a function-typed binding, an inline
5550/// `Closure`, etc.).
5551///
5552/// Under uniform closure ABI, every closure param and the
5553/// return are erased to `(ref $Object)` at the Wasm boundary. This
5554/// call site evaluates and boxes arguments before checking the callable. The
5555/// result uses the physical type chosen by runtime-value propagation.
5556fn emit_indirect_closure_call(
5557    emitter: &mut FunctionEmitter,
5558    ctx: &CodegenCtx,
5559    callee: ExprId,
5560    args: &[ExprId],
5561    result_ty: &Type,
5562) -> Result<(), crate::compiler_error::CompilerFailure> {
5563    let callee_ty = ctx
5564        .ta
5565        .source_type(callee)
5566        .map_err(crate::codegen::arena_failure)?
5567        .clone();
5568    let receiver = emit_callee(emitter, ctx, callee)?;
5569    emitter.call_receiver = receiver;
5570    emit_indirect_closure_call_with_receiver_on_stack(emitter, ctx, &callee_ty, args, result_ty)?;
5571    Ok(())
5572}
5573
5574/// Preserve a property reference until invocation without evaluating its object twice.
5575fn emit_callee(
5576    emitter: &mut FunctionEmitter,
5577    ctx: &CodegenCtx,
5578    callee: ExprId,
5579) -> Result<Option<u32>, crate::compiler_error::CompilerFailure> {
5580    let reference = reference_expression(ctx.ta, callee)?;
5581    let receiver = match &ctx
5582        .ta
5583        .try_expr(reference)
5584        .map_err(crate::codegen::arena_failure)?
5585        .kind
5586    {
5587        TypedExprKind::FieldAccess { receiver, .. }
5588        | TypedExprKind::IndexAccess { receiver, .. }
5589        | TypedExprKind::InterfacePropertyAccess { receiver, .. } => Some(*receiver),
5590        _ => None,
5591    };
5592    let mark = emitter.single_evaluation_mark();
5593    let slot = receiver
5594        .map(|receiver| {
5595            emit_expr(emitter, ctx, receiver)?;
5596            let slot = emitter.add_anonymous_local(
5597                ctx.symbols.value_type(
5598                    &ctx.ta
5599                        .try_expr(receiver)
5600                        .map_err(crate::codegen::arena_failure)?
5601                        .ty,
5602                )?,
5603            )?;
5604            emitter.instruction(Instruction::LocalSet(slot));
5605            emitter.record_single_evaluation(receiver, slot)?;
5606            Ok::<_, crate::compiler_error::CompilerFailure>(slot)
5607        })
5608        .transpose()?;
5609    emit_expr(emitter, ctx, callee)?;
5610    emitter.end_single_evaluations(mark)?;
5611    Ok(
5612        if matches!(
5613            ctx.ta
5614                .try_expr(reference)
5615                .map_err(crate::codegen::arena_failure)?
5616                .kind,
5617            TypedExprKind::OptionalChain { .. }
5618        ) {
5619            emitter.call_receiver.take()
5620        } else {
5621            slot
5622        },
5623    )
5624}
5625
5626fn reference_expression(
5627    ast: &crate::TypedAst,
5628    mut id: ExprId,
5629) -> Result<ExprId, crate::compiler_error::CompilerFailure> {
5630    loop {
5631        match ast
5632            .try_expr(id)
5633            .map_err(crate::codegen::arena_failure)?
5634            .kind
5635        {
5636            TypedExprKind::Cast { value, .. } | TypedExprKind::NonNullAssert { value } => {
5637                id = value;
5638            }
5639            _ => return Ok(id),
5640        }
5641    }
5642}
5643
5644/// Same as [`emit_indirect_closure_call`], but assumes the closure
5645/// value is already on the stack (cast to its per-signature struct
5646/// type). Used optional-chain `Call` arm, where
5647/// `emit_chain_parts` has already evaluated, null-checked, and
5648/// ref-cast the receiver before dispatching.
5649fn emit_indirect_closure_call_with_receiver_on_stack(
5650    emitter: &mut FunctionEmitter,
5651    ctx: &CodegenCtx,
5652    callee_ty: &Type,
5653    args: &[ExprId],
5654    result_ty: &Type,
5655) -> Result<(), crate::compiler_error::CompilerFailure> {
5656    let receiver = emitter.call_receiver.take();
5657    let callee = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5658    emitter.instruction(Instruction::LocalSet(callee));
5659    let mut arguments = Vec::with_capacity(args.len());
5660    for &arg in args {
5661        emit_expr(emitter, ctx, arg)?;
5662        cast::emit_box(
5663            emitter,
5664            ctx,
5665            &ctx.ta
5666                .try_expr(arg)
5667                .map_err(crate::codegen::arena_failure)?
5668                .ty,
5669        )?;
5670        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5671        emitter.instruction(Instruction::LocalSet(slot));
5672        arguments.push(slot);
5673    }
5674    emitter.instruction(Instruction::LocalGet(callee));
5675    crate::codegen::cast_check::emit_operation_cast_on_stack(
5676        emitter,
5677        ctx,
5678        &Type::Unknown,
5679        callee_ty,
5680    )?;
5681    // chain receivers can arrive aliased
5682    // (`type NumberFn = (x: number) => number; let f: NumberFn | null`
5683    // → after `strip_null`, the type is the alias, not the underlying
5684    // function). `classify` panics on non-Function inputs, so peel.
5685    let callee_ty = callee_ty.lowered();
5686    let closure_sig = crate::codegen::closures::classify(callee_ty)?;
5687    let closure_struct_idx = ctx
5688        .symbols
5689        .closure_struct_type_idx(closure_sig)
5690        .ok_or_else(|| {
5691            crate::codegen::internal_failure(
5692                "closure struct type registered for every closure-typed callee",
5693            )
5694        })?;
5695    let fn_type_idx = ctx
5696        .symbols
5697        .closure_func_type_idx(closure_sig)
5698        .ok_or_else(|| {
5699            crate::codegen::internal_failure(
5700                "closure funcref type registered for every closure-typed callee",
5701            )
5702        })?;
5703    let closure_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5704        nullable: false,
5705        heap_type: HeapType::Concrete(closure_struct_idx),
5706    }))?;
5707    emitter.instruction(Instruction::LocalTee(closure_local));
5708    emitter.instruction(Instruction::StructGet {
5709        struct_type_index: closure_struct_idx,
5710        field_index: 2,
5711    });
5712    if let Some(receiver) = receiver {
5713        crate::codegen::this_binding::bind(emitter, ctx, receiver)?;
5714    }
5715    // note: variadic closures arrive with their trailing args
5716    // already packed into a synthesized `ArrayLiteral` filling the
5717    // rest slot, so `args.len()` matches the closure's funcref
5718    // signature arity. The per-arg `emit_box` below is still correct
5719    // — the synthesized array's static type is a concrete
5720    // `Type::Array(_)`, so `emit_box` upcasts it to `(ref $Object)`
5721    // via the same ref-extension that any array arg uses.
5722    for argument in arguments {
5723        emitter.instruction(Instruction::LocalGet(argument));
5724    }
5725    emitter.instruction(Instruction::LocalGet(closure_local));
5726    emitter.instruction(Instruction::StructGet {
5727        struct_type_index: closure_struct_idx,
5728        field_index: 1,
5729    });
5730    emitter.instruction(Instruction::CallRef(fn_type_idx));
5731    // Unbox the erased return to the language-level type. Void keeps the erased
5732    // completion; primitives unwrap and references cast to their declared type.
5733    let Type::Function { ret, .. } = callee_ty.lowered() else {
5734        return Err(crate::codegen::internal_failure(
5735            "indirect call requires a function type",
5736        ));
5737    };
5738    let _: () = if !ret.is_void() {
5739        crate::codegen::cast_check::emit_checked_cast_on_stack(
5740            emitter,
5741            ctx,
5742            &Type::Unknown,
5743            result_ty,
5744        )?;
5745    };
5746    Ok(())
5747}
5748
5749/// Classify the current boxed value rather than folding the retained static
5750/// refinement. Closures use the intrinsic closure supertype regardless of arity.
5751fn emit_typeof_tag(
5752    emitter: &mut FunctionEmitter,
5753    ctx: &CodegenCtx,
5754    value: ExprId,
5755    tag: crate::TypeofTagKind,
5756) -> Result<(), crate::compiler_error::CompilerFailure> {
5757    let _: () = match tag {
5758        crate::TypeofTagKind::BigInt => {
5759            emit_expr(emitter, ctx, value)?;
5760            cast::emit_box(
5761                emitter,
5762                ctx,
5763                &ctx.ta
5764                    .try_expr(value)
5765                    .map_err(crate::codegen::arena_failure)?
5766                    .ty,
5767            )?;
5768            let intrinsic = ctx
5769                .symbols
5770                .bigint_type_idx()
5771                .ok_or_else(|| crate::codegen::internal_failure("bigint intrinsic missing"))?;
5772            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(intrinsic)));
5773        }
5774        crate::TypeofTagKind::Undefined => {
5775            emit_expr(emitter, ctx, value)?;
5776            cast::emit_box(
5777                emitter,
5778                ctx,
5779                &ctx.ta
5780                    .try_expr(value)
5781                    .map_err(crate::codegen::arena_failure)?
5782                    .ty,
5783            )?;
5784            emit_is_undefined(emitter, ctx)?;
5785        }
5786        crate::TypeofTagKind::Number => {
5787            // `typeof x === "number"` lowers directly to
5788            // TypeofTag(Number). `ref.test (ref $BoxedNumber)` —
5789            // returns 1 iff the value is a `$BoxedNumber`. Handles
5790            // nullable LHS (returns 0 for null) without an explicit
5791            // null check.
5792            let boxed = ctx.symbols.boxed_number_type_idx().ok_or_else(|| {
5793                crate::codegen::internal_failure("$BoxedNumber intrinsic type registered")
5794            })?;
5795            emit_expr(emitter, ctx, value)?;
5796            cast::emit_box(
5797                emitter,
5798                ctx,
5799                &ctx.ta
5800                    .try_expr(value)
5801                    .map_err(crate::codegen::arena_failure)?
5802                    .ty,
5803            )?;
5804            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(boxed)));
5805        }
5806        crate::TypeofTagKind::String => {
5807            let string = ctx.symbols.string_type_idx().ok_or_else(|| {
5808                crate::codegen::internal_failure("$string intrinsic type registered")
5809            })?;
5810            emit_expr(emitter, ctx, value)?;
5811            cast::emit_box(
5812                emitter,
5813                ctx,
5814                &ctx.ta
5815                    .try_expr(value)
5816                    .map_err(crate::codegen::arena_failure)?
5817                    .ty,
5818            )?;
5819            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(string)));
5820        }
5821        crate::TypeofTagKind::Boolean => {
5822            let boxed = ctx.symbols.boxed_boolean_type_idx().ok_or_else(|| {
5823                crate::codegen::internal_failure("$BoxedBoolean intrinsic type registered")
5824            })?;
5825            emit_expr(emitter, ctx, value)?;
5826            cast::emit_box(
5827                emitter,
5828                ctx,
5829                &ctx.ta
5830                    .try_expr(value)
5831                    .map_err(crate::codegen::arena_failure)?
5832                    .ty,
5833            )?;
5834            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(boxed)));
5835        }
5836        crate::TypeofTagKind::Function => {
5837            let closure_idx = ctx.symbols.closure_type_idx().ok_or_else(|| {
5838                crate::codegen::internal_failure("$Closure intrinsic type registered")
5839            })?;
5840            emit_expr(emitter, ctx, value)?;
5841            cast::emit_box(
5842                emitter,
5843                ctx,
5844                &ctx.ta
5845                    .try_expr(value)
5846                    .map_err(crate::codegen::arena_failure)?
5847                    .ty,
5848            )?;
5849            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(closure_idx)));
5850        }
5851        crate::TypeofTagKind::Object => {
5852            let scratch_ty = ctx.symbols.value_type(&Type::Unknown)?;
5853            let nullable = matches!(scratch_ty, ValType::Ref(rt) if rt.nullable);
5854            let scratch = emitter.add_anonymous_local(scratch_ty)?;
5855            emit_expr(emitter, ctx, value)?;
5856            cast::emit_box(
5857                emitter,
5858                ctx,
5859                &ctx.ta
5860                    .try_expr(value)
5861                    .map_err(crate::codegen::arena_failure)?
5862                    .ty,
5863            )?;
5864            emitter.instruction(Instruction::LocalSet(scratch));
5865            emit_is_non_primitive(emitter, ctx, scratch, Closures::AreNotObjects)?;
5866            if nullable {
5867                // JS quirk: `typeof null === "object"`. Only OR in
5868                // the null check when the source's Wasm type admits
5869                // null — `ref.is_null` is a validator error on a
5870                // non-nullable ref.
5871                emitter.instruction(Instruction::LocalGet(scratch));
5872                emitter.instruction(Instruction::RefIsNull);
5873                emitter.instruction(Instruction::I32Or);
5874            }
5875        }
5876    };
5877    Ok(())
5878}
5879
5880/// Whether a closure passes [`emit_is_non_primitive`]: it does for the
5881/// `object` type, but `typeof` tags it `"function"`.
5882#[derive(Clone, Copy, PartialEq, Eq)]
5883pub(crate) enum Closures {
5884    AreObjects,
5885    AreNotObjects,
5886}
5887
5888/// Test whether the value in `local` is an object rather than `null`,
5889/// `undefined` or a primitive, leaving an `i32` on the stack.
5890///
5891/// Stated as the complement of the primitive tags rather than a list of
5892/// object-like types: every runtime value is an `$Object` subtype, so a
5893/// positive list silently answers "not an object" for any struct nobody
5894/// remembered to add. The exclusions below are exhaustive by construction —
5895/// they are the `$Object` subtypes that carry a primitive tag of their own.
5896pub(crate) fn emit_is_non_primitive(
5897    emitter: &mut FunctionEmitter,
5898    ctx: &CodegenCtx,
5899    local: u32,
5900    closures: Closures,
5901) -> Result<(), crate::compiler_error::CompilerFailure> {
5902    let intrinsics = ctx
5903        .symbols
5904        .intrinsic_type_indices()
5905        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
5906    emitter.instruction(Instruction::LocalGet(local));
5907    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
5908        intrinsics.object,
5909    )));
5910    let mut excluded = vec![
5911        intrinsics.string,
5912        intrinsics.boxed_number,
5913        intrinsics.boxed_boolean,
5914        intrinsics.bigint,
5915        intrinsics.undefined,
5916    ];
5917    if closures == Closures::AreNotObjects {
5918        excluded.push(intrinsics.closure);
5919    }
5920    for primitive in excluded {
5921        emitter.instruction(Instruction::LocalGet(local));
5922        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(primitive)));
5923        emitter.instruction(Instruction::I32Eqz);
5924        emitter.instruction(Instruction::I32And);
5925    }
5926    Ok(())
5927}
5928
5929/// Whether a bitwise operation is between plain numbers, so it can run on
5930/// `i32` values inline instead of through the dynamic host operation.
5931fn has_numeric_operands(
5932    ctx: &CodegenCtx,
5933    operands: &[ExprId],
5934    result_ty: &Type,
5935) -> Result<bool, crate::compiler_error::CompilerFailure> {
5936    if !matches!(result_ty.lowered(), Type::Number | Type::NumberLiteral(_)) {
5937        return Ok(false);
5938    }
5939    for &operand in operands {
5940        let ty = &ctx
5941            .ta
5942            .try_expr(operand)
5943            .map_err(crate::codegen::arena_failure)?
5944            .ty;
5945        if !matches!(
5946            ty.primitive_behavior(),
5947            Type::Number | Type::NumberLiteral(_)
5948        ) {
5949            return Ok(false);
5950        }
5951    }
5952    Ok(true)
5953}
5954
5955fn emit_number_bitwise(
5956    emitter: &mut FunctionEmitter,
5957    ctx: &CodegenCtx,
5958    op: BinOp,
5959    lhs: ExprId,
5960    rhs: ExprId,
5961) -> Result<(), crate::compiler_error::CompilerFailure> {
5962    emit_primitive_operand(emitter, ctx, lhs)?;
5963    emit_to_int32(emitter, ctx)?;
5964    emit_primitive_operand(emitter, ctx, rhs)?;
5965    emit_to_int32(emitter, ctx)?;
5966    // Wasm shifts already take the count modulo 32, as JavaScript does.
5967    let (instruction, unsigned) = match op {
5968        BinOp::BitAnd => (Instruction::I32And, false),
5969        BinOp::BitOr => (Instruction::I32Or, false),
5970        BinOp::BitXor => (Instruction::I32Xor, false),
5971        BinOp::Shl => (Instruction::I32Shl, false),
5972        BinOp::Shr => (Instruction::I32ShrS, false),
5973        BinOp::UnsignedShr => (Instruction::I32ShrU, true),
5974        _ => {
5975            return Err(crate::codegen::internal_failure(
5976                "a numeric bitwise operation has a bitwise operator",
5977            ));
5978        }
5979    };
5980    emitter.instruction(instruction);
5981    emitter.instruction(if unsigned {
5982        Instruction::F64ConvertI32U
5983    } else {
5984        Instruction::F64ConvertI32S
5985    });
5986    Ok(())
5987}
5988
5989/// JavaScript's ToInt32 of the `f64` on the stack. A finite value below 2^63
5990/// truncates exactly in `i64`, whose low 32 bits are the answer; anything else
5991/// (huge, infinite, NaN) takes the host's `x | 0`.
5992fn emit_to_int32(
5993    emitter: &mut FunctionEmitter,
5994    ctx: &CodegenCtx,
5995) -> Result<(), crate::compiler_error::CompilerFailure> {
5996    let value = emitter.add_anonymous_local(ValType::F64)?;
5997    emitter.instruction(Instruction::LocalTee(value));
5998    emitter.instruction(Instruction::F64Abs);
5999    emitter.instruction(Instruction::F64Const(Ieee64::from(
6000        9_223_372_036_854_775_808.0,
6001    )));
6002    emitter.instruction(Instruction::F64Lt);
6003    emitter.emit_if(BlockType::Result(ValType::I32));
6004    emitter.instruction(Instruction::LocalGet(value));
6005    emitter.instruction(Instruction::I64TruncSatF64S);
6006    emitter.instruction(Instruction::I32WrapI64);
6007    emitter.emit_else();
6008    emitter.instruction(Instruction::LocalGet(value));
6009    cast::emit_box(emitter, ctx, &Type::Number)?;
6010    emitter.instruction(Instruction::F64Const(Ieee64::from(0.0)));
6011    cast::emit_box(emitter, ctx, &Type::Number)?;
6012    let host = ctx
6013        .symbols
6014        .func_idx(&crate::mangle::prelude("__value_bitor"))
6015        .ok_or_else(|| crate::codegen::internal_failure("missing bitwise host import: bitor"))?;
6016    emitter.instruction(Instruction::Call(host));
6017    cast::emit_cast_to(emitter, ctx, &Type::Number)?;
6018    emitter.instruction(Instruction::I32TruncF64S);
6019    emitter.emit_end();
6020    Ok(())
6021}
6022
6023fn emit_bitwise_host(
6024    emitter: &mut FunctionEmitter,
6025    ctx: &CodegenCtx,
6026    name: &str,
6027    operands: &[ExprId],
6028    result_ty: &Type,
6029) -> Result<(), crate::compiler_error::CompilerFailure> {
6030    for &operand in operands {
6031        emit_expr(emitter, ctx, operand)?;
6032        let ty = &ctx
6033            .ta
6034            .try_expr(operand)
6035            .map_err(crate::codegen::arena_failure)?
6036            .ty;
6037        cast::emit_box(emitter, ctx, ty)?;
6038    }
6039    let mangled = crate::mangle::prelude(&format!("__value_{name}"));
6040    let host = ctx.symbols.func_idx(&mangled).ok_or_else(|| {
6041        crate::codegen::internal_failure(format!("missing bitwise host import: {name}"))
6042    })?;
6043    emitter.instruction(Instruction::Call(host));
6044    cast::emit_cast_to(emitter, ctx, result_ty)
6045}
6046
6047fn emit_unary(
6048    emitter: &mut FunctionEmitter,
6049    ctx: &CodegenCtx,
6050    op: UnOp,
6051    operand: ExprId,
6052    result_ty: &Type,
6053) -> Result<(), crate::compiler_error::CompilerFailure> {
6054    if matches!(op, UnOp::Neg | UnOp::Pos)
6055        && try_emit_unreachable_for_never_operand(emitter, ctx, &[operand])?
6056    {
6057        return Ok(());
6058    }
6059    let _: () = match op {
6060        UnOp::BitNot if has_numeric_operands(ctx, &[operand], result_ty)? => {
6061            emit_primitive_operand(emitter, ctx, operand)?;
6062            emit_to_int32(emitter, ctx)?;
6063            emitter.instruction(Instruction::I32Const(-1));
6064            emitter.instruction(Instruction::I32Xor);
6065            emitter.instruction(Instruction::F64ConvertI32S);
6066        }
6067        UnOp::BitNot => emit_bitwise_host(emitter, ctx, "bitnot", &[operand], result_ty)?,
6068        UnOp::Neg => {
6069            // bigint negation routes to inline host call.
6070            if ctx
6071                .ta
6072                .try_expr(operand)
6073                .map_err(crate::codegen::arena_failure)?
6074                .ty
6075                .is_bigint()
6076            {
6077                emit_primitive_operand(emitter, ctx, operand)?;
6078                emit_bigint_extract_to_stack(emitter, ctx);
6079                let host_idx = ctx
6080                    .symbols
6081                    .func_idx(&crate::mangle::host(
6082                        crate::runtime::BIGINT_MODULE_NAME,
6083                        "neg",
6084                    ))
6085                    .ok_or_else(|| {
6086                        crate::codegen::internal_failure(
6087                            "submilli:bigint.neg import recorded by codegen bootstrap",
6088                        )
6089                    })?;
6090                emit_bigint_wrap_host_result(emitter, ctx, host_idx);
6091                return Ok(());
6092            }
6093            emit_primitive_operand(emitter, ctx, operand)?;
6094            emitter.instruction(Instruction::F64Neg);
6095        }
6096        UnOp::Pos => {
6097            // On a string, `+` is the explicit numeric coercion and lowers to
6098            // the same host parse `Number(s)` calls. On a number it is the
6099            // identity, so the operand's value is already what we want. A
6100            // `number | string` operand picks one of the two at run time.
6101            let operand_ty = ctx
6102                .ta
6103                .try_expr(operand)
6104                .map_err(crate::codegen::arena_failure)?
6105                .ty
6106                .clone();
6107            if operand_ty.is_string_shaped() {
6108                emit_primitive_operand(emitter, ctx, operand)?;
6109                emitter.instruction(Instruction::Call(to_number_host(ctx)?));
6110            } else if matches!(operand_ty.primitive_behavior(), Type::Union(_)) {
6111                emit_expr(emitter, ctx, operand)?;
6112                cast::emit_box(emitter, ctx, &operand_ty)?;
6113                emit_union_to_number(emitter, ctx)?;
6114            } else {
6115                emit_primitive_operand(emitter, ctx, operand)?;
6116            }
6117        }
6118        UnOp::Not => {
6119            // `!x` on a boolean is `i32.eqz` — 0 → 1, anything else → 0.
6120            // Plan 75.9: the inferer also accepts nullable
6121            // operands (e.g., `!s` with `s: string | null`); for those
6122            // we first coerce to the i32 truthiness convention
6123            // (`ref.is_null; i32.eqz`) before negating.
6124            let operand_ty = ctx
6125                .ta
6126                .try_expr(operand)
6127                .map_err(crate::codegen::arena_failure)?
6128                .ty
6129                .clone();
6130            emit_expr(emitter, ctx, operand)?;
6131            crate::codegen::function_emitter::cast::emit_condition_to_i32(
6132                emitter,
6133                ctx,
6134                &operand_ty,
6135            )?;
6136            emitter.instruction(Instruction::I32Eqz);
6137        }
6138    };
6139    Ok(())
6140}
6141
6142fn to_number_host(ctx: &CodegenCtx) -> Result<u32, crate::compiler_error::CompilerFailure> {
6143    ctx.symbols
6144        .func_idx(&crate::mangle::host(
6145            crate::runtime::NUMBER_MODULE_NAME,
6146            "toNumber",
6147        ))
6148        .ok_or_else(|| {
6149            crate::codegen::internal_failure(
6150                "submilli:number.toNumber import recorded by codegen analysis",
6151            )
6152        })
6153}
6154
6155/// `+x` on a boxed `number | string`: parses a string member as `Number(s)`
6156/// does and unboxes a number member.
6157fn emit_union_to_number(
6158    emitter: &mut FunctionEmitter,
6159    ctx: &CodegenCtx,
6160) -> Result<(), crate::compiler_error::CompilerFailure> {
6161    let string = ctx
6162        .symbols
6163        .string_type_idx()
6164        .ok_or_else(|| crate::codegen::internal_failure("$string intrinsic type registered"))?;
6165    let tmp = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6166    emitter.instruction(Instruction::LocalTee(tmp));
6167    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(string)));
6168    emitter.emit_if(BlockType::Result(ValType::F64));
6169    emitter.instruction(Instruction::LocalGet(tmp));
6170    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(string)));
6171    emitter.instruction(Instruction::Call(to_number_host(ctx)?));
6172    emitter.emit_else();
6173    emitter.instruction(Instruction::LocalGet(tmp));
6174    cast::emit_cast_to(emitter, ctx, &Type::Number)?;
6175    emitter.emit_end();
6176    Ok(())
6177}
6178
6179/// Dispatch a method through its native wrapper, class or universal vtable,
6180/// or structural interface closure. Missing dispatch metadata is a compiler
6181/// failure, with the source span supplied by the enclosing expression emitter.
6182#[allow(clippy::too_many_arguments)]
6183pub(crate) fn emit_method_call(
6184    emitter: &mut FunctionEmitter,
6185    ctx: &CodegenCtx,
6186    receiver: ExprId,
6187    iface: &crate::MangledName,
6188    method: &str,
6189    args: &[ExprId],
6190    generic_args: Option<&[crate::GenericArgument]>,
6191    type_args: &[Type],
6192    return_cast: Option<&Type>,
6193    call_ret_ty: &Type,
6194) -> Result<(), crate::compiler_error::CompilerFailure> {
6195    let recv_ty = ctx
6196        .ta
6197        .source_type(receiver)
6198        .map_err(crate::codegen::arena_failure)?
6199        .clone();
6200    if !emit_dropped_static_receiver(emitter, ctx, receiver, iface)? {
6201        emit_receiver(emitter, ctx, receiver)?;
6202    }
6203    emit_method_call_with_receiver_on_stack(
6204        emitter,
6205        ctx,
6206        &recv_ty,
6207        iface,
6208        method,
6209        args,
6210        generic_args,
6211        type_args,
6212        return_cast,
6213        call_ret_ty,
6214    )?;
6215    Ok(())
6216}
6217
6218/// A static-dispatch method drops its receiver, so a read of the binding it
6219/// is called on needn't build the object the binding stands for: push a
6220/// typed null in its place. Returns whether it did.
6221fn emit_dropped_static_receiver(
6222    emitter: &mut FunctionEmitter,
6223    ctx: &CodegenCtx,
6224    receiver: ExprId,
6225    iface: &crate::MangledName,
6226) -> Result<bool, crate::compiler_error::CompilerFailure> {
6227    let expr = ctx
6228        .ta
6229        .try_expr(receiver)
6230        .map_err(crate::codegen::arena_failure)?;
6231    if !matches!(expr.kind, TypedExprKind::GlobalRef { .. })
6232        || static_interface(ctx, &expr.ty) != Some(iface)
6233    {
6234        return Ok(false);
6235    }
6236    match ctx.symbols.value_type(&expr.ty)? {
6237        ValType::Ref(RefType { heap_type, .. }) => {
6238            emitter.instruction(Instruction::RefNull(heap_type));
6239        }
6240        other => {
6241            return Err(crate::codegen::internal_failure(format!(
6242                "static-interface binding lowered to {other:?}"
6243            )));
6244        }
6245    }
6246    Ok(true)
6247}
6248
6249/// Same as [`emit_method_call`], but assumes the receiver has
6250/// already been evaluated onto the stack at its
6251/// strip-null-then-`ref.cast` non-null Wasm shape (the contract
6252/// `emit_chain_parts` establishes before calling
6253/// [`emit_chain_access`]). Used optional-chain
6254/// `MethodCall` arm; the same three dispatch paths apply.
6255#[allow(clippy::too_many_arguments)]
6256fn emit_method_call_with_receiver_on_stack(
6257    emitter: &mut FunctionEmitter,
6258    ctx: &CodegenCtx,
6259    recv_ty: &Type,
6260    iface: &crate::MangledName,
6261    method: &str,
6262    args: &[ExprId],
6263    generic_args: Option<&[crate::GenericArgument]>,
6264    type_args: &[Type],
6265    return_cast: Option<&Type>,
6266    call_ret_ty: &Type,
6267) -> Result<(), crate::compiler_error::CompilerFailure> {
6268    let concrete_receiver = crate::typechecker::infer::narrowing::strip_nullish(recv_ty);
6269    if concrete_receiver.primitive_union_members().is_some() {
6270        cast::emit_box(emitter, ctx, recv_ty)?;
6271        emit_live_member_on_stack(emitter, ctx, iface, method, Some(args))?;
6272        return cast::emit_cast_to(emitter, ctx, call_ret_ty);
6273    }
6274    crate::codegen::field_guards::attach(emitter, ctx, &concrete_receiver)?;
6275    if recv_ty.is_regexp_match_array() && *iface == crate::mangle::prelude("Array") {
6276        cast::emit_cast_to(emitter, ctx, &Type::Array(Box::new(Type::regexp_element())))?;
6277    }
6278    let direct_key = crate::mangle::extend(iface, method);
6279    if let Some(func_idx) = ctx.symbols.func_idx(&direct_key) {
6280        if matches!(recv_ty.primitive_behavior(), Type::Number)
6281            && ctx.symbols.value_type(recv_ty)? != ValType::F64
6282        {
6283            super::cast::emit_cast_to(emitter, ctx, &Type::Number)?;
6284        }
6285        // Direct or Static dispatch. The receiver is on the stack;
6286        // adjust per the interface's dispatch kind:
6287        // - `Dispatch::Direct` — keep it (with a defensive
6288        //   `ref.as_non_null` for InterfaceRef receivers whose
6289        //   declared Wasm shape is `(ref null $Object)`).
6290        // - `Dispatch::Static` (Console, Uint8ArrayConstructor) —
6291        //   drop it; the wrapper sees only the user args.
6292        // - `Dispatch::VTable` / unrecorded — keep it as-is.
6293        match ctx.symbols.iface_dispatch(iface) {
6294            Some(crate::Dispatch::Direct) => {
6295                // InterfaceRef-typed receivers (instance
6296                // interface values like `TextEncoder`, future
6297                // `File` / `Stream`) lower to `(ref null $Object)`.
6298                // The Direct wrapper takes `(ref $Object)` (non-
6299                // null), so coerce. Primitive Direct receivers
6300                // (Number/Boolean/String/Array/Uint8Array) already
6301                // have non-null Wasm value-types — no extra cast.
6302                //
6303                // peel so aliased InterfaceRef (e.g.
6304                // `type Query = Map<string, string>`) still
6305                // triggers the coercion.
6306                if matches!(recv_ty.lowered(), Type::InterfaceRef { .. }) {
6307                    emitter.instruction(Instruction::RefAsNonNull);
6308                }
6309            }
6310            Some(crate::Dispatch::Static) => {
6311                emitter.instruction(Instruction::Drop);
6312            }
6313            // Defensive: a registered direct-call wrapper without a
6314            // recorded dispatch means the imports loop didn't tag the
6315            // interface. Treat as Direct (the common case before this
6316            // refactor) so existing behaviour is preserved.
6317            None | Some(crate::Dispatch::VTable) => {}
6318        }
6319        // note: variadic methods (`Array#concat(...others)`)
6320        // arrive here with a synthesized `ArrayLiteral` already
6321        // occupying the rest slot — the typechecker pre-packs. So
6322        // `args.len()` matches the wrapper sig's user-arg count and
6323        // no codegen branching is needed.
6324        //
6325        // An optional-chain `MethodCall` reaches codegen with the signature
6326        // already substituted to concrete types, so the ABI is its only
6327        // surviving record of which slots the interface's generics erased.
6328        let abi = wrapper_abi_for_call(
6329            ctx,
6330            &direct_key,
6331            args,
6332            generic_args,
6333            return_cast,
6334            call_ret_ty,
6335        )?;
6336        let defaults = ctx.symbols.host_call_defaults(func_idx, args.len());
6337        emit_args_into_slots_with_defaults(emitter, ctx, args, Some(&abi.params), defaults)?;
6338        // A runtime-generic host method (`Response#json<T>`) takes its type
6339        // arguments' descriptors after the declared arguments, as a generic
6340        // function does.
6341        if ctx.symbols.runtime_generic_functions.contains(&direct_key) {
6342            crate::codegen::runtime_descriptors::environment(emitter, ctx, type_args)?;
6343        }
6344        emitter.instruction(Instruction::Call(func_idx));
6345        emit_slot_return_cast(emitter, ctx, call_ret_ty, Some(&abi))?;
6346        return Ok(());
6347    }
6348    // Interface aliases write the same method payload a class-typed call reads.
6349    // Generic methods and synthetic accessors retain their dedicated vtable ABI.
6350    if ctx.symbols.is_rewritable_class_method(iface, method) {
6351        emit_interface_method_via_shape_with_receiver_on_stack(
6352            emitter,
6353            ctx,
6354            method,
6355            args,
6356            call_ret_ty,
6357            None,
6358        )?;
6359        return Ok(());
6360    }
6361    // Class method (static path): the receiver is `(ref $Foo)` on the stack.
6362    // Dispatch through the class's own vtable slot (4+); `class_method_slot` is
6363    // only populated for classes, so a `Some` here means a genuine class method
6364    // (a user method named `toString` etc. also resolves here, not slot 0–3).
6365    if ctx.symbols.class_method_slot(iface, method).is_some() {
6366        emit_class_vtable_dispatch(emitter, ctx, iface, method, args, call_ret_ty)?;
6367        return Ok(());
6368    }
6369    if let Some(slot) = vtable_slot_for_method(method) {
6370        emit_vtable_dispatch_on_object_stack(emitter, ctx, slot);
6371        return Ok(());
6372    }
6373    // shape-based dispatch for user-declared interface
6374    // methods. The receiver is `Type::InterfaceRef`, the method
6375    // came through `find_method` against a `VTable`-dispatch
6376    // interface, and there's no built-in vtable slot. Lower as
6377    // field-name scan → cast to closure → call_ref, exactly as if
6378    // the user had written `(recv.method)(args)` against a
6379    // function-typed field. Reuses the `$ObjectShape` field-name scan
6380    // machinery and the closure ABI.
6381    if matches!(recv_ty.lowered(), Type::InterfaceRef { .. }) {
6382        if is_call_signature_call(iface, method) {
6383            // A call signature names no member: the value is the function (a
6384            // constructor such as `Number` is one), or an object with a quoted
6385            // `"@call"` method answers. The runtime member call resolves both.
6386            cast::emit_box(emitter, ctx, recv_ty)?;
6387            emit_live_member_on_stack(emitter, ctx, iface, method, Some(args))?;
6388            return cast::emit_cast_to(emitter, ctx, call_ret_ty);
6389        }
6390        emit_interface_method_via_shape_with_receiver_on_stack(
6391            emitter,
6392            ctx,
6393            method,
6394            args,
6395            call_ret_ty,
6396            None,
6397        )?;
6398        return Ok(());
6399    }
6400    Err(crate::codegen::internal_failure(format!(
6401        "method dispatch was not registered for `{iface}.{method}`"
6402    )))
6403}
6404
6405/// Whether a call on a user interface goes through its call signature
6406/// (`interface F { (v: number): number }`). Codegen analysis notes the runtime
6407/// member helpers such a call needs by the same test.
6408pub(in crate::codegen) fn is_call_signature_call(iface: &crate::MangledName, method: &str) -> bool {
6409    method == "@call" && !crate::codegen::is_host_package(iface.as_str())
6410}
6411
6412fn emit_class_method_value(
6413    emitter: &mut FunctionEmitter,
6414    ctx: &CodegenCtx,
6415    receiver_ty: &Type,
6416    method: &str,
6417    result_ty: &Type,
6418    check_ty: &Type,
6419) -> Result<(), crate::compiler_error::CompilerFailure> {
6420    let object_shape = ctx
6421        .symbols
6422        .intrinsic_type_indices()
6423        .ok_or_else(|| {
6424            crate::codegen::internal_failure("class method value requires object shape intrinsic")
6425        })?
6426        .object_shape;
6427    let receiver = stash_receiver_as_object_shape(emitter, receiver_ty, object_shape)?;
6428    emit_object_property_read_as(emitter, ctx, receiver, method, result_ty, check_ty)
6429}
6430
6431/// shape-based interface method dispatch. The receiver —
6432/// an `InterfaceRef`-typed value carrying its `$Object` shape — is
6433/// already on the stack; the method's closure lives in a field slot
6434/// named by `method`. Pull it via the `$ObjectShape` field-name scan
6435/// (slot 2), cast to the matching closure struct, then invoke
6436/// through the closure ABI (env + boxed args + funcref). The
6437/// leading `RefAsNonNull` + `RefCastNonNull($ObjectShape)` is
6438/// retained because the on-stack value's static type may be a wider
6439/// `(ref $Object)` (or `(ref null $Object)`) — the cast pins it to
6440/// the object shape the field scan expects.
6441///
6442/// `slot_index_local` names a local that already holds the method slot's index.
6443/// The accessor paths resolve it to decide *whether* to dispatch, and the scan
6444/// is the expensive part of this sequence, so they hand it over rather than
6445/// repeat it; every other caller passes `None`.
6446pub(in crate::codegen) fn emit_interface_method_via_shape_with_receiver_on_stack(
6447    emitter: &mut FunctionEmitter,
6448    ctx: &CodegenCtx,
6449    method: &str,
6450    args: &[ExprId],
6451    call_ret_ty: &Type,
6452    slot_index_local: Option<u32>,
6453) -> Result<(), crate::compiler_error::CompilerFailure> {
6454    let args: Vec<_> = args
6455        .iter()
6456        .copied()
6457        .map(ShapeArgument::Expression)
6458        .collect();
6459    emit_shape_method_with_arguments(emitter, ctx, method, &args, call_ret_ty, slot_index_local)?;
6460    Ok(())
6461}
6462
6463enum ShapeArgument {
6464    Expression(ExprId),
6465    Local { slot: u32, ty: Type },
6466}
6467
6468impl ShapeArgument {
6469    fn ty(&self, ctx: &CodegenCtx) -> Result<Type, crate::compiler_error::CompilerFailure> {
6470        Ok(match self {
6471            Self::Expression(id) => ctx
6472                .ta
6473                .try_expr(*id)
6474                .map_err(crate::codegen::arena_failure)?
6475                .ty
6476                .clone(),
6477            Self::Local { ty, .. } => ty.clone(),
6478        })
6479    }
6480
6481    fn emit(
6482        &self,
6483        emitter: &mut FunctionEmitter,
6484        ctx: &CodegenCtx,
6485    ) -> Result<(), crate::compiler_error::CompilerFailure> {
6486        let _: () = match self {
6487            Self::Expression(id) => emit_expr(emitter, ctx, *id)?,
6488            Self::Local { slot, .. } => {
6489                emitter.instruction(Instruction::LocalGet(*slot));
6490            }
6491        };
6492        Ok(())
6493    }
6494}
6495
6496fn emit_shape_method_with_arguments(
6497    emitter: &mut FunctionEmitter,
6498    ctx: &CodegenCtx,
6499    method: &str,
6500    args: &[ShapeArgument],
6501    call_ret_ty: &Type,
6502    slot_index_local: Option<u32>,
6503) -> Result<(), crate::compiler_error::CompilerFailure> {
6504    let intrinsics = ctx
6505        .symbols
6506        .intrinsic_type_indices()
6507        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
6508
6509    // Build the closure signature from the arg types and the
6510    // call's return type. `classify` reduces to (arity, is_void).
6511    let arg_tys: Vec<Type> = args.iter().map(|a| a.ty(ctx)).collect::<Result<_, _>>()?;
6512    let fn_ty = Type::Function {
6513        params: arg_tys,
6514        ret: Box::new(call_ret_ty.clone()),
6515        predicate: None,
6516        // call-site arg shape — variadic packing has already
6517        // happened upstream by this point, so this signature is the
6518        // fixed-arity post-pack form.
6519        has_rest: false,
6520        optional: 0,
6521    };
6522    let closure_sig = crate::codegen::closures::classify(&fn_ty)?;
6523    let closure_struct_idx = ctx
6524        .symbols
6525        .closure_struct_type_idx(closure_sig)
6526        .ok_or_else(|| {
6527            crate::codegen::internal_failure(
6528                "closure struct registered for the method's signature (SUB-166)",
6529            )
6530        })?;
6531    let fn_type_idx = ctx
6532        .symbols
6533        .closure_func_type_idx(closure_sig)
6534        .ok_or_else(|| {
6535            crate::codegen::internal_failure(
6536                "closure funcref type registered for the method's signature",
6537            )
6538        })?;
6539
6540    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
6541        nullable: false,
6542        heap_type: HeapType::Concrete(intrinsics.object_shape),
6543    }))?;
6544    let closure_local = emitter.add_anonymous_local(ValType::Ref(RefType {
6545        nullable: false,
6546        heap_type: HeapType::Concrete(closure_struct_idx),
6547    }))?;
6548
6549    // 1. Receiver is on stack as `(ref [null] $Object)` (or a
6550    //    narrower subtype). The field scan expects `(ref
6551    //    $ObjectShape)` (non-null subtype). Strip null and downcast.
6552    emitter.instruction(Instruction::RefAsNonNull);
6553    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
6554        intrinsics.object_shape,
6555    )));
6556    emitter.instruction(Instruction::LocalSet(rcv_local));
6557
6558    // 2. Read the method's slot: by index when the caller already scanned it,
6559    //    else by the field-name scan.
6560    if let Some(index_local) = slot_index_local {
6561        emit_field_slot_get(emitter, intrinsics, rcv_local, index_local);
6562    } else {
6563        let name_global = ctx
6564            .symbols
6565            .field_name_string_global_idx(method)
6566            .ok_or_else(|| {
6567                crate::codegen::internal_failure(
6568                    ("per-name string global recorded during field-name-strings emission \
6569             for interface method names")
6570                        .to_string(),
6571                )
6572            })?;
6573        emit_object_field_read_by_name(emitter, ctx, rcv_local, name_global);
6574    }
6575
6576    let method_value = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6577    emitter.instruction(Instruction::LocalSet(method_value));
6578    let mut evaluated_args = Vec::with_capacity(args.len());
6579    for arg in args {
6580        arg.emit(emitter, ctx)?;
6581        cast::emit_box(emitter, ctx, &arg.ty(ctx)?)?;
6582        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6583        emitter.instruction(Instruction::LocalSet(slot));
6584        evaluated_args.push(slot);
6585    }
6586    emitter.instruction(Instruction::LocalGet(method_value));
6587    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
6588        intrinsics.closure,
6589    )));
6590    emitter.instruction(Instruction::I32Eqz);
6591    emitter.emit_if(BlockType::Empty);
6592    crate::codegen::throw::emit_type_error_throw(emitter, ctx, "Value is not callable");
6593    emitter.emit_end();
6594    emitter.instruction(Instruction::LocalGet(method_value));
6595
6596    // 3. Cast to the matching closure struct type. closure
6597    // ABI: every Function-typed value stored in an `$Object` slot is
6598    // a `(ref $closure_<sig>)`.
6599    crate::codegen::closure_coercions::emit_erased_cast(
6600        emitter,
6601        ctx,
6602        crate::codegen::closures::ClosureSig::of(args.len(), call_ret_ty)?,
6603    )?;
6604    emitter.instruction(Instruction::LocalTee(closure_local));
6605
6606    // 4. Push the closure env (slot 2) as the implicit first arg.
6607    emitter.instruction(Instruction::StructGet {
6608        struct_type_index: closure_struct_idx,
6609        field_index: 2,
6610    });
6611
6612    crate::codegen::this_binding::bind(emitter, ctx, rcv_local)?;
6613    // 5. Emit each user arg, boxing to `(ref $Object)` per the
6614    // closure ABI's erasure.
6615    for slot in evaluated_args {
6616        emitter.instruction(Instruction::LocalGet(slot));
6617    }
6618
6619    // 6. Push the funcref (slot 1) and call_ref.
6620    emitter.instruction(Instruction::LocalGet(closure_local));
6621    emitter.instruction(Instruction::StructGet {
6622        struct_type_index: closure_struct_idx,
6623        field_index: 1,
6624    });
6625    emitter.instruction(Instruction::CallRef(fn_type_idx));
6626
6627    // 7. Validate and unbox the erased return. Callers hand this the declared
6628    // return type verbatim; a mismatched implementation throws `TypeError`.
6629    let _: () = if !call_ret_ty.is_void() {
6630        crate::codegen::cast_check::emit_checked_cast_on_stack(
6631            emitter,
6632            ctx,
6633            &Type::Unknown,
6634            call_ret_ty,
6635        )?;
6636    };
6637    Ok(())
6638}
6639
6640/// Map a built-in method name to its slot index in the universal
6641/// `$VTable`. The four-slot layout (toString / toJson / equals /
6642/// hash) is hard-coded everywhere in the runtime — see
6643/// [`crate::codegen::intrinsics::IntrinsicTypeIndices`] — and stays
6644/// hardcoded here until per-interface vtables make slot
6645/// indices interface-relative.
6646///
6647/// Returns `None` for any name not in the four-slot table; the
6648/// caller treats `None` as "no vtable dispatch path for this
6649/// method".
6650fn vtable_slot_for_method(method: &str) -> Option<u32> {
6651    match method {
6652        "toString" => Some(0),
6653        "toJson" => Some(1),
6654        "equals" => Some(2),
6655        "hash" => Some(3),
6656        _ => None,
6657    }
6658}
6659
6660/// Emit `recv.vtable.<slot>(recv)` for an `$Object`-shaped receiver.
6661/// Stack at exit: the slot's return type (today: `(ref $string)`
6662/// for slot 0, the only slot reachable through `emit_method_call`).
6663/// `Type::Array` also flows here through generic receivers because
6664/// `$Array <: $Object`; the implicit subtype upcast lets the
6665/// `(ref $Object)` field-0 access work uniformly.
6666/// Dispatches a vtable method (`slot`) on a receiver — a non-null
6667/// `(ref $Object)` (or subtype) — already on the stack. The nullable-union
6668/// `JSON.stringify` arm evaluates the receiver once into a local, null-checks
6669/// it, then re-pushes the non-null cast into this tail.
6670pub(super) fn emit_vtable_dispatch_on_object_stack(
6671    emitter: &mut FunctionEmitter,
6672    ctx: &CodegenCtx,
6673    slot: u32,
6674) {
6675    let Some(intrinsics) = ctx.require(
6676        ctx.symbols.intrinsic_type_indices(),
6677        "intrinsics declared by codegen entry",
6678    ) else {
6679        return;
6680    };
6681    // Per-slot funcref type. Slots 0 (toString) and 1 (toJson) share
6682    // operational signature `(ref $Object) -> (ref $string)` but are
6683    // declared as distinct sub-funcrefs inside the `$Object` rec
6684    // group, so `call_ref` needs the slot's own type index.
6685    let fn_type_idx = match slot {
6686        0 => intrinsics.to_string_fn,
6687        1 => intrinsics.to_json_fn,
6688        _ => {
6689            ctx.fail("vtable string dispatch requires toString or toJson slot");
6690            return;
6691        }
6692    };
6693    let Some(obj_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
6694        nullable: false,
6695        heap_type: HeapType::Concrete(intrinsics.object),
6696    }))) else {
6697        return;
6698    };
6699    let Some(fn_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
6700        nullable: false,
6701        heap_type: HeapType::Concrete(fn_type_idx),
6702    }))) else {
6703        return;
6704    };
6705    // the receiver may arrive as `(ref null $Object)` when
6706    // it's typed `unknown`. Coerce to non-null before stashing in
6707    // `obj_local`. Traps on null at runtime — calling `.toString()`
6708    // on null isn't meaningful, and narrowed-away null branches
6709    // never reach here.
6710    emitter.instruction(Instruction::RefAsNonNull);
6711    emitter.instruction(Instruction::LocalTee(obj_local));
6712    emitter.instruction(Instruction::StructGet {
6713        struct_type_index: intrinsics.object,
6714        field_index: 0,
6715    });
6716    emitter.instruction(Instruction::StructGet {
6717        struct_type_index: intrinsics.vtable,
6718        field_index: slot,
6719    });
6720    emitter.instruction(Instruction::LocalSet(fn_local));
6721    emitter.instruction(Instruction::LocalGet(obj_local));
6722    emitter.instruction(Instruction::LocalGet(fn_local));
6723    emitter.instruction(Instruction::CallRef(fn_type_idx));
6724}
6725
6726/// Push each argument, coercing it into the callee's recorded slot type.
6727/// `slots` is `None` when no ABI was recorded, in which case the arguments are
6728/// already at their declared types.
6729fn emit_args_into_slots(
6730    emitter: &mut FunctionEmitter,
6731    ctx: &CodegenCtx,
6732    args: &[ExprId],
6733    slots: Option<&[ValType]>,
6734) -> Result<(), crate::compiler_error::CompilerFailure> {
6735    emit_args_into_slots_with_defaults(emitter, ctx, args, slots, &[])
6736}
6737
6738fn emit_args_into_slots_with_defaults(
6739    emitter: &mut FunctionEmitter,
6740    ctx: &CodegenCtx,
6741    args: &[ExprId],
6742    slots: Option<&[ValType]>,
6743    defaults: &[Option<crate::DefaultValue>],
6744) -> Result<(), crate::compiler_error::CompilerFailure> {
6745    if slots.is_some_and(|slots| slots.len() != args.len()) {
6746        return Err(crate::codegen::internal_failure(
6747            "call ABI argument count mismatch",
6748        ));
6749    }
6750    let locals: Vec<_> = args
6751        .iter()
6752        .map(|&arg| {
6753            emit_expr(emitter, ctx, arg)?;
6754            let local = emitter.add_anonymous_local(
6755                ctx.symbols.value_type(
6756                    &ctx.ta
6757                        .try_expr(arg)
6758                        .map_err(crate::codegen::arena_failure)?
6759                        .ty,
6760                )?,
6761            )?;
6762            emitter.instruction(Instruction::LocalSet(local));
6763            Ok::<_, crate::compiler_error::CompilerFailure>(local)
6764        })
6765        .collect::<Result<_, _>>()?;
6766    for (i, (&arg, local)) in args.iter().zip(locals).enumerate() {
6767        emitter.instruction(Instruction::LocalGet(local));
6768        if let Some(slot) = slots.and_then(|s| s.get(i).copied()) {
6769            crate::codegen::argument_defaults::emit_argument(
6770                emitter,
6771                ctx,
6772                &ctx.ta
6773                    .try_expr(arg)
6774                    .map_err(crate::codegen::arena_failure)?
6775                    .ty,
6776                slot,
6777                defaults.get(i).and_then(Option::as_ref),
6778            )?;
6779        }
6780    }
6781    Ok(())
6782}
6783
6784/// The physical signature a Direct/Static-dispatch wrapper call must be
6785/// coerced into: the recorded one, or the same shape rebuilt from a
6786/// [`GenericMethodCall`](TypedExprKind::GenericMethodCall)'s typecheck-time
6787/// metadata, which describes the same erasure one call at a time.
6788///
6789/// A wrapper's `func_idx` and its recorded ABI come from two loops gated on
6790/// different sets — `is_value_used` for the value-symbol walk, versus
6791/// `is_interface_member_used` (which also skips `Dispatch::VTable` interfaces
6792/// wholesale) for the walk that records the ABI. A promotion pass keeps the
6793/// two aligned; nothing in either loop *enforces* it, so a resolvable wrapper
6794/// with no record stays possible, in the same sense as the "defensive" arm on
6795/// the dispatch match above. Rebuilding costs a branch and removes a silent
6796/// mis-emit — args pushed at their declared types into erased slots — from the
6797/// failure modes of a future gating change.
6798fn wrapper_abi_for_call(
6799    ctx: &CodegenCtx,
6800    direct_key: &crate::MangledName,
6801    args: &[ExprId],
6802    generic_args: Option<&[crate::GenericArgument]>,
6803    return_cast: Option<&Type>,
6804    call_ret_ty: &Type,
6805) -> Result<MethodSlotAbi, crate::compiler_error::CompilerFailure> {
6806    if let Some(recorded) = ctx.symbols.iface_method_abi(direct_key) {
6807        return Ok(recorded.clone());
6808    }
6809    let erased = ctx.symbols.value_type(&Type::Unknown)?;
6810    let slot_is_erased =
6811        |i: usize| generic_args.is_some_and(|flags| flags.get(i).is_some_and(|g| g.is_generic));
6812    Ok(MethodSlotAbi {
6813        params: args
6814            .iter()
6815            .enumerate()
6816            .map(|(i, &a)| {
6817                Ok::<_, crate::compiler_error::CompilerFailure>(if slot_is_erased(i) {
6818                    erased
6819                } else {
6820                    ctx.symbols.value_type(
6821                        &ctx.ta
6822                            .try_expr(a)
6823                            .map_err(crate::codegen::arena_failure)?
6824                            .ty,
6825                    )?
6826                })
6827            })
6828            .collect::<Result<_, _>>()?,
6829        ret: match return_cast {
6830            Some(_) => Some(erased),
6831            None => (!call_ret_ty.is_void())
6832                .then(|| ctx.symbols.value_type(call_ret_ty))
6833                .transpose()?,
6834        },
6835    })
6836}
6837
6838/// Cast an erased slot result back to the call-site type. A slot whose result
6839/// already matches needs nothing.
6840fn emit_slot_return_cast(
6841    emitter: &mut FunctionEmitter,
6842    ctx: &CodegenCtx,
6843    call_ret_ty: &Type,
6844    abi: Option<&MethodSlotAbi>,
6845) -> Result<(), crate::compiler_error::CompilerFailure> {
6846    if matches!(call_ret_ty.lowered(), Type::Never) {
6847        emitter.instruction(Instruction::Unreachable);
6848        return Ok(());
6849    }
6850    // A resultless slot still produces `undefined`, including when a widened
6851    // call site reads it as `unknown`.
6852    if abi.is_some_and(|abi| abi.ret.is_none()) {
6853        emit_undefined(emitter, ctx)?;
6854    }
6855    if call_ret_ty.is_void() {
6856        return Ok(());
6857    }
6858    if matches!(call_ret_ty, Type::Unknown) {
6859        match abi.and_then(|abi| abi.ret) {
6860            Some(ValType::F64) => cast::emit_box(emitter, ctx, &Type::Number)?,
6861            Some(ValType::I32) => cast::emit_box(emitter, ctx, &Type::Boolean)?,
6862            _ => {}
6863        }
6864        return Ok(());
6865    }
6866    if abi.and_then(|a| a.ret) != Some(ctx.symbols.value_type(call_ret_ty)?) {
6867        crate::codegen::cast_check::emit_checked_cast_on_stack(
6868            emitter,
6869            ctx,
6870            &Type::Unknown,
6871            call_ret_ty,
6872        )?;
6873    }
6874    crate::codegen::field_guards::attach(emitter, ctx, call_ret_ty)?;
6875    Ok(())
6876}
6877
6878/// Static-path class method dispatch. The receiver `(ref $Foo)` is on the stack.
6879/// Loads the vtable (struct slot 0) of the receiver's *static* class, reads the
6880/// method funcref at its slot, then `call_ref`s it with the receiver as self +
6881/// the args. Slots are inherited at the same index, so a parent-typed receiver
6882/// dispatches to the runtime override automatically.
6883///
6884/// Generic-class slots are erased: the physical sig takes/returns
6885/// `(ref null $Object)` where the declared type was a class type param. The
6886/// slot's recorded [`MethodSlotAbi`] drives coercion of the args and the
6887/// cast of the erased return back to `call_ret_ty` — self-sufficiently, since
6888/// plain `MethodCall`, optional-chain parts, and the synthesized accessor
6889/// calls carry no generic metadata.
6890fn emit_class_vtable_dispatch(
6891    emitter: &mut FunctionEmitter,
6892    ctx: &CodegenCtx,
6893    class: &crate::MangledName,
6894    method: &str,
6895    args: &[ExprId],
6896    call_ret_ty: &Type,
6897) -> Result<(), crate::compiler_error::CompilerFailure> {
6898    let struct_idx = ctx
6899        .symbols
6900        .class_struct_type_idx(class)
6901        .ok_or_else(|| crate::codegen::internal_failure("class struct type recorded"))?;
6902    let vtable_idx = ctx
6903        .symbols
6904        .class_vtable_type_idx(class)
6905        .ok_or_else(|| crate::codegen::internal_failure("class vtable type recorded"))?;
6906    let slot = ctx
6907        .symbols
6908        .class_method_slot(class, method)
6909        .ok_or_else(|| crate::codegen::internal_failure("class method slot recorded"))?;
6910    let sig_idx = ctx
6911        .symbols
6912        .class_method_sig(class, method)
6913        .ok_or_else(|| crate::codegen::internal_failure("class method sig recorded"))?;
6914
6915    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
6916        nullable: false,
6917        heap_type: HeapType::Concrete(struct_idx),
6918    }))?;
6919    // Defensive cast: the receiver is `(ref $Foo)` on the direct path, but the
6920    // optional-chain path may hand us a wider `(ref null $Object)`.
6921    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(struct_idx)));
6922    emitter.instruction(Instruction::LocalSet(rcv_local));
6923
6924    // Load the method funcref from the vtable, stash it.
6925    emitter.instruction(Instruction::LocalGet(rcv_local));
6926    emitter.instruction(Instruction::StructGet {
6927        struct_type_index: struct_idx,
6928        field_index: 0,
6929    });
6930    emitter.instruction(Instruction::StructGet {
6931        struct_type_index: vtable_idx,
6932        field_index: slot,
6933    });
6934    let fn_local = emitter.add_anonymous_local(ValType::Ref(RefType {
6935        nullable: false,
6936        heap_type: HeapType::Concrete(sig_idx),
6937    }))?;
6938    emitter.instruction(Instruction::LocalSet(fn_local));
6939
6940    // self (`(ref $Foo)` <: the sig's `(ref $Object)`), then args, then funcref.
6941    let abi = ctx.symbols.class_method_abi(class, method).cloned();
6942    emitter.instruction(Instruction::LocalGet(rcv_local));
6943    emit_args_into_slots(
6944        emitter,
6945        ctx,
6946        args,
6947        abi.as_ref().map(|a| a.params.as_slice()),
6948    )?;
6949    emitter.instruction(Instruction::LocalGet(fn_local));
6950    emitter.instruction(Instruction::CallRef(sig_idx));
6951    emit_slot_return_cast(emitter, ctx, call_ret_ty, abi.as_ref())?;
6952    Ok(())
6953}
6954
6955fn emit_intrinsic_call(
6956    emitter: &mut FunctionEmitter,
6957    ctx: &CodegenCtx,
6958    kind: Intrinsic,
6959    args: &[ExprId],
6960    result_ty: &Type,
6961    source_ty: &Type,
6962) -> Result<(), crate::compiler_error::CompilerFailure> {
6963    let _: () = match kind {
6964        Intrinsic::Assert => {
6965            // `assert(cond, msg)` — throw `new Error(msg)` on false.
6966            // JS-style left-to-right argument evaluation: cond first,
6967            // then msg (stashed in a local for the throw path).
6968            if args.len() != 2 {
6969                return Err(crate::codegen::internal_failure(
6970                    "assert requires two arguments",
6971                ));
6972            }
6973            emit_expr(emitter, ctx, args[0])?;
6974            cast::emit_condition_to_i32(
6975                emitter,
6976                ctx,
6977                &ctx.ta
6978                    .try_expr(args[0])
6979                    .map_err(crate::codegen::arena_failure)?
6980                    .ty,
6981            )?;
6982            emit_expr(emitter, ctx, args[1])?;
6983            let message_type = &ctx
6984                .ta
6985                .try_expr(args[1])
6986                .map_err(crate::codegen::arena_failure)?
6987                .ty;
6988            let msg_local = emitter.add_anonymous_local(ctx.symbols.value_type(message_type)?)?;
6989            emitter.instruction(Instruction::LocalSet(msg_local));
6990            emitter.instruction(Instruction::I32Eqz);
6991            emitter.emit_if(BlockType::Empty);
6992            emitter.instruction(Instruction::LocalGet(msg_local));
6993            cast::emit_coerce_to_slot(emitter, ctx, message_type, &Type::String)?;
6994            let ctor_idx = ctx
6995                .symbols
6996                .func_idx(&crate::mangle::prelude("Error#constructor"))
6997                .ok_or_else(|| {
6998                    crate::codegen::internal_failure("Error#constructor exported from prelude")
6999                })?;
7000            crate::codegen::throw::emit_no_error_options(emitter);
7001            emitter.instruction(Instruction::Call(ctor_idx));
7002            crate::codegen::throw::emit_error_throw(emitter, ctx);
7003            emitter.emit_end();
7004            emit_undefined(emitter, ctx)?;
7005        }
7006        Intrinsic::JsonStringify => {
7007            super::json::emit_stringify(emitter, ctx, args, result_ty, source_ty)?;
7008        }
7009        Intrinsic::JsonParse => {
7010            super::json::emit_parse(emitter, ctx, args)?;
7011        }
7012        Intrinsic::ToString => {
7013            let [value] = args else {
7014                return Err(crate::codegen::internal_failure(
7015                    "string conversion requires one argument",
7016                ));
7017            };
7018            emit_expr(emitter, ctx, *value)?;
7019            cast::emit_box(
7020                emitter,
7021                ctx,
7022                &ctx.ta
7023                    .try_expr(*value)
7024                    .map_err(crate::codegen::arena_failure)?
7025                    .ty,
7026            )?;
7027            let function = ctx
7028                .symbols
7029                .prelude_func_idx("__value_to_string")
7030                .ok_or_else(|| {
7031                    crate::codegen::internal_failure("dynamic string conversion collected")
7032                })?;
7033            emitter.instruction(Instruction::Call(function));
7034        }
7035        Intrinsic::ToObject => {
7036            let [value] = args else {
7037                return Err(crate::codegen::internal_failure(
7038                    "object conversion requires one argument",
7039                ));
7040            };
7041            emit_expr(emitter, ctx, *value)?;
7042            let function = ctx
7043                .symbols
7044                .prelude_func_idx("ObjectConstructor##toObject")
7045                .ok_or_else(|| {
7046                    crate::codegen::internal_failure("object conversion helper collected")
7047                })?;
7048            emitter.instruction(Instruction::Call(function));
7049        }
7050        // `BigInt.fromString(s)` — the remaining
7051        // bigint-producing intrinsic after retired
7052        // `BigIntFromNumber` / `NumberFromBigInt` in favour of the
7053        // call-signature wrappers on `BigIntConstructor` /
7054        // `NumberConstructor`. Wraps the host result `(i32 sign, ref
7055        // $rawBigInt limbs)` into a `$bigint` struct.
7056        Intrinsic::BigIntFromString => {
7057            if args.len() != 1 {
7058                return Err(crate::codegen::internal_failure(
7059                    "BigInt.fromString requires one argument",
7060                ));
7061            }
7062            emit_expr(emitter, ctx, args[0])?;
7063            // The arg is `(ref $string)`; the host fn takes
7064            // `(ref $rawString)`. Pull the raw array out via slot 1.
7065            let string_type_idx = ctx
7066                .symbols
7067                .string_type_idx()
7068                .ok_or_else(|| crate::codegen::internal_failure("$string registered"))?;
7069            emitter.instruction(Instruction::StructGet {
7070                struct_type_index: string_type_idx,
7071                field_index: 1,
7072            });
7073            let host_idx = ctx
7074                .symbols
7075                .func_idx(&crate::mangle::host(
7076                    crate::runtime::BIGINT_MODULE_NAME,
7077                    "fromString",
7078                ))
7079                .ok_or_else(|| {
7080                    crate::codegen::internal_failure(
7081                        "submilli:bigint.fromString import recorded by codegen bootstrap",
7082                    )
7083                })?;
7084            emit_bigint_wrap_host_result(emitter, ctx, host_idx);
7085        }
7086    };
7087    Ok(())
7088}
7089
7090/// Emit `lhs.vtable.equals(lhs, rhs)` for two operands of `$Object`
7091/// subtypes (objects, arrays, functions, boxed values). Stack at entry: `[lhs, rhs]`.
7092/// Stack at exit: `[i32]` (1 if equal, 0 otherwise — flipped via
7093/// `I32Eqz` for `BinOp::NotEq`).
7094fn emit_vtable_equality(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, op: BinOp) {
7095    ctx.latch(emit_vtable_equality_checked(emitter, ctx, op));
7096}
7097
7098fn emit_vtable_equality_checked(
7099    emitter: &mut FunctionEmitter,
7100    ctx: &CodegenCtx,
7101    op: BinOp,
7102) -> Result<(), crate::compiler_error::CompilerFailure> {
7103    let intrinsics = ctx
7104        .symbols
7105        .intrinsic_type_indices()
7106        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
7107    // The operands' own types may differ (two function types of different
7108    // arities), so both are stashed at the common `$Object` shape.
7109    let operand_ref = ValType::Ref(RefType {
7110        nullable: true,
7111        heap_type: HeapType::Concrete(intrinsics.object),
7112    });
7113    let object_ref = ValType::Ref(RefType {
7114        nullable: false,
7115        heap_type: HeapType::Concrete(intrinsics.object),
7116    });
7117    let equals_fn_ref = ValType::Ref(RefType {
7118        nullable: false,
7119        heap_type: HeapType::Concrete(intrinsics.equals_fn),
7120    });
7121
7122    // Stack: [lhs, rhs]. Stash both, load equals fn, restack args.
7123    let Some(lhs_local) = ctx.latch(emitter.add_anonymous_local(operand_ref)) else {
7124        return Ok(());
7125    };
7126    let Some(rhs_local) = ctx.latch(emitter.add_anonymous_local(operand_ref)) else {
7127        return Ok(());
7128    };
7129    let Some(lhs_object) = ctx.latch(emitter.add_anonymous_local(object_ref)) else {
7130        return Ok(());
7131    };
7132    let Some(rhs_object) = ctx.latch(emitter.add_anonymous_local(object_ref)) else {
7133        return Ok(());
7134    };
7135    let Some(eq_fn_local) = ctx.latch(emitter.add_anonymous_local(equals_fn_ref)) else {
7136        return Ok(());
7137    };
7138
7139    emitter.instruction(Instruction::LocalSet(rhs_local));
7140    emitter.instruction(Instruction::LocalSet(lhs_local));
7141
7142    emitter.instruction(Instruction::LocalGet(lhs_local));
7143    emitter.instruction(Instruction::RefAsNonNull);
7144    emitter.instruction(Instruction::LocalSet(lhs_object));
7145    emitter.instruction(Instruction::LocalGet(rhs_local));
7146    emitter.instruction(Instruction::RefAsNonNull);
7147    emitter.instruction(Instruction::LocalSet(rhs_object));
7148
7149    emitter.instruction(Instruction::LocalGet(lhs_object));
7150    emitter.instruction(Instruction::StructGet {
7151        struct_type_index: intrinsics.object,
7152        field_index: 0,
7153    });
7154    emitter.instruction(Instruction::StructGet {
7155        struct_type_index: intrinsics.vtable,
7156        field_index: 2,
7157    });
7158    emitter.instruction(Instruction::LocalSet(eq_fn_local));
7159
7160    emitter.instruction(Instruction::LocalGet(lhs_object));
7161    emitter.instruction(Instruction::LocalGet(rhs_object));
7162    emitter.instruction(Instruction::LocalGet(eq_fn_local));
7163    emitter.instruction(Instruction::CallRef(intrinsics.equals_fn));
7164
7165    if matches!(op, BinOp::NotEq) {
7166        emitter.instruction(Instruction::I32Eqz);
7167    }
7168
7169    Ok(())
7170}
7171
7172/// Boxed, null-aware `===` / `!==` dispatch. Used when either operand's static
7173/// type admits null, or when the operands have different Wasm representations.
7174/// Bridges the operands to a
7175/// common Wasm shape (`(ref null $Object)`) via `emit_box`, then
7176/// emits a 4-way condition:
7177///
7178/// - both null → equal (1).
7179/// - lhs null, rhs non-null → unequal (0).
7180/// - lhs non-null, rhs null → unequal (0).
7181/// - both non-null → `lhs.vtable.equals(lhs, rhs)`.
7182///
7183/// Final `I32Eqz` flips the result for `NotEq`.
7184fn emit_boxed_eq(
7185    emitter: &mut FunctionEmitter,
7186    ctx: &CodegenCtx,
7187    lhs: ExprId,
7188    rhs: ExprId,
7189    op: BinOp,
7190) -> Result<(), crate::compiler_error::CompilerFailure> {
7191    let intrinsics = ctx
7192        .symbols
7193        .intrinsic_type_indices()
7194        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
7195    let object_null_ref = ValType::Ref(RefType {
7196        nullable: true,
7197        heap_type: HeapType::Concrete(intrinsics.object),
7198    });
7199    let object_ref = ValType::Ref(RefType {
7200        nullable: false,
7201        heap_type: HeapType::Concrete(intrinsics.object),
7202    });
7203    let equals_fn_ref = ValType::Ref(RefType {
7204        nullable: false,
7205        heap_type: HeapType::Concrete(intrinsics.equals_fn),
7206    });
7207
7208    let lhs_ty = ctx
7209        .ta
7210        .try_expr(lhs)
7211        .map_err(crate::codegen::arena_failure)?
7212        .ty
7213        .clone();
7214    let rhs_ty = ctx
7215        .ta
7216        .try_expr(rhs)
7217        .map_err(crate::codegen::arena_failure)?
7218        .ty
7219        .clone();
7220
7221    // Materialize lhs as (ref null $Object). `emit_box` wraps
7222    // primitive Wasm values into their `$Boxed*` shape and is a
7223    // no-op for ref types / unions, so both sides land on the
7224    // stack at the universal nullable shape.
7225    let lhs_local = emitter.add_anonymous_local(object_null_ref)?;
7226    let rhs_local = emitter.add_anonymous_local(object_null_ref)?;
7227    let lhs_nonnull_local = emitter.add_anonymous_local(object_ref)?;
7228    let eq_fn_local = emitter.add_anonymous_local(equals_fn_ref)?;
7229
7230    emit_expr(emitter, ctx, lhs)?;
7231    cast::emit_box(emitter, ctx, &lhs_ty)?;
7232    emitter.instruction(Instruction::LocalSet(lhs_local));
7233
7234    emit_expr(emitter, ctx, rhs)?;
7235    cast::emit_box(emitter, ctx, &rhs_ty)?;
7236    emitter.instruction(Instruction::LocalSet(rhs_local));
7237
7238    // if lhs is null: result = (rhs is null)
7239    //     covers both-null (1) and lhs-only-null (0).
7240    // else: if rhs is null → 0; else vtable.equals dispatch.
7241    emitter.instruction(Instruction::LocalGet(lhs_local));
7242    emitter.instruction(Instruction::RefIsNull);
7243    emitter.emit_if(BlockType::Result(ValType::I32));
7244    emitter.instruction(Instruction::LocalGet(rhs_local));
7245    emitter.instruction(Instruction::RefIsNull);
7246    emitter.emit_else();
7247    emitter.instruction(Instruction::LocalGet(rhs_local));
7248    emitter.instruction(Instruction::RefIsNull);
7249    emitter.emit_if(BlockType::Result(ValType::I32));
7250    emitter.instruction(Instruction::I32Const(0));
7251    emitter.emit_else();
7252    // Both non-null — vtable.equals on lhs's vtable.
7253    emitter.instruction(Instruction::LocalGet(lhs_local));
7254    emitter.instruction(Instruction::RefAsNonNull);
7255    emitter.instruction(Instruction::LocalTee(lhs_nonnull_local));
7256    emitter.instruction(Instruction::StructGet {
7257        struct_type_index: intrinsics.object,
7258        field_index: 0,
7259    });
7260    emitter.instruction(Instruction::StructGet {
7261        struct_type_index: intrinsics.vtable,
7262        field_index: 2,
7263    });
7264    emitter.instruction(Instruction::LocalSet(eq_fn_local));
7265    emitter.instruction(Instruction::LocalGet(lhs_nonnull_local));
7266    emitter.instruction(Instruction::LocalGet(rhs_local));
7267    emitter.instruction(Instruction::RefAsNonNull);
7268    emitter.instruction(Instruction::LocalGet(eq_fn_local));
7269    emitter.instruction(Instruction::CallRef(intrinsics.equals_fn));
7270    emitter.emit_end();
7271    emitter.emit_end();
7272
7273    let _: () = if matches!(op, BinOp::NotEq) {
7274        emitter.instruction(Instruction::I32Eqz);
7275    };
7276    Ok(())
7277}
7278
7279/// Emit a bigint literal. Small literals (≤ ±2^53 - 1)
7280/// fast-path through `submilli:bigint.fromNumber`; larger literals
7281/// are looked up in the bigint constants pool and materialized via
7282/// `array.new_data` over a per-program data segment, then wrapped
7283/// directly into `$bigint` with the prelude's vtable.
7284fn emit_bigint_literal(
7285    emitter: &mut FunctionEmitter,
7286    ctx: &CodegenCtx,
7287    digits: &str,
7288) -> Result<(), crate::compiler_error::CompilerFailure> {
7289    use crate::codegen::internal_failure;
7290    if let Some(entry) = ctx.bigints.lookup(digits)? {
7291        // Large literal — pack limbs from data segment, wrap with
7292        // bigint vtable directly (no host fn involved). Data-segment
7293        // index is offset by the string-pool count because string
7294        // segments come first in the module's data section.
7295        let raw_bigint_idx = ctx
7296            .symbols
7297            .raw_bigint_type_idx()
7298            .ok_or_else(|| internal_failure("the $rawBigInt intrinsic is not registered"))?;
7299        let bigint_idx = ctx
7300            .symbols
7301            .bigint_type_idx()
7302            .ok_or_else(|| internal_failure("the $bigint intrinsic is not registered"))?;
7303        let vtable_global = ctx
7304            .symbols
7305            .prelude_global_idx("bigint_vtable")
7306            .ok_or_else(|| internal_failure("bigint_vtable is not imported from the prelude"))?;
7307        let data_idx = crate::codegen::wasm_u32(ctx.strings.strings.len())?
7308            .checked_add(entry.data_idx)
7309            .ok_or_else(|| internal_failure("the module has too many data segments"))?;
7310        emitter.instruction(Instruction::GlobalGet(vtable_global));
7311        emitter.instruction(Instruction::I32Const(i32::from(entry.sign)));
7312        emitter.instruction(Instruction::I32Const(0));
7313        emitter.instruction(Instruction::I32Const(entry.limb_count.cast_signed()));
7314        emitter.instruction(Instruction::ArrayNewData {
7315            array_type_index: raw_bigint_idx,
7316            array_data_index: data_idx,
7317        });
7318        emitter.instruction(Instruction::StructNew(bigint_idx));
7319        return Ok(());
7320    }
7321    // The pool leaves only literals within f64's safe-integer range, so this
7322    // conversion is exact.
7323    // Construct via `submilli:bigint.fromNumber` + standard wrap.
7324    let v: i64 = digits
7325        .parse()
7326        .map_err(|_| internal_failure("an unpooled bigint literal is not a safe integer"))?;
7327    let host_idx = ctx
7328        .symbols
7329        .func_idx(&crate::mangle::host(
7330            crate::runtime::BIGINT_MODULE_NAME,
7331            "fromNumber",
7332        ))
7333        .ok_or_else(|| internal_failure("submilli:bigint.fromNumber is not imported"))?;
7334    emitter.instruction(Instruction::F64Const(Ieee64::from(v as f64)));
7335    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
7336    Ok(())
7337}
7338
7339/// regex-literal lowering. Each literal site allocates a
7340/// fresh `$regex` (JS-faithful per-site `lastIndex` semantics; see
7341/// `docs/regex.md`). Emit the source and flags as constant `$string`
7342/// values, then call the prelude's `RegExpConstructor#new` wrapper —
7343/// which invokes the `submilli:regex.compile` host fn and assembles
7344/// the receiver struct.
7345///
7346/// The pattern + flags have already been validated at infer time
7347/// (`typechecker::infer::expr::infer_regex` calls
7348/// `runtime::prelude::regex::engine::build_regex`), so the wrapper's compile call
7349/// here only fails on memory-cap exhaustion, which traps cleanly.
7350fn emit_regex_literal(
7351    emitter: &mut FunctionEmitter,
7352    ctx: &CodegenCtx,
7353    source: &str,
7354    flags: &str,
7355) -> Result<(), crate::compiler_error::CompilerFailure> {
7356    super::emit_const_string_by_text(emitter, ctx, source)?;
7357    super::emit_const_string_by_text(emitter, ctx, flags)?;
7358    let ctor_idx = ctx
7359        .symbols
7360        .func_idx(&crate::mangle::prelude("RegExpConstructor#new"))
7361        .ok_or_else(|| crate::codegen::internal_failure("RegExpConstructor#new is not imported"))?;
7362    emitter.instruction(Instruction::Call(ctor_idx));
7363    Ok(())
7364}
7365
7366/// helper — given a `(ref $bigint)` on top of the stack,
7367/// destructure it into `(i32 sign, ref $rawBigInt limbs)` on the
7368/// stack. Used by every codegen site that hands a bigint to a host
7369/// fn (`bigint.add` / `bigint.cmp` / `bigint.neg` /
7370/// `number.fromBigInt`).
7371fn emit_bigint_extract_to_stack(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
7372    ctx.latch(emit_bigint_extract_to_stack_checked(emitter, ctx));
7373}
7374
7375fn emit_bigint_extract_to_stack_checked(
7376    emitter: &mut FunctionEmitter,
7377    ctx: &CodegenCtx,
7378) -> Result<(), crate::compiler_error::CompilerFailure> {
7379    let bigint_idx = ctx
7380        .symbols
7381        .bigint_type_idx()
7382        .ok_or_else(|| crate::codegen::internal_failure("$bigint registered"))?;
7383    // Stash the receiver in a scratch local since we need two
7384    // struct.get reads.
7385    let Some(scratch) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
7386        wasm_encoder::RefType {
7387            nullable: false,
7388            heap_type: wasm_encoder::HeapType::Concrete(bigint_idx),
7389        },
7390    ))) else {
7391        return Ok(());
7392    };
7393    emitter.instruction(Instruction::LocalSet(scratch));
7394    emitter.instruction(Instruction::LocalGet(scratch));
7395    emitter.instruction(Instruction::StructGet {
7396        struct_type_index: bigint_idx,
7397        field_index: 1, // sign
7398    });
7399    emitter.instruction(Instruction::LocalGet(scratch));
7400    emitter.instruction(Instruction::StructGet {
7401        struct_type_index: bigint_idx,
7402        field_index: 2, // limbs
7403    });
7404
7405    Ok(())
7406}
7407
7408/// helper — call `host_func_idx` (whose result shape is
7409/// the standard `(i32 sign, ref $rawBigInt limbs)` bigint pair),
7410/// then wrap into a fresh `$bigint` struct with the prelude's
7411/// vtable. Stack at exit: `(ref $bigint)`.
7412fn emit_bigint_wrap_host_result(
7413    emitter: &mut FunctionEmitter,
7414    ctx: &CodegenCtx,
7415    host_func_idx: u32,
7416) {
7417    ctx.latch(emit_bigint_wrap_host_result_checked(
7418        emitter,
7419        ctx,
7420        host_func_idx,
7421    ));
7422}
7423
7424fn emit_bigint_wrap_host_result_checked(
7425    emitter: &mut FunctionEmitter,
7426    ctx: &CodegenCtx,
7427    host_func_idx: u32,
7428) -> Result<(), crate::compiler_error::CompilerFailure> {
7429    emitter.instruction(Instruction::Call(host_func_idx));
7430    let bigint_idx = ctx
7431        .symbols
7432        .bigint_type_idx()
7433        .ok_or_else(|| crate::codegen::internal_failure("$bigint registered"))?;
7434    let raw_bigint_idx = ctx
7435        .symbols
7436        .raw_bigint_type_idx()
7437        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
7438    let vtable_global = ctx
7439        .symbols
7440        .prelude_global_idx("bigint_vtable")
7441        .ok_or_else(|| {
7442            crate::codegen::internal_failure("bigint_vtable imported from prelude bootstrap")
7443        })?;
7444    // Stack: [sign, limbs]. Need [vtable, sign, limbs] for
7445    // struct.new $bigint. Use two scratch locals to reorder.
7446    let Some(scratch_limbs) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
7447        wasm_encoder::RefType {
7448            nullable: false,
7449            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
7450        },
7451    ))) else {
7452        return Ok(());
7453    };
7454    let Some(scratch_sign) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::I32))
7455    else {
7456        return Ok(());
7457    };
7458    emitter.instruction(Instruction::LocalSet(scratch_limbs));
7459    emitter.instruction(Instruction::LocalSet(scratch_sign));
7460    emitter.instruction(Instruction::GlobalGet(vtable_global));
7461    emitter.instruction(Instruction::LocalGet(scratch_sign));
7462    emitter.instruction(Instruction::LocalGet(scratch_limbs));
7463    emitter.instruction(Instruction::StructNew(bigint_idx));
7464
7465    Ok(())
7466}
7467
7468/// inline binary bigint op. `op_name` is one of
7469/// "add" / "sub" / "mul" / "div" / "mod" — looked up
7470/// directly against `submilli:bigint.<op>`.
7471fn emit_bigint_binop_inline(
7472    emitter: &mut FunctionEmitter,
7473    ctx: &CodegenCtx,
7474    lhs: ExprId,
7475    rhs: ExprId,
7476    op_name: &str,
7477) -> Result<(), crate::compiler_error::CompilerFailure> {
7478    emit_expr(emitter, ctx, lhs)?;
7479    emit_bigint_extract_to_stack(emitter, ctx);
7480    let raw_bigint_idx = ctx
7481        .symbols
7482        .raw_bigint_type_idx()
7483        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
7484    let lhs_limbs =
7485        emitter.add_anonymous_local(wasm_encoder::ValType::Ref(wasm_encoder::RefType {
7486            nullable: false,
7487            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
7488        }))?;
7489    let lhs_sign = emitter.add_anonymous_local(wasm_encoder::ValType::I32)?;
7490    emitter.instruction(Instruction::LocalSet(lhs_limbs));
7491    emitter.instruction(Instruction::LocalSet(lhs_sign));
7492    emitter.instruction(Instruction::LocalGet(lhs_sign));
7493    emitter.instruction(Instruction::LocalGet(lhs_limbs));
7494    emit_expr(emitter, ctx, rhs)?;
7495    emit_bigint_extract_to_stack(emitter, ctx);
7496    let host_idx = ctx
7497        .symbols
7498        .func_idx(&crate::mangle::host(
7499            crate::runtime::BIGINT_MODULE_NAME,
7500            op_name,
7501        ))
7502        .ok_or_else(|| {
7503            crate::codegen::internal_failure(format!(
7504                "submilli:bigint.{op_name} import recorded by codegen bootstrap"
7505            ))
7506        })?;
7507    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
7508    Ok(())
7509}
7510
7511/// postfix `++` / `--` compute step for a bigint operand —
7512/// stack `[orig (ref $bigint)]` → `[new (ref $bigint)]`. Mirrors
7513/// [`emit_bigint_binop_inline`] but skips the lhs `emit_expr` (it's
7514/// already on the stack) and emits the rhs as a synthesized `1n`
7515/// literal via [`emit_bigint_literal`] (small-literal path:
7516/// `f64.const 1` + `submilli:bigint.fromNumber`).
7517fn emit_bigint_pm_one(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, op: crate::PostfixOp) {
7518    ctx.latch(emit_bigint_pm_one_checked(emitter, ctx, op));
7519}
7520
7521fn emit_bigint_pm_one_checked(
7522    emitter: &mut FunctionEmitter,
7523    ctx: &CodegenCtx,
7524    op: crate::PostfixOp,
7525) -> Result<(), crate::compiler_error::CompilerFailure> {
7526    emit_bigint_extract_to_stack(emitter, ctx);
7527    let raw_bigint_idx = ctx
7528        .symbols
7529        .raw_bigint_type_idx()
7530        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
7531    let Some(lhs_limbs) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
7532        wasm_encoder::RefType {
7533            nullable: false,
7534            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
7535        },
7536    ))) else {
7537        return Ok(());
7538    };
7539    let Some(lhs_sign) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::I32)) else {
7540        return Ok(());
7541    };
7542    emitter.instruction(Instruction::LocalSet(lhs_limbs));
7543    emitter.instruction(Instruction::LocalSet(lhs_sign));
7544    emitter.instruction(Instruction::LocalGet(lhs_sign));
7545    emitter.instruction(Instruction::LocalGet(lhs_limbs));
7546    if ctx.latch(emit_bigint_literal(emitter, ctx, "1")).is_none() {
7547        return Ok(());
7548    }
7549    emit_bigint_extract_to_stack(emitter, ctx);
7550    let op_name = match op {
7551        crate::PostfixOp::Inc => "add",
7552        crate::PostfixOp::Dec => "sub",
7553        crate::PostfixOp::NonNullAssert => {
7554            return Err(crate::codegen::internal_failure(
7555                "non-null assertion is not PostfixUnary",
7556            ));
7557        }
7558    };
7559    let host_idx = ctx
7560        .symbols
7561        .func_idx(&crate::mangle::host(
7562            crate::runtime::BIGINT_MODULE_NAME,
7563            op_name,
7564        ))
7565        .ok_or_else(|| {
7566            crate::codegen::internal_failure(format!(
7567                "submilli:bigint.{op_name} import recorded by codegen bootstrap"
7568            ))
7569        })?;
7570    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
7571
7572    Ok(())
7573}
7574
7575/// inline `cmp` + signed compare against 0 for
7576/// `<` / `>` / `<=` / `>=`.
7577fn emit_bigint_cmp_inline(
7578    emitter: &mut FunctionEmitter,
7579    ctx: &CodegenCtx,
7580    lhs: ExprId,
7581    rhs: ExprId,
7582    op: BinOp,
7583) -> Result<(), crate::compiler_error::CompilerFailure> {
7584    emit_bigint_cmp_call(emitter, ctx, lhs, rhs)?;
7585    emitter.instruction(Instruction::I32Const(0));
7586    let inst = match op {
7587        BinOp::Lt => Instruction::I32LtS,
7588        BinOp::Gt => Instruction::I32GtS,
7589        BinOp::Le => Instruction::I32LeS,
7590        BinOp::Ge => Instruction::I32GeS,
7591        _ => {
7592            return Err(crate::codegen::internal_failure(
7593                "emit_bigint_cmp_inline called with non-cmp op",
7594            ));
7595        }
7596    };
7597    emitter.instruction(inst);
7598    Ok(())
7599}
7600
7601/// `===` / `!==` on bigint — equal iff `cmp == 0`.
7602fn emit_bigint_cmp_eq_inline(
7603    emitter: &mut FunctionEmitter,
7604    ctx: &CodegenCtx,
7605    lhs: ExprId,
7606    rhs: ExprId,
7607    op: BinOp,
7608) -> Result<(), crate::compiler_error::CompilerFailure> {
7609    emit_bigint_cmp_call(emitter, ctx, lhs, rhs)?;
7610    emitter.instruction(Instruction::I32Eqz);
7611    let _: () = if matches!(op, BinOp::NotEq) {
7612        emitter.instruction(Instruction::I32Eqz);
7613    };
7614    Ok(())
7615}
7616
7617/// shared "two bigint operands → submilli:bigint.cmp"
7618/// setup. Stack at exit: `i32` (the raw cmp result, -1/0/1).
7619fn emit_bigint_cmp_call(
7620    emitter: &mut FunctionEmitter,
7621    ctx: &CodegenCtx,
7622    lhs: ExprId,
7623    rhs: ExprId,
7624) -> Result<(), crate::compiler_error::CompilerFailure> {
7625    emit_expr(emitter, ctx, lhs)?;
7626    emit_bigint_extract_to_stack(emitter, ctx);
7627    let raw_bigint_idx = ctx
7628        .symbols
7629        .raw_bigint_type_idx()
7630        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
7631    let lhs_limbs =
7632        emitter.add_anonymous_local(wasm_encoder::ValType::Ref(wasm_encoder::RefType {
7633            nullable: false,
7634            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
7635        }))?;
7636    let lhs_sign = emitter.add_anonymous_local(wasm_encoder::ValType::I32)?;
7637    emitter.instruction(Instruction::LocalSet(lhs_limbs));
7638    emitter.instruction(Instruction::LocalSet(lhs_sign));
7639    emitter.instruction(Instruction::LocalGet(lhs_sign));
7640    emitter.instruction(Instruction::LocalGet(lhs_limbs));
7641    emit_expr(emitter, ctx, rhs)?;
7642    emit_bigint_extract_to_stack(emitter, ctx);
7643    let cmp_idx = ctx
7644        .symbols
7645        .func_idx(&crate::mangle::host(
7646            crate::runtime::BIGINT_MODULE_NAME,
7647            "cmp",
7648        ))
7649        .ok_or_else(|| {
7650            crate::codegen::internal_failure(
7651                "submilli:bigint.cmp import recorded by codegen bootstrap",
7652            )
7653        })?;
7654    emitter.instruction(Instruction::Call(cmp_idx));
7655    Ok(())
7656}
7657
7658/// Evaluate plain literal values once, including overwritten expressions.
7659fn evaluate_object_members(
7660    emitter: &mut FunctionEmitter<'_>,
7661    ctx: &CodegenCtx<'_>,
7662    members: &[TypedObjectMember],
7663) -> Result<HashMap<ExprId, u32>, crate::compiler_error::CompilerFailure> {
7664    let intrinsics = ctx
7665        .symbols
7666        .intrinsic_type_indices()
7667        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
7668    let mut values = HashMap::new();
7669    for member in members {
7670        let TypedObjectMember::Value(value_id) = *member else {
7671            return Err(crate::codegen::internal_failure(
7672                "spread field reached plain object lowering",
7673            ));
7674        };
7675        let value_ty = &ctx
7676            .ta
7677            .try_expr(value_id)
7678            .map_err(crate::codegen::arena_failure)?
7679            .ty;
7680        emit_expr(emitter, ctx, value_id)?;
7681        cast::emit_box(emitter, ctx, value_ty)?;
7682        let local = emitter.add_anonymous_local(object_ref(intrinsics.object))?;
7683        emitter.instruction(Instruction::LocalSet(local));
7684        values.insert(value_id, local);
7685    }
7686    Ok(values)
7687}
7688
7689/// Pushes whether the field at `index`, whose slot holds `value`, is present
7690/// with a value of `field_ty`, using the same presence flag as `in`.
7691fn emit_field_holds_value_of(
7692    emitter: &mut FunctionEmitter<'_>,
7693    ctx: &CodegenCtx<'_>,
7694    object: u32,
7695    index: u32,
7696    value: u32,
7697    field_ty: &Type,
7698) -> Result<(), crate::compiler_error::CompilerFailure> {
7699    let intrinsics = ctx
7700        .symbols
7701        .intrinsic_type_indices()
7702        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
7703    emitter.instruction(Instruction::LocalGet(object));
7704    emitter.instruction(Instruction::StructGet {
7705        struct_type_index: intrinsics.object_shape,
7706        field_index: 1,
7707    });
7708    emitter.instruction(Instruction::LocalGet(index));
7709    emitter.instruction(Instruction::ArrayGet(intrinsics.field_names));
7710    crate::codegen::field_names::emit_name_presence(emitter, ctx)?;
7711    emitter.instruction(Instruction::LocalGet(value));
7712    emit_is_nullish(emitter, ctx)?;
7713    emitter.instruction(Instruction::I32Eqz);
7714    emitter.instruction(Instruction::I32Or);
7715    let _: () = if field_runtime_type_is_testable(field_ty) {
7716        emit_structural_test(emitter, ctx, value, field_ty, field_ty)?;
7717        emitter.instruction(Instruction::I32And);
7718    };
7719    Ok(())
7720}
7721
7722fn object_ref(object_type: u32) -> ValType {
7723    ValType::Ref(RefType {
7724        nullable: true,
7725        heap_type: HeapType::Concrete(object_type),
7726    })
7727}
7728
7729/// Array indices use JavaScript's string-hint property-key conversion.
7730pub(super) fn emit_index_number(
7731    emitter: &mut FunctionEmitter,
7732    ctx: &CodegenCtx,
7733    ty: &Type,
7734) -> Result<(), crate::compiler_error::CompilerFailure> {
7735    if ty == &Type::Unknown {
7736        let function = ctx
7737            .symbols
7738            .prelude_func_idx("__value_to_index")
7739            .ok_or_else(|| crate::codegen::internal_failure("dynamic index helper collected"))?;
7740        emitter.instruction(Instruction::Call(function));
7741    } else {
7742        cast::emit_coerce_to_slot(emitter, ctx, ty, &Type::Number)?;
7743    };
7744    Ok(())
7745}
7746
7747fn emit_computed_object(
7748    emitter: &mut FunctionEmitter,
7749    ctx: &CodegenCtx,
7750    members: &[TypedObjectMember],
7751) -> Result<(), crate::compiler_error::CompilerFailure> {
7752    let Some(intr) = ctx.require(ctx.symbols.intrinsic_type_indices(), "intrinsics declared")
7753    else {
7754        return Ok(());
7755    };
7756    let Some(merge) = ctx.require(
7757        ctx.symbols.prelude_func_idx("ObjectConstructor##spread"),
7758        "spread helper imported",
7759    ) else {
7760        return Ok(());
7761    };
7762    let object = emitter.add_anonymous_local(ValType::Ref(RefType {
7763        nullable: false,
7764        heap_type: HeapType::Concrete(intr.object_shape),
7765    }))?;
7766    for _ in 0..4 {
7767        emitter.instruction(Instruction::RefNull(HeapType::Concrete(intr.object)));
7768    }
7769    emitter.instruction(Instruction::Call(merge));
7770    emitter.instruction(Instruction::LocalSet(object));
7771    for member in members {
7772        emitter.instruction(Instruction::LocalGet(object));
7773        match member {
7774            TypedObjectMember::Computed { key, value } => {
7775                emit_expr(emitter, ctx, *key)?;
7776                crate::codegen::cast_check::emit_operation_cast_on_stack(
7777                    emitter,
7778                    ctx,
7779                    &ctx.ta
7780                        .try_expr(*key)
7781                        .map_err(crate::codegen::arena_failure)?
7782                        .ty,
7783                    &Type::String,
7784                )?;
7785                emit_expr(emitter, ctx, *value)?;
7786                cast::emit_box(
7787                    emitter,
7788                    ctx,
7789                    &ctx.ta
7790                        .try_expr(*value)
7791                        .map_err(crate::codegen::arena_failure)?
7792                        .ty,
7793                )?;
7794                let Some(symbol) = ctx.require(
7795                    ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
7796                    "dynamic write imported",
7797                ) else {
7798                    return Ok(());
7799                };
7800                emitter.instruction(Instruction::Call(symbol));
7801            }
7802            TypedObjectMember::Spread { source, .. } => {
7803                emit_expr(emitter, ctx, *source)?;
7804                for _ in 0..2 {
7805                    emitter.instruction(Instruction::RefNull(HeapType::Concrete(intr.object)));
7806                }
7807                emitter.instruction(Instruction::Call(merge));
7808                emitter.instruction(Instruction::LocalSet(object));
7809            }
7810            TypedObjectMember::Value(_) => {
7811                ctx.fail("computed literal contains an unlowered named field");
7812                return Ok(());
7813            }
7814        }
7815    }
7816    emitter.instruction(Instruction::LocalGet(object));
7817    Ok(())
7818}
7819
7820fn emit_object_index_postfix(
7821    emitter: &mut FunctionEmitter,
7822    ctx: &CodegenCtx,
7823    receiver: ExprId,
7824    index: ExprId,
7825    op: crate::PostfixOp,
7826    read_ty: &Type,
7827) -> Result<(), crate::compiler_error::CompilerFailure> {
7828    emit_expr(emitter, ctx, receiver)?;
7829    cast::emit_box(
7830        emitter,
7831        ctx,
7832        &ctx.ta
7833            .try_expr(receiver)
7834            .map_err(crate::codegen::arena_failure)?
7835            .ty,
7836    )?;
7837    let object = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
7838    emitter.instruction(Instruction::LocalSet(object));
7839    emit_expr(emitter, ctx, index)?;
7840    crate::codegen::cast_check::emit_operation_cast_on_stack(
7841        emitter,
7842        ctx,
7843        &ctx.ta
7844            .try_expr(index)
7845            .map_err(crate::codegen::arena_failure)?
7846            .ty,
7847        &Type::String,
7848    )?;
7849    emit_record_postfix_on_stack(emitter, ctx, receiver, object, op, read_ty)
7850}
7851
7852/// The receiver and key have already been evaluated exactly once.
7853fn emit_record_postfix_on_stack(
7854    emitter: &mut FunctionEmitter,
7855    ctx: &CodegenCtx,
7856    receiver: ExprId,
7857    object: u32,
7858    op: crate::PostfixOp,
7859    read_ty: &Type,
7860) -> Result<(), crate::compiler_error::CompilerFailure> {
7861    let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
7862    emitter.instruction(Instruction::LocalSet(key));
7863    emitter.instruction(Instruction::LocalGet(object));
7864    emitter.instruction(Instruction::LocalGet(key));
7865    let Some(symbol) = ctx.require(
7866        ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
7867        "record read imported",
7868    ) else {
7869        return Ok(());
7870    };
7871    emitter.instruction(Instruction::Call(symbol));
7872    let label = crate::codegen::bounds::receiver_name(ctx.ta, receiver)?;
7873    emit_checked_record_read(
7874        emitter,
7875        ctx,
7876        read_ty,
7877        KeyOperand::Local(key),
7878        RecordProbe {
7879            record: Some(object),
7880            present_flag: None,
7881        },
7882        &label,
7883    )?;
7884    cast::emit_box(emitter, ctx, read_ty)?;
7885    emit_postfix_numeric(emitter, ctx, &Type::Unknown);
7886    let old = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
7887    emitter.instruction(Instruction::LocalTee(old));
7888    emit_postfix_delta(emitter, ctx, op, &Type::Unknown);
7889    let updated = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
7890    emitter.instruction(Instruction::LocalSet(updated));
7891    emitter.instruction(Instruction::LocalGet(object));
7892    emitter.instruction(Instruction::LocalGet(key));
7893    emitter.instruction(Instruction::LocalGet(updated));
7894    let Some(symbol) = ctx.require(
7895        ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
7896        "record write imported",
7897    ) else {
7898        return Ok(());
7899    };
7900    emitter.instruction(Instruction::Call(symbol));
7901    emitter.instruction(Instruction::LocalGet(old));
7902    Ok(())
7903}
7904
7905#[cfg(test)]
7906mod tests {
7907    use super::*;
7908    use crate::codegen::invariant_tests::{assert_internal, with_context};
7909    use crate::{FileId, Span, TypedAst, TypedExpr};
7910
7911    #[test]
7912    fn missing_method_dispatch_is_a_compiler_error() {
7913        with_context(
7914            &TypedAst::new(),
7915            &crate::codegen::SymbolTable::default(),
7916            |ctx| {
7917                let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
7918                let error = emit_method_call_with_receiver_on_stack(
7919                    &mut emitter,
7920                    ctx,
7921                    &Type::Number,
7922                    &crate::mangle::prelude("MissingInterface"),
7923                    "missingMethod",
7924                    &[],
7925                    None,
7926                    &[],
7927                    None,
7928                    &Type::Number,
7929                )
7930                .unwrap_err();
7931                assert!(error.to_string().contains("MissingInterface.missingMethod"));
7932                assert_internal(error);
7933            },
7934        );
7935    }
7936
7937    #[test]
7938    fn call_and_chain_metadata_are_checked_before_operands() {
7939        with_context(
7940            &TypedAst::new(),
7941            &crate::codegen::SymbolTable::default(),
7942            |ctx| {
7943                let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
7944                assert_internal(
7945                    emit_intrinsic_call(
7946                        &mut emitter,
7947                        ctx,
7948                        Intrinsic::Assert,
7949                        &[],
7950                        &Type::Void,
7951                        &Type::Void,
7952                    )
7953                    .unwrap_err(),
7954                );
7955                assert_internal(
7956                    emit_intrinsic_call(
7957                        &mut emitter,
7958                        ctx,
7959                        Intrinsic::BigIntFromString,
7960                        &[],
7961                        &Type::BigInt,
7962                        &Type::BigInt,
7963                    )
7964                    .unwrap_err(),
7965                );
7966                assert_internal(
7967                    emit_direct_call(
7968                        &mut emitter,
7969                        ctx,
7970                        false,
7971                        0,
7972                        &[Type::Number],
7973                        &Type::Void,
7974                        &[],
7975                    )
7976                    .unwrap_err(),
7977                );
7978                assert_internal(
7979                    emit_args_into_slots(&mut emitter, ctx, &[], Some(&[ValType::F64]))
7980                        .unwrap_err(),
7981                );
7982                assert_internal(
7983                    emit_chain_parts(
7984                        &mut emitter,
7985                        ctx,
7986                        ExprId(0),
7987                        &[],
7988                        1,
7989                        &Type::Number,
7990                        &Type::Number,
7991                        None,
7992                        None,
7993                    )
7994                    .unwrap_err(),
7995                );
7996                assert_internal(
7997                    emit_chain_parts(
7998                        &mut emitter,
7999                        ctx,
8000                        ExprId(0),
8001                        &[],
8002                        0,
8003                        &Type::Number,
8004                        &Type::Number,
8005                        Some(&[]),
8006                        None,
8007                    )
8008                    .unwrap_err(),
8009                );
8010            },
8011        );
8012    }
8013
8014    #[test]
8015    fn uninterned_literal_fails_and_a_fresh_emitter_still_works() {
8016        let mut ta = TypedAst::new();
8017        let string = ta
8018            .try_push_expr(TypedExpr {
8019                kind: TypedExprKind::String("missing".into()),
8020                ty: Type::String,
8021                span: Span::at(FileId(0)),
8022            })
8023            .unwrap();
8024        let number = ta
8025            .try_push_expr(TypedExpr {
8026                kind: TypedExprKind::Number(3.0),
8027                ty: Type::Number,
8028                span: Span::at(FileId(0)),
8029            })
8030            .unwrap();
8031        with_context(&ta, &crate::codegen::SymbolTable::default(), |ctx| {
8032            let mut failed = FunctionEmitter::new(ctx, &[]).unwrap();
8033            assert_internal(emit_expr(&mut failed, ctx, string).unwrap_err());
8034            drop(failed);
8035            let mut healthy = FunctionEmitter::new(ctx, &[]).unwrap();
8036            emit_expr(&mut healthy, ctx, number).unwrap();
8037            healthy.instruction(Instruction::Drop);
8038            healthy.build().unwrap();
8039        });
8040    }
8041}