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