Skip to main content

sim_lib_lang_javascript/
objects.rs

1//! JavaScript object policy over the shared property and managed-identity organs.
2
3use crate::{JavascriptHeap, JavascriptManagedKind, JavascriptManagedObject, JavascriptValue};
4use sim_lib_dispatch::{
5    AccessContext, AccessError, AccessorDescriptor, DataDescriptor, DefineError, Descriptor,
6    PropertyHook, PropertyStore,
7};
8use sim_lib_mutation::{ArenaError, ManagedHandle, ManagedId};
9use std::collections::{BTreeMap, HashSet};
10
11/// JavaScript property keys. Private names are deliberately class-scoped and
12/// cannot be manufactured from strings.
13#[derive(Clone, Debug, Eq, Hash, PartialEq)]
14pub enum JavascriptPropertyKey {
15    /// String property.
16    String(String),
17    /// Symbol identity (ordered after strings).
18    Symbol(u64),
19    /// Declared private name, paired with its declaring class identity.
20    Private {
21        /// Declaring class brand.
22        class: ManagedId,
23        /// Source-level declared name.
24        name: String,
25    },
26}
27
28/// The ordinary function forms admitted by this profile.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum JavascriptFunctionKind {
31    /// Ordinary dynamically-received function.
32    Function,
33    /// Lexically-received arrow closure.
34    Arrow,
35    /// Class constructor, callable only through construction.
36    ClassConstructor,
37}
38
39/// Inspectable callable metadata. Executable bodies remain codec-lowered forms;
40/// this record owns only binding and construction policy.
41#[derive(Clone, Debug)]
42pub struct JavascriptFunction {
43    /// Function form.
44    pub kind: JavascriptFunctionKind,
45    /// Captured lexical environment.
46    pub environment: ManagedHandle,
47    /// Whether `new` is legal.
48    pub constructable: bool,
49    /// Declared private names for a class constructor.
50    pub private_names: Vec<String>,
51}
52
53/// Receiver selected for a call.
54#[derive(Clone, Debug, PartialEq)]
55pub enum JavascriptThis {
56    /// Arrow functions retain their lexical receiver.
57    Lexical(JavascriptValue),
58    /// Ordinary calls receive the call-site receiver.
59    Dynamic(JavascriptValue),
60}
61
62/// Explicit object-model gaps. These are queryable rather than silent partial
63/// emulation of invariants that ordinary objects cannot satisfy.
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum JavascriptObjectGap {
66    /// Proxy trap invariants are not emulated by ordinary descriptors.
67    ProxyInvariants,
68    /// Host and specification exotic internal methods are unsupported.
69    ExoticObjectInvariants,
70}
71
72/// Error from JavaScript-owned prototype or descriptor policy.
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub enum JavascriptObjectError {
75    /// Shared managed arena rejected an operation.
76    Arena(ArenaError),
77    /// Descriptor invariant was violated.
78    Descriptor(DefineError),
79    /// Prototype traversal or interception exceeded its explicit bound.
80    Access,
81    /// A prototype cycle was requested.
82    PrototypeCycle,
83    /// `new` was applied to a non-constructor.
84    NotConstructor,
85    /// A private name was used outside its declaring class.
86    PrivateBrand,
87}
88impl From<ArenaError> for JavascriptObjectError {
89    fn from(v: ArenaError) -> Self {
90        Self::Arena(v)
91    }
92}
93impl From<DefineError> for JavascriptObjectError {
94    fn from(v: DefineError) -> Self {
95        Self::Descriptor(v)
96    }
97}
98
99#[derive(Clone, Debug, PartialEq)]
100struct Accessor {
101    get: Option<JavascriptValue>,
102    set: bool,
103}
104#[derive(Default)]
105struct Hooks {
106    writes: Vec<(ManagedId, JavascriptPropertyKey, JavascriptValue)>,
107}
108impl PropertyHook<ManagedId, JavascriptPropertyKey, JavascriptValue, Accessor> for Hooks {
109    type Error = ();
110    fn get(
111        &mut self,
112        _: &mut AccessContext<ManagedId, JavascriptPropertyKey>,
113        hook: &Accessor,
114        _: &ManagedId,
115        _: &JavascriptPropertyKey,
116    ) -> Result<JavascriptValue, AccessError<()>> {
117        hook.get.clone().ok_or(AccessError::Hook(()))
118    }
119    fn set(
120        &mut self,
121        _: &mut AccessContext<ManagedId, JavascriptPropertyKey>,
122        hook: &Accessor,
123        receiver: &ManagedId,
124        key: &JavascriptPropertyKey,
125        value: JavascriptValue,
126    ) -> Result<(), AccessError<()>> {
127        if !hook.set {
128            return Err(AccessError::Hook(()));
129        }
130        self.writes.push((*receiver, key.clone(), value));
131        Ok(())
132    }
133}
134
135/// Ordinary-object policy composed from `PropertyStore` and `JavascriptHeap`.
136pub struct JavascriptObjects {
137    heap: JavascriptHeap,
138    properties: PropertyStore<ManagedId, JavascriptPropertyKey, JavascriptValue, Accessor>,
139    prototypes: BTreeMap<ManagedId, ManagedHandle>,
140    functions: BTreeMap<ManagedId, JavascriptFunction>,
141    lexical_this: BTreeMap<ManagedId, JavascriptValue>,
142}
143impl JavascriptObjects {
144    /// Create an object graph using the supplied shared heap.
145    pub fn new(heap: JavascriptHeap) -> Self {
146        Self {
147            heap,
148            properties: PropertyStore::new(),
149            prototypes: BTreeMap::new(),
150            functions: BTreeMap::new(),
151            lexical_this: BTreeMap::new(),
152        }
153    }
154    /// Allocate an ordinary object or array. Arrays use the same object and
155    /// descriptor mechanics; only key ordering differs.
156    pub fn ordinary(&mut self) -> Result<ManagedHandle, JavascriptObjectError> {
157        Ok(self.heap.allocate(JavascriptManagedObject::default())?)
158    }
159    /// Allocate a closure/function object and connect it to its environment.
160    pub fn function(
161        &mut self,
162        function: JavascriptFunction,
163        lexical_this: Option<JavascriptValue>,
164    ) -> Result<ManagedHandle, JavascriptObjectError> {
165        let h = self.heap.allocate(JavascriptManagedObject {
166            kind: JavascriptManagedKind::Function,
167            edges: vec![function.environment.id()],
168        })?;
169        if let Some(v) = lexical_this {
170            self.lexical_this.insert(h.id(), v);
171        }
172        self.functions.insert(h.id(), function);
173        Ok(h)
174    }
175    /// Select ECMAScript `this` policy without constraining the callable Shape.
176    pub fn call_this(
177        &self,
178        function: ManagedHandle,
179        receiver: JavascriptValue,
180    ) -> Result<JavascriptThis, JavascriptObjectError> {
181        let f = self
182            .functions
183            .get(&function.id())
184            .ok_or(JavascriptObjectError::NotConstructor)?;
185        Ok(if f.kind == JavascriptFunctionKind::Arrow {
186            JavascriptThis::Lexical(
187                self.lexical_this
188                    .get(&function.id())
189                    .cloned()
190                    .unwrap_or(JavascriptValue::Undefined),
191            )
192        } else {
193            JavascriptThis::Dynamic(receiver)
194        })
195    }
196    /// Invoke a codec-lowered body with the captured environment, selected
197    /// receiver, and call arguments. The caller supplies evaluation, keeping
198    /// executable behavior in the direct evaluator rather than this policy.
199    pub fn call<T, E>(
200        &self,
201        function: ManagedHandle,
202        receiver: JavascriptValue,
203        arguments: &[JavascriptValue],
204        body: impl FnOnce(ManagedHandle, JavascriptThis, &[JavascriptValue]) -> Result<T, E>,
205    ) -> Result<T, E>
206    where
207        E: From<JavascriptObjectError>,
208    {
209        let metadata = self
210            .functions
211            .get(&function.id())
212            .ok_or(JavascriptObjectError::NotConstructor)?;
213        let this = self.call_this(function, receiver)?;
214        body(metadata.environment, this, arguments)
215    }
216    /// Allocate the receiver for `new` and link it to the constructor prototype.
217    pub fn construct(
218        &mut self,
219        function: ManagedHandle,
220    ) -> Result<ManagedHandle, JavascriptObjectError> {
221        let f = self
222            .functions
223            .get(&function.id())
224            .ok_or(JavascriptObjectError::NotConstructor)?;
225        if !f.constructable || f.kind == JavascriptFunctionKind::Arrow {
226            return Err(JavascriptObjectError::NotConstructor);
227        }
228        let instance = self.ordinary()?;
229        self.set_prototype(instance, function)?;
230        Ok(instance)
231    }
232    /// Set an ordinary prototype, rejecting cycles.
233    pub fn set_prototype(
234        &mut self,
235        object: ManagedHandle,
236        prototype: ManagedHandle,
237    ) -> Result<(), JavascriptObjectError> {
238        let mut at = Some(prototype);
239        let mut seen = HashSet::new();
240        while let Some(h) = at {
241            if h.id() == object.id() || !seen.insert(h.id()) {
242                return Err(JavascriptObjectError::PrototypeCycle);
243            }
244            at = self.prototypes.get(&h.id()).copied();
245        }
246        self.heap.connect(object, prototype)?;
247        self.prototypes.insert(object.id(), prototype);
248        Ok(())
249    }
250    /// Define an ordinary data property.
251    pub fn define_data(
252        &mut self,
253        object: ManagedHandle,
254        key: JavascriptPropertyKey,
255        value: JavascriptValue,
256        writable: bool,
257        enumerable: bool,
258        configurable: bool,
259    ) -> Result<(), JavascriptObjectError> {
260        self.properties.define(
261            &object.id(),
262            key,
263            Descriptor::Data(DataDescriptor {
264                value,
265                writable,
266                enumerable,
267                configurable,
268            }),
269        )?;
270        Ok(())
271    }
272    /// Define a bounded accessor property.
273    pub fn define_accessor(
274        &mut self,
275        object: ManagedHandle,
276        key: JavascriptPropertyKey,
277        get: Option<JavascriptValue>,
278        set: bool,
279        enumerable: bool,
280        configurable: bool,
281    ) -> Result<(), JavascriptObjectError> {
282        self.properties.define(
283            &object.id(),
284            key,
285            Descriptor::Accessor(AccessorDescriptor {
286                get: Some(Accessor { get, set }),
287                set: Some(Accessor { get: None, set }),
288                enumerable,
289                configurable,
290            }),
291        )?;
292        Ok(())
293    }
294    fn chain(
295        &self,
296        object: ManagedHandle,
297        budget: usize,
298    ) -> Result<Vec<ManagedId>, JavascriptObjectError> {
299        let mut out = Vec::new();
300        let mut at = Some(object);
301        let mut seen = HashSet::new();
302        while let Some(h) = at {
303            if out.len() >= budget {
304                return Err(JavascriptObjectError::Access);
305            }
306            if !seen.insert(h.id()) {
307                break;
308            }
309            out.push(h.id());
310            at = self.prototypes.get(&h.id()).copied();
311        }
312        Ok(out)
313    }
314    /// Read through the prototype chain with the original receiver.
315    pub fn get(
316        &self,
317        object: ManagedHandle,
318        key: &JavascriptPropertyKey,
319        budget: usize,
320    ) -> Result<Option<JavascriptValue>, JavascriptObjectError> {
321        let chain = self.chain(object, budget)?;
322        let mut hooks = Hooks::default();
323        self.properties
324            .get(
325                &chain,
326                &object.id(),
327                key,
328                &mut AccessContext::new(budget),
329                &mut hooks,
330            )
331            .map_err(|_| JavascriptObjectError::Access)
332    }
333    /// Assign through the first descriptor in the prototype chain. Setter
334    /// hooks retain the original receiver and share the traversal budget.
335    pub fn set(
336        &mut self,
337        object: ManagedHandle,
338        key: &JavascriptPropertyKey,
339        value: JavascriptValue,
340        budget: usize,
341    ) -> Result<bool, JavascriptObjectError> {
342        let chain = self.chain(object, budget)?;
343        let mut hooks = Hooks::default();
344        self.properties
345            .set(
346                &chain,
347                &object.id(),
348                key,
349                value,
350                &mut AccessContext::new(budget),
351                &mut hooks,
352            )
353            .map_err(|_| JavascriptObjectError::Access)
354    }
355    /// Delete an own property, respecting configurability.
356    pub fn delete(
357        &mut self,
358        object: ManagedHandle,
359        key: &JavascriptPropertyKey,
360    ) -> Result<bool, JavascriptObjectError> {
361        Ok(self.properties.delete(&object.id(), key)?)
362    }
363    /// ECMAScript ordinary enumeration order: array-index strings ascending,
364    /// then other strings in definition order, then symbols. Private names are hidden.
365    pub fn enumerable_keys(&self, object: ManagedHandle) -> Vec<JavascriptPropertyKey> {
366        let keys = self.properties.own_keys(&object.id(), true);
367        let mut indices = Vec::new();
368        let mut strings = Vec::new();
369        let mut symbols = Vec::new();
370        for key in keys {
371            match &key {
372                JavascriptPropertyKey::String(s) => match s.parse::<u32>() {
373                    Ok(n) if n != u32::MAX && n.to_string() == *s => indices.push((n, key)),
374                    _ => strings.push(key),
375                },
376                JavascriptPropertyKey::Symbol(_) => symbols.push(key),
377                JavascriptPropertyKey::Private { .. } => {}
378            }
379        }
380        indices.sort_by_key(|(n, _)| *n);
381        indices
382            .into_iter()
383            .map(|(_, k)| k)
384            .chain(strings)
385            .chain(symbols)
386            .collect()
387    }
388    /// Validate a declared private name against an instance's constructor brand.
389    pub fn private_key(
390        &self,
391        class: ManagedHandle,
392        name: &str,
393    ) -> Result<JavascriptPropertyKey, JavascriptObjectError> {
394        let f = self
395            .functions
396            .get(&class.id())
397            .ok_or(JavascriptObjectError::PrivateBrand)?;
398        if !f.private_names.iter().any(|n| n == name) {
399            return Err(JavascriptObjectError::PrivateBrand);
400        }
401        Ok(JavascriptPropertyKey::Private {
402            class: class.id(),
403            name: name.into(),
404        })
405    }
406    /// Collect unreachable function/environment/prototype/accessor/array cycles.
407    pub fn collect(
408        &mut self,
409    ) -> Result<Option<sim_lib_gc_tracing::CollectionReceipt>, sim_lib_gc_tracing::CollectionError>
410    {
411        self.heap.collect()
412    }
413    /// Number of live managed identities.
414    pub fn live_len(&self) -> usize {
415        self.heap.live_len()
416    }
417}
418
419/// Callable browse policy is intentionally neutral: no parameter or return
420/// constraints are synthesized from JavaScript or TypeScript syntax.
421pub const fn javascript_callable_shape_constraints() -> &'static [&'static str] {
422    &[]
423}
424/// Unsupported ordinary-object boundary.
425pub const fn javascript_object_gaps() -> &'static [JavascriptObjectGap] {
426    &[
427        JavascriptObjectGap::ProxyInvariants,
428        JavascriptObjectGap::ExoticObjectInvariants,
429    ]
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use sim_lib_gc_tracing::CollectionLimits;
436    fn model() -> JavascriptObjects {
437        JavascriptObjects::new(
438            JavascriptHeap::standard(
439                32,
440                CollectionLimits {
441                    objects: 32,
442                    edges: 64,
443                    stack: 32,
444                    work: 256,
445                    clears: 32,
446                    finalizers: 0,
447                },
448            )
449            .unwrap(),
450        )
451    }
452    fn s(v: &str) -> JavascriptPropertyKey {
453        JavascriptPropertyKey::String(v.into())
454    }
455    #[test]
456    fn descriptors_prototypes_arrays_and_private_names_are_shared_mechanics() {
457        let mut m = model();
458        let env = m.ordinary().unwrap();
459        let class = m
460            .function(
461                JavascriptFunction {
462                    kind: JavascriptFunctionKind::ClassConstructor,
463                    environment: env,
464                    constructable: true,
465                    private_names: vec!["x".into()],
466                },
467                None,
468            )
469            .unwrap();
470        m.define_data(
471            class,
472            s("inherited"),
473            JavascriptValue::Number(7.0),
474            false,
475            true,
476            false,
477        )
478        .unwrap();
479        let o = m.construct(class).unwrap();
480        assert_eq!(
481            m.get(o, &s("inherited"), 8).unwrap(),
482            Some(JavascriptValue::Number(7.0))
483        );
484        m.define_data(o, s("10"), JavascriptValue::Null, true, true, true)
485            .unwrap();
486        m.define_data(o, s("2"), JavascriptValue::Null, true, true, true)
487            .unwrap();
488        m.define_accessor(
489            o,
490            s("answer"),
491            Some(JavascriptValue::Number(42.0)),
492            false,
493            true,
494            true,
495        )
496        .unwrap();
497        assert_eq!(m.enumerable_keys(o), vec![s("2"), s("10"), s("answer")]);
498        assert_eq!(
499            m.get(o, &s("answer"), 8).unwrap(),
500            Some(JavascriptValue::Number(42.0))
501        );
502        assert!(m.private_key(class, "x").is_ok());
503        assert!(m.private_key(class, "y").is_err());
504        assert!(m.delete(o, &s("answer")).unwrap());
505    }
506    #[test]
507    fn functions_arrows_construction_shapes_and_gaps_are_explicit() {
508        let mut m = model();
509        let env = m.ordinary().unwrap();
510        let arrow = m
511            .function(
512                JavascriptFunction {
513                    kind: JavascriptFunctionKind::Arrow,
514                    environment: env,
515                    constructable: false,
516                    private_names: vec![],
517                },
518                Some(JavascriptValue::String("lexical".into())),
519            )
520            .unwrap();
521        assert_eq!(
522            m.call_this(arrow, JavascriptValue::String("dynamic".into()))
523                .unwrap(),
524            JavascriptThis::Lexical(JavascriptValue::String("lexical".into()))
525        );
526        let called = m
527            .call(
528                arrow,
529                JavascriptValue::Undefined,
530                &[JavascriptValue::Number(42.0)],
531                |captured, this, arguments| {
532                    Ok::<_, JavascriptObjectError>((captured, this, arguments[0].clone()))
533                },
534            )
535            .unwrap();
536        assert_eq!(called.0, env);
537        assert_eq!(called.2, JavascriptValue::Number(42.0));
538        assert_eq!(
539            m.construct(arrow),
540            Err(JavascriptObjectError::NotConstructor)
541        );
542        assert!(javascript_callable_shape_constraints().is_empty());
543        assert_eq!(javascript_object_gaps().len(), 2);
544    }
545    #[test]
546    fn mixed_language_cycles_reclaim_without_changing_observed_values() {
547        let mut m = model();
548        let env = m.ordinary().unwrap();
549        let f = m
550            .function(
551                JavascriptFunction {
552                    kind: JavascriptFunctionKind::Function,
553                    environment: env,
554                    constructable: true,
555                    private_names: vec![],
556                },
557                None,
558            )
559            .unwrap();
560        let array = m.ordinary().unwrap();
561        m.set_prototype(array, f).unwrap();
562        m.define_accessor(
563            array,
564            s("stable"),
565            Some(JavascriptValue::Number(42.0)),
566            false,
567            true,
568            true,
569        )
570        .unwrap();
571        assert_eq!(
572            m.get(array, &s("stable"), 8).unwrap(),
573            Some(JavascriptValue::Number(42.0))
574        );
575        assert_eq!(m.live_len(), 3);
576        assert_eq!(m.collect().unwrap().unwrap().swept.len(), 3);
577        assert_eq!(m.live_len(), 0);
578    }
579}