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