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