Skip to main content

submilli_engine/codegen/
user_subtypes.rs

1//! Per-user-subtype Wasm subtype, vtable, and method emission for structural object shapes.
2
3use std::collections::BTreeSet;
4
5use wasm_encoder::{
6    BlockType, ConstExpr, Function, GlobalSection, GlobalType, HeapType, Instruction, RefType,
7    ValType,
8};
9
10use crate::codegen::intrinsics::IntrinsicTypeIndices;
11use crate::codegen::symbol_table::SymbolTable;
12use crate::codegen::{GuardedBodies, internal_failure, next_index, wasm_u32};
13use crate::compiler_error::CompilerFailure;
14use crate::{ObjectField, Shape, Type, TypeInfoIndex, TypeInfoTable};
15
16pub fn collect_object_shapes<'a>(
17    dependency_shapes: impl IntoIterator<Item = &'a Shape>,
18    own_shapes: &[Shape],
19) -> Vec<Type> {
20    // Dependency entries are walked first so they win the dedup race against own-side duplicates.
21    let mut entries: BTreeSet<Type> = BTreeSet::new();
22    for shape in dependency_shapes {
23        entries.insert(canonical_type(shape));
24    }
25    for shape in own_shapes {
26        entries.insert(canonical_type(shape));
27    }
28
29    // Topo sort: each Shape's deps (Object → field types; Array →
30    // element type) must precede it. Post-order DFS gives us that.
31    let mut sorted: Vec<Type> = Vec::new();
32    let mut visited: BTreeSet<Type> = BTreeSet::new();
33    let keys: Vec<Type> = entries.iter().cloned().collect();
34    for ty in &keys {
35        topo_visit_kind(ty, &entries, &mut visited, &mut sorted);
36    }
37    sorted
38        .into_iter()
39        .filter(|ty| matches!(ty, Type::Object { .. }))
40        .collect()
41}
42
43fn canonical_type(shape: &Shape) -> Type {
44    match shape {
45        Shape::Object { fields, index } => Type::Object {
46            index: index.clone(),
47            fields: fields.clone(),
48        },
49        Shape::Array(elem) => Type::Array(elem.clone()),
50        Shape::Tuple(elements) => Type::Tuple(elements.clone()),
51        // No discriminator struct — runtime distinguishes union members via subtype identity.
52        Shape::Union(members) => Type::Union(members.clone()),
53    }
54}
55
56fn topo_visit_kind(
57    ty: &Type,
58    entries: &BTreeSet<Type>,
59    visited: &mut BTreeSet<Type>,
60    sorted: &mut Vec<Type>,
61) {
62    if visited.contains(ty) {
63        return;
64    }
65    let Some(ty) = entries.get(ty) else {
66        // Primitive types (Number, String, etc.) have no entry; value_type lowers them directly.
67        return;
68    };
69    visited.insert(ty.clone());
70    // Peel so aliased child types (`type Inner = { … }`) hit the entries map lookup.
71    match ty {
72        Type::Object { fields, .. } => {
73            for inner in fields.values() {
74                topo_visit_kind(inner.ty.peel(), entries, visited, sorted);
75            }
76        }
77        Type::Array(elem) => {
78            topo_visit_kind(elem.peel(), entries, visited, sorted);
79        }
80        Type::Tuple(elements) => {
81            for inner in elements {
82                topo_visit_kind(inner.peel(), entries, visited, sorted);
83            }
84        }
85        Type::Union(members) => {
86            for m in members {
87                topo_visit_kind(m.peel(), entries, visited, sorted);
88            }
89        }
90        _ => {}
91    }
92    sorted.push(ty.clone());
93}
94
95/// Emitted artifacts for one user subtype. The Wasm struct type is shared by arity;
96/// the vtable global is shape-specialized and carries the shape-specific behavior.
97#[derive(Clone, Debug)]
98pub struct UserSubtype {
99    pub ty: Type,
100    pub to_string_func: u32,
101    pub to_json_func: u32,
102    pub equals_func: u32,
103    pub hash_func: u32,
104    pub guarded_bodies: GuardedBodies,
105    pub vtable_global_idx: u32,
106}
107
108impl UserSubtype {
109    fn fields(&self) -> Result<&std::collections::BTreeMap<String, ObjectField>, CompilerFailure> {
110        match &self.ty {
111            Type::Object { fields, .. } => Ok(fields),
112            _ => Err(internal_failure(
113                "a structural subtype was allocated for a non-object type",
114            )),
115        }
116    }
117}
118
119/// Pre-allocate method indices so vtable globals can reference them via `ref.func`.
120pub fn allocate_methods(
121    ty: &Type,
122    next_func_idx: &mut u32,
123) -> Result<UserSubtype, CompilerFailure> {
124    Ok(UserSubtype {
125        ty: ty.clone(),
126        to_string_func: next_index(next_func_idx)?,
127        to_json_func: next_index(next_func_idx)?,
128        equals_func: next_index(next_func_idx)?,
129        hash_func: next_index(next_func_idx)?,
130        // Field order is allocation order, which the body emission follows.
131        guarded_bodies: GuardedBodies {
132            to_json: next_index(next_func_idx)?,
133            equals: next_index(next_func_idx)?,
134            hash: next_index(next_func_idx)?,
135        },
136        vtable_global_idx: 0, // filled in by emit_vtable_globals
137    })
138}
139
140pub fn needs_host_object_to_json_adapter(types: &[Type]) -> bool {
141    types.iter().any(|ty| match ty.peel() {
142        Type::Object { fields, .. } => !fields.contains_key("toJson"),
143        _ => false,
144    })
145}
146
147/// Must be called after all other function section entries so the pre-allocated indices match.
148pub fn emit_method_function_entries(
149    functions: &mut wasm_encoder::FunctionSection,
150    subtypes: &[UserSubtype],
151    intrinsics: IntrinsicTypeIndices,
152) {
153    for _ in subtypes {
154        functions.function(intrinsics.to_string_fn);
155        functions.function(intrinsics.to_json_fn);
156        functions.function(intrinsics.equals_fn);
157        functions.function(intrinsics.hash_fn);
158        functions.function(intrinsics.to_json_fn);
159        functions.function(intrinsics.equals_fn);
160        functions.function(intrinsics.hash_fn);
161    }
162}
163
164pub fn emit_vtable_globals(
165    globals: &mut GlobalSection,
166    subtypes: &mut [UserSubtype],
167    symbols: &mut SymbolTable,
168    next_global_idx: &mut u32,
169    intrinsics: IntrinsicTypeIndices,
170) -> Result<(), CompilerFailure> {
171    for subtype in subtypes.iter_mut() {
172        let init = ConstExpr::extended([
173            Instruction::RefFunc(subtype.to_string_func),
174            Instruction::RefFunc(subtype.to_json_func),
175            Instruction::RefFunc(subtype.equals_func),
176            Instruction::RefFunc(subtype.hash_func),
177            Instruction::StructNew(intrinsics.vtable),
178        ]);
179        globals.global(
180            GlobalType {
181                val_type: ref_to(intrinsics.vtable),
182                mutable: false,
183                shared: false,
184            },
185            &init,
186        );
187        let idx = next_index(next_global_idx)?;
188        subtype.vtable_global_idx = idx;
189        symbols.record_vtable_global(subtype.ty.clone(), idx);
190    }
191    Ok(())
192}
193
194pub fn emit_method_bodies(
195    code: &mut wasm_encoder::CodeSection,
196    subtypes: &[UserSubtype],
197    symbols: &SymbolTable,
198    type_info: &TypeInfoTable,
199    type_info_index: &TypeInfoIndex,
200    pkg_string_global_idx: Option<u32>,
201) -> Result<(), crate::compiler_error::CompilerFailure> {
202    let intrinsics = symbols
203        .intrinsic_type_indices()
204        .ok_or_else(|| internal_failure("intrinsic types are not declared"))?;
205    let string_vtable_global_idx = symbols
206        .prelude_global_idx("string_vtable")
207        .ok_or_else(|| internal_failure("string_vtable is not imported from the prelude"))?;
208    let string_concat_func_idx = symbols
209        .prelude_func_idx("string_concat")
210        .ok_or_else(|| internal_failure("string_concat is not imported from the prelude"))?;
211    let string_eq_func_idx = symbols
212        .prelude_func_idx("string_eq")
213        .ok_or_else(|| internal_failure("string_eq is not imported from the prelude"))?;
214
215    let object_vtable_global = symbols
216        .prelude_global_idx("object_vtable")
217        .ok_or_else(|| internal_failure("object_vtable is not imported from the prelude"))?;
218    for subtype in subtypes {
219        code.function(&emit_subtype_to_string_body(
220            subtype,
221            intrinsics,
222            symbols,
223            string_vtable_global_idx,
224            object_vtable_global,
225        )?);
226
227        let bodies = subtype.guarded_bodies;
228        for (body, params, result) in [
229            (bodies.to_json, 1, ref_to(intrinsics.string)),
230            (bodies.equals, 2, ValType::I32),
231            (bodies.hash, 1, ValType::I32),
232        ] {
233            code.function(&super::vtable_walk::guarded_body(
234                body, params, result, symbols,
235            )?);
236        }
237        // The host serializes an object whose shape has type info; others walk
238        // their vtable.
239        let host_json_type = type_info_index
240            .object_type_id(type_info, &subtype.ty)
241            .filter(|_| type_info.supports_host_json_object(&subtype.ty));
242        code.function(&emit_subtype_to_json_body(
243            subtype,
244            intrinsics,
245            symbols,
246            host_json_type,
247            pkg_string_global_idx,
248            string_concat_func_idx,
249            string_vtable_global_idx,
250        )?);
251
252        code.function(&emit_subtype_equals_body(
253            subtype,
254            intrinsics,
255            symbols,
256            string_eq_func_idx,
257            object_vtable_global,
258        )?);
259
260        code.function(&emit_subtype_hash_body(
261            subtype,
262            intrinsics,
263            symbols,
264            object_vtable_global,
265        )?);
266    }
267    Ok(())
268}
269
270fn emit_subtype_to_string_body(
271    subtype: &UserSubtype,
272    intrinsics: IntrinsicTypeIndices,
273    symbols: &SymbolTable,
274    string_vtable_global_idx: u32,
275    object_vtable_global: u32,
276) -> Result<Function, CompilerFailure> {
277    let fields = subtype.fields()?;
278
279    if !fields.contains_key("toString") {
280        let mut f = Function::new(std::iter::empty());
281        emit_indexed_object_dispatch(
282            &mut f,
283            intrinsics,
284            object_vtable_global,
285            IndexedSlot::ToString,
286        );
287        crate::codegen::intrinsics::push_string_literal(
288            &mut f,
289            intrinsics,
290            string_vtable_global_idx,
291            "[object Object]",
292        )?;
293        f.instruction(&Instruction::End);
294        return Ok(f);
295    }
296
297    emit_conversion_override_body(subtype, intrinsics, symbols, ConversionSlot::ToString)
298}
299
300fn emit_subtype_to_json_body(
301    subtype: &UserSubtype,
302    intrinsics: IntrinsicTypeIndices,
303    symbols: &SymbolTable,
304    host_json_type: Option<crate::TypeInfoId>,
305    pkg_string_global_idx: Option<u32>,
306    string_concat_func_idx: u32,
307    string_vtable_global_idx: u32,
308) -> Result<Function, CompilerFailure> {
309    let fields = subtype.fields()?;
310
311    if fields.contains_key("toJson") {
312        return emit_conversion_override_body(subtype, intrinsics, symbols, ConversionSlot::ToJson);
313    }
314
315    let Some(type_id) = host_json_type else {
316        return emit_subtype_to_json_vtable_body(
317            subtype,
318            intrinsics,
319            string_concat_func_idx,
320            string_vtable_global_idx,
321            symbols,
322        );
323    };
324    let pkg_string_global_idx = pkg_string_global_idx
325        .ok_or_else(|| internal_failure("host object serialization needs the package string"))?;
326    let stringify_func_idx = symbols
327        .func_idx(&crate::mangle::host(
328            crate::runtime::JSON_MODULE_NAME,
329            "stringifyTypedObject",
330        ))
331        .ok_or_else(|| internal_failure("submilli:json.stringifyTypedObject is not imported"))?;
332
333    let mut f = Function::new(std::iter::empty());
334
335    f.instruction(&Instruction::GlobalGet(string_vtable_global_idx));
336
337    f.instruction(&Instruction::GlobalGet(pkg_string_global_idx));
338    f.instruction(&Instruction::RefAsNonNull);
339    f.instruction(&Instruction::StructGet {
340        struct_type_index: intrinsics.string,
341        field_index: 1,
342    });
343    f.instruction(&Instruction::I32Const(type_id.as_u32().cast_signed()));
344    f.instruction(&Instruction::LocalGet(0));
345    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
346        intrinsics.object_shape,
347    )));
348    f.instruction(&Instruction::Call(stringify_func_idx));
349    f.instruction(&Instruction::I64Const(0));
350    f.instruction(&Instruction::StructNew(intrinsics.string));
351    f.instruction(&Instruction::End);
352    Ok(f)
353}
354
355/// Static serializers fall back to the dynamic walker after shape growth.
356pub(super) fn emit_grown_object_to_json(
357    function: &mut Function,
358    intrinsics: IntrinsicTypeIndices,
359    symbols: &SymbolTable,
360    original_len: u32,
361) -> Result<(), CompilerFailure> {
362    let dynamic_serializer = symbols
363        .prelude_func_idx("ObjectConstructor##toJson")
364        .ok_or_else(|| internal_failure("the dynamic object serializer is not imported"))?;
365    function.instruction(&Instruction::LocalGet(0));
366    function.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
367        intrinsics.object_shape,
368    )));
369    function.instruction(&Instruction::StructGet {
370        struct_type_index: intrinsics.object_shape,
371        field_index: 1,
372    });
373    function.instruction(&Instruction::ArrayLen);
374    function.instruction(&Instruction::I32Const(original_len.cast_signed()));
375    function.instruction(&Instruction::I32GtU);
376    function.instruction(&Instruction::If(BlockType::Empty));
377    function.instruction(&Instruction::LocalGet(0));
378    function.instruction(&Instruction::Call(dynamic_serializer));
379    function.instruction(&Instruction::Return);
380    function.instruction(&Instruction::End);
381    Ok(())
382}
383
384fn emit_subtype_to_json_vtable_body(
385    subtype: &UserSubtype,
386    intrinsics: IntrinsicTypeIndices,
387    string_concat_func_idx: u32,
388    string_vtable_global_idx: u32,
389    symbols: &SymbolTable,
390) -> Result<Function, CompilerFailure> {
391    let fields = subtype.fields()?;
392
393    let object_shape_ref = ref_to(intrinsics.object_shape);
394    let string_ref = ref_to(intrinsics.string);
395    let object_null_ref = ref_null(intrinsics.object);
396    let to_json_fn_ref = ref_to(intrinsics.to_json_fn);
397
398    let locals: Vec<(u32, ValType)> = vec![
399        (1, object_shape_ref),
400        (1, string_ref),
401        (1, object_null_ref),
402        (1, to_json_fn_ref),
403        (1, ValType::I32), // Whether any serializable field has been emitted.
404    ];
405    let mut f = Function::new(locals);
406    emit_grown_object_to_json(&mut f, intrinsics, symbols, wasm_u32(fields.len())?)?;
407    let self_t = 1u32;
408    let acc = 2u32;
409    let elem = 3u32;
410    let tj_fn = 4u32;
411    let first_local = 5u32;
412
413    f.instruction(&Instruction::LocalGet(0));
414    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
415        intrinsics.object_shape,
416    )));
417    f.instruction(&Instruction::LocalSet(self_t));
418
419    if fields.is_empty() {
420        push_inline_string(
421            &mut f,
422            "{}",
423            intrinsics.string,
424            intrinsics.raw_string,
425            string_vtable_global_idx,
426        )?;
427        f.instruction(&Instruction::End);
428        return Ok(f);
429    }
430
431    push_inline_string(
432        &mut f,
433        "{",
434        intrinsics.string,
435        intrinsics.raw_string,
436        string_vtable_global_idx,
437    )?;
438    f.instruction(&Instruction::LocalSet(acc));
439
440    f.instruction(&Instruction::I32Const(1));
441    f.instruction(&Instruction::LocalSet(first_local));
442
443    for (idx, (field_name, field)) in fields.iter().enumerate() {
444        let idx = wasm_u32(idx)?;
445        let key_no_comma = format!("\"{}\":", json_escape_key(field_name));
446        let key_with_comma = format!(",\"{}\":", json_escape_key(field_name));
447
448        f.instruction(&Instruction::LocalGet(self_t));
449        f.instruction(&Instruction::StructGet {
450            struct_type_index: intrinsics.object_shape,
451            field_index: 2,
452        });
453        f.instruction(&Instruction::I32Const(idx.cast_signed()));
454        f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
455        f.instruction(&Instruction::LocalSet(elem));
456
457        for instruction in
458            super::nullish::is_undefined_or_closure(&[Instruction::LocalGet(elem)], intrinsics)
459        {
460            f.instruction(&instruction);
461        }
462        f.instruction(&Instruction::I32Eqz);
463        f.instruction(&Instruction::If(BlockType::Empty));
464
465        if field.optional {
466            super::field_names::emit_optional_presence(
467                &mut f,
468                intrinsics,
469                symbols.optional_field_name_type()?,
470                self_t,
471                idx,
472                elem,
473            );
474            f.instruction(&Instruction::If(BlockType::Empty));
475        }
476
477        f.instruction(&Instruction::LocalGet(acc));
478        f.instruction(&Instruction::LocalGet(first_local));
479        f.instruction(&Instruction::If(BlockType::Result(string_ref)));
480        push_inline_string(
481            &mut f,
482            &key_no_comma,
483            intrinsics.string,
484            intrinsics.raw_string,
485            string_vtable_global_idx,
486        )?;
487        f.instruction(&Instruction::Else);
488        push_inline_string(
489            &mut f,
490            &key_with_comma,
491            intrinsics.string,
492            intrinsics.raw_string,
493            string_vtable_global_idx,
494        )?;
495        f.instruction(&Instruction::End);
496        f.instruction(&Instruction::Call(string_concat_func_idx));
497        f.instruction(&Instruction::LocalSet(acc));
498
499        // Every field is null-checked: the shape's field type is the object
500        // literal's own, and a binding with a wider type (`{ v: number | null }`
501        // holding `{ v: 1 }`) can later store `null` in it.
502        f.instruction(&Instruction::LocalGet(acc));
503        f.instruction(&Instruction::LocalGet(elem));
504        f.instruction(&Instruction::RefIsNull);
505        f.instruction(&Instruction::If(BlockType::Result(string_ref)));
506        push_inline_string(
507            &mut f,
508            "null",
509            intrinsics.string,
510            intrinsics.raw_string,
511            string_vtable_global_idx,
512        )?;
513        f.instruction(&Instruction::Else);
514        emit_field_value_to_json(&mut f, elem, tj_fn, intrinsics);
515        f.instruction(&Instruction::End);
516        f.instruction(&Instruction::Call(string_concat_func_idx));
517        f.instruction(&Instruction::LocalSet(acc));
518
519        f.instruction(&Instruction::I32Const(0));
520        f.instruction(&Instruction::LocalSet(first_local));
521
522        if field.optional {
523            f.instruction(&Instruction::End);
524        }
525        f.instruction(&Instruction::End);
526    }
527
528    f.instruction(&Instruction::LocalGet(acc));
529    push_inline_string(
530        &mut f,
531        "}",
532        intrinsics.string,
533        intrinsics.raw_string,
534        string_vtable_global_idx,
535    )?;
536    f.instruction(&Instruction::Call(string_concat_func_idx));
537    f.instruction(&Instruction::End);
538    Ok(f)
539}
540
541/// The vtable slots a `toString` or `toJson` field overrides.
542#[derive(Clone, Copy)]
543enum ConversionSlot {
544    ToString,
545    ToJson,
546}
547
548impl ConversionSlot {
549    fn field_name(self) -> &'static str {
550        match self {
551            Self::ToString => "toString",
552            Self::ToJson => "toJson",
553        }
554    }
555
556    fn vtable_field(self) -> u32 {
557        match self {
558            Self::ToString => 0,
559            Self::ToJson => 1,
560        }
561    }
562
563    fn function_type(self, intrinsics: IntrinsicTypeIndices) -> u32 {
564        match self {
565            Self::ToString => intrinsics.to_string_fn,
566            Self::ToJson => intrinsics.to_json_fn,
567        }
568    }
569}
570
571/// A conversion slot backed by the shape's `toString`/`toJson` field. A
572/// `() => string` closure in the field is called directly. Anything else (an
573/// absent optional field, or a function that takes defaults or a rest
574/// parameter) goes to the host object vtable's slot, which calls the field
575/// with JavaScript's argument rules, or converts the object as if it had no
576/// such field.
577fn emit_conversion_override_body(
578    subtype: &UserSubtype,
579    intrinsics: IntrinsicTypeIndices,
580    symbols: &SymbolTable,
581    slot: ConversionSlot,
582) -> Result<Function, CompilerFailure> {
583    let object_vtable_global = symbols
584        .prelude_global_idx("object_vtable")
585        .ok_or_else(|| internal_failure("object_vtable is not imported from the prelude"))?;
586    let conversion_sig = crate::codegen::closures::ClosureSig {
587        arity: 0,
588        is_void: false,
589    };
590    let closure_struct_idx = symbols
591        .closure_struct_type_idx(conversion_sig)
592        .ok_or_else(|| {
593            internal_failure(
594                "closure struct for `() => string` registered when any shape declares a \
595                 conversion override (collect_from_dependencies walks every shape field type)",
596            )
597        })?;
598    let closure_func_type_idx = symbols
599        .closure_func_type_idx(conversion_sig)
600        .ok_or_else(|| {
601            internal_failure(
602                "closure funcref type for `() => string` registered alongside its struct",
603            )
604        })?;
605    let field_slot = field_index(&subtype.ty, slot.field_name())?
606        .ok_or_else(|| internal_failure("an object shape lost its conversion field slot"))?;
607
608    // Locals (after 1 param: self=0):
609    //   1: closure (ref $Closure_string)
610    //   2: env     (ref any)
611    let mut f = Function::new([
612        (1, ref_to(closure_struct_idx)),
613        (
614            1,
615            ValType::Ref(RefType {
616                nullable: false,
617                heap_type: HeapType::ANY,
618            }),
619        ),
620    ]);
621    let closure = 1u32;
622    let env = 2u32;
623
624    // The outer block yields the result string; the inner one is left, with
625    // the field value, for the host fallback below it.
626    f.instruction(&Instruction::Block(BlockType::Result(ref_to(
627        intrinsics.string,
628    ))));
629    f.instruction(&Instruction::Block(BlockType::Result(ValType::Ref(
630        RefType {
631            nullable: true,
632            heap_type: HeapType::Concrete(intrinsics.object),
633        },
634    ))));
635    f.instruction(&Instruction::LocalGet(0));
636    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
637        intrinsics.object_shape,
638    )));
639    f.instruction(&Instruction::StructGet {
640        struct_type_index: intrinsics.object_shape,
641        field_index: 2,
642    });
643    f.instruction(&Instruction::I32Const(field_slot.cast_signed()));
644    f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
645    f.instruction(&Instruction::BrOnCastFail {
646        relative_depth: 0,
647        from_ref_type: RefType {
648            nullable: true,
649            heap_type: HeapType::Concrete(intrinsics.object),
650        },
651        to_ref_type: RefType {
652            nullable: false,
653            heap_type: HeapType::Concrete(closure_struct_idx),
654        },
655    });
656    f.instruction(&Instruction::LocalSet(closure));
657    f.instruction(&Instruction::LocalGet(closure));
658    f.instruction(&Instruction::StructGet {
659        struct_type_index: closure_struct_idx,
660        field_index: 2,
661    });
662    // A function expression's `this` is the object, as for `x.toString()`.
663    if let Some(wrapper) = symbols.this_environment_type {
664        for instruction in super::this_binding::binding_instructions(wrapper, env, 0) {
665            f.instruction(&instruction);
666        }
667    }
668    f.instruction(&Instruction::LocalGet(closure));
669    f.instruction(&Instruction::StructGet {
670        struct_type_index: closure_struct_idx,
671        field_index: 1,
672    });
673    f.instruction(&Instruction::CallRef(closure_func_type_idx));
674    // The closure returns its result boxed as `(ref $Object)`; the slot
675    // returns `(ref $string)`.
676    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
677        intrinsics.string,
678    )));
679    f.instruction(&Instruction::Br(1));
680    f.instruction(&Instruction::End);
681    f.instruction(&Instruction::Drop);
682    f.instruction(&Instruction::LocalGet(0));
683    f.instruction(&Instruction::GlobalGet(object_vtable_global));
684    f.instruction(&Instruction::StructGet {
685        struct_type_index: intrinsics.vtable,
686        field_index: slot.vtable_field(),
687    });
688    f.instruction(&Instruction::CallRef(slot.function_type(intrinsics)));
689    f.instruction(&Instruction::End);
690    f.instruction(&Instruction::End);
691    Ok(f)
692}
693
694/// The universal vtable slots an indexed object dispatches, by slot index.
695#[derive(Clone, Copy)]
696enum IndexedSlot {
697    ToString = 0,
698    Equals = 2,
699    Hash = 3,
700}
701
702impl IndexedSlot {
703    /// How many objects the slot's function takes: `equals` compares two.
704    fn operands(self) -> u32 {
705        match self {
706            IndexedSlot::Equals => 2,
707            IndexedSlot::ToString | IndexedSlot::Hash => 1,
708        }
709    }
710}
711
712/// The index records whether insertion changed the shape. Such values use the
713/// same dynamic `toString`/equality/hash hooks as objects created by
714/// JSON.parse: an inserted `toString` entry is one the shape can't see.
715fn emit_indexed_object_dispatch(
716    body: &mut Function,
717    intrinsics: IntrinsicTypeIndices,
718    vtable_global: u32,
719    slot: IndexedSlot,
720) {
721    let operands = slot.operands();
722    for operand in 0..operands {
723        body.instruction(&Instruction::LocalGet(operand));
724        body.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
725            intrinsics.object_shape,
726        )));
727        body.instruction(&Instruction::StructGet {
728            struct_type_index: intrinsics.object_shape,
729            field_index: 3,
730        });
731        body.instruction(&Instruction::RefIsNull);
732        body.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
733        body.instruction(&Instruction::I32Const(0));
734        body.instruction(&Instruction::Else);
735        body.instruction(&Instruction::LocalGet(operand));
736        body.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
737            intrinsics.object_shape,
738        )));
739        body.instruction(&Instruction::StructGet {
740            struct_type_index: intrinsics.object_shape,
741            field_index: 3,
742        });
743        body.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
744            intrinsics.raw_index_array,
745        )));
746        body.instruction(&Instruction::I32Const(1));
747        body.instruction(&Instruction::ArrayGet(intrinsics.raw_index_array));
748        body.instruction(&Instruction::End);
749    }
750    if operands == 2 {
751        body.instruction(&Instruction::I32Or);
752    }
753    body.instruction(&Instruction::If(BlockType::Empty));
754    for operand in 0..operands {
755        body.instruction(&Instruction::LocalGet(operand));
756    }
757    body.instruction(&Instruction::GlobalGet(vtable_global));
758    body.instruction(&Instruction::StructGet {
759        struct_type_index: intrinsics.vtable,
760        field_index: slot as u32,
761    });
762    body.instruction(&Instruction::CallRef(match slot {
763        IndexedSlot::ToString => intrinsics.to_string_fn,
764        IndexedSlot::Equals => intrinsics.equals_fn,
765        IndexedSlot::Hash => intrinsics.hash_fn,
766    }));
767    body.instruction(&Instruction::Return);
768    body.instruction(&Instruction::End);
769}
770
771fn emit_subtype_equals_body(
772    subtype: &UserSubtype,
773    intrinsics: IntrinsicTypeIndices,
774    symbols: &SymbolTable,
775    string_eq_func_idx: u32,
776    object_vtable_global: u32,
777) -> Result<Function, CompilerFailure> {
778    let fields = subtype.fields()?;
779
780    let object_shape_ref = ref_to(intrinsics.object_shape);
781    let object_ref = ref_to(intrinsics.object);
782    let object_null_ref = ref_null(intrinsics.object);
783    let equals_fn_ref = ref_to(intrinsics.equals_fn);
784
785    // Locals layout (after 2 params):
786    //   2: $a_t            (ref $arity)
787    //   3: $b_t            (ref $arity)
788    //   4: $field_lhs      (ref $Object)
789    //   5: $eq_fn          (ref $equalsFn)
790    //   6: $field_lhs_null (ref null $Object)
791    //   7: $field_rhs_null (ref null $Object)
792    let locals: Vec<(u32, ValType)> = vec![
793        (2, object_shape_ref),
794        (1, object_ref),
795        (1, equals_fn_ref),
796        (2, object_null_ref),
797    ];
798    let mut f = Function::new(locals);
799    let (a, b) = (0u32, 1u32);
800    let (a_t, b_t) = (2u32, 3u32);
801    let field_lhs = 4u32;
802    let eq_fn = 5u32;
803    let field_lhs_null = 6u32;
804    let field_rhs_null = 7u32;
805
806    // Dispatched off a union value, `other` may not be a same-arity object; a
807    // mismatched runtime shape is unequal, not a cast trap.
808    emit_equals_type_guard(&mut f, intrinsics.object_shape);
809
810    // Nominal values (class instances, `$Error`) are `$ObjectShape` subtypes
811    // too, but never equal structural values — their own equals bodies guard
812    // by vtable identity, and this direction must agree. Rejecting them here
813    // also prevents an out-of-bounds payload read when the class payload is
814    // shorter than this shape.
815    f.instruction(&Instruction::LocalGet(1));
816    f.instruction(&Instruction::StructGet {
817        struct_type_index: intrinsics.object,
818        field_index: 0,
819    });
820    f.instruction(&Instruction::RefTestNonNull(HeapType::Concrete(
821        intrinsics.class_vtable,
822    )));
823    f.instruction(&Instruction::If(BlockType::Empty));
824    f.instruction(&Instruction::I32Const(0));
825    f.instruction(&Instruction::Return);
826    f.instruction(&Instruction::End);
827
828    f.instruction(&Instruction::LocalGet(a));
829    f.instruction(&Instruction::LocalGet(b));
830    f.instruction(&Instruction::RefEq);
831    f.instruction(&Instruction::If(BlockType::Empty));
832    f.instruction(&Instruction::I32Const(1));
833    f.instruction(&Instruction::Return);
834    f.instruction(&Instruction::End);
835
836    f.instruction(&Instruction::LocalGet(a));
837    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
838        intrinsics.object_shape,
839    )));
840    f.instruction(&Instruction::LocalSet(a_t));
841    f.instruction(&Instruction::LocalGet(b));
842    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
843        intrinsics.object_shape,
844    )));
845    f.instruction(&Instruction::LocalSet(b_t));
846
847    emit_indexed_object_dispatch(
848        &mut f,
849        intrinsics,
850        object_vtable_global,
851        IndexedSlot::Equals,
852    );
853
854    emit_shape_guard(
855        &mut f,
856        wasm_u32(fields.len())?,
857        intrinsics,
858        string_eq_func_idx,
859        (a_t, b_t),
860    );
861
862    for (slot, field) in fields.values().enumerate() {
863        let field_index = wasm_u32(slot)?;
864
865        for object in [a_t, b_t] {
866            emit_field_presence(
867                &mut f,
868                intrinsics,
869                symbols.optional_field_name_type()?,
870                object,
871                field_index,
872            );
873        }
874        f.instruction(&Instruction::I32Ne);
875        f.instruction(&Instruction::If(BlockType::Empty));
876        f.instruction(&Instruction::I32Const(0));
877        f.instruction(&Instruction::Return);
878        f.instruction(&Instruction::End);
879
880        emit_field_compare(
881            &mut f,
882            intrinsics.object_shape,
883            field_index,
884            &field.ty,
885            intrinsics,
886            (a_t, b_t, field_lhs, eq_fn, field_lhs_null, field_rhs_null),
887        )?;
888
889        f.instruction(&Instruction::I32Eqz);
890        f.instruction(&Instruction::If(BlockType::Empty));
891        f.instruction(&Instruction::I32Const(0));
892        f.instruction(&Instruction::Return);
893        f.instruction(&Instruction::End);
894    }
895
896    f.instruction(&Instruction::I32Const(1));
897    f.instruction(&Instruction::End);
898    Ok(f)
899}
900
901/// Presence belongs to the field-name marker, while a non-null payload also
902/// implies presence. The other operand may use an ordinary required name.
903fn emit_field_presence(
904    body: &mut Function,
905    intrinsics: IntrinsicTypeIndices,
906    optional_name_type: u32,
907    object: u32,
908    slot: u32,
909) {
910    let load_name = |body: &mut Function| {
911        body.instruction(&Instruction::LocalGet(object));
912        body.instruction(&Instruction::StructGet {
913            struct_type_index: intrinsics.object_shape,
914            field_index: 1,
915        });
916        body.instruction(&Instruction::I32Const(slot.cast_signed()));
917        body.instruction(&Instruction::ArrayGet(intrinsics.field_names));
918    };
919    load_name(body);
920    body.instruction(&Instruction::RefTestNonNull(HeapType::Concrete(
921        optional_name_type,
922    )));
923    body.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
924    load_name(body);
925    body.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
926        optional_name_type,
927    )));
928    body.instruction(&Instruction::StructGet {
929        struct_type_index: optional_name_type,
930        field_index: 3,
931    });
932    body.instruction(&Instruction::Else);
933    body.instruction(&Instruction::I32Const(1));
934    body.instruction(&Instruction::End);
935    let load_value = [
936        Instruction::LocalGet(object),
937        Instruction::StructGet {
938            struct_type_index: intrinsics.object_shape,
939            field_index: 2,
940        },
941        Instruction::I32Const(slot.cast_signed()),
942        Instruction::ArrayGet(intrinsics.object_fields),
943    ];
944    for instruction in super::nullish::is_nullish(&load_value, intrinsics) {
945        body.instruction(&instruction);
946    }
947    body.instruction(&Instruction::I32Eqz);
948    body.instruction(&Instruction::I32Or);
949}
950
951/// Compares one field slot of `a_t` and `b_t`, pushing `1` when equal.
952///
953/// Every field goes through the null-aware vtable dispatch rather than the
954/// shape's field type: the shape's type is the object literal's own, and a
955/// binding with a wider type (`{ v: number | null }` holding `{ v: 1 }`) can
956/// later store `null` or another type in the slot. The dynamic `Object#equals`
957/// hook compares the same way, so a grown object agrees with this body.
958fn emit_field_compare(
959    f: &mut Function,
960    object_shape_idx: u32,
961    field_index: u32,
962    field_ty: &Type,
963    intrinsics: IntrinsicTypeIndices,
964    locals: (u32, u32, u32, u32, u32, u32),
965) -> Result<(), CompilerFailure> {
966    let (a_t, b_t, field_lhs, eq_fn, field_lhs_null, field_rhs_null) = locals;
967    reject_unrepresentable_field(field_ty)?;
968    emit_nullable_field_compare(
969        f,
970        object_shape_idx,
971        field_index,
972        intrinsics,
973        (field_lhs, eq_fn, field_lhs_null, field_rhs_null, a_t, b_t),
974    );
975    Ok(())
976}
977
978/// FNV-1a-32 field-by-field hash. Must use same field order and dispatch as `equals`.
979fn emit_subtype_hash_body(
980    subtype: &UserSubtype,
981    intrinsics: IntrinsicTypeIndices,
982    symbols: &SymbolTable,
983    object_vtable_global: u32,
984) -> Result<Function, CompilerFailure> {
985    let fields = subtype.fields()?;
986
987    let object_shape_ref = ref_to(intrinsics.object_shape);
988    let object_ref = ref_to(intrinsics.object);
989    let object_null_ref = ref_null(intrinsics.object);
990    let hash_fn_ref = ref_to(intrinsics.hash_fn);
991
992    // Locals (after 1 param self=0):
993    //   1: self_t    (ref $arity)
994    //   2: hash      (i32) — running accumulator
995    //   3: field_obj (ref $Object)
996    //   4: hash_fn   (ref $hashFn)
997    //   5: f_null    (ref null $Object)
998    let locals: Vec<(u32, ValType)> = vec![
999        (1, object_shape_ref),
1000        (1, ValType::I32),
1001        (1, object_ref),
1002        (1, hash_fn_ref),
1003        (1, object_null_ref),
1004    ];
1005    let mut f = Function::new(locals);
1006    let self_param = 0u32;
1007    let self_t = 1u32;
1008    let hash = 2u32;
1009    let field_obj = 3u32;
1010    let hash_fn = 4u32;
1011    let f_null = 5u32;
1012    f.instruction(&Instruction::LocalGet(self_param));
1013    f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(
1014        intrinsics.object_shape,
1015    )));
1016    f.instruction(&Instruction::LocalSet(self_t));
1017
1018    emit_indexed_object_dispatch(&mut f, intrinsics, object_vtable_global, IndexedSlot::Hash);
1019
1020    // hash = FNV-1a offset basis
1021    f.instruction(&Instruction::I32Const(0x811c9dc5u32.cast_signed()));
1022    f.instruction(&Instruction::LocalSet(hash));
1023
1024    for (slot, (name, field)) in fields.iter().enumerate() {
1025        let field_index = wasm_u32(slot)?;
1026        if field.optional {
1027            f.instruction(&Instruction::LocalGet(self_t));
1028            f.instruction(&Instruction::StructGet {
1029                struct_type_index: intrinsics.object_shape,
1030                field_index: 2,
1031            });
1032            f.instruction(&Instruction::I32Const(field_index.cast_signed()));
1033            f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
1034            f.instruction(&Instruction::LocalSet(f_null));
1035            super::field_names::emit_optional_presence(
1036                &mut f,
1037                intrinsics,
1038                symbols.optional_field_name_type()?,
1039                self_t,
1040                field_index,
1041                f_null,
1042            );
1043            f.instruction(&Instruction::If(BlockType::Empty));
1044        }
1045        emit_field_hash(
1046            &mut f,
1047            intrinsics.object_shape,
1048            field_index,
1049            &field.ty,
1050            intrinsics,
1051            (self_t, field_obj, hash_fn, f_null),
1052        )?;
1053        // Match the dynamic hook's order-independent name/value combination.
1054        let name_hash = crate::runtime::prelude::vtable::hash_utf16_units(
1055            crate::literal_units::literal_units(name),
1056        );
1057        f.instruction(&Instruction::I32Const(13));
1058        f.instruction(&Instruction::I32Rotl);
1059        f.instruction(&Instruction::I32Const(name_hash.cast_signed()));
1060        f.instruction(&Instruction::I32Xor);
1061        f.instruction(&Instruction::LocalGet(hash));
1062        f.instruction(&Instruction::I32Add);
1063        f.instruction(&Instruction::LocalSet(hash));
1064        if field.optional {
1065            f.instruction(&Instruction::End);
1066        }
1067    }
1068
1069    f.instruction(&Instruction::LocalGet(hash));
1070    f.instruction(&Instruction::End);
1071    Ok(f)
1072}
1073
1074/// Hashes one field slot of `self_t`, by the same dispatch as
1075/// [`emit_field_compare`], so equal objects hash alike.
1076fn emit_field_hash(
1077    f: &mut Function,
1078    object_shape_idx: u32,
1079    field_index: u32,
1080    field_ty: &Type,
1081    intrinsics: IntrinsicTypeIndices,
1082    locals: (u32, u32, u32, u32),
1083) -> Result<(), CompilerFailure> {
1084    reject_unrepresentable_field(field_ty)?;
1085    emit_nullable_field_hash(f, object_shape_idx, field_index, intrinsics, locals);
1086    Ok(())
1087}
1088
1089/// Peeled, typechecked object fields never have these types; one reaching
1090/// codegen means an earlier phase broke that invariant.
1091fn reject_unrepresentable_field(field_ty: &Type) -> Result<(), CompilerFailure> {
1092    let field_ty = field_ty.peel();
1093    match field_ty {
1094        Type::Error | Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => {
1095            Err(internal_failure(format!(
1096                "an object field of type `{field_ty}` reached structural equality or hashing"
1097            )))
1098        }
1099        _ => Ok(()),
1100    }
1101}
1102
1103/// Null-aware vtable-hash dispatch for one field. Pushes a single `i32`:
1104/// `0` if the slot is null, otherwise the value's `vtable.hash(value)`.
1105fn emit_nullable_field_hash(
1106    f: &mut Function,
1107    object_shape_idx: u32,
1108    field_index: u32,
1109    intrinsics: IntrinsicTypeIndices,
1110    locals: (u32, u32, u32, u32),
1111) {
1112    let (self_t, field_obj, hash_fn, f_null) = locals;
1113    f.instruction(&Instruction::LocalGet(self_t));
1114    f.instruction(&Instruction::StructGet {
1115        struct_type_index: object_shape_idx,
1116        field_index: 2,
1117    });
1118    f.instruction(&Instruction::I32Const(field_index.cast_signed()));
1119    f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
1120    f.instruction(&Instruction::LocalSet(f_null));
1121
1122    f.instruction(&Instruction::LocalGet(f_null));
1123    f.instruction(&Instruction::RefIsNull);
1124    f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
1125    f.instruction(&Instruction::I32Const(0));
1126    f.instruction(&Instruction::Else);
1127    f.instruction(&Instruction::LocalGet(f_null));
1128    f.instruction(&Instruction::RefAsNonNull);
1129    f.instruction(&Instruction::LocalTee(field_obj));
1130    f.instruction(&Instruction::StructGet {
1131        struct_type_index: intrinsics.object,
1132        field_index: 0,
1133    });
1134    f.instruction(&Instruction::StructGet {
1135        struct_type_index: intrinsics.vtable,
1136        field_index: 3,
1137    });
1138    f.instruction(&Instruction::LocalSet(hash_fn));
1139    f.instruction(&Instruction::LocalGet(field_obj));
1140    f.instruction(&Instruction::LocalGet(hash_fn));
1141    f.instruction(&Instruction::CallRef(intrinsics.hash_fn));
1142    f.instruction(&Instruction::End);
1143}
1144
1145/// Null-aware vtable-equals dispatch for one field. Pushes i32: 1=equal, 0=unequal.
1146fn emit_nullable_field_compare(
1147    f: &mut Function,
1148    object_shape_idx: u32,
1149    field_index: u32,
1150    intrinsics: IntrinsicTypeIndices,
1151    locals: (u32, u32, u32, u32, u32, u32),
1152) {
1153    let (field_lhs, eq_fn, field_lhs_null, field_rhs_null, a_t, b_t) = locals;
1154    f.instruction(&Instruction::LocalGet(a_t));
1155    f.instruction(&Instruction::StructGet {
1156        struct_type_index: object_shape_idx,
1157        field_index: 2,
1158    });
1159    f.instruction(&Instruction::I32Const(field_index.cast_signed()));
1160    f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
1161    f.instruction(&Instruction::LocalSet(field_lhs_null));
1162
1163    f.instruction(&Instruction::LocalGet(b_t));
1164    f.instruction(&Instruction::StructGet {
1165        struct_type_index: object_shape_idx,
1166        field_index: 2,
1167    });
1168    f.instruction(&Instruction::I32Const(field_index.cast_signed()));
1169    f.instruction(&Instruction::ArrayGet(intrinsics.object_fields));
1170    f.instruction(&Instruction::LocalSet(field_rhs_null));
1171
1172    f.instruction(&Instruction::LocalGet(field_lhs_null));
1173    f.instruction(&Instruction::RefIsNull);
1174    f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
1175    f.instruction(&Instruction::LocalGet(field_rhs_null));
1176    f.instruction(&Instruction::RefIsNull);
1177    f.instruction(&Instruction::Else);
1178    f.instruction(&Instruction::LocalGet(field_rhs_null));
1179    f.instruction(&Instruction::RefIsNull);
1180    f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
1181    f.instruction(&Instruction::I32Const(0));
1182    f.instruction(&Instruction::Else);
1183    f.instruction(&Instruction::LocalGet(field_lhs_null));
1184    f.instruction(&Instruction::RefAsNonNull);
1185    f.instruction(&Instruction::LocalTee(field_lhs));
1186    f.instruction(&Instruction::StructGet {
1187        struct_type_index: intrinsics.object,
1188        field_index: 0,
1189    });
1190    f.instruction(&Instruction::StructGet {
1191        struct_type_index: intrinsics.vtable,
1192        field_index: 2,
1193    });
1194    f.instruction(&Instruction::LocalSet(eq_fn));
1195    f.instruction(&Instruction::LocalGet(field_lhs));
1196    f.instruction(&Instruction::LocalGet(field_rhs_null));
1197    f.instruction(&Instruction::RefAsNonNull);
1198    f.instruction(&Instruction::LocalGet(eq_fn));
1199    f.instruction(&Instruction::CallRef(intrinsics.equals_fn));
1200    f.instruction(&Instruction::End);
1201    f.instruction(&Instruction::End);
1202}
1203
1204/// Returns all method function indices; must be declared in the element section for `ref.func` to be valid.
1205pub fn declared_method_funcs(subtypes: &[UserSubtype]) -> Vec<u32> {
1206    subtypes
1207        .iter()
1208        .flat_map(|s| [s.to_string_func, s.to_json_func, s.equals_func, s.hash_func])
1209        .collect()
1210}
1211
1212/// Payload-array index for a named field.
1213pub fn field_index(ty: &Type, field_name: &str) -> Result<Option<u32>, CompilerFailure> {
1214    let Type::Object { fields, .. } = ty else {
1215        return Ok(None);
1216    };
1217    fields
1218        .keys()
1219        .position(|k| k == field_name)
1220        .map(wasm_u32)
1221        .transpose()
1222}
1223
1224fn emit_field_value_to_json(
1225    f: &mut Function,
1226    elem: u32,
1227    tj_fn: u32,
1228    intrinsics: IntrinsicTypeIndices,
1229) {
1230    f.instruction(&Instruction::LocalGet(elem));
1231    f.instruction(&Instruction::RefAsNonNull);
1232    f.instruction(&Instruction::StructGet {
1233        struct_type_index: intrinsics.object,
1234        field_index: 0,
1235    });
1236    f.instruction(&Instruction::StructGet {
1237        struct_type_index: intrinsics.vtable,
1238        field_index: 1,
1239    });
1240    f.instruction(&Instruction::LocalSet(tj_fn));
1241
1242    f.instruction(&Instruction::LocalGet(elem));
1243    f.instruction(&Instruction::RefAsNonNull);
1244    f.instruction(&Instruction::LocalGet(tj_fn));
1245    f.instruction(&Instruction::CallRef(intrinsics.to_json_fn));
1246}
1247
1248fn push_inline_string(
1249    f: &mut Function,
1250    s: &str,
1251    string_idx: u32,
1252    raw_string_idx: u32,
1253    string_vtable_global_idx: u32,
1254) -> Result<(), CompilerFailure> {
1255    f.instruction(&Instruction::GlobalGet(string_vtable_global_idx));
1256    for b in s.bytes() {
1257        f.instruction(&Instruction::I32Const(i32::from(b)));
1258    }
1259    f.instruction(&Instruction::ArrayNewFixed {
1260        array_type_index: raw_string_idx,
1261        array_size: wasm_u32(s.len())?,
1262    });
1263    f.instruction(&Instruction::I64Const(0));
1264    f.instruction(&Instruction::StructNew(string_idx));
1265    Ok(())
1266}
1267
1268pub(crate) fn json_escape_key(s: &str) -> String {
1269    let mut out = String::with_capacity(s.len());
1270    for ch in s.chars() {
1271        match ch {
1272            '\\' => out.push_str("\\\\"),
1273            '"' => out.push_str("\\\""),
1274            '\n' => out.push_str("\\n"),
1275            '\r' => out.push_str("\\r"),
1276            '\t' => out.push_str("\\t"),
1277            c if c.is_control() => out.push_str(&format!("\\u{:04x}", u32::from(c))),
1278            c => out.push(c),
1279        }
1280    }
1281    out
1282}
1283
1284fn ref_to(idx: u32) -> ValType {
1285    ValType::Ref(RefType {
1286        nullable: false,
1287        heap_type: HeapType::Concrete(idx),
1288    })
1289}
1290
1291fn ref_null(idx: u32) -> ValType {
1292    ValType::Ref(RefType {
1293        nullable: true,
1294        heap_type: HeapType::Concrete(idx),
1295    })
1296}
1297
1298/// Prepend `if (other is not `type_idx`) return 0` to a vtable `equals` body.
1299/// `equals(self, other)` is dispatched off `self`'s vtable, so when `self` is a
1300/// union value its `other` may be any `$Object` subtype; a different runtime type
1301/// is unequal, never a cast trap. (Param 1 is always `other`.)
1302fn emit_equals_type_guard(f: &mut Function, type_idx: u32) {
1303    f.instruction(&Instruction::LocalGet(1));
1304    f.instruction(&Instruction::RefTestNonNull(HeapType::Concrete(type_idx)));
1305    f.instruction(&Instruction::I32Eqz);
1306    f.instruction(&Instruction::If(BlockType::Empty));
1307    f.instruction(&Instruction::I32Const(0));
1308    f.instruction(&Instruction::Return);
1309    f.instruction(&Instruction::End);
1310}
1311
1312/// `if (other's field-name list differs from self's shape) return 0`, emitted
1313/// before the per-slot value loop. Guards both wrongness modes of a shape-blind
1314/// compare: same-arity different names reading equal (`{a: 1} === {b: 1}` via
1315/// `unknown`), and a shorter `other` payload trapping out-of-bounds.
1316///
1317/// Field-name arrays are per-module canonical globals, so `ref.eq` decides the
1318/// intra-module case. A same-shape value built in another module carries a
1319/// different (equal-content) array — the miss path compares arity, then each
1320/// name via `string_eq`.
1321fn emit_shape_guard(
1322    f: &mut Function,
1323    n_fields: u32,
1324    intrinsics: IntrinsicTypeIndices,
1325    string_eq_func_idx: u32,
1326    (a_t, b_t): (u32, u32),
1327) {
1328    let load_names = |f: &mut Function, side: u32| {
1329        f.instruction(&Instruction::LocalGet(side));
1330        f.instruction(&Instruction::StructGet {
1331            struct_type_index: intrinsics.object_shape,
1332            field_index: 1,
1333        });
1334    };
1335    let load_name_at = |f: &mut Function, side: u32, i: u32| {
1336        load_names(f, side);
1337        f.instruction(&Instruction::I32Const(i.cast_signed()));
1338        f.instruction(&Instruction::ArrayGet(intrinsics.field_names));
1339    };
1340
1341    load_names(f, a_t);
1342    load_names(f, b_t);
1343    f.instruction(&Instruction::RefEq);
1344    f.instruction(&Instruction::I32Eqz);
1345    f.instruction(&Instruction::If(BlockType::Empty));
1346
1347    load_names(f, b_t);
1348    f.instruction(&Instruction::ArrayLen);
1349    f.instruction(&Instruction::I32Const(n_fields.cast_signed()));
1350    f.instruction(&Instruction::I32Ne);
1351    f.instruction(&Instruction::If(BlockType::Empty));
1352    f.instruction(&Instruction::I32Const(0));
1353    f.instruction(&Instruction::Return);
1354    f.instruction(&Instruction::End);
1355
1356    for i in 0..n_fields {
1357        load_name_at(f, a_t, i);
1358        load_name_at(f, b_t, i);
1359        f.instruction(&Instruction::Call(string_eq_func_idx));
1360        f.instruction(&Instruction::I32Eqz);
1361        f.instruction(&Instruction::If(BlockType::Empty));
1362        f.instruction(&Instruction::I32Const(0));
1363        f.instruction(&Instruction::Return);
1364        f.instruction(&Instruction::End);
1365    }
1366
1367    f.instruction(&Instruction::End);
1368}