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