Skip to main content

submilli_engine/runtime/prelude/object/
mod.rs

1//! The Rust port of the prelude's `ObjectConstructor` statics —
2//! `Object.keys`/`values`/`entries`/`hasOwn`/`is` — reading the `$ObjectShape`
3//! field-name/field-value arrays directly. Every per-arity object subtype
4//! subtypes `$ObjectShape`, so one `matches_ty` test covers them all; any other
5//! value takes the "no fields" path (`[]`/`false`), and a null receiver throws
6//! a catchable `Error`, matching the Wasm bodies in
7//! `codegen/prelude/object_shape.rs`.
8
9use crate::runtime::host::{abi_arg, abi_result};
10mod dynamic;
11mod index;
12mod static_value;
13
14use wasmtime::{
15    ArrayRef, ArrayRefPre, Caller, FuncType, HeapType, Linker, RefType, Rooted, StructRef,
16    StructRefPre, StructType, Val, ValType,
17};
18
19use crate::MangledName;
20use crate::runtime::StoreData;
21use crate::runtime::host::{
22    host_object_vtable, intrinsic_array_type, intrinsic_string_type, register_host_fn,
23    register_host_fn_async, write_submilli_array_struct,
24};
25use crate::runtime::intrinsic_types::{build_intrinsic_types, intrinsic_types};
26use crate::runtime::prelude::collection::{FIELD_NAME, is_a};
27use crate::runtime::prelude::iterator::as_struct;
28use crate::runtime::prelude::vtable::{dispatch_vtable_slot, read_string_units};
29use crate::runtime::prelude::{MODULE_NAME, declare_method};
30use crate::{PackageDeclaration, Param, Type};
31
32fn ctor_key(method: &str) -> MangledName {
33    crate::mangle::extend(&crate::mangle::prelude("ObjectConstructor"), method)
34}
35
36#[derive(Clone, Copy)]
37enum Enumerate {
38    Keys,
39    Values,
40    Entries,
41}
42
43impl Enumerate {
44    fn method(self) -> &'static str {
45        match self {
46            Enumerate::Keys => "keys",
47            Enumerate::Values => "values",
48            Enumerate::Entries => "entries",
49        }
50    }
51}
52
53pub(super) fn find_data_slot(
54    caller: &mut Caller<'_, StoreData>,
55    object: &Val,
56    key: &[u16],
57) -> wasmtime::Result<Option<u32>> {
58    let object = as_struct(caller, object, "object equality receiver")?;
59    index::lookup(caller, &object, key, false, false)
60}
61
62pub(super) fn find_field_slot(
63    caller: &mut Caller<'_, StoreData>,
64    object: &Rooted<StructRef>,
65    key: &[u16],
66    accessor: bool,
67) -> wasmtime::Result<Option<u32>> {
68    index::lookup(caller, object, key, accessor, false)
69}
70
71pub(crate) fn data_field_count(
72    caller: &mut Caller<'_, StoreData>,
73    object: &Val,
74) -> wasmtime::Result<usize> {
75    let Some((names, values)) = shape_arrays(caller, object)? else {
76        return Ok(0);
77    };
78    let count = field_count(caller, object)?;
79    let mut present = 0;
80    for slot in 0..count {
81        let name = names.get(&mut *caller, slot)?;
82        let value = values.get(&mut *caller, slot)?;
83        if field_is_present(caller, &name, &value)? && !is_accessor_slot(caller, &name)? {
84            present += 1;
85        }
86    }
87    Ok(present)
88}
89
90pub(crate) fn field_count(
91    caller: &mut Caller<'_, StoreData>,
92    object: &Val,
93) -> wasmtime::Result<u32> {
94    let object = as_struct(caller, object, "object field count")?;
95    index::len(caller, &object)
96}
97
98/// The `$ObjectShape` field arrays `(field_names, object_fields)` of `obj`, or
99/// `None` when `obj` is any other (non-null) value — the "no fields" path.
100fn shape_arrays(
101    caller: &mut Caller<'_, StoreData>,
102    obj: &Val,
103) -> wasmtime::Result<Option<(Rooted<ArrayRef>, Rooted<ArrayRef>)>> {
104    let Val::AnyRef(Some(any)) = obj else {
105        return Ok(None);
106    };
107    let Some(st) = any.as_struct(&mut *caller)? else {
108        return Ok(None);
109    };
110    let shape = intrinsic_types(&mut *caller)?.object_shape.clone();
111    if !st.matches_ty(&*caller, &shape)? {
112        return Ok(None);
113    }
114    let names = field_array(caller, &st, 1)?;
115    let values = field_array(caller, &st, 2)?;
116    Ok(Some((names, values)))
117}
118
119pub(super) fn field_array(
120    caller: &mut Caller<'_, StoreData>,
121    st: &Rooted<StructRef>,
122    idx: usize,
123) -> wasmtime::Result<Rooted<ArrayRef>> {
124    match st.field(&mut *caller, idx)? {
125        Val::AnyRef(Some(a)) => a.unwrap_array(&mut *caller),
126        other => Err(wasmtime::Error::msg(format!(
127            "$ObjectShape field {idx} is not an array: {other:?}"
128        ))),
129    }
130}
131
132/// An optional nullish slot has a separate presence flag on its field name.
133pub(crate) fn field_is_present(
134    caller: &mut Caller<'_, StoreData>,
135    name: &Val,
136    value: &Val,
137) -> wasmtime::Result<bool> {
138    if !super::undefined::is_nullish(caller, value)? {
139        return Ok(true);
140    }
141    let name = as_struct(caller, name, "field name")?;
142    let string = intrinsic_types(&mut *caller)?.string.clone();
143    if StructType::eq(&name.ty(&*caller)?, &string) {
144        return Ok(true);
145    }
146    Ok(matches!(name.field(&mut *caller, 3)?, Val::I32(value) if value != 0))
147}
148
149fn enumerate(
150    caller: &mut Caller<'_, StoreData>,
151    obj: &Val,
152    kind: Enumerate,
153) -> wasmtime::Result<Val> {
154    if super::undefined::is_nullish(caller, obj)? {
155        return Err(wasmtime::Error::msg(format!(
156            "Object.{} called on null or undefined",
157            kind.method()
158        )));
159    }
160    let mut elems = Vec::new();
161    if let Some((names, values)) = shape_arrays(caller, obj)? {
162        let len = field_count(caller, obj)?;
163        elems.reserve(len as usize);
164        let error_slots = super::error::non_enumerable_slots(caller, obj)?;
165        for i in 0..len {
166            if error_slots.as_ref().is_some_and(|hidden| hidden.hides(i)) {
167                continue;
168            }
169            let name = names.get(&mut *caller, i)?;
170            let value = values.get(&mut *caller, i)?;
171            if !field_is_present(caller, &name, &value)? || is_accessor_slot(caller, &name)? {
172                continue;
173            }
174            let elem = match kind {
175                Enumerate::Keys => names.get(&mut *caller, i)?,
176                Enumerate::Values => values.get(&mut *caller, i)?,
177                Enumerate::Entries => {
178                    let name = names.get(&mut *caller, i)?;
179                    let value = values.get(&mut *caller, i)?;
180                    let pair = write_submilli_array_struct(caller, &[name, value])?;
181                    Val::AnyRef(Some(pair.to_anyref()))
182                }
183            };
184            elems.push(elem);
185        }
186    }
187    let arr = write_submilli_array_struct(caller, &elems)?;
188    Ok(Val::AnyRef(Some(arr.to_anyref())))
189}
190
191/// A marked name identifies an internal accessor slot independently of its
192/// spelling or current value; user data named `get x` remains ordinary data.
193pub(crate) fn is_accessor_slot(
194    caller: &mut Caller<'_, StoreData>,
195    name: &Val,
196) -> wasmtime::Result<bool> {
197    let name = as_struct(caller, name, "field name")?;
198    let string = intrinsic_types(&mut *caller)?.string.clone();
199    if StructType::eq(&name.ty(&*caller)?, &string) {
200        return Ok(false);
201    }
202    Ok(matches!(name.field(&mut *caller, 3)?, Val::I32(-1)))
203}
204
205/// Visibility is carried only by compiler-created marked names. Host-created
206/// names predate that metadata and describe public data properties.
207pub(crate) fn field_is_private(
208    caller: &mut Caller<'_, StoreData>,
209    name: &Val,
210) -> wasmtime::Result<bool> {
211    let name = as_struct(caller, name, "field name")?;
212    if name.ty(&*caller)?.fields().count() < 5 {
213        return Ok(false);
214    }
215    Ok(matches!(name.field(&mut *caller, 4)?, Val::I32(1)))
216}
217
218/// Copy present own fields while preserving UTF-16 names and boxed values.
219/// Each call snapshots its source before the next literal member is evaluated.
220fn spread(
221    caller: &mut Caller<'_, StoreData>,
222    target: &Val,
223    source: &Val,
224    shape: &Val,
225    mask: &Val,
226) -> wasmtime::Result<Val> {
227    let intr = intrinsic_types(&mut *caller)?;
228    let mut entries = std::collections::BTreeMap::new();
229    let omitted = spread_omitted_fields(caller, mask)?;
230    for (source_index, object) in [target, source].into_iter().enumerate() {
231        let Some((names, values)) = shape_arrays(caller, object)? else {
232            continue;
233        };
234        for index in 0..field_count(caller, object)? {
235            let name = names.get(&mut *caller, index)?;
236            let value = values.get(&mut *caller, index)?;
237            if !field_is_present(caller, &name, &value)? || is_accessor_slot(caller, &name)? {
238                continue;
239            }
240            let units = read_string_units(caller, &name, FIELD_NAME)?;
241            if source_index == 1 && omitted.contains(&units) {
242                continue;
243            }
244            entries.insert(units, (copy_field_name(caller, name, true)?, value));
245        }
246    }
247    if let Some((names, _)) = shape_arrays(caller, shape)? {
248        for index in 0..field_count(caller, shape)? {
249            let name = names.get(&mut *caller, index)?;
250            let units = read_string_units(caller, &name, FIELD_NAME)?;
251            if let std::collections::btree_map::Entry::Vacant(entry) = entries.entry(units) {
252                entry.insert((
253                    copy_field_name(caller, name, false)?,
254                    super::undefined::value(caller)?,
255                ));
256            }
257        }
258    }
259    let (names, values): (Vec<_>, Vec<_>) = entries.into_values().unzip();
260    let names_pre = ArrayRefPre::new(&mut *caller, intr.field_names.clone());
261    let values_pre = ArrayRefPre::new(&mut *caller, intr.object_fields.clone());
262    let shape_pre = StructRefPre::new(&mut *caller, intr.object_shape.clone());
263    let names = ArrayRef::new_fixed(&mut *caller, &names_pre, &names)?;
264    let values = ArrayRef::new_fixed(&mut *caller, &values_pre, &values)?;
265    let vtable = host_object_vtable(caller)?;
266    let object = StructRef::new(
267        &mut *caller,
268        &shape_pre,
269        &[
270            vtable,
271            Val::AnyRef(Some(names.to_anyref())),
272            Val::AnyRef(Some(values.to_anyref())),
273            Val::AnyRef(None),
274        ],
275    )?;
276    Ok(Val::AnyRef(Some(object.to_anyref())))
277}
278
279fn copy_field_name(
280    caller: &mut Caller<'_, StoreData>,
281    name: Val,
282    present: bool,
283) -> wasmtime::Result<Val> {
284    let object = as_struct(caller, &name, "field name")?;
285    let string = intrinsic_types(&mut *caller)?.string.clone();
286    if StructType::eq(&object.ty(&*caller)?, &string) {
287        return Ok(name);
288    }
289    let ty = if present {
290        string
291    } else {
292        object.ty(&*caller)?
293    };
294    let mut fields = vec![
295        object.field(&mut *caller, 0)?,
296        object.field(&mut *caller, 1)?,
297        object.field(&mut *caller, 2)?,
298    ];
299    if !present {
300        fields.push(Val::I32(0));
301        if ty.fields().count() > 4 {
302            fields.push(object.field(&mut *caller, 4)?);
303        }
304    }
305    let pre = StructRefPre::new(&mut *caller, ty);
306    Ok(Val::AnyRef(Some(
307        StructRef::new(&mut *caller, &pre, &fields)?.to_anyref(),
308    )))
309}
310
311/// Append without replacing the receiver, so every alias sees the new slot.
312/// Existing indices (including class payload and guard slots) stay unchanged.
313fn insert_field(
314    caller: &mut Caller<'_, StoreData>,
315    obj: &Val,
316    name: &Val,
317    value: &Val,
318) -> wasmtime::Result<()> {
319    let object = as_struct(caller, obj, "field insertion receiver")?;
320    let old_names = field_array(caller, &object, 1)?;
321    let old_values = field_array(caller, &object, 2)?;
322    let count = index::len(caller, &object)?;
323    let capacity = old_names.len(&mut *caller)?;
324    let new_count = count
325        .checked_add(1)
326        .filter(|n| *n <= i32::MAX as u32)
327        .ok_or_else(|| crate::runtime::host::range_error("object field count limit exceeded"))?;
328    let name = inserted_field_name(caller, name)?;
329    let (names, values, table) = if count == capacity {
330        let (names, values) = grow_fields(caller, &old_names, &old_values, &name)?;
331        let new_capacity = names.len(&mut *caller)?;
332        let table = index::build(caller, &names, count, new_capacity)?;
333        (names, values, table)
334    } else {
335        let table = match index::cached(caller, &object)? {
336            Some(table) => table,
337            None => index::build(caller, &old_names, count, capacity)?,
338        };
339        (old_names, old_values, table)
340    };
341    let bucket = index::empty_bucket(caller, &table, &name)?;
342    // All fuel and allocation checks precede publication. Guard rows use the
343    // backing capacity as their stride, so spare named slots need no reshuffle.
344    names.set(&mut *caller, count, name)?;
345    values.set(&mut *caller, count, *value)?;
346    table.set(&mut *caller, bucket, Val::I32(new_count as i32))?;
347    table.set(&mut *caller, 0, Val::I32(new_count as i32))?;
348    table.set(&mut *caller, 1, Val::I32(1))?;
349    object.set_field(&mut *caller, 1, Val::AnyRef(Some(names.to_anyref())))?;
350    object.set_field(&mut *caller, 2, Val::AnyRef(Some(values.to_anyref())))?;
351    object.set_field(
352        &mut *caller,
353        index::INDEX_FIELD,
354        Val::AnyRef(Some(table.to_anyref())),
355    )?;
356    Ok(())
357}
358
359fn grow_fields(
360    caller: &mut Caller<'_, StoreData>,
361    names: &Rooted<ArrayRef>,
362    values: &Rooted<ArrayRef>,
363    filler: &Val,
364) -> wasmtime::Result<(Rooted<ArrayRef>, Rooted<ArrayRef>)> {
365    let capacity = names.len(&mut *caller)?;
366    let value_len = values.len(&mut *caller)?;
367    let width = capacity
368        .checked_add(1)
369        .ok_or_else(|| crate::runtime::host::fatal_host_error("object guard row width overflow"))?;
370    let hidden = value_len
371        .checked_sub(capacity)
372        .filter(|n| n % width == 0)
373        .ok_or_else(|| crate::runtime::host::fatal_host_error("malformed object guard rows"))?;
374    let rows = hidden / width;
375    let new_capacity = capacity
376        .checked_mul(2)
377        .map(|n| n.max(8))
378        .filter(|n| *n < i32::MAX as u32)
379        .ok_or_else(|| crate::runtime::host::range_error("object field capacity limit exceeded"))?;
380    let new_width = new_capacity + 1;
381    let new_value_len = rows
382        .checked_mul(new_width)
383        .and_then(|n| n.checked_add(new_capacity))
384        .ok_or_else(|| {
385            crate::runtime::host::range_error("object payload capacity limit exceeded")
386        })?;
387    crate::runtime::fuel::charge(
388        &mut *caller,
389        crate::runtime::fuel::ELEM,
390        u64::from(new_capacity)
391            + u64::from(new_value_len)
392            + u64::from(capacity)
393            + u64::from(value_len),
394    )?;
395    let intr = intrinsic_types(&mut *caller)?;
396    let names_pre = ArrayRefPre::new(&mut *caller, intr.field_names.clone());
397    let values_pre = ArrayRefPre::new(&mut *caller, intr.object_fields.clone());
398    let new_names = ArrayRef::new(&mut *caller, &names_pre, filler, new_capacity)?;
399    let new_values = ArrayRef::new(&mut *caller, &values_pre, &Val::AnyRef(None), new_value_len)?;
400    for slot in 0..capacity {
401        let name = names.get(&mut *caller, slot)?;
402        let value = values.get(&mut *caller, slot)?;
403        new_names.set(&mut *caller, slot, name)?;
404        new_values.set(&mut *caller, slot, value)?;
405    }
406    for row in 0..rows {
407        let old_start = capacity + row * width;
408        let new_start = new_capacity + row * new_width;
409        for slot in 0..capacity {
410            let guard = values.get(&mut *caller, old_start + slot)?;
411            new_values.set(&mut *caller, new_start + slot, guard)?;
412        }
413        let context = values.get(&mut *caller, old_start + capacity)?;
414        new_values.set(&mut *caller, new_start + new_capacity, context)?;
415    }
416    Ok((new_names, new_values))
417}
418
419/// A present inserted name also tells typed serializers that the original
420/// static shape no longer describes all of this object's fields.
421fn inserted_field_name(caller: &mut Caller<'_, StoreData>, name: &Val) -> wasmtime::Result<Val> {
422    use wasmtime::{FieldType, Finality, Mutability, StorageType};
423    let intr = intrinsic_types(&mut *caller)?;
424    let mut fields: Vec<_> = intr.string.fields().collect();
425    fields.push(FieldType::new(
426        Mutability::Var,
427        StorageType::ValType(ValType::I32),
428    ));
429    let ty = crate::runtime::gc_singleton::singleton_struct(
430        caller.engine(),
431        Finality::Final,
432        Some(intr.string.clone()),
433        fields,
434    )?;
435    let name = as_struct(caller, name, "inserted field name")?;
436    let values = [
437        name.field(&mut *caller, 0)?,
438        name.field(&mut *caller, 1)?,
439        name.field(&mut *caller, 2)?,
440        Val::I32(2),
441    ];
442    let pre = StructRefPre::new(&mut *caller, ty);
443    Ok(Val::AnyRef(Some(
444        StructRef::new(&mut *caller, &pre, &values)?.to_anyref(),
445    )))
446}
447
448pub(crate) fn field_was_inserted(
449    caller: &mut Caller<'_, StoreData>,
450    name: &Val,
451) -> wasmtime::Result<bool> {
452    let name = as_struct(caller, name, "field name")?;
453    let string = intrinsic_types(&mut *caller)?.string.clone();
454    if StructType::eq(&name.ty(&*caller)?, &string) {
455        return Ok(false);
456    }
457    Ok(matches!(name.field(&mut *caller, 3)?, Val::I32(2)))
458}
459
460/// The compiler marks rejected known fields with non-null mask slots.
461fn spread_omitted_fields(
462    caller: &mut Caller<'_, StoreData>,
463    mask: &Val,
464) -> wasmtime::Result<std::collections::BTreeSet<Vec<u16>>> {
465    let mut omitted = std::collections::BTreeSet::new();
466    if let Some((names, values)) = shape_arrays(caller, mask)? {
467        for index in 0..field_count(caller, mask)? {
468            if !matches!(values.get(&mut *caller, index)?, Val::AnyRef(None)) {
469                let name = names.get(&mut *caller, index)?;
470                omitted.insert(read_string_units(caller, &name, FIELD_NAME)?);
471            }
472        }
473    }
474    Ok(omitted)
475}
476
477fn has_own(caller: &mut Caller<'_, StoreData>, obj: &Val, key: &Val) -> wasmtime::Result<bool> {
478    if super::undefined::is_nullish(caller, obj)? {
479        return Err(wasmtime::Error::msg(
480            "Object.hasOwn called on null or undefined",
481        ));
482    }
483    let Some((names, values)) = shape_arrays(caller, obj)? else {
484        return Ok(false);
485    };
486    let target = read_string_units(caller, key, FIELD_NAME)?;
487    let Some(slot) = find_data_slot(caller, obj, &target)? else {
488        return Ok(false);
489    };
490    let name = names.get(&mut *caller, slot)?;
491    let value = values.get(&mut *caller, slot)?;
492    field_is_present(caller, &name, &value)
493}
494
495/// SameValue on two `f64` bit patterns: any NaN equals any NaN (Wasm arithmetic
496/// varies sign/payload bits), otherwise exact bit equality — which separates
497/// `+0` from `-0` and compares ordinary values exactly.
498fn same_value_bits(a: u64, b: u64) -> bool {
499    (f64::from_bits(a).is_nan() && f64::from_bits(b).is_nan()) || a == b
500}
501
502fn boxed_number_bits(caller: &mut Caller<'_, StoreData>, val: &Val) -> wasmtime::Result<u64> {
503    let st = as_struct(caller, val, "Object.is operand")?;
504    match st.field(&mut *caller, 1)? {
505        Val::F64(bits) => Ok(bits),
506        other => Err(wasmtime::Error::msg(format!(
507            "Object.is: boxed number payload is {other:?}, not f64"
508        ))),
509    }
510}
511
512async fn same_value(
513    caller: &mut Caller<'_, StoreData>,
514    a: &Val,
515    b: &Val,
516) -> wasmtime::Result<bool> {
517    match (a, b) {
518        (Val::AnyRef(None), Val::AnyRef(None)) => return Ok(true),
519        (Val::AnyRef(None), _) | (_, Val::AnyRef(None)) => return Ok(false),
520        _ => {}
521    }
522    // The number arm must run before vtable dispatch: the boxed-number `equals`
523    // slot is `===` (`0 === -0`, `NaN !== NaN`), while SameValue distinguishes
524    // both.
525    let boxed_number = intrinsic_types(&mut *caller)?.boxed_number.clone();
526    if is_a(caller, a, &boxed_number)? && is_a(caller, b, &boxed_number)? {
527        let (a_bits, b_bits) = (boxed_number_bits(caller, a)?, boxed_number_bits(caller, b)?);
528        return Ok(same_value_bits(a_bits, b_bits));
529    }
530    match dispatch_vtable_slot(caller, a, 2, std::slice::from_ref(b)).await? {
531        Val::I32(v) => Ok(v != 0),
532        other => Err(wasmtime::Error::msg(format!(
533            "Object.is: equals returned {other:?}, expected i32"
534        ))),
535    }
536}
537
538pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
539    dynamic::install(linker)?;
540    let engine = linker.engine().clone();
541    let intr = build_intrinsic_types(&engine)?;
542    let obj = ValType::Ref(RefType::new(
543        true,
544        HeapType::ConcreteStruct(intr.object.clone()),
545    ));
546    let string = ValType::Ref(RefType::new(
547        false,
548        HeapType::ConcreteStruct(intrinsic_string_type(&engine)?),
549    ));
550    let array = ValType::Ref(RefType::new(
551        false,
552        HeapType::ConcreteStruct(intrinsic_array_type(&engine)?),
553    ));
554    let boolean = ValType::I32;
555    let ft = |params: Vec<ValType>, results: Vec<ValType>| FuncType::new(&engine, params, results);
556
557    for kind in [Enumerate::Keys, Enumerate::Values, Enumerate::Entries] {
558        register_host_fn(
559            linker,
560            MODULE_NAME,
561            ctor_key(kind.method()),
562            ft(vec![obj.clone()], vec![array.clone()]),
563            true,
564            move |caller, params, results| {
565                *abi_result(results, 0)? = enumerate(caller, abi_arg(params, 0)?, kind)?;
566                Ok(())
567            },
568        )?;
569    }
570
571    register_host_fn(
572        linker,
573        MODULE_NAME,
574        ctor_key("hasOwn"),
575        ft(vec![obj.clone(), string.clone()], vec![boolean.clone()]),
576        true,
577        |caller, params, results| {
578            *abi_result(results, 0)? = Val::I32(i32::from(has_own(
579                caller,
580                abi_arg(params, 0)?,
581                abi_arg(params, 1)?,
582            )?));
583            Ok(())
584        },
585    )?;
586
587    register_host_fn(
588        linker,
589        MODULE_NAME,
590        ctor_key("#insertField"),
591        ft(vec![obj.clone(), string, obj.clone()], vec![]),
592        true,
593        |caller, params, _| {
594            insert_field(
595                caller,
596                abi_arg(params, 0)?,
597                abi_arg(params, 1)?,
598                abi_arg(params, 2)?,
599            )
600        },
601    )?;
602
603    let shape = ValType::Ref(RefType::new(
604        false,
605        HeapType::ConcreteStruct(intr.object_shape.clone()),
606    ));
607    register_host_fn(
608        linker,
609        MODULE_NAME,
610        ctor_key("#spread"),
611        ft(
612            vec![obj.clone(), obj.clone(), obj.clone(), obj.clone()],
613            vec![shape],
614        ),
615        true,
616        |caller, params, results| {
617            *abi_result(results, 0)? = spread(
618                caller,
619                abi_arg(params, 0)?,
620                abi_arg(params, 1)?,
621                abi_arg(params, 2)?,
622                abi_arg(params, 3)?,
623            )?;
624            Ok(())
625        },
626    )?;
627    register_host_fn_async(
628        linker,
629        MODULE_NAME,
630        ctor_key("#toJson"),
631        ft(
632            vec![obj.clone()],
633            vec![ValType::Ref(RefType::new(
634                false,
635                HeapType::ConcreteStruct(intr.string.clone()),
636            ))],
637        ),
638        true,
639        |caller, params, results| {
640            Box::pin(async move {
641                let intr = intrinsic_types(&mut *caller)?;
642                *abi_result(results, 0)? = super::vtable::object_to_json(
643                    caller,
644                    abi_arg(params, 0)?,
645                    &intr.raw_string,
646                    &intr.string,
647                )
648                .await?;
649                Ok(())
650            })
651        },
652    )?;
653    register_host_fn(
654        linker,
655        MODULE_NAME,
656        ctor_key("#staticValue"),
657        ft(
658            vec![ValType::Ref(RefType::new(
659                false,
660                HeapType::ConcreteStruct(intr.string.clone()),
661            ))],
662            vec![obj.clone()],
663        ),
664        true,
665        |caller, params, results| {
666            *abi_result(results, 0)? = static_value::static_value(caller, abi_arg(params, 0)?)?;
667            Ok(())
668        },
669    )?;
670    register_host_fn_async(
671        linker,
672        MODULE_NAME,
673        ctor_key("is"),
674        ft(vec![obj.clone(), obj], vec![boolean]),
675        true,
676        |caller, params, results| {
677            Box::pin(async move {
678                *abi_result(results, 0)? = Val::I32(i32::from(
679                    same_value(caller, abi_arg(params, 0)?, abi_arg(params, 1)?).await?,
680                ));
681                Ok(())
682            })
683        },
684    )
685}
686
687pub fn declare(defs: &mut PackageDeclaration) {
688    dynamic::declare(defs);
689    declare_method(
690        defs,
691        "#toJson",
692        ctor_key("#toJson"),
693        vec![Param::new("object", Type::Unknown)],
694        Type::String,
695    );
696    // Compiler-only helper: the value a static-dispatch binding reads as.
697    declare_method(
698        defs,
699        "#staticValue",
700        ctor_key("#staticValue"),
701        vec![Param::new("tag", Type::String)],
702        Type::Unknown,
703    );
704    declare_method(
705        defs,
706        "#insertField",
707        ctor_key("#insertField"),
708        vec![
709            Param::new("object", Type::Unknown),
710            Param::new("name", Type::String),
711            Param::new("value", Type::Unknown),
712        ],
713        Type::Void,
714    );
715    // Compiler-only helper: deliberately absent from ObjectConstructor's public surface.
716    declare_method(
717        defs,
718        "#spread",
719        ctor_key("#spread"),
720        vec![
721            Param::new("target", Type::Unknown),
722            Param::new("source", Type::Unknown),
723            Param::new("shape", Type::Unknown),
724            Param::new("mask", Type::Unknown),
725        ],
726        Type::Object {
727            index: None,
728            fields: Default::default(),
729        },
730    );
731    // `Dispatch::Static` drops the constructor receiver, so no receiver param.
732    // Types mirror the prelude MethodSigs: `Type::Array`/`Type::Tuple` lower to
733    // `(ref $Array)`, matching the registered FuncTypes above.
734    let obj = || Param::new("obj", Type::Unknown);
735    declare_method(
736        defs,
737        "keys",
738        ctor_key("keys"),
739        vec![obj()],
740        Type::Array(Box::new(Type::String)),
741    );
742    declare_method(
743        defs,
744        "values",
745        ctor_key("values"),
746        vec![obj()],
747        Type::Array(Box::new(Type::Unknown)),
748    );
749    declare_method(
750        defs,
751        "entries",
752        ctor_key("entries"),
753        vec![obj()],
754        Type::Array(Box::new(Type::Tuple(
755            vec![Type::String, Type::Unknown].into(),
756        ))),
757    );
758    declare_method(
759        defs,
760        "hasOwn",
761        ctor_key("hasOwn"),
762        vec![obj(), Param::new("key", Type::String)],
763        Type::Boolean,
764    );
765    declare_method(
766        defs,
767        "is",
768        ctor_key("is"),
769        vec![
770            Param::with_default("a", Type::Unknown, crate::DefaultValue::Undefined),
771            Param::with_default("b", Type::Unknown, crate::DefaultValue::Undefined),
772        ],
773        Type::Boolean,
774    );
775}
776
777/// The type/interface surface this module implements — its slice of the
778/// prelude declaration (see `declaration::prelude_package_declaration`).
779#[allow(clippy::too_many_lines)]
780pub(crate) fn declare_types(defs: &mut crate::PackageDeclaration) {
781    use crate::runtime::prelude::declaration::doc;
782    use crate::{
783        Dispatch, MethodSig, Param, Span, Type, TypeKind, TypeSymbol, ValueKind, ValueSymbol,
784    };
785    use std::collections::BTreeMap;
786    defs.types.insert(
787        "Object".to_string(),
788        TypeSymbol {
789            name: "Object".to_string(),
790            mangled_name: crate::mangle::prelude("Object"),
791            declaration_span: Span::at(crate::FileId::PRELUDE),
792            kind: TypeKind::Interface { index: None,
793                generics: Vec::new(),
794                methods: BTreeMap::from([
795                    (
796                        "toString".to_string(),
797                        MethodSig {
798                            optional: false,
799                            generics: Vec::new(),
800                            params: Vec::new(),
801                            ret: Type::String,
802                            predicate: None,
803                            doc: doc("/** Returns `\"[object Object]\"`. */"),
804                        },
805                    ),
806                    (
807                        "toJson".to_string(),
808                        MethodSig {
809                            optional: false,
810                            generics: Vec::new(),
811                            params: Vec::new(),
812                            ret: Type::String,
813                            predicate: None,
814                            doc: doc(
815                                "/** Returns the JSON representation of this object — `\"{\"` + `\"key\":value-json` pairs joined with `\",\"` + `\"}\"`. Field iteration order matches the type's struct layout. */",
816                            ),
817                        },
818                    ),
819                ]),
820                properties: BTreeMap::new(),
821                // Object opts OUT of direct dispatch — `obj.toString()`
822                // and `obj.toJson()` route through the per-shape vtable
823                // (slots 0 / 1). Each user-object shape generates its
824                // own per-type body.
825                dispatch: Dispatch::VTable,
826                doc: doc("/** Universal base type for every object shape. */"),
827            },
828        },
829    );
830    defs.types.insert(
831        "ObjectConstructor".to_string(),
832        TypeSymbol {
833            name: "ObjectConstructor".to_string(),
834            mangled_name: crate::mangle::prelude("ObjectConstructor"),
835            declaration_span: Span::at(crate::FileId::PRELUDE),
836            kind: TypeKind::Interface { index: None,
837                generics: Vec::new(),
838                methods: BTreeMap::from([
839                    (
840                        "keys".to_string(),
841                        MethodSig {
842                            optional: false,
843                            generics: Vec::new(),
844                            params: vec![Param::new("obj", Type::Unknown)],
845                            ret: Type::Array(Box::new(Type::String)),
846                            predicate: None,
847                            doc: doc(
848                                "/**\n * Returns the object's field names in canonical sorted order (the same order JSON output uses).\n * An omitted optional field is not listed; one set to `undefined` or `null` is. Non-object values yield `[]`; `null` or `undefined` throws a catchable `Error`.\n * @param obj The object to enumerate.\n */",
849                            ),
850                        },
851                    ),
852                    (
853                        "values".to_string(),
854                        MethodSig {
855                            optional: false,
856                            generics: Vec::new(),
857                            params: vec![Param::new("obj", Type::Unknown)],
858                            ret: Type::Array(Box::new(Type::Unknown)),
859                            predicate: None,
860                            doc: doc(
861                                "/**\n * Returns the object's field values, aligned with `Object.keys` order. Element types are erased to `unknown` — narrow with `typeof` / `as`.\n * Non-object values yield `[]`; `null` or `undefined` throws a catchable `Error`.\n * @param obj The object to enumerate.\n */",
862                            ),
863                        },
864                    ),
865                    (
866                        "entries".to_string(),
867                        MethodSig {
868                            optional: false,
869                            generics: Vec::new(),
870                            params: vec![Param::new("obj", Type::Unknown)],
871                            ret: Type::Array(Box::new(Type::Tuple(vec![
872                                Type::String,
873                                Type::Unknown,
874                            ].into()))),
875                            predicate: None,
876                            doc: doc(
877                                "/**\n * Returns `[name, value]` pairs in canonical sorted key order. Values are erased to `unknown` — narrow with `typeof` / `as`.\n * Non-object values yield `[]`; `null` or `undefined` throws a catchable `Error`.\n * @param obj The object to enumerate.\n */",
878                            ),
879                        },
880                    ),
881                    (
882                        "hasOwn".to_string(),
883                        MethodSig {
884                            optional: false,
885                            generics: Vec::new(),
886                            params: vec![
887                                Param::new("obj", Type::Unknown),
888                                Param::new("key", Type::String),
889                            ],
890                            ret: Type::Boolean,
891                            predicate: None,
892                            doc: doc(
893                                "/**\n * Returns `true` when `obj` has a field named `key`: an omitted optional field answers `false`, one holding `undefined` answers `true`. Non-object values have no fields; `null` or `undefined` throws a catchable `Error`.\n * @param obj The object to test.\n * @param key The field name.\n */",
894                            ),
895                        },
896                    ),
897                    (
898                        "is".to_string(),
899                        MethodSig {
900                            optional: false,
901                            generics: Vec::new(),
902                            params: vec![
903                                Param::with_default("a", Type::Unknown, crate::DefaultValue::Undefined),
904                                Param::with_default("b", Type::Unknown, crate::DefaultValue::Undefined),
905                            ],
906                            ret: Type::Boolean,
907                            predicate: None,
908                            doc: doc(
909                                "/**\n * SameValue comparison: like `===` but `Object.is(NaN, NaN)` is `true` and `Object.is(0, -0)` is `false`.\n * Object comparison follows the language's structural `===`, not JS reference identity.\n */",
910                            ),
911                        },
912                    ),
913                ]),
914                properties: BTreeMap::new(),
915                dispatch: Dispatch::Static,
916                doc: doc(
917                    "/** Constructor object for `Object` — the enumeration statics (`keys`/`values`/`entries`/`hasOwn`) and `is`. Accessed via the global `Object` binding. */",
918                ),
919            },
920        },
921    );
922
923    defs.values.insert(
924        "Object".to_string(),
925        ValueSymbol {
926            name: "Object".to_string(),
927            mangled_name: crate::mangle::prelude("Object"),
928            declaration_span: Span::at(crate::FileId::PRELUDE),
929            kind: ValueKind::Const {
930                ty: Type::prelude_interface("ObjectConstructor".to_string(), Vec::new()),
931                doc: doc(
932                    "/** The `Object` namespace — `Object.keys(o)`, `Object.values(o)`, `Object.entries(o)`, `Object.hasOwn(o, k)`, `Object.is(a, b)`. */",
933                ),
934            },
935        },
936    );
937}
938
939#[cfg(test)]
940mod tests {
941    use super::same_value_bits;
942
943    #[test]
944    fn same_value_bit_rules() {
945        assert!(same_value_bits(f64::NAN.to_bits(), (-f64::NAN).to_bits()));
946        assert!(!same_value_bits(0f64.to_bits(), (-0f64).to_bits()));
947        assert!(same_value_bits((-0f64).to_bits(), (-0f64).to_bits()));
948        assert!(same_value_bits(1.5f64.to_bits(), 1.5f64.to_bits()));
949        assert!(!same_value_bits(1f64.to_bits(), 2f64.to_bits()));
950    }
951}