just_engine/runner/plugin/super_global.rs
1//! Super-global environment — the bottom of the scope chain.
2//!
3//! This environment sits below the global scope and lazily resolves
4//! built-in and plugin-provided objects on first access. Objects are
5//! cached after first resolution so each name is materialized at most once.
6//!
7//! ## How It Works
8//!
9//! When JavaScript code references a name that isn't found in the lexical
10//! environment chain or global scope, the super-global environment is consulted:
11//!
12//! ```text
13//! JavaScript: Math.abs(-5)
14//! ↓
15//! 1. Check local scope → not found
16//! 2. Check outer scopes → not found
17//! 3. Check global scope → not found
18//! 4. Check super-global → "Math" found!
19//! ↓
20//! 5. Query resolvers: Does anyone provide "Math"?
21//! 6. CorePluginResolver says "yes"
22//! 7. Cache the result
23//! 8. Dispatch Math.abs() via resolver
24//! ```
25//!
26//! ## Caching Strategy
27//!
28//! - **First lookup**: Query all resolvers in order, cache which one owns the name
29//! - **Subsequent lookups**: Use cached resolver index directly
30//! - **Method calls**: Can bypass object materialization entirely
31//!
32//! ## Example
33//!
34//! ```
35//! use just::runner::plugin::super_global::SuperGlobalEnvironment;
36//! use just::runner::plugin::resolver::PluginResolver;
37//! use just::runner::plugin::types::EvalContext;
38//! use just::runner::ds::value::{JsValue, JsNumberType};
39//! use just::runner::ds::error::JErrorType;
40//!
41//! struct MyPlugin;
42//!
43//! impl PluginResolver for MyPlugin {
44//! fn has_binding(&self, name: &str) -> bool {
45//! name == "MyObject"
46//! }
47//!
48//! fn resolve(&self, _name: &str, _ctx: &mut EvalContext) -> Result<JsValue, JErrorType> {
49//! Ok(JsValue::Number(JsNumberType::Integer(42)))
50//! }
51//!
52//! fn call_method(&self, _obj: &str, _method: &str, _ctx: &mut EvalContext,
53//! _this: JsValue, _args: Vec<JsValue>) -> Option<Result<JsValue, JErrorType>> {
54//! None
55//! }
56//!
57//! fn name(&self) -> &str { "my_plugin" }
58//! }
59//!
60//! let mut sg = SuperGlobalEnvironment::new();
61//! sg.add_resolver(Box::new(MyPlugin));
62//!
63//! // Now "MyObject" is available in the super-global scope
64//! ```
65
66use std::collections::HashMap;
67
68use crate::runner::ds::error::JErrorType;
69use crate::runner::ds::value::JsValue;
70use crate::runner::plugin::resolver::PluginResolver;
71use crate::runner::plugin::types::EvalContext;
72
73/// The super-global environment for lazy resolution of built-in objects.
74///
75/// This is the outermost scope in the resolution chain, sitting below even
76/// the global scope. It provides built-in objects (Math, console, etc.) and
77/// plugin-provided objects through a lazy resolution mechanism.
78///
79/// ## Key Features
80///
81/// - **Lazy Resolution**: Objects are only materialized when first accessed
82/// - **Caching**: Once resolved, values are cached for fast subsequent access
83/// - **Read-Only**: JavaScript code cannot create or modify super-global bindings
84/// - **Plugin System**: Multiple resolvers can be registered, queried in order
85/// - **Method Dispatch**: Efficient method calls without object materialization
86///
87/// ## Resolution Order
88///
89/// When a name is looked up:
90/// 1. Check if it's in the cache → return cached value
91/// 2. Query each resolver's `has_binding()` in registration order
92/// 3. First resolver that claims the name wins
93/// 4. Call resolver's `resolve()` to materialize the value
94/// 5. Cache the value and resolver index
95/// 6. Return the value
96///
97/// ## Method Call Optimization
98///
99/// For method calls like `Math.abs(-5)`, the super-global can dispatch directly
100/// to the resolver's `call_method()` without materializing the Math object,
101/// providing better performance for built-in method calls.
102///
103/// ## Thread Safety
104///
105/// This struct is typically wrapped in `Rc<RefCell<>>` (see [`SharedSuperGlobal`](super::types::SharedSuperGlobal))
106/// to allow shared mutable access across the evaluation context.
107pub struct SuperGlobalEnvironment {
108 /// Registered plugin resolvers, queried in order.
109 resolvers: Vec<Box<dyn PluginResolver>>,
110 /// Cache of already-resolved bindings (name → value).
111 cache: HashMap<String, JsValue>,
112 /// Cache of which resolver index owns which name.
113 resolver_map: HashMap<String, usize>,
114}
115
116impl SuperGlobalEnvironment {
117 pub fn new() -> Self {
118 SuperGlobalEnvironment {
119 resolvers: Vec::new(),
120 cache: HashMap::new(),
121 resolver_map: HashMap::new(),
122 }
123 }
124
125 /// Register a plugin resolver. Resolvers are queried in registration order.
126 pub fn add_resolver(&mut self, resolver: Box<dyn PluginResolver>) {
127 self.resolvers.push(resolver);
128 }
129
130 /// Find which resolver (if any) provides the given name.
131 fn find_resolver_index(&self, name: &str) -> Option<usize> {
132 // Check the cache first
133 if let Some(&idx) = self.resolver_map.get(name) {
134 return Some(idx);
135 }
136 // Query resolvers in order
137 for (i, resolver) in self.resolvers.iter().enumerate() {
138 if resolver.has_binding(name) {
139 return Some(i);
140 }
141 }
142 None
143 }
144
145 /// Call a method on a super-global object, dispatching to the owning resolver.
146 ///
147 /// Returns `None` if no resolver owns `object_name` or the resolver
148 /// doesn't provide the method (allowing fallback to property lookup).
149 pub fn call_method(
150 &self,
151 object_name: &str,
152 method_name: &str,
153 ctx: &mut EvalContext,
154 this: JsValue,
155 args: Vec<JsValue>,
156 ) -> Option<Result<JsValue, JErrorType>> {
157 let idx = if let Some(&idx) = self.resolver_map.get(object_name) {
158 idx
159 } else {
160 self.find_resolver_index(object_name)?
161 };
162 self.resolvers[idx].call_method(object_name, method_name, ctx, this, args)
163 }
164
165 /// Call a constructor on a super-global object.
166 pub fn call_constructor(
167 &self,
168 object_name: &str,
169 ctx: &mut EvalContext,
170 args: Vec<JsValue>,
171 ) -> Option<Result<JsValue, JErrorType>> {
172 let idx = if let Some(&idx) = self.resolver_map.get(object_name) {
173 idx
174 } else {
175 self.find_resolver_index(object_name)?
176 };
177 self.resolvers[idx].call_constructor(object_name, ctx, args)
178 }
179
180 /// Check if any resolver provides the given name.
181 pub fn has_name(&self, name: &str) -> bool {
182 self.cache.contains_key(name) || self.find_resolver_index(name).is_some()
183 }
184
185 /// Resolve a name, caching the result.
186 /// `ctx` is needed because some resolvers may need it during materialization.
187 pub fn resolve_binding(
188 &mut self,
189 name: &str,
190 ctx: &mut EvalContext,
191 ) -> Result<JsValue, JErrorType> {
192 // Return cached value if available
193 if let Some(val) = self.cache.get(name) {
194 return Ok(val.clone());
195 }
196
197 // Find the owning resolver
198 if let Some(idx) = self.find_resolver_index(name) {
199 let value = self.resolvers[idx].resolve(name, ctx)?;
200 self.cache.insert(name.to_string(), value.clone());
201 self.resolver_map.insert(name.to_string(), idx);
202 Ok(value)
203 } else {
204 Err(JErrorType::ReferenceError(format!(
205 "{} is not defined",
206 name
207 )))
208 }
209 }
210
211 /// Get a reference to the resolvers (for inspection/testing).
212 pub fn resolvers(&self) -> &[Box<dyn PluginResolver>] {
213 &self.resolvers
214 }
215}