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