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