Skip to main content

smelt_stdlib/
host_object.rs

1//! Canonical registry for host-object identities.
2//!
3//! Several JavaScript host builtins and boxed primitive wrappers — `ArrayBuffer`,
4//! `DataView`, `WeakMap`, `WeakSet`, `SharedArrayBuffer`, `File`, `Blob`,
5//! `DOMException`, and the boxed `Number`/`Boolean`/`String`/`Symbol` wrappers —
6//! have no useful structural shape that source code reads. They are constructed
7//! and then only tested with `value instanceof X` (the `isWeakMap`/`isArrayBuffer`
8//! family and the deep-clone dispatch). Their identity is known *statically* at
9//! the construction site.
10//!
11//! Rather than let each host type invent its own `__smelt_<marker>` string in the
12//! frontend construction path, the `instanceof` codegen path, and the runtime
13//! for-in / structural-equality helpers independently, this module is the single
14//! source of truth for that identity. All three consumers read from
15//! [`HOST_OBJECTS`] so the construct side, the `instanceof` side, and the runtime
16//! host-marker registry can never drift apart (a drift that previously left the
17//! boxed-`Boolean` marker out of the runtime for-in filter).
18//!
19//! This is deliberately *not* a general dynamic boundary: each entry is a concrete
20//! host identity with a known constructor. Genuine `unknown`/interop values still
21//! flow through the tagged dynamic ABI; this registry only names the host objects
22//! whose identity Smelt can resolve ahead of time.
23
24/// A single host-object identity: the JavaScript constructor name, the dedicated
25/// identity marker key stamped onto the constructed record, and whether the
26/// identity denotes a boxed primitive wrapper.
27///
28/// The `marker` is the `__smelt_<name>` key that gives the constructed record its
29/// distinct identity. `instanceof` resolves through this key, and the runtime
30/// for-in / `Object.keys` filters hide records carrying it so a host object never
31/// leaks its internal marker keys as enumerable properties.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[non_exhaustive]
34pub struct HostObject {
35    /// The JavaScript constructor / class name (`"WeakMap"`, `"ArrayBuffer"`).
36    pub class_name: &'static str,
37    /// The dedicated identity marker key (`"__smelt_weakmap"`).
38    pub marker: &'static str,
39    /// Whether this identity is a boxed primitive wrapper (`new Number(1)`).
40    ///
41    /// Boxed wrappers are distinct from the same-named coercion calls
42    /// (`Number(x)`), which lower to primitive values. The wrapper object has
43    /// `typeof === "object"`, so the runtime `typeof` narrowing must miss it while
44    /// `instanceof` still resolves through the marker.
45    pub is_boxed_primitive: bool,
46}
47
48/// Concise constructor for a host-object registry entry.
49const fn host(class_name: &'static str, marker: &'static str) -> HostObject {
50    HostObject {
51        class_name,
52        marker,
53        is_boxed_primitive: false,
54    }
55}
56
57/// Concise constructor for a boxed-primitive-wrapper registry entry.
58const fn boxed(class_name: &'static str, marker: &'static str) -> HostObject {
59    HostObject {
60        class_name,
61        marker,
62        is_boxed_primitive: true,
63    }
64}
65
66/// The canonical set of host objects whose identity Smelt models with a dedicated
67/// marker record.
68///
69/// Ordering is irrelevant; lookups are by `class_name` or `marker`. Adding a new
70/// host identity here automatically wires it into the frontend construction
71/// helper, the `instanceof` lowering, and the runtime host-marker registry.
72pub const HOST_OBJECTS: &[HostObject] = &[
73    host("ArrayBuffer", "__smelt_arraybuffer"),
74    host("SharedArrayBuffer", "__smelt_sharedarraybuffer"),
75    // Node's `Buffer` byte-buffer host object. es-toolkit constructs it
76    // (`Buffer.from`/`Buffer.alloc`/`Buffer.concat`) and inspects it via
77    // `Buffer.isBuffer(x)` / `value instanceof Buffer`, both of which resolve
78    // through this marker (see `buffer_constructor_expression` and
79    // `instance_of_text`). Modeled as a concrete byte-buffer record rather than
80    // a shapeless dynamic value.
81    host("Buffer", "__smelt_buffer"),
82    host("DataView", "__smelt_dataview"),
83    host("WeakMap", "__smelt_weakmap"),
84    host("WeakSet", "__smelt_weakset"),
85    host("File", "__smelt_file"),
86    host("Blob", "__smelt_blob"),
87    // Fetch API `Request` host object. Source code (es-toolkit's `isPlainObject`
88    // spec) constructs it only to probe host identity
89    // (`isPlainObject(new Request('...')) === false`); none of its structural
90    // surface is read, so it is a marker-only host object like `WeakMap` /
91    // `DataView`. `instanceof Request` resolves through this marker.
92    host("Request", "__smelt_request"),
93    host("DOMException", "__smelt_domexception"),
94    // ECMA-402 `Intl` namespace constructors. Source code constructs these only
95    // to probe host identity (`isPlainObject(new Intl.Locale('en')) === false`);
96    // none of their structural surface is read, so each is a marker-only host
97    // object keyed by its full qualified path (the construction site is always
98    // `new Intl.<Constructor>(...)`). `Intl.DateTimeFormat` and
99    // `Intl.RelativeTimeFormat` are deliberately absent: the opaque-formatter
100    // model claims them first and never stamps a marker (see
101    // `intl_date_time_format_constructor_expression`).
102    host("Intl.Collator", "__smelt_intl_collator"),
103    host("Intl.DisplayNames", "__smelt_intl_displaynames"),
104    host("Intl.DurationFormat", "__smelt_intl_durationformat"),
105    host("Intl.ListFormat", "__smelt_intl_listformat"),
106    host("Intl.Locale", "__smelt_intl_locale"),
107    host("Intl.NumberFormat", "__smelt_intl_numberformat"),
108    host("Intl.PluralRules", "__smelt_intl_pluralrules"),
109    host("Intl.Segmenter", "__smelt_intl_segmenter"),
110    boxed("Number", "__smelt_number"),
111    boxed("Boolean", "__smelt_boolean"),
112    boxed("String", "__smelt_string"),
113    boxed("Symbol", "__smelt_symbol"),
114];
115
116/// Look up the host-object identity for a JavaScript constructor name.
117///
118/// Returns `None` for names that are not modeled host objects so callers can fall
119/// through to their existing user-class / stdlib dispatch.
120#[must_use]
121pub fn host_object_by_class(class_name: &str) -> Option<&'static HostObject> {
122    HOST_OBJECTS
123        .iter()
124        .find(|entry| entry.class_name == class_name)
125}
126
127/// Return the identity marker key for a modeled host constructor, or `None`.
128///
129/// Thin convenience over [`host_object_by_class`] for callers that only need the
130/// marker string.
131#[must_use]
132pub fn host_object_marker(class_name: &str) -> Option<&'static str> {
133    host_object_by_class(class_name).map(|entry| entry.marker)
134}
135
136/// Every host-object identity marker key, for the runtime host-marker registry.
137///
138/// The generated runtime uses this to hide host records from `for-in` /
139/// `Object.keys` enumeration. It intentionally excludes markers owned by other
140/// subsystems (dates, errors, regexps, abort controllers, namespaces) which the
141/// runtime tracks through their own dedicated helpers.
142pub fn host_object_markers() -> impl Iterator<Item = &'static str> {
143    HOST_OBJECTS.iter().map(|entry| entry.marker)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    /// Every registry marker is a distinct `__smelt_`-prefixed key. Distinctness
151    /// is what makes `instanceof X` unambiguous, so a duplicate would silently
152    /// collide two host identities.
153    #[test]
154    fn markers_are_unique_and_prefixed() {
155        let mut seen = std::collections::HashSet::new();
156        for entry in HOST_OBJECTS {
157            assert!(
158                entry.marker.starts_with("__smelt_"),
159                "marker `{}` for `{}` must be `__smelt_`-prefixed",
160                entry.marker,
161                entry.class_name,
162            );
163            assert!(
164                seen.insert(entry.marker),
165                "duplicate host-object marker `{}`",
166                entry.marker,
167            );
168        }
169    }
170
171    /// Class-name lookup and marker lookup agree for every registry entry, so the
172    /// construction side (`by_class`) and the runtime registry (`markers`) stay in
173    /// lock-step.
174    #[test]
175    fn lookups_round_trip() {
176        for entry in HOST_OBJECTS {
177            assert_eq!(host_object_by_class(entry.class_name), Some(entry));
178            assert_eq!(host_object_marker(entry.class_name), Some(entry.marker));
179        }
180        assert_eq!(host_object_by_class("NotAHostObject"), None);
181        assert_eq!(host_object_marker("NotAHostObject"), None);
182    }
183
184    /// The boxed primitive wrappers are exactly `Number`/`Boolean`/`String`/
185    /// `Symbol`. Their objects have `typeof === "object"` so `instanceof` must
186    /// resolve through the marker while `typeof` narrowing misses them.
187    #[test]
188    fn boxed_primitive_wrappers_are_classified() {
189        let boxed = HOST_OBJECTS
190            .iter()
191            .filter(|entry| entry.is_boxed_primitive)
192            .map(|entry| entry.class_name)
193            .collect::<std::collections::HashSet<_>>();
194        assert_eq!(
195            boxed,
196            ["Number", "Boolean", "String", "Symbol"]
197                .into_iter()
198                .collect(),
199        );
200    }
201
202    /// `host_object_markers` yields the same set the entries carry, so the runtime
203    /// for-in filter hides every host record's internal marker key — including the
204    /// boxed-primitive markers that previously leaked as enumerable properties.
205    #[test]
206    fn markers_iterator_covers_boxed_primitives() {
207        let markers = host_object_markers().collect::<std::collections::HashSet<_>>();
208        for expected in ["__smelt_boolean", "__smelt_string", "__smelt_number"] {
209            assert!(
210                markers.contains(expected),
211                "runtime host-marker set must include `{expected}`",
212            );
213        }
214    }
215}