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}