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::McpCall { server, tool, args } => {
222            // No per-tool import: serialize the args to JSON, dispatch the single
223            // `submilli:mcp.call` host fn, which returns guest objects as `unknown`.
224            // Known-return tools are wrapped in a normal `Cast` by typecheck.
225            super::mcp::emit_mcp_call(emitter, ctx, server, 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.peel() {
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.peel() {
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.peel() 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            .peel(),
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.peel() == &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.peel() {
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.peel() {
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        .peel()
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.peel() {
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(_) => true,
2663        _ => false,
2664    }
2665}
2666
2667/// With the accumulator and then the source on the stack, opens an `if` on
2668/// whether the source is an object, whose result is the merged object. Inside
2669/// it, the accumulator and the source, as an `$ObjectShape`, are on the stack.
2670/// Returns the local holding the accumulator, for the `else` arm.
2671fn emit_object_source_test(
2672    emitter: &mut FunctionEmitter,
2673    ctx: &CodegenCtx,
2674    source_ty: &Type,
2675    object: u32,
2676    object_shape: u32,
2677) -> Result<u32, crate::compiler_error::CompilerFailure> {
2678    let source_type = match ctx.symbols.value_type(source_ty)? {
2679        ValType::Ref(reference) => ValType::Ref(RefType {
2680            nullable: true,
2681            ..reference
2682        }),
2683        other => other,
2684    };
2685    let source = emitter.add_anonymous_local(source_type)?;
2686    emitter.instruction(Instruction::LocalSet(source));
2687    let accumulator = emitter.add_anonymous_local(object_ref(object))?;
2688    emitter.instruction(Instruction::LocalSet(accumulator));
2689    emitter.instruction(Instruction::LocalGet(source));
2690    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
2691        object_shape,
2692    )));
2693    emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
2694        nullable: false,
2695        heap_type: HeapType::Concrete(object_shape),
2696    })));
2697    emitter.instruction(Instruction::LocalGet(accumulator));
2698    emitter.instruction(Instruction::LocalGet(source));
2699    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
2700        object_shape,
2701    )));
2702    Ok(accumulator)
2703}
2704
2705/// Union sources may carry a known field with an incompatible hidden value.
2706/// Preserve the checked-field contract for those names without discarding
2707/// runtime fields outside the source's static view.
2708fn emit_spread_mask(
2709    emitter: &mut FunctionEmitter,
2710    ctx: &CodegenCtx,
2711    source: u32,
2712    source_expr: crate::ExprId,
2713    shape: &Type,
2714) -> Result<(), crate::compiler_error::CompilerFailure> {
2715    let checked = ctx
2716        .ta
2717        .spread_mask_fields
2718        .get(&source_expr)
2719        .ok_or_else(|| crate::codegen::internal_failure("by-name spread fields recorded"))?;
2720    // A name an object rest leaves out is masked whatever its value; any other
2721    // is masked when its value isn't of the field's type.
2722    let omitted = ctx.ta.spread_omitted_fields.get(&source_expr);
2723    let fields: Vec<(&String, Option<&Type>)> = checked
2724        .iter()
2725        .map(|(name, ty)| (name, Some(ty)))
2726        .chain(omitted.into_iter().flatten().map(|name| (name, None)))
2727        .collect();
2728    let intrinsics = ctx
2729        .symbols
2730        .intrinsic_type_indices()
2731        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
2732    let vtable = ctx
2733        .symbols
2734        .vtable_global_idx(shape)
2735        .ok_or_else(|| crate::codegen::internal_failure("spread shape vtable collected"))?;
2736    emitter.instruction(Instruction::GlobalGet(vtable));
2737    for (name, _) in &fields {
2738        let global = ctx
2739            .symbols
2740            .field_name_string_global_idx(name)
2741            .ok_or_else(|| crate::codegen::internal_failure("spread name collected"))?;
2742        emitter.instruction(Instruction::GlobalGet(global));
2743    }
2744    emitter.instruction(Instruction::ArrayNewFixed {
2745        array_type_index: intrinsics.field_names,
2746        array_size: crate::codegen::wasm_u32(fields.len())?,
2747    });
2748    for (name, ty) in fields.iter().copied() {
2749        let global = ctx
2750            .symbols
2751            .field_name_string_global_idx(name)
2752            .ok_or_else(|| crate::codegen::internal_failure("spread name collected"))?;
2753        let Some(ty) = ty else {
2754            emitter.instruction(Instruction::GlobalGet(global));
2755            continue;
2756        };
2757        let index = emitter.add_anonymous_local(ValType::I32)?;
2758        let value = emitter.add_anonymous_local(object_ref(intrinsics.object))?;
2759        emit_object_field_index_by_name(emitter, ctx, source, global);
2760        emitter.instruction(Instruction::LocalTee(index));
2761        emitter.instruction(Instruction::I32Const(0));
2762        emitter.instruction(Instruction::I32GeS);
2763        emitter.emit_if(BlockType::Result(ValType::I32));
2764        emit_field_slot_get(emitter, intrinsics, source, index);
2765        emitter.instruction(Instruction::LocalSet(value));
2766        emit_field_holds_value_of(emitter, ctx, source, index, value, ty)?;
2767        emitter.emit_else();
2768        emitter.instruction(Instruction::I32Const(0));
2769        emitter.emit_end();
2770        emitter.emit_if(BlockType::Result(object_ref(intrinsics.object)));
2771        emitter.instruction(Instruction::RefNull(HeapType::Concrete(intrinsics.object)));
2772        emitter.emit_else();
2773        emitter.instruction(Instruction::GlobalGet(global));
2774        emitter.emit_end();
2775    }
2776    emitter.instruction(Instruction::ArrayNewFixed {
2777        array_type_index: intrinsics.object_fields,
2778        array_size: crate::codegen::wasm_u32(fields.len())?,
2779    });
2780    emitter.instruction(Instruction::RefNull(HeapType::ANY));
2781    emitter.instruction(Instruction::StructNew(intrinsics.object_shape));
2782    Ok(())
2783}
2784
2785/// The final merge restores optional markers and writable absent slots from
2786/// the inferred result shape, while retaining unnamed runtime fields.
2787fn emit_spread_shape(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, shape: &Type) {
2788    ctx.latch(emit_spread_shape_checked(emitter, ctx, shape));
2789}
2790
2791fn emit_spread_shape_checked(
2792    emitter: &mut FunctionEmitter,
2793    ctx: &CodegenCtx,
2794    shape: &Type,
2795) -> Result<(), crate::compiler_error::CompilerFailure> {
2796    let Type::Object { fields, .. } = shape else {
2797        return Err(crate::codegen::internal_failure("structural spread shape"));
2798    };
2799    let intrinsics = ctx
2800        .symbols
2801        .intrinsic_type_indices()
2802        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
2803    let names: Vec<_> = fields
2804        .iter()
2805        .map(|(name, field)| crate::codegen::field_names::FieldName {
2806            name: name.clone(),
2807            optional: field.optional,
2808            is_accessor: false,
2809            is_private: false,
2810        })
2811        .collect();
2812    let names_global = ctx
2813        .symbols
2814        .field_names_global_idx(&names)
2815        .ok_or_else(|| crate::codegen::internal_failure("spread shape names collected"))?;
2816    let vtable = ctx
2817        .symbols
2818        .vtable_global_idx(shape)
2819        .ok_or_else(|| crate::codegen::internal_failure("spread shape vtable collected"))?;
2820    emitter.instruction(Instruction::GlobalGet(vtable));
2821    emitter.instruction(Instruction::GlobalGet(names_global));
2822    emitter.instruction(Instruction::I32Const(
2823        crate::codegen::wasm_u32(fields.len())? as i32,
2824    ));
2825    emitter.instruction(Instruction::ArrayNewDefault(intrinsics.object_fields));
2826    emitter.instruction(Instruction::RefNull(HeapType::ANY));
2827    emitter.instruction(Instruction::StructNew(intrinsics.object_shape));
2828
2829    Ok(())
2830}
2831
2832/// Coerces a field-read receiver already on the stack to `(ref $ObjectShape)`,
2833/// which is what the field-name scan takes. Only receivers that lower to the
2834/// universal `$Object` slot need it: an `InterfaceRef`, and any union carrying a
2835/// nominal member (`value_type`'s mixed-union arm). A union of object shapes
2836/// already lowers to an `$ObjectShape` subtype, and so does a class instance —
2837/// its struct's header slots 0-2 are the `$ObjectShape` prefix.
2838/// Coerce the receiver on the stack into an anonymous non-null
2839/// `(ref $ObjectShape)` local, and return the slot.
2840///
2841/// Every property path — read, write, and read-modify-write — needs the
2842/// receiver in a local it can push more than once, and every one of them needs
2843/// the same coercion first: the helpers take `(ref $ObjectShape)` non-null,
2844/// while an interface-typed receiver (or a union with a non-object member)
2845/// lowers to `(ref null $Object)`. A receiver that is already a narrower
2846/// `$ObjectShape` subtype needs nothing — Wasm subtyping accepts it.
2847pub(super) fn stash_receiver_as_object_shape(
2848    emitter: &mut FunctionEmitter<'_>,
2849    receiver_ty: &Type,
2850    object_shape: u32,
2851) -> Result<u32, crate::compiler_error::CompilerFailure> {
2852    coerce_field_receiver_to_object_shape(emitter, receiver_ty, object_shape);
2853    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
2854        nullable: false,
2855        heap_type: HeapType::Concrete(object_shape),
2856    }))?;
2857    emitter.instruction(Instruction::LocalSet(rcv_local));
2858    Ok(rcv_local)
2859}
2860
2861fn coerce_field_receiver_to_object_shape(
2862    emitter: &mut FunctionEmitter<'_>,
2863    receiver_ty: &Type,
2864    object_shape: u32,
2865) {
2866    let universal = match receiver_ty.peel() {
2867        Type::InterfaceRef { .. } | Type::Unknown => true,
2868        Type::Union(members) => members
2869            .iter()
2870            .any(|m| !matches!(m.peel(), Type::Object { .. })),
2871        _ => false,
2872    };
2873    if universal {
2874        emitter.instruction(Instruction::RefAsNonNull);
2875        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
2876            object_shape,
2877        )));
2878    }
2879}
2880
2881/// Emit one chain step's access on a receiver already on the stack.
2882/// Mirrors the non-chain `TypedExprKind::FieldAccess` / `IndexAccess`
2883/// codegen at lines 391-457 / 560+ above, with the leading
2884/// `emit_expr(receiver)` elided. After this returns, the part's
2885/// result is on the stack with `part.result_ty`'s Wasm shape.
2886fn emit_chain_access(
2887    emitter: &mut FunctionEmitter<'_>,
2888    ctx: &CodegenCtx<'_>,
2889    part: &crate::TypedChainPart,
2890    receiver_ty: &Type,
2891    check_ty: &Type,
2892) -> Result<(), crate::compiler_error::CompilerFailure> {
2893    let _: () = match part {
2894        crate::TypedChainPart::Field {
2895            name, result_ty, ..
2896        } => {
2897            if ctx.ta.has_string_index(receiver_ty) {
2898                let Some(symbol) = ctx.require(
2899                    ctx.symbols.field_name_string_global_idx(&name.name),
2900                    "property name collected",
2901                ) else {
2902                    return Ok(());
2903                };
2904                emitter.instruction(Instruction::GlobalGet(symbol));
2905                let Some(symbol) = ctx.require(
2906                    ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
2907                    "record read imported",
2908                ) else {
2909                    return Ok(());
2910                };
2911                emitter.instruction(Instruction::Call(symbol));
2912                crate::codegen::cast_check::emit_checked_cast_on_stack(
2913                    emitter,
2914                    ctx,
2915                    &Type::Unknown,
2916                    check_ty,
2917                )?;
2918                cast::emit_coerce_to_slot(emitter, ctx, check_ty, result_ty)?;
2919                return Ok(());
2920            }
2921            let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
2922                crate::codegen::internal_failure("intrinsics declared by codegen entry")
2923            })?;
2924            // Class receiver: resolve the slot statically, mirroring the
2925            // non-optional `FieldAccess` path. The name scan below would read a
2926            // null for an accessor property, which backs no payload slot.
2927            if let Type::ClassRef { mangled, .. } = receiver_ty.peel() {
2928                if ctx.symbols.is_class_payload_method(mangled, &name.name) {
2929                    emit_class_method_value(
2930                        emitter,
2931                        ctx,
2932                        receiver_ty,
2933                        &name.name,
2934                        result_ty,
2935                        check_ty,
2936                    )?;
2937                    return Ok(());
2938                }
2939                let Some(slot) = ctx.symbols.class_field_slot(mangled, &name.name) else {
2940                    let getter = crate::codegen::classes::accessor_getter_name(&name.name);
2941                    emit_class_vtable_dispatch(emitter, ctx, mangled, &getter, &[], result_ty)?;
2942                    return Ok(());
2943                };
2944                let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
2945                    crate::codegen::internal_failure("class struct type recorded in classes::emit")
2946                })?;
2947                let object = emitter.add_anonymous_local(ValType::Ref(RefType {
2948                    nullable: false,
2949                    heap_type: HeapType::Concrete(struct_idx),
2950                }))?;
2951                emitter.instruction(Instruction::LocalTee(object));
2952                emitter.instruction(Instruction::StructGet {
2953                    struct_type_index: struct_idx,
2954                    field_index: 2,
2955                });
2956                emitter.instruction(Instruction::I32Const(slot as i32));
2957                emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
2958                if ctx
2959                    .symbols
2960                    .class_field_narrowing_check(mangled, &name.name)
2961                    .is_some()
2962                {
2963                    crate::codegen::field_guards::check(emitter, ctx, object, mangled, &name.name)?;
2964                }
2965                emit_class_field_slot_cast_as(
2966                    emitter, ctx, mangled, &name.name, result_ty, check_ty,
2967                )?;
2968                return Ok(());
2969            }
2970            let rcv_local =
2971                stash_receiver_as_object_shape(emitter, receiver_ty, intrinsics.object_shape)?;
2972            emit_object_property_read_as(emitter, ctx, rcv_local, &name.name, result_ty, check_ty)?;
2973        }
2974        crate::TypedChainPart::Index { idx, result_ty, .. } => {
2975            if receiver_ty.is_structural_object() {
2976                emit_expr(emitter, ctx, *idx)?;
2977                crate::codegen::cast_check::emit_operation_cast_on_stack(
2978                    emitter,
2979                    ctx,
2980                    &ctx.ta
2981                        .try_expr(*idx)
2982                        .map_err(crate::codegen::arena_failure)?
2983                        .ty,
2984                    &Type::String,
2985                )?;
2986                let Some(symbol) = ctx.require(
2987                    ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
2988                    "record read imported",
2989                ) else {
2990                    return Ok(());
2991                };
2992                emitter.instruction(Instruction::Call(symbol));
2993                crate::codegen::cast_check::emit_checked_cast_on_stack(
2994                    emitter,
2995                    ctx,
2996                    &Type::Unknown,
2997                    check_ty,
2998                )?;
2999                cast::emit_coerce_to_slot(emitter, ctx, check_ty, result_ty)?;
3000            } else if receiver_ty.peel() == &Type::Uint8Array {
3001                emit_uint8_index_with_receiver_on_stack(emitter, ctx, *idx)?;
3002            } else {
3003                let source_ty = match receiver_ty.peel() {
3004                    Type::Array(element) => element.as_ref(),
3005                    Type::Tuple(elements) => match &ctx
3006                        .ta
3007                        .try_expr(*idx)
3008                        .map_err(crate::codegen::arena_failure)?
3009                        .kind
3010                    {
3011                        TypedExprKind::Number(index) => {
3012                            elements.get(*index as usize).unwrap_or(result_ty)
3013                        }
3014                        _ => result_ty,
3015                    },
3016                    _ => result_ty,
3017                };
3018                emit_bounds_checked_index_with_receiver_on_stack(
3019                    emitter,
3020                    ctx,
3021                    *idx,
3022                    source_ty,
3023                    is_optional_tuple_position(ctx, receiver_ty, *idx)?,
3024                )?;
3025                if source_ty != result_ty {
3026                    crate::codegen::cast_check::emit_checked_cast_on_stack(
3027                        emitter, ctx, source_ty, result_ty,
3028                    )?;
3029                }
3030            }
3031        }
3032        crate::TypedChainPart::InterfaceProperty { iface, name, .. } => {
3033            // same lookup the non-chain
3034            // `InterfacePropertyAccess` arm uses. The chain has the
3035            // receiver on stack already; the wrapper takes
3036            // `(ref $Object)` non-null, so coerce only when the static
3037            // receiver type is `InterfaceRef`.
3038            //
3039            // The getter returns the property's declared type, which is
3040            // exactly this part's `result_ty` — `?.` adds `| null` once,
3041            // to the whole chain, never to a step's own result. So the
3042            // value already sits in the slot the next step expects.
3043            if let Some(struct_idx) = inline_length_struct_idx(ctx, iface, &name.name)? {
3044                emit_inline_length(emitter, ctx, struct_idx);
3045                return Ok(());
3046            }
3047            let key = crate::mangle::extend(iface, &name.name);
3048            let func_idx = ctx.symbols.func_idx(&key).ok_or_else(|| {
3049                crate::codegen::internal_failure(
3050                    "interface property getter recorded during the dependency-import pass",
3051                )
3052            })?;
3053            if matches!(receiver_ty.peel(), Type::InterfaceRef { .. }) {
3054                emitter.instruction(Instruction::RefAsNonNull);
3055            }
3056            emitter.instruction(Instruction::Call(func_idx));
3057        }
3058        crate::TypedChainPart::Call {
3059            args,
3060            union_signature,
3061            ..
3062        } => {
3063            // receiver on stack is the closure value
3064            // (`(ref $closure_N)` after `emit_chain_parts`'s
3065            // strip-null cast). Helper extracts env + funcref and
3066            // call_refs, unboxing the erased return to the function's
3067            // declared `ret` — which is this part's `result_ty`, so the
3068            // value already sits in the slot the next step expects.
3069            emit_indirect_closure_call_with_receiver_on_stack(
3070                emitter,
3071                ctx,
3072                union_signature.as_ref().unwrap_or(receiver_ty),
3073                args,
3074                part.result_ty(),
3075            )?;
3076        }
3077        crate::TypedChainPart::MethodCall {
3078            iface,
3079            name,
3080            args,
3081            result_ty,
3082            ..
3083        } => {
3084            // dispatch through the same three paths the
3085            // non-chain `emit_method_call` uses (Direct/Static wrapper,
3086            // built-in vtable slot, shape-based fallback) — the helper
3087            // assumes the receiver is on the stack. `result_ty` is the
3088            // method's substituted return type, not the chain's
3089            // short-circuit union (`infer_optional_chain` adds `| null`
3090            // once, to the whole chain), so it is `call_ret_ty` verbatim:
3091            // a nullable `T | null` from `at` is the wrapper's real
3092            // return and must reach the next step still nullable.
3093            emit_method_call_with_receiver_on_stack(
3094                emitter,
3095                ctx,
3096                receiver_ty,
3097                iface,
3098                &name.name,
3099                args,
3100                None,
3101                None,
3102                result_ty,
3103            )?;
3104        }
3105        crate::TypedChainPart::NonNull { result_ty, .. } => {
3106            crate::codegen::cast_check::emit_non_null_assert_on_stack(
3107                emitter,
3108                ctx,
3109                receiver_ty,
3110                result_ty,
3111            )?;
3112        }
3113    };
3114    Ok(())
3115}
3116
3117/// Emit the load for one captured-binding value at the closure-creation
3118/// site. The captured slot in the *outer* scope already has the right
3119/// representation for the env field — boxed captures store
3120/// `(ref $box_T)`, unboxed (`const`) captures store `T` — so we just
3121/// `local.get` the slot.
3122///
3123/// Transitive captures (the outer scope is itself a closure body)
3124/// work uniformly: the outer closure's prologue copied its captures
3125/// into ordinary Wasm locals, so the lookup here resolves to a
3126/// FunctionLocal slot with the right type either way.
3127fn emit_captured_load(
3128    emitter: &mut FunctionEmitter,
3129    _ctx: &CodegenCtx,
3130    c: &crate::CapturedVar,
3131) -> Result<(), crate::compiler_error::CompilerFailure> {
3132    let slot = emitter.local_slot(&c.name.name).ok_or_else(|| {
3133        crate::codegen::internal_failure("captured binding is missing from the outer scope")
3134    })?;
3135    emitter.instruction(Instruction::LocalGet(slot));
3136    Ok(())
3137}
3138
3139fn emit_binary(
3140    emitter: &mut FunctionEmitter,
3141    ctx: &CodegenCtx,
3142    op: BinOp,
3143    lhs: ExprId,
3144    rhs: ExprId,
3145    result_ty: &Type,
3146) -> Result<(), crate::compiler_error::CompilerFailure> {
3147    let admits_never_operand = matches!(
3148        op,
3149        BinOp::Add
3150            | BinOp::Sub
3151            | BinOp::Mul
3152            | BinOp::Div
3153            | BinOp::Rem
3154            | BinOp::Pow
3155            | BinOp::Lt
3156            | BinOp::Gt
3157            | BinOp::Le
3158            | BinOp::Ge
3159    );
3160    if admits_never_operand && try_emit_unreachable_for_never_operand(emitter, ctx, &[lhs, rhs])? {
3161        return Ok(());
3162    }
3163    let _: () = match op {
3164        // `+` dispatches on the result type the typechecker chose: numeric
3165        // operands → `f64.add`, string operands → `string_concat` from the
3166        // prelude. Mixed-type or other operand combinations were rejected
3167        // by the inferer.
3168        BinOp::BitAnd
3169        | BinOp::BitOr
3170        | BinOp::BitXor
3171        | BinOp::Shl
3172        | BinOp::Shr
3173        | BinOp::UnsignedShr => {
3174            if has_numeric_operands(ctx, &[lhs, rhs], result_ty)? {
3175                emit_number_bitwise(emitter, ctx, op, lhs, rhs)?;
3176            } else {
3177                let name = op.bitwise_name().ok_or_else(|| {
3178                    crate::codegen::internal_failure("missing bitwise host operation")
3179                })?;
3180                emit_bitwise_host(emitter, ctx, name, &[lhs, rhs], result_ty)?;
3181            }
3182        }
3183        // A template of constants has a string literal type: concatenate it as
3184        // a `string`.
3185        BinOp::Add => match &result_ty.widen_literal() {
3186            Type::Number => {
3187                emit_primitive_operand(emitter, ctx, lhs)?;
3188                emit_primitive_operand(emitter, ctx, rhs)?;
3189                emitter.instruction(Instruction::F64Add);
3190            }
3191            Type::String => {
3192                emit_primitive_operand(emitter, ctx, lhs)?;
3193                emit_primitive_operand(emitter, ctx, rhs)?;
3194                let idx = ctx
3195                    .symbols
3196                    .prelude_func_idx("string_concat")
3197                    .ok_or_else(|| {
3198                        crate::codegen::internal_failure("string_concat imported from prelude")
3199                    })?;
3200                emitter.instruction(Instruction::Call(idx));
3201            }
3202            // `bigint + bigint` → inline host call.
3203            Type::BigInt => emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "add")?,
3204            other => {
3205                return Err(crate::codegen::internal_failure(format!(
3206                    "typechecker rejects `+` for {other:?}"
3207                )));
3208            }
3209        },
3210        BinOp::Sub | BinOp::Mul | BinOp::Div => {
3211            // bigint operands route to inline host calls.
3212            if result_ty.is_bigint() {
3213                let name = match op {
3214                    BinOp::Sub => "sub",
3215                    BinOp::Mul => "mul",
3216                    BinOp::Div => "div",
3217                    _ => {
3218                        return Err(crate::codegen::internal_failure(
3219                            "invalid compiler emission dispatch",
3220                        ));
3221                    }
3222                };
3223                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, name)?;
3224                return Ok(());
3225            }
3226            if !matches!(result_ty, Type::Number) {
3227                return Err(crate::codegen::internal_failure(
3228                    "numeric operator has a non-number result type",
3229                ));
3230            }
3231            emit_primitive_operand(emitter, ctx, lhs)?;
3232            emit_primitive_operand(emitter, ctx, rhs)?;
3233            let inst = match op {
3234                BinOp::Sub => Instruction::F64Sub,
3235                BinOp::Mul => Instruction::F64Mul,
3236                BinOp::Div => Instruction::F64Div,
3237                _ => {
3238                    return Err(crate::codegen::internal_failure(
3239                        "invalid compiler emission dispatch",
3240                    ));
3241                }
3242            };
3243            emitter.instruction(inst);
3244        }
3245        BinOp::Pow => {
3246            // exponentiation. BigInt routes to the
3247            // `submilli:bigint.pow` host call (which validates the
3248            // exponent is non-negative and fits in u32, else traps).
3249            // Number routes to the prelude-host `Math#pow` function.
3250            if result_ty.is_bigint() {
3251                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "pow")?;
3252                return Ok(());
3253            }
3254            if !matches!(result_ty, Type::Number) {
3255                return Err(crate::codegen::internal_failure(
3256                    "numeric operator has a non-number result type",
3257                ));
3258            }
3259            emit_primitive_operand(emitter, ctx, lhs)?;
3260            emit_primitive_operand(emitter, ctx, rhs)?;
3261            let idx = ctx
3262                .symbols
3263                .func_idx(&crate::runtime::prelude::math::math_key("pow"))
3264                .ok_or_else(|| crate::codegen::internal_failure("Math#pow host import recorded"))?;
3265            emitter.instruction(Instruction::Call(idx));
3266        }
3267        BinOp::Rem => {
3268            // `bigint % bigint` → inline host call.
3269            if result_ty.is_bigint() {
3270                emit_bigint_binop_inline(emitter, ctx, lhs, rhs, "mod")?;
3271                return Ok(());
3272            }
3273            // Wasm has no `f64.rem`; lower to JS-truncation modulo using
3274            // `a - trunc(a / b) * b`. Two scratch f64 locals so each operand
3275            // is evaluated exactly once (preserves side-effect order).
3276            let a = emitter.add_anonymous_local(ValType::F64)?;
3277            let b = emitter.add_anonymous_local(ValType::F64)?;
3278            emit_primitive_operand(emitter, ctx, lhs)?;
3279            emitter.instruction(Instruction::LocalSet(a));
3280            emit_primitive_operand(emitter, ctx, rhs)?;
3281            emitter.instruction(Instruction::LocalSet(b));
3282            // Build `a - trunc(a/b)*b` on the stack.
3283            emitter.instruction(Instruction::LocalGet(a));
3284            emitter.instruction(Instruction::LocalGet(a));
3285            emitter.instruction(Instruction::LocalGet(b));
3286            emitter.instruction(Instruction::F64Div);
3287            emitter.instruction(Instruction::F64Trunc);
3288            emitter.instruction(Instruction::LocalGet(b));
3289            emitter.instruction(Instruction::F64Mul);
3290            emitter.instruction(Instruction::F64Sub);
3291        }
3292        BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
3293            // bigint → inline `bigint.cmp` host call; number → f64 ops;
3294            // string → `string_cmp` (signed lexicographic diff) vs 0.
3295            let operand_ty = ctx
3296                .ta
3297                .try_expr(lhs)
3298                .map_err(crate::codegen::arena_failure)?
3299                .ty
3300                .primitive_behavior()
3301                .clone();
3302            if operand_ty.is_bigint() {
3303                emit_bigint_cmp_inline(emitter, ctx, lhs, rhs, op)?;
3304                return Ok(());
3305            }
3306            emit_primitive_operand(emitter, ctx, lhs)?;
3307            emit_primitive_operand(emitter, ctx, rhs)?;
3308            if matches!(operand_ty, Type::String | Type::StringLiteral(_)) {
3309                let idx = ctx.symbols.prelude_func_idx("string_cmp").ok_or_else(|| {
3310                    crate::codegen::internal_failure("string_cmp imported from prelude")
3311                })?;
3312                emitter.instruction(Instruction::Call(idx));
3313                emitter.instruction(Instruction::I32Const(0));
3314                emitter.instruction(match op {
3315                    BinOp::Lt => Instruction::I32LtS,
3316                    BinOp::Gt => Instruction::I32GtS,
3317                    BinOp::Le => Instruction::I32LeS,
3318                    BinOp::Ge => Instruction::I32GeS,
3319                    _ => {
3320                        return Err(crate::codegen::internal_failure(
3321                            "invalid compiler emission dispatch",
3322                        ));
3323                    }
3324                });
3325            } else {
3326                emitter.instruction(match op {
3327                    BinOp::Lt => Instruction::F64Lt,
3328                    BinOp::Gt => Instruction::F64Gt,
3329                    BinOp::Le => Instruction::F64Le,
3330                    BinOp::Ge => Instruction::F64Ge,
3331                    _ => {
3332                        return Err(crate::codegen::internal_failure(
3333                            "invalid compiler emission dispatch",
3334                        ));
3335                    }
3336                });
3337            }
3338        }
3339        BinOp::Eq | BinOp::NotEq => emit_equality(emitter, ctx, op, lhs, rhs)?,
3340        BinOp::And | BinOp::Or => emit_logical(emitter, ctx, op, lhs, rhs, result_ty)?,
3341        BinOp::In => emit_in_operator(emitter, ctx, lhs, rhs)?,
3342        // infer lifts `Binary { op: NullishCoalesce,.. }`
3343        // into the dedicated `TypedExprKind::NullishCoalesce` node, so
3344        // this arm is unreachable in well-typed input.
3345        BinOp::NullishCoalesce => {
3346            return Err(crate::codegen::internal_failure(
3347                "BinOp::NullishCoalesce should be lifted into TypedExprKind::NullishCoalesce by infer",
3348            ));
3349        }
3350    };
3351    Ok(())
3352}
3353
3354// Substantial recursive arms live outside the dispatcher so their temporaries
3355// do not enlarge every arithmetic expression's stack frame.
3356fn emit_equality(
3357    emitter: &mut FunctionEmitter,
3358    ctx: &CodegenCtx,
3359    op: BinOp,
3360    lhs: ExprId,
3361    rhs: ExprId,
3362) -> Result<(), crate::compiler_error::CompilerFailure> {
3363    // Plan 75.8: when either operand is the `null` literal,
3364    // emit the non-null side followed by `ref.is_null`
3365    // (negated for `NotEq`). This covers `x === null` /
3366    // `x !== null` against any nullable value, the primary
3367    // null-narrowing predicate. Both-null collapses to a
3368    // constant.
3369    let lhs_is_null_lit = matches!(
3370        ctx.ta
3371            .try_expr(lhs)
3372            .map_err(crate::codegen::arena_failure)?
3373            .kind,
3374        TypedExprKind::Null
3375    );
3376    let rhs_is_null_lit = matches!(
3377        ctx.ta
3378            .try_expr(rhs)
3379            .map_err(crate::codegen::arena_failure)?
3380            .kind,
3381        TypedExprKind::Null
3382    );
3383    if lhs_is_null_lit && rhs_is_null_lit {
3384        let v: i32 = if matches!(op, BinOp::Eq) { 1 } else { 0 };
3385        emitter.instruction(Instruction::I32Const(v));
3386        return Ok(());
3387    }
3388    if lhs_is_null_lit || rhs_is_null_lit {
3389        let non_null_side = if lhs_is_null_lit { rhs } else { lhs };
3390        let non_null_ty = ctx
3391            .ta
3392            .try_expr(non_null_side)
3393            .map_err(crate::codegen::arena_failure)?
3394            .ty
3395            .clone();
3396        let non_null_val = ctx.symbols.value_type(&non_null_ty)?;
3397        // Plan 75.10 PR 2: when the non-null side has a
3398        // primitive Wasm type (f64 / i32 — e.g., a
3399        // narrowed-to-`Number` after assignment), `ref.is_null`
3400        // would be a validation error. The answer is
3401        // statically known — a primitive can never be null —
3402        // so emit the operand for side effects, drop, then
3403        // push the constant result.
3404        if matches!(non_null_val, ValType::F64 | ValType::I32) {
3405            emit_expr(emitter, ctx, non_null_side)?;
3406            emitter.instruction(Instruction::Drop);
3407            let v: i32 = if matches!(op, BinOp::NotEq) { 1 } else { 0 };
3408            emitter.instruction(Instruction::I32Const(v));
3409            return Ok(());
3410        }
3411        emit_expr(emitter, ctx, non_null_side)?;
3412        emitter.instruction(Instruction::RefIsNull);
3413        if matches!(op, BinOp::NotEq) {
3414            emitter.instruction(Instruction::I32Eqz);
3415        }
3416        return Ok(());
3417    }
3418    let lhs_ty = ctx
3419        .ta
3420        .try_expr(lhs)
3421        .map_err(crate::codegen::arena_failure)?
3422        .ty
3423        .clone();
3424    let rhs_ty = ctx
3425        .ta
3426        .try_expr(rhs)
3427        .map_err(crate::codegen::arena_failure)?
3428        .ty
3429        .clone();
3430    let operand_val = ctx.symbols.value_type(lhs_ty.primitive_behavior())?;
3431    // The typed dispatch below needs both operands in one Wasm shape. One that
3432    // admits null is a `(ref null $Object)` beside the other's f64 or
3433    // `(ref $string)`, and comparable operands need not share a representation
3434    // at all (`t === 1` with `t: T`, `n === e` with `e: {}`). Those pairs box both
3435    // sides to `(ref [null] $Object)` and dispatch through `vtable.equals`.
3436    if may_hold_null(&lhs_ty)
3437        || may_hold_null(&rhs_ty)
3438        || operand_val != ctx.symbols.value_type(rhs_ty.primitive_behavior())?
3439    {
3440        emit_boxed_eq(emitter, ctx, lhs, rhs, op)?;
3441        return Ok(());
3442    }
3443    // bigint equality short-circuits to a direct
3444    // `submilli:bigint.cmp == 0` call — a faster path than the
3445    // generic `ValType::Ref(_)` arm's vtable `equals` dispatch.
3446    if lhs_ty.is_bigint() {
3447        emit_bigint_cmp_eq_inline(emitter, ctx, lhs, rhs, op)?;
3448        return Ok(());
3449    }
3450    let string_idx = ctx.symbols.string_type_idx().ok_or_else(|| {
3451        crate::codegen::internal_failure("string type registered with intrinsics")
3452    })?;
3453    // Operands that share a Wasm `value_type` dispatch off it. The language
3454    // `Type` might be a literal-refined primitive or a literal-only union; at
3455    // the Wasm level those collapse to the same compare path as their base
3456    // primitive (equality is value-level; the literal refinement is a
3457    // typecheck-time restriction, not a runtime distinction).
3458    match operand_val {
3459        ValType::F64 => {
3460            emit_primitive_operand(emitter, ctx, lhs)?;
3461            emit_primitive_operand(emitter, ctx, rhs)?;
3462            emitter.instruction(if matches!(op, BinOp::Eq) {
3463                Instruction::F64Eq
3464            } else {
3465                Instruction::F64Ne
3466            });
3467        }
3468        ValType::I32 => {
3469            emit_expr(emitter, ctx, lhs)?;
3470            emit_expr(emitter, ctx, rhs)?;
3471            emitter.instruction(if matches!(op, BinOp::Eq) {
3472                Instruction::I32Eq
3473            } else {
3474                Instruction::I32Ne
3475            });
3476        }
3477        ValType::Ref(RefType {
3478            heap_type: HeapType::Concrete(idx),
3479            ..
3480        }) if idx == string_idx => {
3481            emit_expr(emitter, ctx, lhs)?;
3482            emit_expr(emitter, ctx, rhs)?;
3483            let func = ctx.symbols.prelude_func_idx("string_eq").ok_or_else(|| {
3484                crate::codegen::internal_failure("string_eq imported from prelude")
3485            })?;
3486            emitter.instruction(Instruction::Call(func));
3487            if matches!(op, BinOp::NotEq) {
3488                emitter.instruction(Instruction::I32Eqz);
3489            }
3490        }
3491        ValType::Ref(_) => {
3492            // Object / array / closure / generic-erased (all
3493            // subtypes of `$Object`): load lhs.vtable.equals
3494            // and call it. Each subtype's body runs the
3495            // structural compare for its own shape; nested
3496            // objects / arrays recurse through their own
3497            // vtable.equals slots.
3498            //
3499            // Enum values fall here too — they share the
3500            // `$BoxedNumber` / `$string` shapes whose
3501            // vtable `equals` slot is wired by the prelude.
3502            emit_expr(emitter, ctx, lhs)?;
3503            cast::emit_box(emitter, ctx, &lhs_ty)?;
3504            emit_expr(emitter, ctx, rhs)?;
3505            cast::emit_box(emitter, ctx, &rhs_ty)?;
3506            emit_vtable_equality(emitter, ctx, op);
3507        }
3508        other => {
3509            return Err(crate::codegen::internal_failure(format!(
3510                "typechecker rejects equality on Wasm value-type `{other:?}`"
3511            )));
3512        }
3513    }
3514    Ok(())
3515}
3516
3517fn emit_logical(
3518    emitter: &mut FunctionEmitter,
3519    ctx: &CodegenCtx,
3520    op: BinOp,
3521    lhs: ExprId,
3522    rhs: ExprId,
3523    result_ty: &Type,
3524) -> Result<(), crate::compiler_error::CompilerFailure> {
3525    // JS value-returning short-circuit, mirroring the `??` lowering:
3526    // stash the LHS in an anonymous local, truthiness-test the teed
3527    // copy, then either evaluate the RHS or replay the kept LHS —
3528    // each branch coerced into the result union's slot. `&&` keeps
3529    // the LHS when falsy, `||` when truthy.
3530    let result_val = ctx.symbols.value_type(result_ty)?;
3531    let lhs_ty = ctx
3532        .ta
3533        .try_expr(lhs)
3534        .map_err(crate::codegen::arena_failure)?
3535        .ty
3536        .clone();
3537    let lhs_val = ctx.symbols.value_type(&lhs_ty)?;
3538    let lhs_is_ref = matches!(lhs_val, ValType::Ref(_));
3539    let tmp = emitter.add_anonymous_local(lhs_val)?;
3540    emit_expr(emitter, ctx, lhs)?;
3541    emitter.instruction(Instruction::LocalTee(tmp));
3542    crate::codegen::function_emitter::cast::emit_condition_to_i32(emitter, ctx, &lhs_ty)?;
3543    let emit_rhs_branch =
3544        |emitter: &mut FunctionEmitter| -> Result<(), crate::compiler_error::CompilerFailure> {
3545            let rhs_ty = ctx
3546                .ta
3547                .try_expr(rhs)
3548                .map_err(crate::codegen::arena_failure)?
3549                .ty
3550                .clone();
3551            emit_expr(emitter, ctx, rhs)?;
3552            crate::codegen::function_emitter::cast::emit_coerce_to_slot(
3553                emitter, ctx, &rhs_ty, result_ty,
3554            )?;
3555            Ok(())
3556        };
3557    // An LHS narrowed to always-truthy (`&&`) or always-falsy (`||`)
3558    // is never the result, and the result slot has no room for it:
3559    // `c && n()` under `c === true` is a bare f64.
3560    let kept_lhs_ty = match op {
3561        BinOp::And => falsy_part(&lhs_ty),
3562        _ => truthy_part(&lhs_ty),
3563    };
3564    let lhs_is_never_kept = matches!(kept_lhs_ty.peel(), Type::Never);
3565    let emit_kept_lhs_branch = |emitter: &mut FunctionEmitter| {
3566        if lhs_is_never_kept {
3567            emitter.instruction(Instruction::Unreachable);
3568            return Ok(());
3569        }
3570        emitter.instruction(Instruction::LocalGet(tmp));
3571        if lhs_is_ref {
3572            // Ref-repr LHS: cast into the result slot's form —
3573            // `ref.as_non_null` lift, downcast, or unbox as needed
3574            // (same contract as the `??` kept branch).
3575            crate::codegen::function_emitter::cast::emit_cast_to(emitter, ctx, result_ty)?;
3576        } else {
3577            crate::codegen::function_emitter::cast::emit_coerce_to_slot(
3578                emitter, ctx, &lhs_ty, result_ty,
3579            )?;
3580        }
3581        Ok::<(), crate::compiler_error::CompilerFailure>(())
3582    };
3583    emitter.emit_if(BlockType::Result(result_val));
3584    match op {
3585        BinOp::And => emit_rhs_branch(emitter)?,
3586        _ => emit_kept_lhs_branch(emitter)?,
3587    }
3588    emitter.emit_else();
3589    match op {
3590        BinOp::And => emit_kept_lhs_branch(emitter)?,
3591        _ => emit_rhs_branch(emitter)?,
3592    }
3593    emitter.emit_end();
3594    Ok(())
3595}
3596
3597/// Evaluates `operands` in order and ends in `unreachable` when any of them is
3598/// `never`, returning whether it did. The typechecker accepts arithmetic on a
3599/// `never` operand because no value of it exists, but that operand's slot is a
3600/// reference no numeric instruction takes, so the operator itself isn't emitted.
3601fn try_emit_unreachable_for_never_operand(
3602    emitter: &mut FunctionEmitter,
3603    ctx: &CodegenCtx,
3604    operands: &[ExprId],
3605) -> Result<bool, crate::compiler_error::CompilerFailure> {
3606    let mut has_never_operand = false;
3607    for &operand in operands {
3608        let ty = &ctx
3609            .ta
3610            .try_expr(operand)
3611            .map_err(crate::codegen::arena_failure)?
3612            .ty;
3613        has_never_operand |= matches!(ty.peel(), Type::Never);
3614    }
3615    if !has_never_operand {
3616        return Ok(false);
3617    }
3618    for &operand in operands {
3619        emit_expr(emitter, ctx, operand)?;
3620        emitter.instruction(Instruction::Drop);
3621    }
3622    emitter.instruction(Instruction::Unreachable);
3623    Ok(true)
3624}
3625
3626fn emit_primitive_operand(
3627    emitter: &mut FunctionEmitter,
3628    ctx: &CodegenCtx,
3629    expr: ExprId,
3630) -> Result<(), crate::compiler_error::CompilerFailure> {
3631    emit_expr(emitter, ctx, expr)?;
3632    let ty = &ctx
3633        .ta
3634        .try_expr(expr)
3635        .map_err(crate::codegen::arena_failure)?
3636        .ty;
3637    let _: () = if matches!(ty.primitive_behavior(), Type::Number)
3638        && ctx.symbols.value_type(ty)? != ValType::F64
3639    {
3640        super::cast::emit_cast_to(emitter, ctx, &Type::Number)?;
3641    };
3642    Ok(())
3643}
3644
3645/// `"field" in obj` codegen. Walks the receiver's
3646/// `$ObjectShape.field_names` array twice — once with `ref.eq` (the
3647/// same-module fast path, sharing the per-name string global) and
3648/// once with the prelude `$string_eq` (the cross-module slow path).
3649/// Returns an i32 boolean (0/1) on the stack.
3650///
3651/// Presence uses the same slot scan as reads, without invoking accessors.
3652fn emit_in_operator(
3653    emitter: &mut FunctionEmitter,
3654    ctx: &CodegenCtx,
3655    lhs: ExprId,
3656    rhs: ExprId,
3657) -> Result<(), crate::compiler_error::CompilerFailure> {
3658    emit_expr(emitter, ctx, lhs)?;
3659    crate::codegen::cast_check::emit_operation_cast_on_stack(
3660        emitter,
3661        ctx,
3662        &ctx.ta
3663            .try_expr(lhs)
3664            .map_err(crate::codegen::arena_failure)?
3665            .ty,
3666        &Type::String,
3667    )?;
3668    let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
3669    emitter.instruction(Instruction::LocalSet(key));
3670    emit_expr(emitter, ctx, rhs)?;
3671    cast::emit_box(
3672        emitter,
3673        ctx,
3674        &ctx.ta
3675            .try_expr(rhs)
3676            .map_err(crate::codegen::arena_failure)?
3677            .ty,
3678    )?;
3679    emitter.instruction(Instruction::LocalGet(key));
3680    let Some(symbol) = ctx.require(
3681        ctx.symbols.prelude_func_idx("ObjectConstructor##hasField"),
3682        "presence helper imported",
3683    ) else {
3684        return Ok(());
3685    };
3686    emitter.instruction(Instruction::Call(symbol));
3687    Ok(())
3688}
3689
3690/// Emit the constructor field-setup sequence for `mangled`: each own-field
3691/// initializer (and, later, parameter-property copy) as `this.field = value`
3692/// into the object-fields payload, in recorded order. Runs after `super(...)`
3693/// (or at the top of a base class's init fn), so `this`/parent fields are live.
3694pub(crate) fn emit_class_field_setup(
3695    emitter: &mut FunctionEmitter,
3696    ctx: &CodegenCtx,
3697    mangled: &crate::MangledName,
3698) -> Result<(), crate::compiler_error::CompilerFailure> {
3699    emit_class_field_setup_phase(emitter, ctx, mangled, FieldSetupPhase::All)
3700}
3701
3702#[derive(Clone, Copy)]
3703pub(crate) enum FieldSetupPhase {
3704    All,
3705    Initializers,
3706    ParameterProperties,
3707}
3708
3709pub(crate) fn emit_class_field_setup_phase(
3710    emitter: &mut FunctionEmitter,
3711    ctx: &CodegenCtx,
3712    mangled: &crate::MangledName,
3713    phase: FieldSetupPhase,
3714) -> Result<(), crate::compiler_error::CompilerFailure> {
3715    use crate::codegen::symbol_table::FieldSetup;
3716    let setup: Vec<FieldSetup> = match ctx.symbols.class_field_setup(mangled) {
3717        Some(s) if !s.is_empty() => s.to_vec(),
3718        _ => return Ok(()),
3719    };
3720    let struct_idx = ctx.symbols.class_struct_type_idx(mangled).ok_or_else(|| {
3721        crate::codegen::internal_failure("class struct type recorded in classes::emit")
3722    })?;
3723    let intrinsics = ctx
3724        .symbols
3725        .intrinsic_type_indices()
3726        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
3727    let this = emitter.this_local().ok_or_else(|| {
3728        crate::codegen::internal_failure("field setup runs inside a constructor init fn")
3729    })?;
3730    for step in &setup {
3731        let parameter_property = matches!(step, FieldSetup::ParamCopy { .. });
3732        if matches!(phase, FieldSetupPhase::Initializers) && parameter_property
3733            || matches!(phase, FieldSetupPhase::ParameterProperties) && !parameter_property
3734        {
3735            continue;
3736        }
3737        let field = match step {
3738            FieldSetup::Init { field, .. }
3739            | FieldSetup::ParamCopy { field, .. }
3740            | FieldSetup::Reset { field } => field,
3741        };
3742        let slot = ctx
3743            .symbols
3744            .class_field_slot(mangled, field)
3745            .ok_or_else(|| {
3746                crate::codegen::internal_failure("class field slot recorded in classes::emit")
3747            })?;
3748        match step {
3749            FieldSetup::Init { value, .. } => {
3750                let value_ty = ctx
3751                    .ta
3752                    .try_expr(*value)
3753                    .map_err(crate::codegen::arena_failure)?
3754                    .ty
3755                    .clone();
3756                emit_expr(emitter, ctx, *value)?;
3757                crate::codegen::function_emitter::cast::emit_box(emitter, ctx, &value_ty)?;
3758            }
3759            FieldSetup::ParamCopy { param, .. } => {
3760                let local = emitter.require_local_slot(&param.name.name)?;
3761                if param.boxed {
3762                    let box_type = ctx.symbols.box_type_idx(&param.ty)?.ok_or_else(|| {
3763                        crate::codegen::internal_failure(
3764                            "constructor parameter cell is not registered",
3765                        )
3766                    })?;
3767                    super::binding_cells::read(emitter, ctx, local, box_type)?;
3768                    cast::emit_unerase(emitter, ctx, &param.ty)?;
3769                } else {
3770                    emitter.instruction(Instruction::LocalGet(local));
3771                }
3772                cast::emit_box(emitter, ctx, &param.ty)?;
3773            }
3774            FieldSetup::Reset { .. } => {
3775                emit_undefined(emitter, ctx)?;
3776            }
3777        }
3778        let stored = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
3779        emitter.instruction(Instruction::LocalSet(stored));
3780        emitter.instruction(Instruction::LocalGet(this));
3781        emitter.instruction(Instruction::StructGet {
3782            struct_type_index: struct_idx,
3783            field_index: 2,
3784        });
3785        emitter.instruction(Instruction::I32Const(slot as i32));
3786        emitter.instruction(Instruction::LocalGet(stored));
3787        emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
3788        let index = emitter.add_anonymous_local(ValType::I32)?;
3789        emitter.instruction(Instruction::I32Const(slot as i32));
3790        emitter.instruction(Instruction::LocalSet(index));
3791        crate::codegen::field_names::emit_mark_present(emitter, ctx, this, index)?;
3792    }
3793    Ok(())
3794}
3795
3796/// Push the payload slot at `index_local` from an `$ObjectShape`-typed
3797/// receiver. Slot 2 of the shape header is the fields array; the caller owns
3798/// proving the index is in range.
3799pub(crate) fn emit_field_slot_get(
3800    emitter: &mut FunctionEmitter,
3801    intrinsics: crate::codegen::intrinsics::IntrinsicTypeIndices,
3802    object_local: u32,
3803    index_local: u32,
3804) {
3805    emitter.instruction(Instruction::LocalGet(object_local));
3806    emitter.instruction(Instruction::StructGet {
3807        struct_type_index: intrinsics.object_shape,
3808        field_index: 2,
3809    });
3810    emitter.instruction(Instruction::LocalGet(index_local));
3811    emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
3812}
3813
3814pub(crate) fn emit_object_field_read_by_name(
3815    emitter: &mut FunctionEmitter,
3816    ctx: &CodegenCtx,
3817    object_local: u32,
3818    name_global: u32,
3819) {
3820    ctx.latch(emit_object_field_read_by_name_checked(
3821        emitter,
3822        ctx,
3823        object_local,
3824        name_global,
3825    ));
3826}
3827
3828fn emit_object_field_read_by_name_checked(
3829    emitter: &mut FunctionEmitter,
3830    ctx: &CodegenCtx,
3831    object_local: u32,
3832    name_global: u32,
3833) -> Result<(), crate::compiler_error::CompilerFailure> {
3834    let intrinsics = ctx
3835        .symbols
3836        .intrinsic_type_indices()
3837        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
3838    let Some(index_local) = ctx.latch(emitter.add_anonymous_local(ValType::I32)) else {
3839        return Ok(());
3840    };
3841    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
3842    emitter.instruction(Instruction::LocalSet(index_local));
3843    emitter.emit_block(BlockType::Result(ValType::Ref(RefType {
3844        nullable: true,
3845        heap_type: HeapType::Concrete(intrinsics.object),
3846    })));
3847    emitter.instruction(Instruction::LocalGet(index_local));
3848    emitter.instruction(Instruction::I32Const(0));
3849    emitter.instruction(Instruction::I32LtS);
3850    emitter.emit_if(BlockType::Empty);
3851    emit_undefined(emitter, ctx)?;
3852    emitter.instruction(Instruction::Br(1));
3853    emitter.emit_end();
3854    emitter.instruction(Instruction::LocalGet(object_local));
3855    emitter.instruction(Instruction::StructGet {
3856        struct_type_index: intrinsics.object_shape,
3857        field_index: 2,
3858    });
3859    emitter.instruction(Instruction::LocalGet(index_local));
3860    emitter.instruction(Instruction::ArrayGet(intrinsics.object_fields));
3861    emitter.emit_end();
3862
3863    Ok(())
3864}
3865
3866pub(crate) fn emit_object_field_write_by_name(
3867    emitter: &mut FunctionEmitter,
3868    ctx: &CodegenCtx,
3869    object_local: u32,
3870    name_global: u32,
3871) -> Result<(), crate::compiler_error::CompilerFailure> {
3872    let intrinsics = ctx
3873        .symbols
3874        .intrinsic_type_indices()
3875        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
3876    let value_local = emitter.add_anonymous_local(ValType::Ref(RefType {
3877        nullable: true,
3878        heap_type: HeapType::Concrete(intrinsics.object),
3879    }))?;
3880    let index_local = emitter.add_anonymous_local(ValType::I32)?;
3881    emitter.instruction(Instruction::LocalSet(value_local));
3882    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
3883    emitter.instruction(Instruction::LocalTee(index_local));
3884    emitter.instruction(Instruction::I32Const(0));
3885    emitter.instruction(Instruction::I32GeS);
3886    emitter.emit_if(BlockType::Empty);
3887    emitter.instruction(Instruction::LocalGet(object_local));
3888    emitter.instruction(Instruction::StructGet {
3889        struct_type_index: intrinsics.object_shape,
3890        field_index: 2,
3891    });
3892    emitter.instruction(Instruction::LocalGet(index_local));
3893    emitter.instruction(Instruction::LocalGet(value_local));
3894    emitter.instruction(Instruction::ArraySet(intrinsics.object_fields));
3895    crate::codegen::field_names::emit_mark_present(emitter, ctx, object_local, index_local)?;
3896    emitter.emit_else();
3897    emitter.instruction(Instruction::LocalGet(object_local));
3898    emitter.instruction(Instruction::GlobalGet(name_global));
3899    emitter.instruction(Instruction::LocalGet(value_local));
3900    let insert = ctx
3901        .symbols
3902        .prelude_func_idx("ObjectConstructor##insertField")
3903        .ok_or_else(|| {
3904            crate::codegen::internal_failure("field insertion helper is not imported")
3905        })?;
3906    emitter.instruction(Instruction::Call(insert));
3907    emitter.emit_end();
3908    Ok(())
3909}
3910
3911/// Whether a shaped property read may emit its accessor branch, which scans for
3912/// `getter_name` and so needs its `$string` global. Always true today —
3913/// `analysis::note_shaped_property_access` interns the name for every access it
3914/// classifies as shaped, which is exactly the set reaching here. It stays as a
3915/// fail-soft guard against an unclassified receiver, which would otherwise panic
3916/// on the missing lookup.
3917pub(crate) fn accessor_branch_emittable(ctx: &CodegenCtx, getter_name: &str) -> bool {
3918    ctx.symbols
3919        .field_name_string_global_idx(getter_name)
3920        .is_some()
3921}
3922
3923/// Pushes i32 1 if `object_local` is backed by the accessor `accessor_name`, 0
3924/// otherwise: the name scan finds a slot, the receiver's vtable is a
3925/// `$ClassVTable` (only a class declares accessors), and that slot holds a
3926/// closure of the accessor's shape. Callers must have checked the name has a
3927/// string global.
3928///
3929/// All three, because `get <prop>` is an ordinary string and a value can carry a
3930/// *data* field spelled exactly that — `JSON.parse('{"get x": 5}') as Foo` is
3931/// enough, and an object literal can even store a matching closure under it —
3932/// while the branch this gates goes straight on to `ref.cast` the slot to the
3933/// accessor's closure type. Anything else falls through to the data / absent
3934/// arms.
3935///
3936/// Returns the local holding the accessor slot's index, valid on the branch
3937/// where this pushed 1 — so the dispatch that follows can read the slot directly
3938/// instead of scanning the same name a second time.
3939pub(crate) fn emit_is_accessor_backed(
3940    emitter: &mut FunctionEmitter,
3941    ctx: &CodegenCtx,
3942    object_local: u32,
3943    accessor_name: &str,
3944    kind: crate::AccessorKind,
3945) -> Result<u32, crate::compiler_error::CompilerFailure> {
3946    let intrinsics = ctx
3947        .symbols
3948        .intrinsic_type_indices()
3949        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
3950    let accessor_global = ctx
3951        .symbols
3952        .field_name_string_global_idx(accessor_name)
3953        .ok_or_else(|| {
3954            crate::codegen::internal_failure("caller checked the accessor name has a string global")
3955        })?;
3956    let closure_struct_idx = ctx
3957        .symbols
3958        .closure_struct_type_idx(crate::codegen::classes::accessor_closure_sig(kind))
3959        .ok_or_else(|| {
3960            crate::codegen::internal_failure(
3961                "accessor closure struct collected by analysis::note_shaped_property_access",
3962            )
3963        })?;
3964    let index_local = emitter.add_anonymous_local(ValType::I32)?;
3965    emit_field_index_by_name(emitter, ctx, object_local, accessor_global, true);
3966    emitter.instruction(Instruction::LocalTee(index_local));
3967    emitter.instruction(Instruction::I32Const(0));
3968    emitter.instruction(Instruction::I32GeS);
3969    emitter.instruction(Instruction::LocalGet(object_local));
3970    emitter.instruction(Instruction::StructGet {
3971        struct_type_index: intrinsics.object,
3972        field_index: 0,
3973    });
3974    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
3975        intrinsics.class_vtable,
3976    )));
3977    emitter.instruction(Instruction::I32And);
3978    emitter.emit_if(BlockType::Result(ValType::I32));
3979    emitter.instruction(Instruction::LocalGet(object_local));
3980    emitter.instruction(Instruction::StructGet {
3981        struct_type_index: intrinsics.object_shape,
3982        field_index: 1,
3983    });
3984    emitter.instruction(Instruction::LocalGet(index_local));
3985    emitter.instruction(Instruction::ArrayGet(intrinsics.field_names));
3986    crate::codegen::field_names::emit_name_is_accessor(emitter, ctx)?;
3987    emit_field_slot_get(emitter, intrinsics, object_local, index_local);
3988    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
3989        closure_struct_idx,
3990    )));
3991    emitter.instruction(Instruction::I32And);
3992    emitter.emit_else();
3993    emitter.instruction(Instruction::I32Const(0));
3994    emitter.emit_end();
3995    Ok(index_local)
3996}
3997
3998/// Read property `prop_name` from an `$ObjectShape`-typed receiver, accessor-aware:
3999/// a data field reads its payload slot; an accessor property invokes its
4000/// `get <prop>` method closure; a property with neither slot is absent and reads
4001/// `null`. The choice is made at runtime, since an interface property may be
4002/// backed by a field on one impl, an accessor on another, and nothing at all on a
4003/// third that reached the interface type through an optional member. Deciding it
4004/// on the data-slot miss alone cannot separate the last two, and traps on
4005/// whichever it does not pick. Leaves the value on the stack at `field_ty`'s
4006/// value type.
4007pub(crate) fn emit_object_property_read(
4008    emitter: &mut FunctionEmitter,
4009    ctx: &CodegenCtx,
4010    object_local: u32,
4011    prop_name: &str,
4012    field_ty: &Type,
4013) -> Result<(), crate::compiler_error::CompilerFailure> {
4014    emit_object_property_read_as(emitter, ctx, object_local, prop_name, field_ty, field_ty)?;
4015    Ok(())
4016}
4017
4018fn emit_object_property_read_as(
4019    emitter: &mut FunctionEmitter,
4020    ctx: &CodegenCtx,
4021    object_local: u32,
4022    prop_name: &str,
4023    field_ty: &Type,
4024    check_ty: &Type,
4025) -> Result<(), crate::compiler_error::CompilerFailure> {
4026    let getter = crate::codegen::classes::accessor_getter_name(prop_name);
4027    let name_global = ctx
4028        .symbols
4029        .field_name_string_global_idx(prop_name)
4030        .ok_or_else(|| {
4031            crate::codegen::internal_failure(
4032                "per-name string global recorded during field-name-strings emission",
4033            )
4034        })?;
4035    if !accessor_branch_emittable(ctx, &getter) {
4036        emit_object_field_read_by_name(emitter, ctx, object_local, name_global);
4037        emit_dynamic_narrowed_property_read(
4038            emitter,
4039            ctx,
4040            object_local,
4041            prop_name,
4042            field_ty,
4043            check_ty,
4044        )?;
4045        return Ok(());
4046    }
4047    let intrinsics = ctx
4048        .symbols
4049        .intrinsic_type_indices()
4050        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4051    let result_vt = ctx.symbols.value_type(field_ty)?;
4052    let index_local = emitter.add_anonymous_local(ValType::I32)?;
4053    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
4054    emitter.instruction(Instruction::LocalSet(index_local));
4055    emitter.instruction(Instruction::LocalGet(index_local));
4056    emitter.instruction(Instruction::I32Const(0));
4057    emitter.instruction(Instruction::I32GeS);
4058    emitter.emit_if(BlockType::Result(result_vt));
4059    // data slot
4060    emit_field_slot_get(emitter, intrinsics, object_local, index_local);
4061    emit_dynamic_narrowed_property_read(emitter, ctx, object_local, prop_name, field_ty, check_ty)?;
4062    emitter.emit_else();
4063    let getter_slot_local = emit_is_accessor_backed(
4064        emitter,
4065        ctx,
4066        object_local,
4067        &getter,
4068        crate::AccessorKind::Get,
4069    )?;
4070    emitter.emit_if(BlockType::Result(result_vt));
4071    // accessor slot
4072    emitter.instruction(Instruction::LocalGet(object_local));
4073    emit_interface_method_via_shape_with_receiver_on_stack(
4074        emitter,
4075        ctx,
4076        &getter,
4077        &[],
4078        field_ty,
4079        Some(getter_slot_local),
4080    )?;
4081    emitter.emit_else();
4082    // neither: absent — an optional member the value never materialised, so
4083    // `field_ty` admits null and the cast is a widen. An accessor-backed value
4084    // does not reach here even when the accessor is declared in another module:
4085    // the `get <prop>` name is interned off the *access*, not off any accessor
4086    // declaration this module can see (`analysis::note_shaped_property_access`).
4087    emit_undefined(emitter, ctx)?;
4088    cast::emit_cast_to(emitter, ctx, field_ty)?;
4089    emitter.emit_end();
4090    emitter.emit_end();
4091    Ok(())
4092}
4093
4094fn emit_dynamic_narrowed_property_read(
4095    emitter: &mut FunctionEmitter,
4096    ctx: &CodegenCtx,
4097    object_local: u32,
4098    prop_name: &str,
4099    field_ty: &Type,
4100    check_ty: &Type,
4101) -> Result<(), crate::compiler_error::CompilerFailure> {
4102    let checks: Vec<_> = ctx
4103        .symbols
4104        .class_field_narrowing_checks(prop_name)
4105        .filter_map(|(class, check)| {
4106            ctx.symbols
4107                .class_vtable_global_idx(class)
4108                .map(|vtable| (vtable, check))
4109        })
4110        .collect();
4111    if checks.is_empty() {
4112        cast::emit_cast_to(emitter, ctx, field_ty)?;
4113        return Ok(());
4114    }
4115
4116    let intrinsics = ctx
4117        .symbols
4118        .intrinsic_type_indices()
4119        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
4120    let raw = emitter.add_anonymous_local(ValType::Ref(RefType {
4121        nullable: true,
4122        heap_type: HeapType::Concrete(intrinsics.object),
4123    }))?;
4124    emitter.instruction(Instruction::LocalSet(raw));
4125    emit_dynamic_narrowing_check(
4126        emitter,
4127        ctx,
4128        object_local,
4129        raw,
4130        field_ty,
4131        check_ty,
4132        &checks,
4133        0,
4134    )?;
4135    Ok(())
4136}
4137
4138#[allow(clippy::too_many_arguments)]
4139fn emit_dynamic_narrowing_check(
4140    emitter: &mut FunctionEmitter,
4141    ctx: &CodegenCtx,
4142    object_local: u32,
4143    raw: u32,
4144    field_ty: &Type,
4145    check_ty: &Type,
4146    checks: &[(u32, &crate::FieldNarrowingCheck)],
4147    index: usize,
4148) -> Result<(), crate::compiler_error::CompilerFailure> {
4149    let Some((vtable, check)) = checks.get(index) else {
4150        emitter.instruction(Instruction::LocalGet(raw));
4151        cast::emit_cast_to(emitter, ctx, field_ty)?;
4152        return Ok(());
4153    };
4154    emitter.instruction(Instruction::LocalGet(object_local));
4155    crate::codegen::function_emitter::cast::emit_nominal_instance_test(emitter, ctx, *vtable)?;
4156    emitter.emit_if(BlockType::Result(ctx.symbols.value_type(field_ty)?));
4157    emitter.instruction(Instruction::LocalGet(raw));
4158    crate::codegen::cast_check::emit_narrowed_field_read_as(
4159        emitter, ctx, check, field_ty, check_ty,
4160    )?;
4161    emitter.emit_else();
4162    emit_dynamic_narrowing_check(
4163        emitter,
4164        ctx,
4165        object_local,
4166        raw,
4167        field_ty,
4168        check_ty,
4169        checks,
4170        index + 1,
4171    )?;
4172    emitter.emit_end();
4173    Ok(())
4174}
4175
4176/// Write `value` to property `prop` on an `$ObjectShape`-typed receiver,
4177/// accessor-aware: a data field writes its payload slot; an accessor property
4178/// invokes its `set <prop>` method closure; a property backed by a getter with no
4179/// setter throws, since the target exists but is read-only; a property with none
4180/// of the three gets a new data slot. `value` is evaluated exactly once.
4181pub(crate) fn emit_object_property_write(
4182    emitter: &mut FunctionEmitter,
4183    ctx: &CodegenCtx,
4184    object_local: u32,
4185    prop: &Ident,
4186    value: ExprId,
4187) -> Result<(), crate::compiler_error::CompilerFailure> {
4188    emit_object_property_write_value(
4189        emitter,
4190        ctx,
4191        object_local,
4192        prop,
4193        &ShapeArgument::Expression(value),
4194    )?;
4195    Ok(())
4196}
4197
4198fn emit_object_property_write_value(
4199    emitter: &mut FunctionEmitter,
4200    ctx: &CodegenCtx,
4201    object_local: u32,
4202    prop: &Ident,
4203    value: &ShapeArgument,
4204) -> Result<(), crate::compiler_error::CompilerFailure> {
4205    let prop_name = prop.name.as_str();
4206    let setter = crate::codegen::classes::accessor_setter_name(prop_name);
4207    let getter = crate::codegen::classes::accessor_getter_name(prop_name);
4208    let has_setter_name = ctx.symbols.field_name_string_global_idx(&setter).is_some();
4209    let has_getter_name = ctx.symbols.field_name_string_global_idx(&getter).is_some();
4210    let name_global = ctx
4211        .symbols
4212        .field_name_string_global_idx(prop_name)
4213        .ok_or_else(|| {
4214            crate::codegen::internal_failure(
4215                "per-name string global recorded during field-name-strings emission",
4216            )
4217        })?;
4218    // Fail-soft twin of [`accessor_branch_emittable`], asked of both halves:
4219    // reaching here means analysis classified the access, which interns both
4220    // accessor names — so neither name can actually be missing.
4221    if !has_setter_name && !has_getter_name {
4222        let value_ty = value.ty(ctx)?;
4223        value.emit(emitter, ctx)?;
4224        cast::emit_box(emitter, ctx, &value_ty)?;
4225        emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
4226        return Ok(());
4227    }
4228    let index_local = emitter.add_anonymous_local(ValType::I32)?;
4229    emit_object_field_index_by_name(emitter, ctx, object_local, name_global);
4230    emitter.instruction(Instruction::LocalSet(index_local));
4231    emitter.instruction(Instruction::LocalGet(index_local));
4232    emitter.instruction(Instruction::I32Const(0));
4233    emitter.instruction(Instruction::I32GeS);
4234    emitter.emit_if(BlockType::Empty);
4235    // Evaluate the RHS before loading the payload: it may insert another
4236    // field and replace the receiver's backing arrays.
4237    let value_ty = value.ty(ctx)?;
4238    value.emit(emitter, ctx)?;
4239    cast::emit_box(emitter, ctx, &value_ty)?;
4240    emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
4241    emitter.emit_else();
4242    // One nested `if` per accessor the program declares, innermost `else` being
4243    // the absent case — so the arm list *is* the nesting depth. An accessor the
4244    // program never declares is skipped: its name has no string global to scan
4245    // for, and no value can be backed by it.
4246    let arms: Vec<(&str, AccessorWrite)> = [
4247        has_setter_name.then_some((setter.as_str(), AccessorWrite::Setter)),
4248        has_getter_name.then_some((getter.as_str(), AccessorWrite::ReadOnly)),
4249    ]
4250    .into_iter()
4251    .flatten()
4252    .collect();
4253    for (accessor, arm) in &arms {
4254        let slot_index_local =
4255            emit_is_accessor_backed(emitter, ctx, object_local, accessor, arm.scanned_kind())?;
4256        emitter.emit_if(BlockType::Empty);
4257        match arm {
4258            AccessorWrite::Setter => {
4259                emitter.instruction(Instruction::LocalGet(object_local));
4260                emit_shape_method_with_arguments(
4261                    emitter,
4262                    ctx,
4263                    accessor,
4264                    std::slice::from_ref(value),
4265                    &Type::Void,
4266                    Some(slot_index_local),
4267                )?;
4268                emitter.instruction(Instruction::Drop);
4269            }
4270            // A getter with no setter: the property is there and is read-only.
4271            // Discarding the write here would lose it silently.
4272            AccessorWrite::ReadOnly => {
4273                value.emit(emitter, ctx)?;
4274                emitter.instruction(Instruction::Drop);
4275                // The value's own span was recorded last; point the backtrace at
4276                // the property instead, which is what the write failed on.
4277                emitter.record_span(prop.span);
4278                crate::codegen::throw::emit_type_error_throw(
4279                    emitter,
4280                    ctx,
4281                    crate::codegen::throw::READ_ONLY_PROPERTY_MESSAGE,
4282                );
4283            }
4284        }
4285        emitter.emit_else();
4286    }
4287    let value_ty = value.ty(ctx)?;
4288    value.emit(emitter, ctx)?;
4289    cast::emit_box(emitter, ctx, &value_ty)?;
4290    emit_object_field_write_by_name(emitter, ctx, object_local, name_global)?;
4291    for _ in &arms {
4292        emitter.emit_end();
4293    }
4294    emitter.emit_end();
4295    Ok(())
4296}
4297
4298/// `<receiver>[idx]` with the `(ref $Array)` receiver already on the stack:
4299///
4300/// ```text
4301///   struct.get $Array 1; local.set $raw
4302///   <idx>; i32.trunc_sat_f64_u; local.set $i
4303///   bounds-check $i against len($raw)   ;; throws RangeError on a miss
4304///   local.get $raw; local.get $i; array.get $rawArray
4305///   emit_cast_to(result_ty)             ;; unboxes if primitive
4306/// ```
4307///
4308/// Shared by the plain `IndexAccess` lowering and the optional chain's `Index`
4309/// step, which differ only in how the receiver gets onto the stack. Uint8Array
4310/// has its own packed-storage path and does not come through here.
4311fn emit_bounds_checked_index_with_receiver_on_stack(
4312    emitter: &mut FunctionEmitter,
4313    ctx: &CodegenCtx,
4314    idx: ExprId,
4315    result_ty: &Type,
4316    allow_missing: bool,
4317) -> Result<(), crate::compiler_error::CompilerFailure> {
4318    let array_idx = ctx.symbols.array_type_idx().ok_or_else(|| {
4319        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
4320    })?;
4321    let raw_array_idx = ctx.symbols.raw_array_type_idx().ok_or_else(|| {
4322        crate::codegen::internal_failure("Type::Array requires intrinsic types declared")
4323    })?;
4324    let raw_arr_local = emitter.add_anonymous_local(ValType::Ref(RefType {
4325        nullable: false,
4326        heap_type: HeapType::Concrete(raw_array_idx),
4327    }))?;
4328    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
4329    emitter.instruction(Instruction::LocalSet(receiver));
4330    emit_expr(emitter, ctx, idx)?;
4331    let key = emitter.add_anonymous_local(
4332        ctx.symbols.value_type(
4333            &ctx.ta
4334                .try_expr(idx)
4335                .map_err(crate::codegen::arena_failure)?
4336                .ty,
4337        )?,
4338    )?;
4339    emitter.instruction(Instruction::LocalSet(key));
4340    emitter.instruction(Instruction::LocalGet(receiver));
4341    crate::codegen::cast_check::emit_operation_cast_on_stack(
4342        emitter,
4343        ctx,
4344        &Type::Unknown,
4345        &Type::Array(Box::new(Type::Unknown)),
4346    )?;
4347    emitter.instruction(Instruction::LocalGet(key));
4348    emit_index_number(
4349        emitter,
4350        ctx,
4351        &ctx.ta
4352            .try_expr(idx)
4353            .map_err(crate::codegen::arena_failure)?
4354            .ty,
4355    )?;
4356    let idx_f64_local = stash_index_operand(emitter)?;
4357    let length = stash_array_length(emitter, array_idx)?;
4358    emitter.instruction(Instruction::StructGet {
4359        struct_type_index: array_idx,
4360        field_index: 1,
4361    });
4362    emitter.instruction(Instruction::LocalSet(raw_arr_local));
4363    if allow_missing {
4364        emitter.instruction(Instruction::LocalGet(idx_f64_local));
4365        emitter.instruction(Instruction::LocalGet(length));
4366        emitter.instruction(Instruction::F64ConvertI32U);
4367        emitter.instruction(Instruction::F64Ge);
4368        emitter.emit_if(BlockType::Result(ctx.symbols.value_type(result_ty)?));
4369        emit_undefined(emitter, ctx)?;
4370        cast::emit_coerce_to_slot(emitter, ctx, &Type::Undefined, result_ty)?;
4371        emitter.emit_else();
4372    }
4373    let idx_local = emit_checked_index_with_length(emitter, ctx, length, idx_f64_local)?;
4374    emitter.instruction(Instruction::LocalGet(raw_arr_local));
4375    emitter.instruction(Instruction::LocalGet(idx_local));
4376    emitter.instruction(Instruction::ArrayGet(raw_array_idx));
4377    cast::emit_cast_to(emitter, ctx, result_ty)?;
4378    if allow_missing {
4379        emitter.emit_end();
4380    }
4381    Ok(())
4382}
4383
4384fn is_optional_tuple_position(
4385    ctx: &CodegenCtx<'_>,
4386    receiver: &Type,
4387    index: ExprId,
4388) -> Result<bool, crate::compiler_error::CompilerFailure> {
4389    let expression = ctx
4390        .ta
4391        .try_expr(index)
4392        .map_err(crate::codegen::arena_failure)?;
4393    let TypedExprKind::Number(index) = expression.kind else {
4394        return Ok(false);
4395    };
4396    Ok(index.fract() == 0.0 && tuple_permits_missing_index(receiver, index))
4397}
4398
4399fn tuple_permits_missing_index(receiver: &Type, index: f64) -> bool {
4400    match receiver.peel() {
4401        Type::Tuple(elements) => {
4402            index >= elements.required_len() as f64 && index < elements.len() as f64
4403        }
4404        Type::Union(members) => members
4405            .iter()
4406            .any(|member| tuple_permits_missing_index(member, index)),
4407        _ => false,
4408    }
4409}
4410
4411fn emit_uint8_index_with_receiver_on_stack(
4412    emitter: &mut FunctionEmitter,
4413    ctx: &CodegenCtx,
4414    index: ExprId,
4415) -> Result<(), crate::compiler_error::CompilerFailure> {
4416    let uint8_idx = ctx.symbols.uint8_array_type_idx().ok_or_else(|| {
4417        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
4418    })?;
4419    let raw_uint8_idx = ctx.symbols.raw_uint8_array_type_idx().ok_or_else(|| {
4420        crate::codegen::internal_failure("Type::Uint8Array requires intrinsic types declared")
4421    })?;
4422    let raw_local = emitter.add_anonymous_local(ValType::Ref(RefType {
4423        nullable: false,
4424        heap_type: HeapType::Concrete(raw_uint8_idx),
4425    }))?;
4426    let receiver = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
4427    emitter.instruction(Instruction::LocalSet(receiver));
4428    emit_expr(emitter, ctx, index)?;
4429    let key = emitter.add_anonymous_local(
4430        ctx.symbols.value_type(
4431            &ctx.ta
4432                .try_expr(index)
4433                .map_err(crate::codegen::arena_failure)?
4434                .ty,
4435        )?,
4436    )?;
4437    emitter.instruction(Instruction::LocalSet(key));
4438    emitter.instruction(Instruction::LocalGet(receiver));
4439    crate::codegen::cast_check::emit_operation_cast_on_stack(
4440        emitter,
4441        ctx,
4442        &Type::Unknown,
4443        &Type::Uint8Array,
4444    )?;
4445    emitter.instruction(Instruction::LocalGet(key));
4446    emit_index_number(
4447        emitter,
4448        ctx,
4449        &ctx.ta
4450            .try_expr(index)
4451            .map_err(crate::codegen::arena_failure)?
4452            .ty,
4453    )?;
4454    let idx_f64_local = stash_index_operand(emitter)?;
4455    emitter.instruction(Instruction::StructGet {
4456        struct_type_index: uint8_idx,
4457        field_index: 1,
4458    });
4459    emitter.instruction(Instruction::LocalSet(raw_local));
4460    let idx_local = emit_checked_index(emitter, ctx, raw_local, idx_f64_local)?;
4461    emitter.instruction(Instruction::LocalGet(raw_local));
4462    emitter.instruction(Instruction::LocalGet(idx_local));
4463    emitter.instruction(Instruction::ArrayGetU(raw_uint8_idx));
4464    emitter.instruction(Instruction::F64ConvertI32U);
4465    Ok(())
4466}
4467
4468/// Which accessor a write dispatches to when the data slot is missing.
4469#[derive(Clone, Copy)]
4470enum AccessorWrite {
4471    Setter,
4472    ReadOnly,
4473}
4474
4475impl AccessorWrite {
4476    /// Which accessor slot this arm scans for. The read-only arm has no setter
4477    /// to find, so it scans the *getter* as proof the property is there at all.
4478    fn scanned_kind(self) -> crate::AccessorKind {
4479        match self {
4480            AccessorWrite::Setter => crate::AccessorKind::Set,
4481            AccessorWrite::ReadOnly => crate::AccessorKind::Get,
4482        }
4483    }
4484}
4485
4486pub(crate) fn emit_object_field_index_by_name(
4487    emitter: &mut FunctionEmitter,
4488    ctx: &CodegenCtx,
4489    object_local: u32,
4490    name_global: u32,
4491) {
4492    emit_field_index_by_name(emitter, ctx, object_local, name_global, false);
4493}
4494
4495fn emit_field_index_by_name(
4496    emitter: &mut FunctionEmitter,
4497    ctx: &CodegenCtx,
4498    object_local: u32,
4499    name_global: u32,
4500    accessor: bool,
4501) {
4502    ctx.latch(emit_field_index_by_name_checked(
4503        emitter,
4504        ctx,
4505        object_local,
4506        name_global,
4507        accessor,
4508    ));
4509}
4510
4511fn emit_field_index_by_name_checked(
4512    emitter: &mut FunctionEmitter,
4513    ctx: &CodegenCtx,
4514    object_local: u32,
4515    name_global: u32,
4516    accessor: bool,
4517) -> Result<(), crate::compiler_error::CompilerFailure> {
4518    emitter.instruction(Instruction::LocalGet(object_local));
4519    emitter.instruction(Instruction::GlobalGet(name_global));
4520    emitter.instruction(Instruction::I32Const(i32::from(accessor)));
4521    emitter.instruction(Instruction::Call(
4522        ctx.symbols
4523            .field_lookup_function
4524            .ok_or_else(|| crate::codegen::internal_failure("field lookup allocated"))?,
4525    ));
4526
4527    Ok(())
4528}
4529
4530/// Emit a direct call to an imported function. Host imports get the
4531/// raw-string round-trip applied around the call (extract `(ref
4532/// $rawString)` on `Type::String` args, wrap on a `Type::String`
4533/// return). Non-host imports (prelude `string_concat`, etc.) skip
4534/// both conversions.
4535fn emit_direct_call(
4536    emitter: &mut FunctionEmitter,
4537    ctx: &CodegenCtx,
4538    is_host: bool,
4539    wasm_idx: u32,
4540    params: &[Type],
4541    ret: &Type,
4542    args: &[ExprId],
4543) -> Result<(), crate::compiler_error::CompilerFailure> {
4544    if params.len() != args.len() {
4545        return Err(crate::codegen::internal_failure(
4546            "direct-call argument count mismatch",
4547        ));
4548    }
4549    let string_type_idx = ctx.symbols.string_type_idx().ok_or_else(|| {
4550        crate::codegen::internal_failure("$string type registered before call codegen")
4551    })?;
4552    let raw_string_type_idx = ctx.symbols.raw_string_type_idx().ok_or_else(|| {
4553        crate::codegen::internal_failure("$rawString type registered before call codegen")
4554    })?;
4555    let mut evaluated = Vec::with_capacity(args.len());
4556    for &arg in args {
4557        emit_expr(emitter, ctx, arg)?;
4558        let slot = emitter.add_anonymous_local(
4559            ctx.symbols.value_type(
4560                &ctx.ta
4561                    .try_expr(arg)
4562                    .map_err(crate::codegen::arena_failure)?
4563                    .ty,
4564            )?,
4565        )?;
4566        emitter.instruction(Instruction::LocalSet(slot));
4567        evaluated.push(slot);
4568    }
4569    for (i, (&arg_id, local)) in args.iter().zip(evaluated).enumerate() {
4570        emitter.instruction(Instruction::LocalGet(local));
4571        // Plan 75.8: coerce a primitive arg into a wider ref-typed
4572        // param slot (e.g., calling `f(x: number | null)` with `5`
4573        // requires boxing the f64 before the call). No-op when the
4574        // value's Wasm type already satisfies the param slot via
4575        // subtyping.
4576        if let Some(param_ty) = params.get(i) {
4577            let arg_ty = ctx
4578                .ta
4579                .try_expr(arg_id)
4580                .map_err(crate::codegen::arena_failure)?
4581                .ty
4582                .clone();
4583            let default = ctx
4584                .symbols
4585                .host_call_defaults(wasm_idx, args.len())
4586                .get(i)
4587                .and_then(Option::as_ref);
4588            crate::codegen::argument_defaults::emit_argument(
4589                emitter,
4590                ctx,
4591                &arg_ty,
4592                ctx.symbols.value_type(param_ty)?,
4593                default,
4594            )?;
4595        }
4596        // follow-up: peel param / return so aliased strings
4597        // (`type S = string`) still trigger the raw-string unwrap/
4598        // rewrap that matches the host import's `$rawString`
4599        // signature.
4600        if is_host
4601            && params
4602                .get(i)
4603                .is_some_and(|p| matches!(p.peel(), Type::String))
4604        {
4605            emitter.instruction(Instruction::StructGet {
4606                struct_type_index: string_type_idx,
4607                field_index: 1,
4608            });
4609        }
4610    }
4611    emitter.instruction(Instruction::Call(wasm_idx));
4612    if ctx.symbols.resultless_functions.contains(&wasm_idx) {
4613        emit_undefined(emitter, ctx)?;
4614    }
4615    let _: () = if is_host && matches!(ret.peel(), Type::String) {
4616        let scratch = emitter.add_anonymous_local(ValType::Ref(RefType {
4617            nullable: false,
4618            heap_type: HeapType::Concrete(raw_string_type_idx),
4619        }))?;
4620        emitter.instruction(Instruction::LocalSet(scratch));
4621        let vtable_global = ctx
4622            .symbols
4623            .prelude_global_idx("string_vtable")
4624            .ok_or_else(|| {
4625                crate::codegen::internal_failure("string_vtable imported from prelude")
4626            })?;
4627        emitter.instruction(Instruction::GlobalGet(vtable_global));
4628        emitter.instruction(Instruction::LocalGet(scratch));
4629        emitter.instruction(Instruction::I64Const(0));
4630        emitter.instruction(Instruction::StructNew(string_type_idx));
4631    };
4632    Ok(())
4633}
4634
4635/// Emit an indirect call via `call_ref` through a closure value
4636/// (`LocalRef` / `GlobalRef` to a function-typed binding, an inline
4637/// `Closure`, etc.).
4638///
4639/// Under uniform closure ABI, every closure param and the
4640/// return are erased to `(ref $Object)` at the Wasm boundary. This
4641/// call site evaluates and boxes arguments before checking the callable. The
4642/// result uses the physical type chosen by runtime-value propagation.
4643fn emit_indirect_closure_call(
4644    emitter: &mut FunctionEmitter,
4645    ctx: &CodegenCtx,
4646    callee: ExprId,
4647    args: &[ExprId],
4648    result_ty: &Type,
4649) -> Result<(), crate::compiler_error::CompilerFailure> {
4650    let callee_ty = ctx
4651        .ta
4652        .source_type(callee)
4653        .map_err(crate::codegen::arena_failure)?
4654        .clone();
4655    let receiver = emit_callee(emitter, ctx, callee)?;
4656    emitter.call_receiver = receiver;
4657    emit_indirect_closure_call_with_receiver_on_stack(emitter, ctx, &callee_ty, args, result_ty)?;
4658    Ok(())
4659}
4660
4661/// Preserve a property reference until invocation without evaluating its object twice.
4662fn emit_callee(
4663    emitter: &mut FunctionEmitter,
4664    ctx: &CodegenCtx,
4665    callee: ExprId,
4666) -> Result<Option<u32>, crate::compiler_error::CompilerFailure> {
4667    let reference = reference_expression(ctx.ta, callee)?;
4668    let receiver = match &ctx
4669        .ta
4670        .try_expr(reference)
4671        .map_err(crate::codegen::arena_failure)?
4672        .kind
4673    {
4674        TypedExprKind::FieldAccess { receiver, .. }
4675        | TypedExprKind::IndexAccess { receiver, .. }
4676        | TypedExprKind::InterfacePropertyAccess { receiver, .. } => Some(*receiver),
4677        _ => None,
4678    };
4679    let mark = emitter.single_evaluation_mark();
4680    let slot = receiver
4681        .map(|receiver| {
4682            emit_expr(emitter, ctx, receiver)?;
4683            let slot = emitter.add_anonymous_local(
4684                ctx.symbols.value_type(
4685                    &ctx.ta
4686                        .try_expr(receiver)
4687                        .map_err(crate::codegen::arena_failure)?
4688                        .ty,
4689                )?,
4690            )?;
4691            emitter.instruction(Instruction::LocalSet(slot));
4692            emitter.record_single_evaluation(receiver, slot)?;
4693            Ok::<_, crate::compiler_error::CompilerFailure>(slot)
4694        })
4695        .transpose()?;
4696    emit_expr(emitter, ctx, callee)?;
4697    emitter.end_single_evaluations(mark)?;
4698    Ok(
4699        if matches!(
4700            ctx.ta
4701                .try_expr(reference)
4702                .map_err(crate::codegen::arena_failure)?
4703                .kind,
4704            TypedExprKind::OptionalChain { .. }
4705        ) {
4706            emitter.call_receiver.take()
4707        } else {
4708            slot
4709        },
4710    )
4711}
4712
4713fn reference_expression(
4714    ast: &crate::TypedAst,
4715    mut id: ExprId,
4716) -> Result<ExprId, crate::compiler_error::CompilerFailure> {
4717    loop {
4718        match ast
4719            .try_expr(id)
4720            .map_err(crate::codegen::arena_failure)?
4721            .kind
4722        {
4723            TypedExprKind::Cast { value, .. } | TypedExprKind::NonNullAssert { value } => {
4724                id = value;
4725            }
4726            _ => return Ok(id),
4727        }
4728    }
4729}
4730
4731/// Same as [`emit_indirect_closure_call`], but assumes the closure
4732/// value is already on the stack (cast to its per-signature struct
4733/// type). Used optional-chain `Call` arm, where
4734/// `emit_chain_parts` has already evaluated, null-checked, and
4735/// ref-cast the receiver before dispatching.
4736fn emit_indirect_closure_call_with_receiver_on_stack(
4737    emitter: &mut FunctionEmitter,
4738    ctx: &CodegenCtx,
4739    callee_ty: &Type,
4740    args: &[ExprId],
4741    result_ty: &Type,
4742) -> Result<(), crate::compiler_error::CompilerFailure> {
4743    let receiver = emitter.call_receiver.take();
4744    let callee = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
4745    emitter.instruction(Instruction::LocalSet(callee));
4746    let mut arguments = Vec::with_capacity(args.len());
4747    for &arg in args {
4748        emit_expr(emitter, ctx, arg)?;
4749        cast::emit_box(
4750            emitter,
4751            ctx,
4752            &ctx.ta
4753                .try_expr(arg)
4754                .map_err(crate::codegen::arena_failure)?
4755                .ty,
4756        )?;
4757        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
4758        emitter.instruction(Instruction::LocalSet(slot));
4759        arguments.push(slot);
4760    }
4761    emitter.instruction(Instruction::LocalGet(callee));
4762    crate::codegen::cast_check::emit_operation_cast_on_stack(
4763        emitter,
4764        ctx,
4765        &Type::Unknown,
4766        callee_ty,
4767    )?;
4768    // chain receivers can arrive aliased
4769    // (`type NumberFn = (x: number) => number; let f: NumberFn | null`
4770    // → after `strip_null`, the type is the alias, not the underlying
4771    // function). `classify` panics on non-Function inputs, so peel.
4772    let callee_ty = callee_ty.peel();
4773    let closure_sig = crate::codegen::closures::classify(callee_ty)?;
4774    let closure_struct_idx = ctx
4775        .symbols
4776        .closure_struct_type_idx(closure_sig)
4777        .ok_or_else(|| {
4778            crate::codegen::internal_failure(
4779                "closure struct type registered for every closure-typed callee",
4780            )
4781        })?;
4782    let fn_type_idx = ctx
4783        .symbols
4784        .closure_func_type_idx(closure_sig)
4785        .ok_or_else(|| {
4786            crate::codegen::internal_failure(
4787                "closure funcref type registered for every closure-typed callee",
4788            )
4789        })?;
4790    let closure_local = emitter.add_anonymous_local(ValType::Ref(RefType {
4791        nullable: false,
4792        heap_type: HeapType::Concrete(closure_struct_idx),
4793    }))?;
4794    emitter.instruction(Instruction::LocalTee(closure_local));
4795    emitter.instruction(Instruction::StructGet {
4796        struct_type_index: closure_struct_idx,
4797        field_index: 2,
4798    });
4799    if let Some(receiver) = receiver {
4800        crate::codegen::this_binding::bind(emitter, ctx, receiver)?;
4801    }
4802    // note: variadic closures arrive with their trailing args
4803    // already packed into a synthesized `ArrayLiteral` filling the
4804    // rest slot, so `args.len()` matches the closure's funcref
4805    // signature arity. The per-arg `emit_box` below is still correct
4806    // — the synthesized array's static type is a concrete
4807    // `Type::Array(_)`, so `emit_box` upcasts it to `(ref $Object)`
4808    // via the same ref-extension that any array arg uses.
4809    for argument in arguments {
4810        emitter.instruction(Instruction::LocalGet(argument));
4811    }
4812    emitter.instruction(Instruction::LocalGet(closure_local));
4813    emitter.instruction(Instruction::StructGet {
4814        struct_type_index: closure_struct_idx,
4815        field_index: 1,
4816    });
4817    emitter.instruction(Instruction::CallRef(fn_type_idx));
4818    // Unbox the erased return to the language-level type. Void keeps the erased
4819    // completion; primitives unwrap and references cast to their declared type.
4820    let Type::Function { ret, .. } = callee_ty.peel() else {
4821        return Err(crate::codegen::internal_failure(
4822            "indirect call requires a function type",
4823        ));
4824    };
4825    let _: () = if !ret.is_void() {
4826        crate::codegen::cast_check::emit_checked_cast_on_stack(
4827            emitter,
4828            ctx,
4829            &Type::Unknown,
4830            result_ty,
4831        )?;
4832    };
4833    Ok(())
4834}
4835
4836/// Classify the current boxed value rather than folding the retained static
4837/// refinement. Closures use the intrinsic closure supertype regardless of arity.
4838fn emit_typeof_tag(
4839    emitter: &mut FunctionEmitter,
4840    ctx: &CodegenCtx,
4841    value: ExprId,
4842    tag: crate::TypeofTagKind,
4843) -> Result<(), crate::compiler_error::CompilerFailure> {
4844    let _: () = match tag {
4845        crate::TypeofTagKind::BigInt => {
4846            emit_expr(emitter, ctx, value)?;
4847            cast::emit_box(
4848                emitter,
4849                ctx,
4850                &ctx.ta
4851                    .try_expr(value)
4852                    .map_err(crate::codegen::arena_failure)?
4853                    .ty,
4854            )?;
4855            let intrinsic = ctx
4856                .symbols
4857                .bigint_type_idx()
4858                .ok_or_else(|| crate::codegen::internal_failure("bigint intrinsic missing"))?;
4859            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(intrinsic)));
4860        }
4861        crate::TypeofTagKind::Undefined => {
4862            emit_expr(emitter, ctx, value)?;
4863            cast::emit_box(
4864                emitter,
4865                ctx,
4866                &ctx.ta
4867                    .try_expr(value)
4868                    .map_err(crate::codegen::arena_failure)?
4869                    .ty,
4870            )?;
4871            emit_is_undefined(emitter, ctx)?;
4872        }
4873        crate::TypeofTagKind::Number => {
4874            // `typeof x === "number"` lowers directly to
4875            // TypeofTag(Number). `ref.test (ref $BoxedNumber)` —
4876            // returns 1 iff the value is a `$BoxedNumber`. Handles
4877            // nullable LHS (returns 0 for null) without an explicit
4878            // null check.
4879            let boxed = ctx.symbols.boxed_number_type_idx().ok_or_else(|| {
4880                crate::codegen::internal_failure("$BoxedNumber intrinsic type registered")
4881            })?;
4882            emit_expr(emitter, ctx, value)?;
4883            cast::emit_box(
4884                emitter,
4885                ctx,
4886                &ctx.ta
4887                    .try_expr(value)
4888                    .map_err(crate::codegen::arena_failure)?
4889                    .ty,
4890            )?;
4891            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(boxed)));
4892        }
4893        crate::TypeofTagKind::String => {
4894            let string = ctx.symbols.string_type_idx().ok_or_else(|| {
4895                crate::codegen::internal_failure("$string intrinsic type registered")
4896            })?;
4897            emit_expr(emitter, ctx, value)?;
4898            cast::emit_box(
4899                emitter,
4900                ctx,
4901                &ctx.ta
4902                    .try_expr(value)
4903                    .map_err(crate::codegen::arena_failure)?
4904                    .ty,
4905            )?;
4906            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(string)));
4907        }
4908        crate::TypeofTagKind::Boolean => {
4909            let boxed = ctx.symbols.boxed_boolean_type_idx().ok_or_else(|| {
4910                crate::codegen::internal_failure("$BoxedBoolean intrinsic type registered")
4911            })?;
4912            emit_expr(emitter, ctx, value)?;
4913            cast::emit_box(
4914                emitter,
4915                ctx,
4916                &ctx.ta
4917                    .try_expr(value)
4918                    .map_err(crate::codegen::arena_failure)?
4919                    .ty,
4920            )?;
4921            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(boxed)));
4922        }
4923        crate::TypeofTagKind::Function => {
4924            let closure_idx = ctx.symbols.closure_type_idx().ok_or_else(|| {
4925                crate::codegen::internal_failure("$Closure intrinsic type registered")
4926            })?;
4927            emit_expr(emitter, ctx, value)?;
4928            cast::emit_box(
4929                emitter,
4930                ctx,
4931                &ctx.ta
4932                    .try_expr(value)
4933                    .map_err(crate::codegen::arena_failure)?
4934                    .ty,
4935            )?;
4936            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(closure_idx)));
4937        }
4938        crate::TypeofTagKind::Object => {
4939            let scratch_ty = ctx.symbols.value_type(&Type::Unknown)?;
4940            let nullable = matches!(scratch_ty, ValType::Ref(rt) if rt.nullable);
4941            let scratch = emitter.add_anonymous_local(scratch_ty)?;
4942            emit_expr(emitter, ctx, value)?;
4943            cast::emit_box(
4944                emitter,
4945                ctx,
4946                &ctx.ta
4947                    .try_expr(value)
4948                    .map_err(crate::codegen::arena_failure)?
4949                    .ty,
4950            )?;
4951            emitter.instruction(Instruction::LocalSet(scratch));
4952
4953            // Stated as the complement of the primitive tags rather than a list
4954            // of object-like types: every runtime value is an `$Object`
4955            // subtype, so a positive list silently answers "not an object" for
4956            // any struct nobody remembered to add. The exclusions below are
4957            // exhaustive by construction — they are the `$Object` subtypes that
4958            // carry a primitive tag of their own.
4959            let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
4960                crate::codegen::internal_failure("intrinsics declared by codegen entry")
4961            })?;
4962            emitter.instruction(Instruction::LocalGet(scratch));
4963            emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
4964                intrinsics.object,
4965            )));
4966            for primitive in [
4967                intrinsics.string,
4968                intrinsics.boxed_number,
4969                intrinsics.boxed_boolean,
4970                intrinsics.bigint,
4971                intrinsics.closure,
4972                intrinsics.undefined,
4973            ] {
4974                emitter.instruction(Instruction::LocalGet(scratch));
4975                emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(primitive)));
4976                emitter.instruction(Instruction::I32Eqz);
4977                emitter.instruction(Instruction::I32And);
4978            }
4979            if nullable {
4980                // JS quirk: `typeof null === "object"`. Only OR in
4981                // the null check when the source's Wasm type admits
4982                // null — `ref.is_null` is a validator error on a
4983                // non-nullable ref.
4984                emitter.instruction(Instruction::LocalGet(scratch));
4985                emitter.instruction(Instruction::RefIsNull);
4986                emitter.instruction(Instruction::I32Or);
4987            }
4988        }
4989    };
4990    Ok(())
4991}
4992
4993/// Whether a bitwise operation is between plain numbers, so it can run on
4994/// `i32` values inline instead of through the dynamic host operation.
4995fn has_numeric_operands(
4996    ctx: &CodegenCtx,
4997    operands: &[ExprId],
4998    result_ty: &Type,
4999) -> Result<bool, crate::compiler_error::CompilerFailure> {
5000    if !matches!(result_ty.peel(), Type::Number | Type::NumberLiteral(_)) {
5001        return Ok(false);
5002    }
5003    for &operand in operands {
5004        let ty = &ctx
5005            .ta
5006            .try_expr(operand)
5007            .map_err(crate::codegen::arena_failure)?
5008            .ty;
5009        if !matches!(
5010            ty.primitive_behavior(),
5011            Type::Number | Type::NumberLiteral(_)
5012        ) {
5013            return Ok(false);
5014        }
5015    }
5016    Ok(true)
5017}
5018
5019fn emit_number_bitwise(
5020    emitter: &mut FunctionEmitter,
5021    ctx: &CodegenCtx,
5022    op: BinOp,
5023    lhs: ExprId,
5024    rhs: ExprId,
5025) -> Result<(), crate::compiler_error::CompilerFailure> {
5026    emit_primitive_operand(emitter, ctx, lhs)?;
5027    emit_to_int32(emitter, ctx)?;
5028    emit_primitive_operand(emitter, ctx, rhs)?;
5029    emit_to_int32(emitter, ctx)?;
5030    // Wasm shifts already take the count modulo 32, as JavaScript does.
5031    let (instruction, unsigned) = match op {
5032        BinOp::BitAnd => (Instruction::I32And, false),
5033        BinOp::BitOr => (Instruction::I32Or, false),
5034        BinOp::BitXor => (Instruction::I32Xor, false),
5035        BinOp::Shl => (Instruction::I32Shl, false),
5036        BinOp::Shr => (Instruction::I32ShrS, false),
5037        BinOp::UnsignedShr => (Instruction::I32ShrU, true),
5038        _ => {
5039            return Err(crate::codegen::internal_failure(
5040                "a numeric bitwise operation has a bitwise operator",
5041            ));
5042        }
5043    };
5044    emitter.instruction(instruction);
5045    emitter.instruction(if unsigned {
5046        Instruction::F64ConvertI32U
5047    } else {
5048        Instruction::F64ConvertI32S
5049    });
5050    Ok(())
5051}
5052
5053/// JavaScript's ToInt32 of the `f64` on the stack. A finite value below 2^63
5054/// truncates exactly in `i64`, whose low 32 bits are the answer; anything else
5055/// (huge, infinite, NaN) takes the host's `x | 0`.
5056fn emit_to_int32(
5057    emitter: &mut FunctionEmitter,
5058    ctx: &CodegenCtx,
5059) -> Result<(), crate::compiler_error::CompilerFailure> {
5060    let value = emitter.add_anonymous_local(ValType::F64)?;
5061    emitter.instruction(Instruction::LocalTee(value));
5062    emitter.instruction(Instruction::F64Abs);
5063    emitter.instruction(Instruction::F64Const(Ieee64::from(
5064        9_223_372_036_854_775_808.0,
5065    )));
5066    emitter.instruction(Instruction::F64Lt);
5067    emitter.emit_if(BlockType::Result(ValType::I32));
5068    emitter.instruction(Instruction::LocalGet(value));
5069    emitter.instruction(Instruction::I64TruncSatF64S);
5070    emitter.instruction(Instruction::I32WrapI64);
5071    emitter.emit_else();
5072    emitter.instruction(Instruction::LocalGet(value));
5073    cast::emit_box(emitter, ctx, &Type::Number)?;
5074    emitter.instruction(Instruction::F64Const(Ieee64::from(0.0)));
5075    cast::emit_box(emitter, ctx, &Type::Number)?;
5076    let host = ctx
5077        .symbols
5078        .func_idx(&crate::mangle::prelude("__value_bitor"))
5079        .ok_or_else(|| crate::codegen::internal_failure("missing bitwise host import: bitor"))?;
5080    emitter.instruction(Instruction::Call(host));
5081    cast::emit_cast_to(emitter, ctx, &Type::Number)?;
5082    emitter.instruction(Instruction::I32TruncF64S);
5083    emitter.emit_end();
5084    Ok(())
5085}
5086
5087fn emit_bitwise_host(
5088    emitter: &mut FunctionEmitter,
5089    ctx: &CodegenCtx,
5090    name: &str,
5091    operands: &[ExprId],
5092    result_ty: &Type,
5093) -> Result<(), crate::compiler_error::CompilerFailure> {
5094    for &operand in operands {
5095        emit_expr(emitter, ctx, operand)?;
5096        let ty = &ctx
5097            .ta
5098            .try_expr(operand)
5099            .map_err(crate::codegen::arena_failure)?
5100            .ty;
5101        cast::emit_box(emitter, ctx, ty)?;
5102    }
5103    let mangled = crate::mangle::prelude(&format!("__value_{name}"));
5104    let host = ctx.symbols.func_idx(&mangled).ok_or_else(|| {
5105        crate::codegen::internal_failure(format!("missing bitwise host import: {name}"))
5106    })?;
5107    emitter.instruction(Instruction::Call(host));
5108    cast::emit_cast_to(emitter, ctx, result_ty)
5109}
5110
5111fn emit_unary(
5112    emitter: &mut FunctionEmitter,
5113    ctx: &CodegenCtx,
5114    op: UnOp,
5115    operand: ExprId,
5116    result_ty: &Type,
5117) -> Result<(), crate::compiler_error::CompilerFailure> {
5118    if matches!(op, UnOp::Neg | UnOp::Pos)
5119        && try_emit_unreachable_for_never_operand(emitter, ctx, &[operand])?
5120    {
5121        return Ok(());
5122    }
5123    let _: () = match op {
5124        UnOp::BitNot if has_numeric_operands(ctx, &[operand], result_ty)? => {
5125            emit_primitive_operand(emitter, ctx, operand)?;
5126            emit_to_int32(emitter, ctx)?;
5127            emitter.instruction(Instruction::I32Const(-1));
5128            emitter.instruction(Instruction::I32Xor);
5129            emitter.instruction(Instruction::F64ConvertI32S);
5130        }
5131        UnOp::BitNot => emit_bitwise_host(emitter, ctx, "bitnot", &[operand], result_ty)?,
5132        UnOp::Neg => {
5133            // bigint negation routes to inline host call.
5134            if ctx
5135                .ta
5136                .try_expr(operand)
5137                .map_err(crate::codegen::arena_failure)?
5138                .ty
5139                .is_bigint()
5140            {
5141                emit_primitive_operand(emitter, ctx, operand)?;
5142                emit_bigint_extract_to_stack(emitter, ctx);
5143                let host_idx = ctx
5144                    .symbols
5145                    .func_idx(&crate::mangle::host(
5146                        crate::runtime::BIGINT_MODULE_NAME,
5147                        "neg",
5148                    ))
5149                    .ok_or_else(|| {
5150                        crate::codegen::internal_failure(
5151                            "submilli:bigint.neg import recorded by codegen bootstrap",
5152                        )
5153                    })?;
5154                emit_bigint_wrap_host_result(emitter, ctx, host_idx);
5155                return Ok(());
5156            }
5157            emit_primitive_operand(emitter, ctx, operand)?;
5158            emitter.instruction(Instruction::F64Neg);
5159        }
5160        UnOp::Pos => {
5161            // On a string, `+` is the explicit numeric coercion and lowers to
5162            // the same host parse `Number(s)` calls. On a number it is the
5163            // identity, so the operand's value is already what we want.
5164            emit_primitive_operand(emitter, ctx, operand)?;
5165            if ctx
5166                .ta
5167                .try_expr(operand)
5168                .map_err(crate::codegen::arena_failure)?
5169                .ty
5170                .is_string_shaped()
5171            {
5172                let host_idx = ctx
5173                    .symbols
5174                    .func_idx(&crate::mangle::host(
5175                        crate::runtime::NUMBER_MODULE_NAME,
5176                        "toNumber",
5177                    ))
5178                    .ok_or_else(|| {
5179                        crate::codegen::internal_failure(
5180                            "submilli:number.toNumber import recorded by codegen analysis",
5181                        )
5182                    })?;
5183                emitter.instruction(Instruction::Call(host_idx));
5184            }
5185        }
5186        UnOp::Not => {
5187            // `!x` on a boolean is `i32.eqz` — 0 → 1, anything else → 0.
5188            // Plan 75.9: the inferer also accepts nullable
5189            // operands (e.g., `!s` with `s: string | null`); for those
5190            // we first coerce to the i32 truthiness convention
5191            // (`ref.is_null; i32.eqz`) before negating.
5192            let operand_ty = ctx
5193                .ta
5194                .try_expr(operand)
5195                .map_err(crate::codegen::arena_failure)?
5196                .ty
5197                .clone();
5198            emit_expr(emitter, ctx, operand)?;
5199            crate::codegen::function_emitter::cast::emit_condition_to_i32(
5200                emitter,
5201                ctx,
5202                &operand_ty,
5203            )?;
5204            emitter.instruction(Instruction::I32Eqz);
5205        }
5206    };
5207    Ok(())
5208}
5209
5210/// Dispatch a method through its native wrapper, class or universal vtable,
5211/// or structural interface closure. Missing dispatch metadata is a compiler
5212/// failure, with the source span supplied by the enclosing expression emitter.
5213#[allow(clippy::too_many_arguments)]
5214pub(crate) fn emit_method_call(
5215    emitter: &mut FunctionEmitter,
5216    ctx: &CodegenCtx,
5217    receiver: ExprId,
5218    iface: &crate::MangledName,
5219    method: &str,
5220    args: &[ExprId],
5221    generic_args: Option<&[crate::GenericArgument]>,
5222    return_cast: Option<&Type>,
5223    call_ret_ty: &Type,
5224) -> Result<(), crate::compiler_error::CompilerFailure> {
5225    let recv_ty = ctx
5226        .ta
5227        .source_type(receiver)
5228        .map_err(crate::codegen::arena_failure)?
5229        .clone();
5230    if !emit_dropped_static_receiver(emitter, ctx, receiver, iface)? {
5231        emit_receiver(emitter, ctx, receiver)?;
5232    }
5233    emit_method_call_with_receiver_on_stack(
5234        emitter,
5235        ctx,
5236        &recv_ty,
5237        iface,
5238        method,
5239        args,
5240        generic_args,
5241        return_cast,
5242        call_ret_ty,
5243    )?;
5244    Ok(())
5245}
5246
5247/// A static-dispatch method drops its receiver, so a read of the binding it
5248/// is called on needn't build the object the binding stands for: push a
5249/// typed null in its place. Returns whether it did.
5250fn emit_dropped_static_receiver(
5251    emitter: &mut FunctionEmitter,
5252    ctx: &CodegenCtx,
5253    receiver: ExprId,
5254    iface: &crate::MangledName,
5255) -> Result<bool, crate::compiler_error::CompilerFailure> {
5256    let expr = ctx
5257        .ta
5258        .try_expr(receiver)
5259        .map_err(crate::codegen::arena_failure)?;
5260    if !matches!(expr.kind, TypedExprKind::GlobalRef { .. })
5261        || static_interface(ctx, &expr.ty) != Some(iface)
5262    {
5263        return Ok(false);
5264    }
5265    match ctx.symbols.value_type(&expr.ty)? {
5266        ValType::Ref(RefType { heap_type, .. }) => {
5267            emitter.instruction(Instruction::RefNull(heap_type));
5268        }
5269        other => {
5270            return Err(crate::codegen::internal_failure(format!(
5271                "static-interface binding lowered to {other:?}"
5272            )));
5273        }
5274    }
5275    Ok(true)
5276}
5277
5278/// Same as [`emit_method_call`], but assumes the receiver has
5279/// already been evaluated onto the stack at its
5280/// strip-null-then-`ref.cast` non-null Wasm shape (the contract
5281/// `emit_chain_parts` establishes before calling
5282/// [`emit_chain_access`]). Used optional-chain
5283/// `MethodCall` arm; the same three dispatch paths apply.
5284#[allow(clippy::too_many_arguments)]
5285fn emit_method_call_with_receiver_on_stack(
5286    emitter: &mut FunctionEmitter,
5287    ctx: &CodegenCtx,
5288    recv_ty: &Type,
5289    iface: &crate::MangledName,
5290    method: &str,
5291    args: &[ExprId],
5292    generic_args: Option<&[crate::GenericArgument]>,
5293    return_cast: Option<&Type>,
5294    call_ret_ty: &Type,
5295) -> Result<(), crate::compiler_error::CompilerFailure> {
5296    let concrete_receiver = crate::typechecker::infer::narrowing::strip_nullish(recv_ty);
5297    crate::codegen::field_guards::attach(emitter, ctx, &concrete_receiver)?;
5298    let direct_key = crate::mangle::extend(iface, method);
5299    if let Some(func_idx) = ctx.symbols.func_idx(&direct_key) {
5300        if matches!(recv_ty.primitive_behavior(), Type::Number)
5301            && ctx.symbols.value_type(recv_ty)? != ValType::F64
5302        {
5303            super::cast::emit_cast_to(emitter, ctx, &Type::Number)?;
5304        }
5305        // Direct or Static dispatch. The receiver is on the stack;
5306        // adjust per the interface's dispatch kind:
5307        // - `Dispatch::Direct` — keep it (with a defensive
5308        //   `ref.as_non_null` for InterfaceRef receivers whose
5309        //   declared Wasm shape is `(ref null $Object)`).
5310        // - `Dispatch::Static` (Console, Uint8ArrayConstructor) —
5311        //   drop it; the wrapper sees only the user args.
5312        // - `Dispatch::VTable` / unrecorded — keep it as-is.
5313        match ctx.symbols.iface_dispatch(iface) {
5314            Some(crate::Dispatch::Direct) => {
5315                // InterfaceRef-typed receivers (instance
5316                // interface values like `TextEncoder`, future
5317                // `File` / `Stream`) lower to `(ref null $Object)`.
5318                // The Direct wrapper takes `(ref $Object)` (non-
5319                // null), so coerce. Primitive Direct receivers
5320                // (Number/Boolean/String/Array/Uint8Array) already
5321                // have non-null Wasm value-types — no extra cast.
5322                //
5323                // peel so aliased InterfaceRef (e.g.
5324                // `type Query = Map<string, string>`) still
5325                // triggers the coercion.
5326                if matches!(recv_ty.peel(), Type::InterfaceRef { .. }) {
5327                    emitter.instruction(Instruction::RefAsNonNull);
5328                }
5329            }
5330            Some(crate::Dispatch::Static) => {
5331                emitter.instruction(Instruction::Drop);
5332            }
5333            // Defensive: a registered direct-call wrapper without a
5334            // recorded dispatch means the imports loop didn't tag the
5335            // interface. Treat as Direct (the common case before this
5336            // refactor) so existing behaviour is preserved.
5337            None | Some(crate::Dispatch::VTable) => {}
5338        }
5339        // note: variadic methods (`Array#concat(...others)`)
5340        // arrive here with a synthesized `ArrayLiteral` already
5341        // occupying the rest slot — the typechecker pre-packs. So
5342        // `args.len()` matches the wrapper sig's user-arg count and
5343        // no codegen branching is needed.
5344        //
5345        // An optional-chain `MethodCall` reaches codegen with the signature
5346        // already substituted to concrete types, so the ABI is its only
5347        // surviving record of which slots the interface's generics erased.
5348        let abi = wrapper_abi_for_call(
5349            ctx,
5350            &direct_key,
5351            args,
5352            generic_args,
5353            return_cast,
5354            call_ret_ty,
5355        )?;
5356        let defaults = ctx.symbols.host_call_defaults(func_idx, args.len());
5357        emit_args_into_slots_with_defaults(emitter, ctx, args, Some(&abi.params), defaults)?;
5358        emitter.instruction(Instruction::Call(func_idx));
5359        emit_slot_return_cast(emitter, ctx, call_ret_ty, Some(&abi))?;
5360        return Ok(());
5361    }
5362    // Interface aliases write the same method payload a class-typed call reads.
5363    // Generic methods and synthetic accessors retain their dedicated vtable ABI.
5364    if ctx.symbols.is_rewritable_class_method(iface, method) {
5365        emit_interface_method_via_shape_with_receiver_on_stack(
5366            emitter,
5367            ctx,
5368            method,
5369            args,
5370            call_ret_ty,
5371            None,
5372        )?;
5373        return Ok(());
5374    }
5375    // Class method (static path): the receiver is `(ref $Foo)` on the stack.
5376    // Dispatch through the class's own vtable slot (4+); `class_method_slot` is
5377    // only populated for classes, so a `Some` here means a genuine class method
5378    // (a user method named `toString` etc. also resolves here, not slot 0–3).
5379    if ctx.symbols.class_method_slot(iface, method).is_some() {
5380        emit_class_vtable_dispatch(emitter, ctx, iface, method, args, call_ret_ty)?;
5381        return Ok(());
5382    }
5383    if let Some(slot) = vtable_slot_for_method(method) {
5384        emit_vtable_dispatch_on_object_stack(emitter, ctx, slot);
5385        return Ok(());
5386    }
5387    // shape-based dispatch for user-declared interface
5388    // methods. The receiver is `Type::InterfaceRef`, the method
5389    // came through `find_method` against a `VTable`-dispatch
5390    // interface, and there's no built-in vtable slot. Lower as
5391    // field-name scan → cast to closure → call_ref, exactly as if
5392    // the user had written `(recv.method)(args)` against a
5393    // function-typed field. Reuses the `$ObjectShape` field-name scan
5394    // machinery and the closure ABI.
5395    if matches!(recv_ty.peel(), Type::InterfaceRef { .. }) {
5396        emit_interface_method_via_shape_with_receiver_on_stack(
5397            emitter,
5398            ctx,
5399            method,
5400            args,
5401            call_ret_ty,
5402            None,
5403        )?;
5404        return Ok(());
5405    }
5406    Err(crate::codegen::internal_failure(format!(
5407        "method dispatch was not registered for `{iface}.{method}`"
5408    )))
5409}
5410
5411fn emit_class_method_value(
5412    emitter: &mut FunctionEmitter,
5413    ctx: &CodegenCtx,
5414    receiver_ty: &Type,
5415    method: &str,
5416    result_ty: &Type,
5417    check_ty: &Type,
5418) -> Result<(), crate::compiler_error::CompilerFailure> {
5419    let object_shape = ctx
5420        .symbols
5421        .intrinsic_type_indices()
5422        .ok_or_else(|| {
5423            crate::codegen::internal_failure("class method value requires object shape intrinsic")
5424        })?
5425        .object_shape;
5426    let receiver = stash_receiver_as_object_shape(emitter, receiver_ty, object_shape)?;
5427    emit_object_property_read_as(emitter, ctx, receiver, method, result_ty, check_ty)
5428}
5429
5430/// shape-based interface method dispatch. The receiver —
5431/// an `InterfaceRef`-typed value carrying its `$Object` shape — is
5432/// already on the stack; the method's closure lives in a field slot
5433/// named by `method`. Pull it via the `$ObjectShape` field-name scan
5434/// (slot 2), cast to the matching closure struct, then invoke
5435/// through the closure ABI (env + boxed args + funcref). The
5436/// leading `RefAsNonNull` + `RefCastNonNull($ObjectShape)` is
5437/// retained because the on-stack value's static type may be a wider
5438/// `(ref $Object)` (or `(ref null $Object)`) — the cast pins it to
5439/// the object shape the field scan expects.
5440///
5441/// `slot_index_local` names a local that already holds the method slot's index.
5442/// The accessor paths resolve it to decide *whether* to dispatch, and the scan
5443/// is the expensive part of this sequence, so they hand it over rather than
5444/// repeat it; every other caller passes `None`.
5445pub(in crate::codegen) fn emit_interface_method_via_shape_with_receiver_on_stack(
5446    emitter: &mut FunctionEmitter,
5447    ctx: &CodegenCtx,
5448    method: &str,
5449    args: &[ExprId],
5450    call_ret_ty: &Type,
5451    slot_index_local: Option<u32>,
5452) -> Result<(), crate::compiler_error::CompilerFailure> {
5453    let args: Vec<_> = args
5454        .iter()
5455        .copied()
5456        .map(ShapeArgument::Expression)
5457        .collect();
5458    emit_shape_method_with_arguments(emitter, ctx, method, &args, call_ret_ty, slot_index_local)?;
5459    Ok(())
5460}
5461
5462enum ShapeArgument {
5463    Expression(ExprId),
5464    Local { slot: u32, ty: Type },
5465}
5466
5467impl ShapeArgument {
5468    fn ty(&self, ctx: &CodegenCtx) -> Result<Type, crate::compiler_error::CompilerFailure> {
5469        Ok(match self {
5470            Self::Expression(id) => ctx
5471                .ta
5472                .try_expr(*id)
5473                .map_err(crate::codegen::arena_failure)?
5474                .ty
5475                .clone(),
5476            Self::Local { ty, .. } => ty.clone(),
5477        })
5478    }
5479
5480    fn emit(
5481        &self,
5482        emitter: &mut FunctionEmitter,
5483        ctx: &CodegenCtx,
5484    ) -> Result<(), crate::compiler_error::CompilerFailure> {
5485        let _: () = match self {
5486            Self::Expression(id) => emit_expr(emitter, ctx, *id)?,
5487            Self::Local { slot, .. } => {
5488                emitter.instruction(Instruction::LocalGet(*slot));
5489            }
5490        };
5491        Ok(())
5492    }
5493}
5494
5495fn emit_shape_method_with_arguments(
5496    emitter: &mut FunctionEmitter,
5497    ctx: &CodegenCtx,
5498    method: &str,
5499    args: &[ShapeArgument],
5500    call_ret_ty: &Type,
5501    slot_index_local: Option<u32>,
5502) -> Result<(), crate::compiler_error::CompilerFailure> {
5503    let intrinsics = ctx
5504        .symbols
5505        .intrinsic_type_indices()
5506        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
5507
5508    // Build the closure signature from the arg types and the
5509    // call's return type. `classify` reduces to (arity, is_void).
5510    let arg_tys: Vec<Type> = args.iter().map(|a| a.ty(ctx)).collect::<Result<_, _>>()?;
5511    let fn_ty = Type::Function {
5512        params: arg_tys,
5513        ret: Box::new(call_ret_ty.clone()),
5514        predicate: None,
5515        // call-site arg shape — variadic packing has already
5516        // happened upstream by this point, so this signature is the
5517        // fixed-arity post-pack form.
5518        has_rest: false,
5519        optional: 0,
5520    };
5521    let closure_sig = crate::codegen::closures::classify(&fn_ty)?;
5522    let closure_struct_idx = ctx
5523        .symbols
5524        .closure_struct_type_idx(closure_sig)
5525        .ok_or_else(|| {
5526            crate::codegen::internal_failure(
5527                "closure struct registered for the method's signature (SUB-166)",
5528            )
5529        })?;
5530    let fn_type_idx = ctx
5531        .symbols
5532        .closure_func_type_idx(closure_sig)
5533        .ok_or_else(|| {
5534            crate::codegen::internal_failure(
5535                "closure funcref type registered for the method's signature",
5536            )
5537        })?;
5538
5539    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5540        nullable: false,
5541        heap_type: HeapType::Concrete(intrinsics.object_shape),
5542    }))?;
5543    let closure_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5544        nullable: false,
5545        heap_type: HeapType::Concrete(closure_struct_idx),
5546    }))?;
5547
5548    // 1. Receiver is on stack as `(ref [null] $Object)` (or a
5549    //    narrower subtype). The field scan expects `(ref
5550    //    $ObjectShape)` (non-null subtype). Strip null and downcast.
5551    emitter.instruction(Instruction::RefAsNonNull);
5552    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
5553        intrinsics.object_shape,
5554    )));
5555    emitter.instruction(Instruction::LocalSet(rcv_local));
5556
5557    // 2. Read the method's slot: by index when the caller already scanned it,
5558    //    else by the field-name scan.
5559    if let Some(index_local) = slot_index_local {
5560        emit_field_slot_get(emitter, intrinsics, rcv_local, index_local);
5561    } else {
5562        let name_global = ctx
5563            .symbols
5564            .field_name_string_global_idx(method)
5565            .ok_or_else(|| {
5566                crate::codegen::internal_failure(
5567                    ("per-name string global recorded during field-name-strings emission \
5568             for interface method names")
5569                        .to_string(),
5570                )
5571            })?;
5572        emit_object_field_read_by_name(emitter, ctx, rcv_local, name_global);
5573    }
5574
5575    let method_value = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5576    emitter.instruction(Instruction::LocalSet(method_value));
5577    let mut evaluated_args = Vec::with_capacity(args.len());
5578    for arg in args {
5579        arg.emit(emitter, ctx)?;
5580        cast::emit_box(emitter, ctx, &arg.ty(ctx)?)?;
5581        let slot = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
5582        emitter.instruction(Instruction::LocalSet(slot));
5583        evaluated_args.push(slot);
5584    }
5585    emitter.instruction(Instruction::LocalGet(method_value));
5586    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
5587        intrinsics.closure,
5588    )));
5589    emitter.instruction(Instruction::I32Eqz);
5590    emitter.emit_if(BlockType::Empty);
5591    crate::codegen::throw::emit_type_error_throw(emitter, ctx, "Value is not callable");
5592    emitter.emit_end();
5593    emitter.instruction(Instruction::LocalGet(method_value));
5594
5595    // 3. Cast to the matching closure struct type. closure
5596    // ABI: every Function-typed value stored in an `$Object` slot is
5597    // a `(ref $closure_<sig>)`.
5598    crate::codegen::closure_coercions::emit_erased_cast(
5599        emitter,
5600        ctx,
5601        crate::codegen::closures::ClosureSig::of(args.len(), call_ret_ty)?,
5602    )?;
5603    emitter.instruction(Instruction::LocalTee(closure_local));
5604
5605    // 4. Push the closure env (slot 2) as the implicit first arg.
5606    emitter.instruction(Instruction::StructGet {
5607        struct_type_index: closure_struct_idx,
5608        field_index: 2,
5609    });
5610
5611    crate::codegen::this_binding::bind(emitter, ctx, rcv_local)?;
5612    // 5. Emit each user arg, boxing to `(ref $Object)` per the
5613    // closure ABI's erasure.
5614    for slot in evaluated_args {
5615        emitter.instruction(Instruction::LocalGet(slot));
5616    }
5617
5618    // 6. Push the funcref (slot 1) and call_ref.
5619    emitter.instruction(Instruction::LocalGet(closure_local));
5620    emitter.instruction(Instruction::StructGet {
5621        struct_type_index: closure_struct_idx,
5622        field_index: 1,
5623    });
5624    emitter.instruction(Instruction::CallRef(fn_type_idx));
5625
5626    // 7. Validate and unbox the erased return. Callers hand this the declared
5627    // return type verbatim; a mismatched implementation throws `TypeError`.
5628    let _: () = if !call_ret_ty.is_void() {
5629        crate::codegen::cast_check::emit_checked_cast_on_stack(
5630            emitter,
5631            ctx,
5632            &Type::Unknown,
5633            call_ret_ty,
5634        )?;
5635    };
5636    Ok(())
5637}
5638
5639/// Map a built-in method name to its slot index in the universal
5640/// `$VTable`. The four-slot layout (toString / toJson / equals /
5641/// hash) is hard-coded everywhere in the runtime — see
5642/// [`crate::codegen::intrinsics::IntrinsicTypeIndices`] — and stays
5643/// hardcoded here until per-interface vtables make slot
5644/// indices interface-relative.
5645///
5646/// Returns `None` for any name not in the four-slot table; the
5647/// caller treats `None` as "no vtable dispatch path for this
5648/// method".
5649fn vtable_slot_for_method(method: &str) -> Option<u32> {
5650    match method {
5651        "toString" => Some(0),
5652        "toJson" => Some(1),
5653        "equals" => Some(2),
5654        "hash" => Some(3),
5655        _ => None,
5656    }
5657}
5658
5659/// Emit `recv.vtable.<slot>(recv)` for an `$Object`-shaped receiver.
5660/// Stack at exit: the slot's return type (today: `(ref $string)`
5661/// for slot 0, the only slot reachable through `emit_method_call`).
5662/// `Type::Array` also flows here through generic receivers because
5663/// `$Array <: $Object`; the implicit subtype upcast lets the
5664/// `(ref $Object)` field-0 access work uniformly.
5665/// Dispatches a vtable method (`slot`) on a receiver — a non-null
5666/// `(ref $Object)` (or subtype) — already on the stack. The nullable-union
5667/// `JSON.stringify` arm evaluates the receiver once into a local, null-checks
5668/// it, then re-pushes the non-null cast into this tail.
5669pub(super) fn emit_vtable_dispatch_on_object_stack(
5670    emitter: &mut FunctionEmitter,
5671    ctx: &CodegenCtx,
5672    slot: u32,
5673) {
5674    let Some(intrinsics) = ctx.require(
5675        ctx.symbols.intrinsic_type_indices(),
5676        "intrinsics declared by codegen entry",
5677    ) else {
5678        return;
5679    };
5680    // Per-slot funcref type. Slots 0 (toString) and 1 (toJson) share
5681    // operational signature `(ref $Object) -> (ref $string)` but are
5682    // declared as distinct sub-funcrefs inside the `$Object` rec
5683    // group, so `call_ref` needs the slot's own type index.
5684    let fn_type_idx = match slot {
5685        0 => intrinsics.to_string_fn,
5686        1 => intrinsics.to_json_fn,
5687        _ => {
5688            ctx.fail("vtable string dispatch requires toString or toJson slot");
5689            return;
5690        }
5691    };
5692    let Some(obj_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
5693        nullable: false,
5694        heap_type: HeapType::Concrete(intrinsics.object),
5695    }))) else {
5696        return;
5697    };
5698    let Some(fn_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
5699        nullable: false,
5700        heap_type: HeapType::Concrete(fn_type_idx),
5701    }))) else {
5702        return;
5703    };
5704    // the receiver may arrive as `(ref null $Object)` when
5705    // it's typed `unknown`. Coerce to non-null before stashing in
5706    // `obj_local`. Traps on null at runtime — calling `.toString()`
5707    // on null isn't meaningful, and narrowed-away null branches
5708    // never reach here.
5709    emitter.instruction(Instruction::RefAsNonNull);
5710    emitter.instruction(Instruction::LocalTee(obj_local));
5711    emitter.instruction(Instruction::StructGet {
5712        struct_type_index: intrinsics.object,
5713        field_index: 0,
5714    });
5715    emitter.instruction(Instruction::StructGet {
5716        struct_type_index: intrinsics.vtable,
5717        field_index: slot,
5718    });
5719    emitter.instruction(Instruction::LocalSet(fn_local));
5720    emitter.instruction(Instruction::LocalGet(obj_local));
5721    emitter.instruction(Instruction::LocalGet(fn_local));
5722    emitter.instruction(Instruction::CallRef(fn_type_idx));
5723}
5724
5725/// Push each argument, coercing it into the callee's recorded slot type.
5726/// `slots` is `None` when no ABI was recorded, in which case the arguments are
5727/// already at their declared types.
5728fn emit_args_into_slots(
5729    emitter: &mut FunctionEmitter,
5730    ctx: &CodegenCtx,
5731    args: &[ExprId],
5732    slots: Option<&[ValType]>,
5733) -> Result<(), crate::compiler_error::CompilerFailure> {
5734    emit_args_into_slots_with_defaults(emitter, ctx, args, slots, &[])
5735}
5736
5737fn emit_args_into_slots_with_defaults(
5738    emitter: &mut FunctionEmitter,
5739    ctx: &CodegenCtx,
5740    args: &[ExprId],
5741    slots: Option<&[ValType]>,
5742    defaults: &[Option<crate::DefaultValue>],
5743) -> Result<(), crate::compiler_error::CompilerFailure> {
5744    if slots.is_some_and(|slots| slots.len() != args.len()) {
5745        return Err(crate::codegen::internal_failure(
5746            "call ABI argument count mismatch",
5747        ));
5748    }
5749    let locals: Vec<_> = args
5750        .iter()
5751        .map(|&arg| {
5752            emit_expr(emitter, ctx, arg)?;
5753            let local = emitter.add_anonymous_local(
5754                ctx.symbols.value_type(
5755                    &ctx.ta
5756                        .try_expr(arg)
5757                        .map_err(crate::codegen::arena_failure)?
5758                        .ty,
5759                )?,
5760            )?;
5761            emitter.instruction(Instruction::LocalSet(local));
5762            Ok::<_, crate::compiler_error::CompilerFailure>(local)
5763        })
5764        .collect::<Result<_, _>>()?;
5765    for (i, (&arg, local)) in args.iter().zip(locals).enumerate() {
5766        emitter.instruction(Instruction::LocalGet(local));
5767        if let Some(slot) = slots.and_then(|s| s.get(i).copied()) {
5768            crate::codegen::argument_defaults::emit_argument(
5769                emitter,
5770                ctx,
5771                &ctx.ta
5772                    .try_expr(arg)
5773                    .map_err(crate::codegen::arena_failure)?
5774                    .ty,
5775                slot,
5776                defaults.get(i).and_then(Option::as_ref),
5777            )?;
5778        }
5779    }
5780    Ok(())
5781}
5782
5783/// The physical signature a Direct/Static-dispatch wrapper call must be
5784/// coerced into: the recorded one, or the same shape rebuilt from a
5785/// [`GenericMethodCall`](TypedExprKind::GenericMethodCall)'s typecheck-time
5786/// metadata, which describes the same erasure one call at a time.
5787///
5788/// A wrapper's `func_idx` and its recorded ABI come from two loops gated on
5789/// different sets — `is_value_used` for the value-symbol walk, versus
5790/// `is_interface_member_used` (which also skips `Dispatch::VTable` interfaces
5791/// wholesale) for the walk that records the ABI. A promotion pass keeps the
5792/// two aligned; nothing in either loop *enforces* it, so a resolvable wrapper
5793/// with no record stays possible, in the same sense as the "defensive" arm on
5794/// the dispatch match above. Rebuilding costs a branch and removes a silent
5795/// mis-emit — args pushed at their declared types into erased slots — from the
5796/// failure modes of a future gating change.
5797fn wrapper_abi_for_call(
5798    ctx: &CodegenCtx,
5799    direct_key: &crate::MangledName,
5800    args: &[ExprId],
5801    generic_args: Option<&[crate::GenericArgument]>,
5802    return_cast: Option<&Type>,
5803    call_ret_ty: &Type,
5804) -> Result<MethodSlotAbi, crate::compiler_error::CompilerFailure> {
5805    if let Some(recorded) = ctx.symbols.iface_method_abi(direct_key) {
5806        return Ok(recorded.clone());
5807    }
5808    let erased = ctx.symbols.value_type(&Type::Unknown)?;
5809    let slot_is_erased =
5810        |i: usize| generic_args.is_some_and(|flags| flags.get(i).is_some_and(|g| g.is_generic));
5811    Ok(MethodSlotAbi {
5812        params: args
5813            .iter()
5814            .enumerate()
5815            .map(|(i, &a)| {
5816                Ok::<_, crate::compiler_error::CompilerFailure>(if slot_is_erased(i) {
5817                    erased
5818                } else {
5819                    ctx.symbols.value_type(
5820                        &ctx.ta
5821                            .try_expr(a)
5822                            .map_err(crate::codegen::arena_failure)?
5823                            .ty,
5824                    )?
5825                })
5826            })
5827            .collect::<Result<_, _>>()?,
5828        ret: match return_cast {
5829            Some(_) => Some(erased),
5830            None => (!call_ret_ty.is_void())
5831                .then(|| ctx.symbols.value_type(call_ret_ty))
5832                .transpose()?,
5833        },
5834    })
5835}
5836
5837/// Cast an erased slot result back to the call-site type. A slot whose result
5838/// already matches needs nothing.
5839fn emit_slot_return_cast(
5840    emitter: &mut FunctionEmitter,
5841    ctx: &CodegenCtx,
5842    call_ret_ty: &Type,
5843    abi: Option<&MethodSlotAbi>,
5844) -> Result<(), crate::compiler_error::CompilerFailure> {
5845    if matches!(call_ret_ty.peel(), Type::Never) {
5846        emitter.instruction(Instruction::Unreachable);
5847        return Ok(());
5848    }
5849    // A resultless slot still produces `undefined`, including when a widened
5850    // call site reads it as `unknown`.
5851    if abi.is_some_and(|abi| abi.ret.is_none()) {
5852        emit_undefined(emitter, ctx)?;
5853    }
5854    if call_ret_ty.is_void() {
5855        return Ok(());
5856    }
5857    if matches!(call_ret_ty, Type::Unknown) {
5858        match abi.and_then(|abi| abi.ret) {
5859            Some(ValType::F64) => cast::emit_box(emitter, ctx, &Type::Number)?,
5860            Some(ValType::I32) => cast::emit_box(emitter, ctx, &Type::Boolean)?,
5861            _ => {}
5862        }
5863        return Ok(());
5864    }
5865    if abi.and_then(|a| a.ret) != Some(ctx.symbols.value_type(call_ret_ty)?) {
5866        crate::codegen::cast_check::emit_checked_cast_on_stack(
5867            emitter,
5868            ctx,
5869            &Type::Unknown,
5870            call_ret_ty,
5871        )?;
5872    }
5873    crate::codegen::field_guards::attach(emitter, ctx, call_ret_ty)?;
5874    Ok(())
5875}
5876
5877/// Static-path class method dispatch. The receiver `(ref $Foo)` is on the stack.
5878/// Loads the vtable (struct slot 0) of the receiver's *static* class, reads the
5879/// method funcref at its slot, then `call_ref`s it with the receiver as self +
5880/// the args. Slots are inherited at the same index, so a parent-typed receiver
5881/// dispatches to the runtime override automatically.
5882///
5883/// Generic-class slots are erased: the physical sig takes/returns
5884/// `(ref null $Object)` where the declared type was a class type param. The
5885/// slot's recorded [`MethodSlotAbi`] drives coercion of the args and the
5886/// cast of the erased return back to `call_ret_ty` — self-sufficiently, since
5887/// plain `MethodCall`, optional-chain parts, and the synthesized accessor
5888/// calls carry no generic metadata.
5889fn emit_class_vtable_dispatch(
5890    emitter: &mut FunctionEmitter,
5891    ctx: &CodegenCtx,
5892    class: &crate::MangledName,
5893    method: &str,
5894    args: &[ExprId],
5895    call_ret_ty: &Type,
5896) -> Result<(), crate::compiler_error::CompilerFailure> {
5897    let struct_idx = ctx
5898        .symbols
5899        .class_struct_type_idx(class)
5900        .ok_or_else(|| crate::codegen::internal_failure("class struct type recorded"))?;
5901    let vtable_idx = ctx
5902        .symbols
5903        .class_vtable_type_idx(class)
5904        .ok_or_else(|| crate::codegen::internal_failure("class vtable type recorded"))?;
5905    let slot = ctx
5906        .symbols
5907        .class_method_slot(class, method)
5908        .ok_or_else(|| crate::codegen::internal_failure("class method slot recorded"))?;
5909    let sig_idx = ctx
5910        .symbols
5911        .class_method_sig(class, method)
5912        .ok_or_else(|| crate::codegen::internal_failure("class method sig recorded"))?;
5913
5914    let rcv_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5915        nullable: false,
5916        heap_type: HeapType::Concrete(struct_idx),
5917    }))?;
5918    // Defensive cast: the receiver is `(ref $Foo)` on the direct path, but the
5919    // optional-chain path may hand us a wider `(ref null $Object)`.
5920    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(struct_idx)));
5921    emitter.instruction(Instruction::LocalSet(rcv_local));
5922
5923    // Load the method funcref from the vtable, stash it.
5924    emitter.instruction(Instruction::LocalGet(rcv_local));
5925    emitter.instruction(Instruction::StructGet {
5926        struct_type_index: struct_idx,
5927        field_index: 0,
5928    });
5929    emitter.instruction(Instruction::StructGet {
5930        struct_type_index: vtable_idx,
5931        field_index: slot,
5932    });
5933    let fn_local = emitter.add_anonymous_local(ValType::Ref(RefType {
5934        nullable: false,
5935        heap_type: HeapType::Concrete(sig_idx),
5936    }))?;
5937    emitter.instruction(Instruction::LocalSet(fn_local));
5938
5939    // self (`(ref $Foo)` <: the sig's `(ref $Object)`), then args, then funcref.
5940    let abi = ctx.symbols.class_method_abi(class, method).cloned();
5941    emitter.instruction(Instruction::LocalGet(rcv_local));
5942    emit_args_into_slots(
5943        emitter,
5944        ctx,
5945        args,
5946        abi.as_ref().map(|a| a.params.as_slice()),
5947    )?;
5948    emitter.instruction(Instruction::LocalGet(fn_local));
5949    emitter.instruction(Instruction::CallRef(sig_idx));
5950    emit_slot_return_cast(emitter, ctx, call_ret_ty, abi.as_ref())?;
5951    Ok(())
5952}
5953
5954fn emit_intrinsic_call(
5955    emitter: &mut FunctionEmitter,
5956    ctx: &CodegenCtx,
5957    kind: Intrinsic,
5958    args: &[ExprId],
5959    result_ty: &Type,
5960) -> Result<(), crate::compiler_error::CompilerFailure> {
5961    let _: () = match kind {
5962        Intrinsic::Assert => {
5963            // `assert(cond, msg)` — throw `new Error(msg)` on false.
5964            // JS-style left-to-right argument evaluation: cond first,
5965            // then msg (stashed in a local for the throw path).
5966            if args.len() != 2 {
5967                return Err(crate::codegen::internal_failure(
5968                    "assert requires two arguments",
5969                ));
5970            }
5971            emit_expr(emitter, ctx, args[0])?;
5972            cast::emit_condition_to_i32(
5973                emitter,
5974                ctx,
5975                &ctx.ta
5976                    .try_expr(args[0])
5977                    .map_err(crate::codegen::arena_failure)?
5978                    .ty,
5979            )?;
5980            emit_expr(emitter, ctx, args[1])?;
5981            let message_type = &ctx
5982                .ta
5983                .try_expr(args[1])
5984                .map_err(crate::codegen::arena_failure)?
5985                .ty;
5986            let msg_local = emitter.add_anonymous_local(ctx.symbols.value_type(message_type)?)?;
5987            emitter.instruction(Instruction::LocalSet(msg_local));
5988            emitter.instruction(Instruction::I32Eqz);
5989            emitter.emit_if(BlockType::Empty);
5990            emitter.instruction(Instruction::LocalGet(msg_local));
5991            cast::emit_coerce_to_slot(emitter, ctx, message_type, &Type::String)?;
5992            let ctor_idx = ctx
5993                .symbols
5994                .func_idx(&crate::mangle::prelude("Error#constructor"))
5995                .ok_or_else(|| {
5996                    crate::codegen::internal_failure("Error#constructor exported from prelude")
5997                })?;
5998            emitter.instruction(Instruction::Call(ctor_idx));
5999            crate::codegen::throw::emit_error_throw(emitter, ctx);
6000            emitter.emit_end();
6001            emit_undefined(emitter, ctx)?;
6002        }
6003        Intrinsic::JsonStringify => {
6004            super::json::emit_stringify(emitter, ctx, args, result_ty)?;
6005        }
6006        Intrinsic::JsonParse => {
6007            super::json::emit_parse(emitter, ctx, args)?;
6008        }
6009        Intrinsic::ToString => {
6010            let [value] = args else {
6011                return Err(crate::codegen::internal_failure(
6012                    "string conversion requires one argument",
6013                ));
6014            };
6015            emit_expr(emitter, ctx, *value)?;
6016            cast::emit_box(
6017                emitter,
6018                ctx,
6019                &ctx.ta
6020                    .try_expr(*value)
6021                    .map_err(crate::codegen::arena_failure)?
6022                    .ty,
6023            )?;
6024            let function = ctx
6025                .symbols
6026                .prelude_func_idx("__value_to_string")
6027                .ok_or_else(|| {
6028                    crate::codegen::internal_failure("dynamic string conversion collected")
6029                })?;
6030            emitter.instruction(Instruction::Call(function));
6031        }
6032        // `BigInt.fromString(s)` — the remaining
6033        // bigint-producing intrinsic after retired
6034        // `BigIntFromNumber` / `NumberFromBigInt` in favour of the
6035        // call-signature wrappers on `BigIntConstructor` /
6036        // `NumberConstructor`. Wraps the host result `(i32 sign, ref
6037        // $rawBigInt limbs)` into a `$bigint` struct.
6038        Intrinsic::BigIntFromString => {
6039            if args.len() != 1 {
6040                return Err(crate::codegen::internal_failure(
6041                    "BigInt.fromString requires one argument",
6042                ));
6043            }
6044            emit_expr(emitter, ctx, args[0])?;
6045            // The arg is `(ref $string)`; the host fn takes
6046            // `(ref $rawString)`. Pull the raw array out via slot 1.
6047            let string_type_idx = ctx
6048                .symbols
6049                .string_type_idx()
6050                .ok_or_else(|| crate::codegen::internal_failure("$string registered"))?;
6051            emitter.instruction(Instruction::StructGet {
6052                struct_type_index: string_type_idx,
6053                field_index: 1,
6054            });
6055            let host_idx = ctx
6056                .symbols
6057                .func_idx(&crate::mangle::host(
6058                    crate::runtime::BIGINT_MODULE_NAME,
6059                    "fromString",
6060                ))
6061                .ok_or_else(|| {
6062                    crate::codegen::internal_failure(
6063                        "submilli:bigint.fromString import recorded by codegen bootstrap",
6064                    )
6065                })?;
6066            emit_bigint_wrap_host_result(emitter, ctx, host_idx);
6067        }
6068    };
6069    Ok(())
6070}
6071
6072/// Emit `lhs.vtable.equals(lhs, rhs)` for two operands of `$Object`
6073/// subtypes (objects, arrays, functions, boxed values). Stack at entry: `[lhs, rhs]`.
6074/// Stack at exit: `[i32]` (1 if equal, 0 otherwise — flipped via
6075/// `I32Eqz` for `BinOp::NotEq`).
6076fn emit_vtable_equality(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, op: BinOp) {
6077    ctx.latch(emit_vtable_equality_checked(emitter, ctx, op));
6078}
6079
6080fn emit_vtable_equality_checked(
6081    emitter: &mut FunctionEmitter,
6082    ctx: &CodegenCtx,
6083    op: BinOp,
6084) -> Result<(), crate::compiler_error::CompilerFailure> {
6085    let intrinsics = ctx
6086        .symbols
6087        .intrinsic_type_indices()
6088        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
6089    // The operands' own types may differ (two function types of different
6090    // arities), so both are stashed at the common `$Object` shape.
6091    let operand_ref = ValType::Ref(RefType {
6092        nullable: true,
6093        heap_type: HeapType::Concrete(intrinsics.object),
6094    });
6095    let object_ref = ValType::Ref(RefType {
6096        nullable: false,
6097        heap_type: HeapType::Concrete(intrinsics.object),
6098    });
6099    let equals_fn_ref = ValType::Ref(RefType {
6100        nullable: false,
6101        heap_type: HeapType::Concrete(intrinsics.equals_fn),
6102    });
6103
6104    // Stack: [lhs, rhs]. Stash both, load equals fn, restack args.
6105    let Some(lhs_local) = ctx.latch(emitter.add_anonymous_local(operand_ref)) else {
6106        return Ok(());
6107    };
6108    let Some(rhs_local) = ctx.latch(emitter.add_anonymous_local(operand_ref)) else {
6109        return Ok(());
6110    };
6111    let Some(lhs_object) = ctx.latch(emitter.add_anonymous_local(object_ref)) else {
6112        return Ok(());
6113    };
6114    let Some(rhs_object) = ctx.latch(emitter.add_anonymous_local(object_ref)) else {
6115        return Ok(());
6116    };
6117    let Some(eq_fn_local) = ctx.latch(emitter.add_anonymous_local(equals_fn_ref)) else {
6118        return Ok(());
6119    };
6120
6121    emitter.instruction(Instruction::LocalSet(rhs_local));
6122    emitter.instruction(Instruction::LocalSet(lhs_local));
6123
6124    emitter.instruction(Instruction::LocalGet(lhs_local));
6125    emitter.instruction(Instruction::RefAsNonNull);
6126    emitter.instruction(Instruction::LocalSet(lhs_object));
6127    emitter.instruction(Instruction::LocalGet(rhs_local));
6128    emitter.instruction(Instruction::RefAsNonNull);
6129    emitter.instruction(Instruction::LocalSet(rhs_object));
6130
6131    emitter.instruction(Instruction::LocalGet(lhs_object));
6132    emitter.instruction(Instruction::StructGet {
6133        struct_type_index: intrinsics.object,
6134        field_index: 0,
6135    });
6136    emitter.instruction(Instruction::StructGet {
6137        struct_type_index: intrinsics.vtable,
6138        field_index: 2,
6139    });
6140    emitter.instruction(Instruction::LocalSet(eq_fn_local));
6141
6142    emitter.instruction(Instruction::LocalGet(lhs_object));
6143    emitter.instruction(Instruction::LocalGet(rhs_object));
6144    emitter.instruction(Instruction::LocalGet(eq_fn_local));
6145    emitter.instruction(Instruction::CallRef(intrinsics.equals_fn));
6146
6147    if matches!(op, BinOp::NotEq) {
6148        emitter.instruction(Instruction::I32Eqz);
6149    }
6150
6151    Ok(())
6152}
6153
6154/// Boxed, null-aware `===` / `!==` dispatch. Used when either operand's static
6155/// type admits null, or when the operands have different Wasm representations.
6156/// Bridges the operands to a
6157/// common Wasm shape (`(ref null $Object)`) via `emit_box`, then
6158/// emits a 4-way condition:
6159///
6160/// - both null → equal (1).
6161/// - lhs null, rhs non-null → unequal (0).
6162/// - lhs non-null, rhs null → unequal (0).
6163/// - both non-null → `lhs.vtable.equals(lhs, rhs)`.
6164///
6165/// Final `I32Eqz` flips the result for `NotEq`.
6166fn emit_boxed_eq(
6167    emitter: &mut FunctionEmitter,
6168    ctx: &CodegenCtx,
6169    lhs: ExprId,
6170    rhs: ExprId,
6171    op: BinOp,
6172) -> Result<(), crate::compiler_error::CompilerFailure> {
6173    let intrinsics = ctx
6174        .symbols
6175        .intrinsic_type_indices()
6176        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
6177    let object_null_ref = ValType::Ref(RefType {
6178        nullable: true,
6179        heap_type: HeapType::Concrete(intrinsics.object),
6180    });
6181    let object_ref = ValType::Ref(RefType {
6182        nullable: false,
6183        heap_type: HeapType::Concrete(intrinsics.object),
6184    });
6185    let equals_fn_ref = ValType::Ref(RefType {
6186        nullable: false,
6187        heap_type: HeapType::Concrete(intrinsics.equals_fn),
6188    });
6189
6190    let lhs_ty = ctx
6191        .ta
6192        .try_expr(lhs)
6193        .map_err(crate::codegen::arena_failure)?
6194        .ty
6195        .clone();
6196    let rhs_ty = ctx
6197        .ta
6198        .try_expr(rhs)
6199        .map_err(crate::codegen::arena_failure)?
6200        .ty
6201        .clone();
6202
6203    // Materialize lhs as (ref null $Object). `emit_box` wraps
6204    // primitive Wasm values into their `$Boxed*` shape and is a
6205    // no-op for ref types / unions, so both sides land on the
6206    // stack at the universal nullable shape.
6207    let lhs_local = emitter.add_anonymous_local(object_null_ref)?;
6208    let rhs_local = emitter.add_anonymous_local(object_null_ref)?;
6209    let lhs_nonnull_local = emitter.add_anonymous_local(object_ref)?;
6210    let eq_fn_local = emitter.add_anonymous_local(equals_fn_ref)?;
6211
6212    emit_expr(emitter, ctx, lhs)?;
6213    cast::emit_box(emitter, ctx, &lhs_ty)?;
6214    emitter.instruction(Instruction::LocalSet(lhs_local));
6215
6216    emit_expr(emitter, ctx, rhs)?;
6217    cast::emit_box(emitter, ctx, &rhs_ty)?;
6218    emitter.instruction(Instruction::LocalSet(rhs_local));
6219
6220    // if lhs is null: result = (rhs is null)
6221    //     covers both-null (1) and lhs-only-null (0).
6222    // else: if rhs is null → 0; else vtable.equals dispatch.
6223    emitter.instruction(Instruction::LocalGet(lhs_local));
6224    emitter.instruction(Instruction::RefIsNull);
6225    emitter.emit_if(BlockType::Result(ValType::I32));
6226    emitter.instruction(Instruction::LocalGet(rhs_local));
6227    emitter.instruction(Instruction::RefIsNull);
6228    emitter.emit_else();
6229    emitter.instruction(Instruction::LocalGet(rhs_local));
6230    emitter.instruction(Instruction::RefIsNull);
6231    emitter.emit_if(BlockType::Result(ValType::I32));
6232    emitter.instruction(Instruction::I32Const(0));
6233    emitter.emit_else();
6234    // Both non-null — vtable.equals on lhs's vtable.
6235    emitter.instruction(Instruction::LocalGet(lhs_local));
6236    emitter.instruction(Instruction::RefAsNonNull);
6237    emitter.instruction(Instruction::LocalTee(lhs_nonnull_local));
6238    emitter.instruction(Instruction::StructGet {
6239        struct_type_index: intrinsics.object,
6240        field_index: 0,
6241    });
6242    emitter.instruction(Instruction::StructGet {
6243        struct_type_index: intrinsics.vtable,
6244        field_index: 2,
6245    });
6246    emitter.instruction(Instruction::LocalSet(eq_fn_local));
6247    emitter.instruction(Instruction::LocalGet(lhs_nonnull_local));
6248    emitter.instruction(Instruction::LocalGet(rhs_local));
6249    emitter.instruction(Instruction::RefAsNonNull);
6250    emitter.instruction(Instruction::LocalGet(eq_fn_local));
6251    emitter.instruction(Instruction::CallRef(intrinsics.equals_fn));
6252    emitter.emit_end();
6253    emitter.emit_end();
6254
6255    let _: () = if matches!(op, BinOp::NotEq) {
6256        emitter.instruction(Instruction::I32Eqz);
6257    };
6258    Ok(())
6259}
6260
6261/// Emit a bigint literal. Small literals (≤ ±2^53 - 1)
6262/// fast-path through `submilli:bigint.fromNumber`; larger literals
6263/// are looked up in the bigint constants pool and materialized via
6264/// `array.new_data` over a per-program data segment, then wrapped
6265/// directly into `$bigint` with the prelude's vtable.
6266fn emit_bigint_literal(
6267    emitter: &mut FunctionEmitter,
6268    ctx: &CodegenCtx,
6269    digits: &str,
6270) -> Result<(), crate::compiler_error::CompilerFailure> {
6271    use crate::codegen::internal_failure;
6272    if let Some(entry) = ctx.bigints.lookup(digits)? {
6273        // Large literal — pack limbs from data segment, wrap with
6274        // bigint vtable directly (no host fn involved). Data-segment
6275        // index is offset by the string-pool count because string
6276        // segments come first in the module's data section.
6277        let raw_bigint_idx = ctx
6278            .symbols
6279            .raw_bigint_type_idx()
6280            .ok_or_else(|| internal_failure("the $rawBigInt intrinsic is not registered"))?;
6281        let bigint_idx = ctx
6282            .symbols
6283            .bigint_type_idx()
6284            .ok_or_else(|| internal_failure("the $bigint intrinsic is not registered"))?;
6285        let vtable_global = ctx
6286            .symbols
6287            .prelude_global_idx("bigint_vtable")
6288            .ok_or_else(|| internal_failure("bigint_vtable is not imported from the prelude"))?;
6289        let data_idx = crate::codegen::wasm_u32(ctx.strings.strings.len())?
6290            .checked_add(entry.data_idx)
6291            .ok_or_else(|| internal_failure("the module has too many data segments"))?;
6292        emitter.instruction(Instruction::GlobalGet(vtable_global));
6293        emitter.instruction(Instruction::I32Const(i32::from(entry.sign)));
6294        emitter.instruction(Instruction::I32Const(0));
6295        emitter.instruction(Instruction::I32Const(entry.limb_count.cast_signed()));
6296        emitter.instruction(Instruction::ArrayNewData {
6297            array_type_index: raw_bigint_idx,
6298            array_data_index: data_idx,
6299        });
6300        emitter.instruction(Instruction::StructNew(bigint_idx));
6301        return Ok(());
6302    }
6303    // The pool leaves only literals within f64's safe-integer range, so this
6304    // conversion is exact.
6305    // Construct via `submilli:bigint.fromNumber` + standard wrap.
6306    let v: i64 = digits
6307        .parse()
6308        .map_err(|_| internal_failure("an unpooled bigint literal is not a safe integer"))?;
6309    let host_idx = ctx
6310        .symbols
6311        .func_idx(&crate::mangle::host(
6312            crate::runtime::BIGINT_MODULE_NAME,
6313            "fromNumber",
6314        ))
6315        .ok_or_else(|| internal_failure("submilli:bigint.fromNumber is not imported"))?;
6316    emitter.instruction(Instruction::F64Const(Ieee64::from(v as f64)));
6317    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
6318    Ok(())
6319}
6320
6321/// regex-literal lowering. Each literal site allocates a
6322/// fresh `$regex` (JS-faithful per-site `lastIndex` semantics; see
6323/// `docs/regex.md`). Emit the source and flags as constant `$string`
6324/// values, then call the prelude's `RegExpConstructor#new` wrapper —
6325/// which invokes the `submilli:regex.compile` host fn and assembles
6326/// the receiver struct.
6327///
6328/// The pattern + flags have already been validated at infer time
6329/// (`typechecker::infer::expr::infer_regex` calls
6330/// `runtime::prelude::regex::engine::build_regex`), so the wrapper's compile call
6331/// here only fails on memory-cap exhaustion, which traps cleanly.
6332fn emit_regex_literal(
6333    emitter: &mut FunctionEmitter,
6334    ctx: &CodegenCtx,
6335    source: &str,
6336    flags: &str,
6337) -> Result<(), crate::compiler_error::CompilerFailure> {
6338    super::emit_const_string_by_text(emitter, ctx, source)?;
6339    super::emit_const_string_by_text(emitter, ctx, flags)?;
6340    let ctor_idx = ctx
6341        .symbols
6342        .func_idx(&crate::mangle::prelude("RegExpConstructor#new"))
6343        .ok_or_else(|| crate::codegen::internal_failure("RegExpConstructor#new is not imported"))?;
6344    emitter.instruction(Instruction::Call(ctor_idx));
6345    Ok(())
6346}
6347
6348/// helper — given a `(ref $bigint)` on top of the stack,
6349/// destructure it into `(i32 sign, ref $rawBigInt limbs)` on the
6350/// stack. Used by every codegen site that hands a bigint to a host
6351/// fn (`bigint.add` / `bigint.cmp` / `bigint.neg` /
6352/// `number.fromBigInt`).
6353fn emit_bigint_extract_to_stack(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
6354    ctx.latch(emit_bigint_extract_to_stack_checked(emitter, ctx));
6355}
6356
6357fn emit_bigint_extract_to_stack_checked(
6358    emitter: &mut FunctionEmitter,
6359    ctx: &CodegenCtx,
6360) -> Result<(), crate::compiler_error::CompilerFailure> {
6361    let bigint_idx = ctx
6362        .symbols
6363        .bigint_type_idx()
6364        .ok_or_else(|| crate::codegen::internal_failure("$bigint registered"))?;
6365    // Stash the receiver in a scratch local since we need two
6366    // struct.get reads.
6367    let Some(scratch) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
6368        wasm_encoder::RefType {
6369            nullable: false,
6370            heap_type: wasm_encoder::HeapType::Concrete(bigint_idx),
6371        },
6372    ))) else {
6373        return Ok(());
6374    };
6375    emitter.instruction(Instruction::LocalSet(scratch));
6376    emitter.instruction(Instruction::LocalGet(scratch));
6377    emitter.instruction(Instruction::StructGet {
6378        struct_type_index: bigint_idx,
6379        field_index: 1, // sign
6380    });
6381    emitter.instruction(Instruction::LocalGet(scratch));
6382    emitter.instruction(Instruction::StructGet {
6383        struct_type_index: bigint_idx,
6384        field_index: 2, // limbs
6385    });
6386
6387    Ok(())
6388}
6389
6390/// helper — call `host_func_idx` (whose result shape is
6391/// the standard `(i32 sign, ref $rawBigInt limbs)` bigint pair),
6392/// then wrap into a fresh `$bigint` struct with the prelude's
6393/// vtable. Stack at exit: `(ref $bigint)`.
6394fn emit_bigint_wrap_host_result(
6395    emitter: &mut FunctionEmitter,
6396    ctx: &CodegenCtx,
6397    host_func_idx: u32,
6398) {
6399    ctx.latch(emit_bigint_wrap_host_result_checked(
6400        emitter,
6401        ctx,
6402        host_func_idx,
6403    ));
6404}
6405
6406fn emit_bigint_wrap_host_result_checked(
6407    emitter: &mut FunctionEmitter,
6408    ctx: &CodegenCtx,
6409    host_func_idx: u32,
6410) -> Result<(), crate::compiler_error::CompilerFailure> {
6411    emitter.instruction(Instruction::Call(host_func_idx));
6412    let bigint_idx = ctx
6413        .symbols
6414        .bigint_type_idx()
6415        .ok_or_else(|| crate::codegen::internal_failure("$bigint registered"))?;
6416    let raw_bigint_idx = ctx
6417        .symbols
6418        .raw_bigint_type_idx()
6419        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
6420    let vtable_global = ctx
6421        .symbols
6422        .prelude_global_idx("bigint_vtable")
6423        .ok_or_else(|| {
6424            crate::codegen::internal_failure("bigint_vtable imported from prelude bootstrap")
6425        })?;
6426    // Stack: [sign, limbs]. Need [vtable, sign, limbs] for
6427    // struct.new $bigint. Use two scratch locals to reorder.
6428    let Some(scratch_limbs) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
6429        wasm_encoder::RefType {
6430            nullable: false,
6431            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
6432        },
6433    ))) else {
6434        return Ok(());
6435    };
6436    let Some(scratch_sign) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::I32))
6437    else {
6438        return Ok(());
6439    };
6440    emitter.instruction(Instruction::LocalSet(scratch_limbs));
6441    emitter.instruction(Instruction::LocalSet(scratch_sign));
6442    emitter.instruction(Instruction::GlobalGet(vtable_global));
6443    emitter.instruction(Instruction::LocalGet(scratch_sign));
6444    emitter.instruction(Instruction::LocalGet(scratch_limbs));
6445    emitter.instruction(Instruction::StructNew(bigint_idx));
6446
6447    Ok(())
6448}
6449
6450/// inline binary bigint op. `op_name` is one of
6451/// "add" / "sub" / "mul" / "div" / "mod" — looked up
6452/// directly against `submilli:bigint.<op>`.
6453fn emit_bigint_binop_inline(
6454    emitter: &mut FunctionEmitter,
6455    ctx: &CodegenCtx,
6456    lhs: ExprId,
6457    rhs: ExprId,
6458    op_name: &str,
6459) -> Result<(), crate::compiler_error::CompilerFailure> {
6460    emit_expr(emitter, ctx, lhs)?;
6461    emit_bigint_extract_to_stack(emitter, ctx);
6462    let raw_bigint_idx = ctx
6463        .symbols
6464        .raw_bigint_type_idx()
6465        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
6466    let lhs_limbs =
6467        emitter.add_anonymous_local(wasm_encoder::ValType::Ref(wasm_encoder::RefType {
6468            nullable: false,
6469            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
6470        }))?;
6471    let lhs_sign = emitter.add_anonymous_local(wasm_encoder::ValType::I32)?;
6472    emitter.instruction(Instruction::LocalSet(lhs_limbs));
6473    emitter.instruction(Instruction::LocalSet(lhs_sign));
6474    emitter.instruction(Instruction::LocalGet(lhs_sign));
6475    emitter.instruction(Instruction::LocalGet(lhs_limbs));
6476    emit_expr(emitter, ctx, rhs)?;
6477    emit_bigint_extract_to_stack(emitter, ctx);
6478    let host_idx = ctx
6479        .symbols
6480        .func_idx(&crate::mangle::host(
6481            crate::runtime::BIGINT_MODULE_NAME,
6482            op_name,
6483        ))
6484        .ok_or_else(|| {
6485            crate::codegen::internal_failure(format!(
6486                "submilli:bigint.{op_name} import recorded by codegen bootstrap"
6487            ))
6488        })?;
6489    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
6490    Ok(())
6491}
6492
6493/// postfix `++` / `--` compute step for a bigint operand —
6494/// stack `[orig (ref $bigint)]` → `[new (ref $bigint)]`. Mirrors
6495/// [`emit_bigint_binop_inline`] but skips the lhs `emit_expr` (it's
6496/// already on the stack) and emits the rhs as a synthesized `1n`
6497/// literal via [`emit_bigint_literal`] (small-literal path:
6498/// `f64.const 1` + `submilli:bigint.fromNumber`).
6499fn emit_bigint_pm_one(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, op: crate::PostfixOp) {
6500    ctx.latch(emit_bigint_pm_one_checked(emitter, ctx, op));
6501}
6502
6503fn emit_bigint_pm_one_checked(
6504    emitter: &mut FunctionEmitter,
6505    ctx: &CodegenCtx,
6506    op: crate::PostfixOp,
6507) -> Result<(), crate::compiler_error::CompilerFailure> {
6508    emit_bigint_extract_to_stack(emitter, ctx);
6509    let raw_bigint_idx = ctx
6510        .symbols
6511        .raw_bigint_type_idx()
6512        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
6513    let Some(lhs_limbs) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::Ref(
6514        wasm_encoder::RefType {
6515            nullable: false,
6516            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
6517        },
6518    ))) else {
6519        return Ok(());
6520    };
6521    let Some(lhs_sign) = ctx.latch(emitter.add_anonymous_local(wasm_encoder::ValType::I32)) else {
6522        return Ok(());
6523    };
6524    emitter.instruction(Instruction::LocalSet(lhs_limbs));
6525    emitter.instruction(Instruction::LocalSet(lhs_sign));
6526    emitter.instruction(Instruction::LocalGet(lhs_sign));
6527    emitter.instruction(Instruction::LocalGet(lhs_limbs));
6528    if ctx.latch(emit_bigint_literal(emitter, ctx, "1")).is_none() {
6529        return Ok(());
6530    }
6531    emit_bigint_extract_to_stack(emitter, ctx);
6532    let op_name = match op {
6533        crate::PostfixOp::Inc => "add",
6534        crate::PostfixOp::Dec => "sub",
6535        crate::PostfixOp::NonNullAssert => {
6536            return Err(crate::codegen::internal_failure(
6537                "non-null assertion is not PostfixUnary",
6538            ));
6539        }
6540    };
6541    let host_idx = ctx
6542        .symbols
6543        .func_idx(&crate::mangle::host(
6544            crate::runtime::BIGINT_MODULE_NAME,
6545            op_name,
6546        ))
6547        .ok_or_else(|| {
6548            crate::codegen::internal_failure(format!(
6549                "submilli:bigint.{op_name} import recorded by codegen bootstrap"
6550            ))
6551        })?;
6552    emit_bigint_wrap_host_result(emitter, ctx, host_idx);
6553
6554    Ok(())
6555}
6556
6557/// inline `cmp` + signed compare against 0 for
6558/// `<` / `>` / `<=` / `>=`.
6559fn emit_bigint_cmp_inline(
6560    emitter: &mut FunctionEmitter,
6561    ctx: &CodegenCtx,
6562    lhs: ExprId,
6563    rhs: ExprId,
6564    op: BinOp,
6565) -> Result<(), crate::compiler_error::CompilerFailure> {
6566    emit_bigint_cmp_call(emitter, ctx, lhs, rhs)?;
6567    emitter.instruction(Instruction::I32Const(0));
6568    let inst = match op {
6569        BinOp::Lt => Instruction::I32LtS,
6570        BinOp::Gt => Instruction::I32GtS,
6571        BinOp::Le => Instruction::I32LeS,
6572        BinOp::Ge => Instruction::I32GeS,
6573        _ => {
6574            return Err(crate::codegen::internal_failure(
6575                "emit_bigint_cmp_inline called with non-cmp op",
6576            ));
6577        }
6578    };
6579    emitter.instruction(inst);
6580    Ok(())
6581}
6582
6583/// `===` / `!==` on bigint — equal iff `cmp == 0`.
6584fn emit_bigint_cmp_eq_inline(
6585    emitter: &mut FunctionEmitter,
6586    ctx: &CodegenCtx,
6587    lhs: ExprId,
6588    rhs: ExprId,
6589    op: BinOp,
6590) -> Result<(), crate::compiler_error::CompilerFailure> {
6591    emit_bigint_cmp_call(emitter, ctx, lhs, rhs)?;
6592    emitter.instruction(Instruction::I32Eqz);
6593    let _: () = if matches!(op, BinOp::NotEq) {
6594        emitter.instruction(Instruction::I32Eqz);
6595    };
6596    Ok(())
6597}
6598
6599/// shared "two bigint operands → submilli:bigint.cmp"
6600/// setup. Stack at exit: `i32` (the raw cmp result, -1/0/1).
6601fn emit_bigint_cmp_call(
6602    emitter: &mut FunctionEmitter,
6603    ctx: &CodegenCtx,
6604    lhs: ExprId,
6605    rhs: ExprId,
6606) -> Result<(), crate::compiler_error::CompilerFailure> {
6607    emit_expr(emitter, ctx, lhs)?;
6608    emit_bigint_extract_to_stack(emitter, ctx);
6609    let raw_bigint_idx = ctx
6610        .symbols
6611        .raw_bigint_type_idx()
6612        .ok_or_else(|| crate::codegen::internal_failure("$rawBigInt registered"))?;
6613    let lhs_limbs =
6614        emitter.add_anonymous_local(wasm_encoder::ValType::Ref(wasm_encoder::RefType {
6615            nullable: false,
6616            heap_type: wasm_encoder::HeapType::Concrete(raw_bigint_idx),
6617        }))?;
6618    let lhs_sign = emitter.add_anonymous_local(wasm_encoder::ValType::I32)?;
6619    emitter.instruction(Instruction::LocalSet(lhs_limbs));
6620    emitter.instruction(Instruction::LocalSet(lhs_sign));
6621    emitter.instruction(Instruction::LocalGet(lhs_sign));
6622    emitter.instruction(Instruction::LocalGet(lhs_limbs));
6623    emit_expr(emitter, ctx, rhs)?;
6624    emit_bigint_extract_to_stack(emitter, ctx);
6625    let cmp_idx = ctx
6626        .symbols
6627        .func_idx(&crate::mangle::host(
6628            crate::runtime::BIGINT_MODULE_NAME,
6629            "cmp",
6630        ))
6631        .ok_or_else(|| {
6632            crate::codegen::internal_failure(
6633                "submilli:bigint.cmp import recorded by codegen bootstrap",
6634            )
6635        })?;
6636    emitter.instruction(Instruction::Call(cmp_idx));
6637    Ok(())
6638}
6639
6640/// Evaluate plain literal values once, including overwritten expressions.
6641fn evaluate_object_members(
6642    emitter: &mut FunctionEmitter<'_>,
6643    ctx: &CodegenCtx<'_>,
6644    members: &[TypedObjectMember],
6645) -> Result<HashMap<ExprId, u32>, crate::compiler_error::CompilerFailure> {
6646    let intrinsics = ctx
6647        .symbols
6648        .intrinsic_type_indices()
6649        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared"))?;
6650    let mut values = HashMap::new();
6651    for member in members {
6652        let TypedObjectMember::Value(value_id) = *member else {
6653            return Err(crate::codegen::internal_failure(
6654                "spread field reached plain object lowering",
6655            ));
6656        };
6657        let value_ty = &ctx
6658            .ta
6659            .try_expr(value_id)
6660            .map_err(crate::codegen::arena_failure)?
6661            .ty;
6662        emit_expr(emitter, ctx, value_id)?;
6663        cast::emit_box(emitter, ctx, value_ty)?;
6664        let local = emitter.add_anonymous_local(object_ref(intrinsics.object))?;
6665        emitter.instruction(Instruction::LocalSet(local));
6666        values.insert(value_id, local);
6667    }
6668    Ok(values)
6669}
6670
6671/// Pushes whether the field at `index`, whose slot holds `value`, is present
6672/// with a value of `field_ty`, using the same presence flag as `in`.
6673fn emit_field_holds_value_of(
6674    emitter: &mut FunctionEmitter<'_>,
6675    ctx: &CodegenCtx<'_>,
6676    object: u32,
6677    index: u32,
6678    value: u32,
6679    field_ty: &Type,
6680) -> Result<(), crate::compiler_error::CompilerFailure> {
6681    let intrinsics = ctx
6682        .symbols
6683        .intrinsic_type_indices()
6684        .ok_or_else(|| crate::codegen::internal_failure("intrinsics declared by codegen entry"))?;
6685    emitter.instruction(Instruction::LocalGet(object));
6686    emitter.instruction(Instruction::StructGet {
6687        struct_type_index: intrinsics.object_shape,
6688        field_index: 1,
6689    });
6690    emitter.instruction(Instruction::LocalGet(index));
6691    emitter.instruction(Instruction::ArrayGet(intrinsics.field_names));
6692    crate::codegen::field_names::emit_name_presence(emitter, ctx)?;
6693    emitter.instruction(Instruction::LocalGet(value));
6694    emit_is_nullish(emitter, ctx)?;
6695    emitter.instruction(Instruction::I32Eqz);
6696    emitter.instruction(Instruction::I32Or);
6697    let _: () = if field_runtime_type_is_testable(field_ty) {
6698        emit_structural_test(emitter, ctx, value, field_ty, field_ty)?;
6699        emitter.instruction(Instruction::I32And);
6700    };
6701    Ok(())
6702}
6703
6704fn object_ref(object_type: u32) -> ValType {
6705    ValType::Ref(RefType {
6706        nullable: true,
6707        heap_type: HeapType::Concrete(object_type),
6708    })
6709}
6710
6711/// Array indices use JavaScript's string-hint property-key conversion.
6712pub(super) fn emit_index_number(
6713    emitter: &mut FunctionEmitter,
6714    ctx: &CodegenCtx,
6715    ty: &Type,
6716) -> Result<(), crate::compiler_error::CompilerFailure> {
6717    if ty == &Type::Unknown {
6718        let function = ctx
6719            .symbols
6720            .prelude_func_idx("__value_to_index")
6721            .ok_or_else(|| crate::codegen::internal_failure("dynamic index helper collected"))?;
6722        emitter.instruction(Instruction::Call(function));
6723    } else {
6724        cast::emit_coerce_to_slot(emitter, ctx, ty, &Type::Number)?;
6725    };
6726    Ok(())
6727}
6728
6729fn emit_computed_object(
6730    emitter: &mut FunctionEmitter,
6731    ctx: &CodegenCtx,
6732    members: &[TypedObjectMember],
6733) -> Result<(), crate::compiler_error::CompilerFailure> {
6734    let Some(intr) = ctx.require(ctx.symbols.intrinsic_type_indices(), "intrinsics declared")
6735    else {
6736        return Ok(());
6737    };
6738    let Some(merge) = ctx.require(
6739        ctx.symbols.prelude_func_idx("ObjectConstructor##spread"),
6740        "spread helper imported",
6741    ) else {
6742        return Ok(());
6743    };
6744    let object = emitter.add_anonymous_local(ValType::Ref(RefType {
6745        nullable: false,
6746        heap_type: HeapType::Concrete(intr.object_shape),
6747    }))?;
6748    for _ in 0..4 {
6749        emitter.instruction(Instruction::RefNull(HeapType::Concrete(intr.object)));
6750    }
6751    emitter.instruction(Instruction::Call(merge));
6752    emitter.instruction(Instruction::LocalSet(object));
6753    for member in members {
6754        emitter.instruction(Instruction::LocalGet(object));
6755        match member {
6756            TypedObjectMember::Computed { key, value } => {
6757                emit_expr(emitter, ctx, *key)?;
6758                crate::codegen::cast_check::emit_operation_cast_on_stack(
6759                    emitter,
6760                    ctx,
6761                    &ctx.ta
6762                        .try_expr(*key)
6763                        .map_err(crate::codegen::arena_failure)?
6764                        .ty,
6765                    &Type::String,
6766                )?;
6767                emit_expr(emitter, ctx, *value)?;
6768                cast::emit_box(
6769                    emitter,
6770                    ctx,
6771                    &ctx.ta
6772                        .try_expr(*value)
6773                        .map_err(crate::codegen::arena_failure)?
6774                        .ty,
6775                )?;
6776                let Some(symbol) = ctx.require(
6777                    ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
6778                    "dynamic write imported",
6779                ) else {
6780                    return Ok(());
6781                };
6782                emitter.instruction(Instruction::Call(symbol));
6783            }
6784            TypedObjectMember::Spread { source, .. } => {
6785                emit_expr(emitter, ctx, *source)?;
6786                for _ in 0..2 {
6787                    emitter.instruction(Instruction::RefNull(HeapType::Concrete(intr.object)));
6788                }
6789                emitter.instruction(Instruction::Call(merge));
6790                emitter.instruction(Instruction::LocalSet(object));
6791            }
6792            TypedObjectMember::Value(_) => {
6793                ctx.fail("computed literal contains an unlowered named field");
6794                return Ok(());
6795            }
6796        }
6797    }
6798    emitter.instruction(Instruction::LocalGet(object));
6799    Ok(())
6800}
6801
6802fn emit_object_index_postfix(
6803    emitter: &mut FunctionEmitter,
6804    ctx: &CodegenCtx,
6805    receiver: ExprId,
6806    index: ExprId,
6807    op: crate::PostfixOp,
6808    read_ty: &Type,
6809) -> Result<(), crate::compiler_error::CompilerFailure> {
6810    emit_expr(emitter, ctx, receiver)?;
6811    cast::emit_box(
6812        emitter,
6813        ctx,
6814        &ctx.ta
6815            .try_expr(receiver)
6816            .map_err(crate::codegen::arena_failure)?
6817            .ty,
6818    )?;
6819    let object = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6820    emitter.instruction(Instruction::LocalSet(object));
6821    emit_expr(emitter, ctx, index)?;
6822    crate::codegen::cast_check::emit_operation_cast_on_stack(
6823        emitter,
6824        ctx,
6825        &ctx.ta
6826            .try_expr(index)
6827            .map_err(crate::codegen::arena_failure)?
6828            .ty,
6829        &Type::String,
6830    )?;
6831    let key = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::String)?)?;
6832    emitter.instruction(Instruction::LocalSet(key));
6833    emitter.instruction(Instruction::LocalGet(object));
6834    emitter.instruction(Instruction::LocalGet(key));
6835    let Some(symbol) = ctx.require(
6836        ctx.symbols.prelude_func_idx("ObjectConstructor##getField"),
6837        "record read imported",
6838    ) else {
6839        return Ok(());
6840    };
6841    emitter.instruction(Instruction::Call(symbol));
6842    crate::codegen::cast_check::emit_operation_cast_on_stack(
6843        emitter,
6844        ctx,
6845        &Type::Unknown,
6846        read_ty,
6847    )?;
6848    cast::emit_box(emitter, ctx, read_ty)?;
6849    emit_postfix_numeric(emitter, ctx, &Type::Unknown);
6850    let old = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6851    emitter.instruction(Instruction::LocalTee(old));
6852    emit_postfix_delta(emitter, ctx, op, &Type::Unknown);
6853    let updated = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
6854    emitter.instruction(Instruction::LocalSet(updated));
6855    emitter.instruction(Instruction::LocalGet(object));
6856    emitter.instruction(Instruction::LocalGet(key));
6857    emitter.instruction(Instruction::LocalGet(updated));
6858    let Some(symbol) = ctx.require(
6859        ctx.symbols.prelude_func_idx("ObjectConstructor##setField"),
6860        "record write imported",
6861    ) else {
6862        return Ok(());
6863    };
6864    emitter.instruction(Instruction::Call(symbol));
6865    emitter.instruction(Instruction::LocalGet(old));
6866    Ok(())
6867}
6868
6869#[cfg(test)]
6870mod tests {
6871    use super::*;
6872    use crate::codegen::invariant_tests::{assert_internal, with_context};
6873    use crate::{FileId, Span, TypedAst, TypedExpr};
6874
6875    #[test]
6876    fn missing_method_dispatch_is_a_compiler_error() {
6877        with_context(
6878            &TypedAst::new(),
6879            &crate::codegen::SymbolTable::default(),
6880            |ctx| {
6881                let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
6882                let error = emit_method_call_with_receiver_on_stack(
6883                    &mut emitter,
6884                    ctx,
6885                    &Type::Number,
6886                    &crate::mangle::prelude("MissingInterface"),
6887                    "missingMethod",
6888                    &[],
6889                    None,
6890                    None,
6891                    &Type::Number,
6892                )
6893                .unwrap_err();
6894                assert!(error.to_string().contains("MissingInterface.missingMethod"));
6895                assert_internal(error);
6896            },
6897        );
6898    }
6899
6900    #[test]
6901    fn call_and_chain_metadata_are_checked_before_operands() {
6902        with_context(
6903            &TypedAst::new(),
6904            &crate::codegen::SymbolTable::default(),
6905            |ctx| {
6906                let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
6907                assert_internal(
6908                    emit_intrinsic_call(&mut emitter, ctx, Intrinsic::Assert, &[], &Type::Void)
6909                        .unwrap_err(),
6910                );
6911                assert_internal(
6912                    emit_intrinsic_call(
6913                        &mut emitter,
6914                        ctx,
6915                        Intrinsic::BigIntFromString,
6916                        &[],
6917                        &Type::BigInt,
6918                    )
6919                    .unwrap_err(),
6920                );
6921                assert_internal(
6922                    emit_direct_call(
6923                        &mut emitter,
6924                        ctx,
6925                        false,
6926                        0,
6927                        &[Type::Number],
6928                        &Type::Void,
6929                        &[],
6930                    )
6931                    .unwrap_err(),
6932                );
6933                assert_internal(
6934                    emit_args_into_slots(&mut emitter, ctx, &[], Some(&[ValType::F64]))
6935                        .unwrap_err(),
6936                );
6937                assert_internal(
6938                    emit_chain_parts(
6939                        &mut emitter,
6940                        ctx,
6941                        &[],
6942                        1,
6943                        &Type::Number,
6944                        &Type::Number,
6945                        None,
6946                    )
6947                    .unwrap_err(),
6948                );
6949                assert_internal(
6950                    emit_chain_parts(
6951                        &mut emitter,
6952                        ctx,
6953                        &[],
6954                        0,
6955                        &Type::Number,
6956                        &Type::Number,
6957                        Some(&[]),
6958                    )
6959                    .unwrap_err(),
6960                );
6961            },
6962        );
6963    }
6964
6965    #[test]
6966    fn uninterned_literal_fails_and_a_fresh_emitter_still_works() {
6967        let mut ta = TypedAst::new();
6968        let string = ta
6969            .try_push_expr(TypedExpr {
6970                kind: TypedExprKind::String("missing".into()),
6971                ty: Type::String,
6972                span: Span::at(FileId(0)),
6973            })
6974            .unwrap();
6975        let number = ta
6976            .try_push_expr(TypedExpr {
6977                kind: TypedExprKind::Number(3.0),
6978                ty: Type::Number,
6979                span: Span::at(FileId(0)),
6980            })
6981            .unwrap();
6982        with_context(&ta, &crate::codegen::SymbolTable::default(), |ctx| {
6983            let mut failed = FunctionEmitter::new(ctx, &[]).unwrap();
6984            assert_internal(emit_expr(&mut failed, ctx, string).unwrap_err());
6985            drop(failed);
6986            let mut healthy = FunctionEmitter::new(ctx, &[]).unwrap();
6987            emit_expr(&mut healthy, ctx, number).unwrap();
6988            healthy.instruction(Instruction::Drop);
6989            healthy.build().unwrap();
6990        });
6991    }
6992}