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 host("DOMException", "__smelt_domexception"),
88 boxed("Number", "__smelt_number"),
89 boxed("Boolean", "__smelt_boolean"),
90 boxed("String", "__smelt_string"),
91 boxed("Symbol", "__smelt_symbol"),
92];
93
94/// Look up the host-object identity for a JavaScript constructor name.
95///
96/// Returns `None` for names that are not modeled host objects so callers can fall
97/// through to their existing user-class / stdlib dispatch.
98#[must_use]
99pub fn host_object_by_class(class_name: &str) -> Option<&'static HostObject> {
100 HOST_OBJECTS
101 .iter()
102 .find(|entry| entry.class_name == class_name)
103}
104
105/// Return the identity marker key for a modeled host constructor, or `None`.
106///
107/// Thin convenience over [`host_object_by_class`] for callers that only need the
108/// marker string.
109#[must_use]
110pub fn host_object_marker(class_name: &str) -> Option<&'static str> {
111 host_object_by_class(class_name).map(|entry| entry.marker)
112}
113
114/// Every host-object identity marker key, for the runtime host-marker registry.
115///
116/// The generated runtime uses this to hide host records from `for-in` /
117/// `Object.keys` enumeration. It intentionally excludes markers owned by other
118/// subsystems (dates, errors, regexps, abort controllers, namespaces) which the
119/// runtime tracks through their own dedicated helpers.
120pub fn host_object_markers() -> impl Iterator<Item = &'static str> {
121 HOST_OBJECTS.iter().map(|entry| entry.marker)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 /// Every registry marker is a distinct `__smelt_`-prefixed key. Distinctness
129 /// is what makes `instanceof X` unambiguous, so a duplicate would silently
130 /// collide two host identities.
131 #[test]
132 fn markers_are_unique_and_prefixed() {
133 let mut seen = std::collections::HashSet::new();
134 for entry in HOST_OBJECTS {
135 assert!(
136 entry.marker.starts_with("__smelt_"),
137 "marker `{}` for `{}` must be `__smelt_`-prefixed",
138 entry.marker,
139 entry.class_name,
140 );
141 assert!(
142 seen.insert(entry.marker),
143 "duplicate host-object marker `{}`",
144 entry.marker,
145 );
146 }
147 }
148
149 /// Class-name lookup and marker lookup agree for every registry entry, so the
150 /// construction side (`by_class`) and the runtime registry (`markers`) stay in
151 /// lock-step.
152 #[test]
153 fn lookups_round_trip() {
154 for entry in HOST_OBJECTS {
155 assert_eq!(host_object_by_class(entry.class_name), Some(entry));
156 assert_eq!(host_object_marker(entry.class_name), Some(entry.marker));
157 }
158 assert_eq!(host_object_by_class("NotAHostObject"), None);
159 assert_eq!(host_object_marker("NotAHostObject"), None);
160 }
161
162 /// The boxed primitive wrappers are exactly `Number`/`Boolean`/`String`/
163 /// `Symbol`. Their objects have `typeof === "object"` so `instanceof` must
164 /// resolve through the marker while `typeof` narrowing misses them.
165 #[test]
166 fn boxed_primitive_wrappers_are_classified() {
167 let boxed = HOST_OBJECTS
168 .iter()
169 .filter(|entry| entry.is_boxed_primitive)
170 .map(|entry| entry.class_name)
171 .collect::<std::collections::HashSet<_>>();
172 assert_eq!(
173 boxed,
174 ["Number", "Boolean", "String", "Symbol"]
175 .into_iter()
176 .collect(),
177 );
178 }
179
180 /// `host_object_markers` yields the same set the entries carry, so the runtime
181 /// for-in filter hides every host record's internal marker key — including the
182 /// boxed-primitive markers that previously leaked as enumerable properties.
183 #[test]
184 fn markers_iterator_covers_boxed_primitives() {
185 let markers = host_object_markers().collect::<std::collections::HashSet<_>>();
186 for expected in ["__smelt_boolean", "__smelt_string", "__smelt_number"] {
187 assert!(
188 markers.contains(expected),
189 "runtime host-marker set must include `{expected}`",
190 );
191 }
192 }
193}