Skip to main content

runmat_runtime/
workspace.rs

1use runmat_value::Value;
2
3pub mod session;
4
5#[cfg(test)]
6use once_cell::sync::Lazy;
7#[cfg(test)]
8use std::sync::Mutex;
9
10/// Resolver used by the runtime to access the caller workspace when builtins
11/// (such as `save`) need to look up variables by name.
12type AssignFn = fn(&str, Value) -> Result<(), String>;
13type ClearFn = fn() -> Result<(), String>;
14type RemoveFn = fn(&str) -> Result<(), String>;
15
16#[derive(Clone, Copy, Debug)]
17pub struct WorkspaceResolver {
18    pub lookup: fn(&str) -> Option<Value>,
19    pub snapshot: fn() -> Vec<(String, Value)>,
20    pub globals: fn() -> Vec<String>,
21    pub assign: Option<AssignFn>,
22    pub clear: Option<ClearFn>,
23    pub remove: Option<RemoveFn>,
24}
25
26mod resolver_storage {
27    use super::WorkspaceResolver;
28
29    pub(super) fn set(resolver: WorkspaceResolver) {
30        imp::set(resolver)
31    }
32
33    pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
34        imp::with(f)
35    }
36
37    #[cfg(test)]
38    mod imp {
39        use super::WorkspaceResolver;
40        use std::cell::RefCell;
41
42        // In tests, the resolver is frequently swapped by many modules. Using a global resolver
43        // makes tests flaky under the default parallel test runner.
44        // Thread-local storage matches the "resolver is tied to an executing context" model and
45        // avoids cross-test interference.
46        thread_local! {
47            static RESOLVER: RefCell<Option<WorkspaceResolver>> = const { RefCell::new(None) };
48        }
49
50        pub(super) fn set(resolver: WorkspaceResolver) {
51            RESOLVER.with(|slot| {
52                *slot.borrow_mut() = Some(resolver);
53            });
54        }
55
56        pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
57            RESOLVER.with(|slot| {
58                let guard = slot.borrow();
59                f(guard.as_ref())
60            })
61        }
62    }
63
64    #[cfg(not(test))]
65    mod imp {
66        use super::WorkspaceResolver;
67        use once_cell::sync::Lazy;
68        use std::sync::RwLock;
69
70        static RESOLVER: Lazy<RwLock<Option<WorkspaceResolver>>> = Lazy::new(|| RwLock::new(None));
71
72        pub(super) fn set(resolver: WorkspaceResolver) {
73            let mut guard = RESOLVER
74                .write()
75                .unwrap_or_else(|poison| poison.into_inner());
76            *guard = Some(resolver);
77        }
78
79        pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
80            let guard = RESOLVER.read().unwrap_or_else(|poison| poison.into_inner());
81            f(guard.as_ref())
82        }
83    }
84}
85
86#[cfg(test)]
87static TEST_WORKSPACE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
88
89/// Register the workspace resolver. The VM installs this once during
90/// initialization so that language builtins can query variables lazily.
91pub fn register_workspace_resolver(resolver: WorkspaceResolver) {
92    if let Some(context) = crate::context::legacy::active() {
93        context.state().workspace.replace(Some(resolver));
94    }
95    // The VM resolver consists only of context-independent function pointers. Retain it as the
96    // standalone fallback as well: one-time registration may occur while a RuntimeContext is
97    // active, and later interpreter sessions must not depend on that first context's lifetime.
98    resolver_storage::set(resolver);
99}
100
101/// Lookup a variable by name in the active workspace.
102pub fn lookup(name: &str) -> Option<Value> {
103    if let Some(context) = crate::context::legacy::active() {
104        if let Some(service) = context.service_ports().workspace() {
105            return service.lookup(name);
106        }
107        if let Some(resolver) = context.state().workspace.borrow().as_ref() {
108            return (resolver.lookup)(name);
109        }
110    }
111    resolver_storage::with(|resolver| resolver.and_then(|r| (r.lookup)(name)))
112}
113
114/// Snapshot the active workspace into a vector of `(name, value)` pairs.
115/// Returns `None` when no resolver/workspace is active.
116pub fn snapshot() -> Option<Vec<(String, Value)>> {
117    if let Some(context) = crate::context::legacy::active() {
118        if let Some(service) = context.service_ports().workspace() {
119            return Some(service.snapshot());
120        }
121        if let Some(resolver) = context.state().workspace.borrow().as_ref() {
122            return Some((resolver.snapshot)());
123        }
124    }
125    resolver_storage::with(|resolver| resolver.map(|r| (r.snapshot)()))
126}
127
128/// Return the list of global variable names visible to the active workspace.
129pub fn global_names() -> Vec<String> {
130    if let Some(context) = crate::context::legacy::active() {
131        if let Some(service) = context.service_ports().workspace() {
132            return service.global_names();
133        }
134        if let Some(resolver) = context.state().workspace.borrow().as_ref() {
135            return (resolver.globals)();
136        }
137    }
138    resolver_storage::with(|resolver| resolver.map(|r| (r.globals)()).unwrap_or_default())
139}
140
141pub fn assign(name: &str, value: Value) -> Result<(), String> {
142    if let Some(context) = crate::context::legacy::active() {
143        if let Some(service) = context.service_ports().workspace() {
144            return service
145                .assign(name, value)
146                .map_err(|error| error.to_string());
147        }
148        if context.state().workspace.borrow().is_some() {
149            return with_context_resolver(&context, |resolver| {
150                let assign = resolver
151                    .assign
152                    .ok_or_else(|| "workspace assignment unavailable".to_string())?;
153                assign(name, value)
154            });
155        }
156    }
157    resolver_storage::with(|resolver| {
158        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
159        let assign = resolver
160            .assign
161            .ok_or_else(|| "workspace assignment unavailable".to_string())?;
162        (assign)(name, value)
163    })
164}
165
166pub fn clear() -> Result<(), String> {
167    if let Some(context) = crate::context::legacy::active() {
168        if let Some(service) = context.service_ports().workspace() {
169            return service.clear().map_err(|error| error.to_string());
170        }
171        if context.state().workspace.borrow().is_some() {
172            return with_context_resolver(&context, |resolver| {
173                let clear = resolver
174                    .clear
175                    .ok_or_else(|| "workspace clearing unavailable".to_string())?;
176                clear()
177            });
178        }
179    }
180    resolver_storage::with(|resolver| {
181        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
182        let clear = resolver
183            .clear
184            .ok_or_else(|| "workspace clearing unavailable".to_string())?;
185        (clear)()
186    })
187}
188
189pub fn remove(name: &str) -> Result<(), String> {
190    if let Some(context) = crate::context::legacy::active() {
191        if let Some(service) = context.service_ports().workspace() {
192            return service.remove(name).map_err(|error| error.to_string());
193        }
194        if context.state().workspace.borrow().is_some() {
195            return with_context_resolver(&context, |resolver| {
196                let remove = resolver
197                    .remove
198                    .ok_or_else(|| "workspace removal unavailable".to_string())?;
199                remove(name)
200            });
201        }
202    }
203    resolver_storage::with(|resolver| {
204        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
205        let remove = resolver
206            .remove
207            .ok_or_else(|| "workspace removal unavailable".to_string())?;
208        (remove)(name)
209    })
210}
211
212/// Returns true when a resolver has been registered.
213pub fn is_available() -> bool {
214    if let Some(context) = crate::context::legacy::active() {
215        if context.service_ports().workspace().is_some()
216            || context.state().workspace.borrow().is_some()
217        {
218            return true;
219        }
220    }
221    resolver_storage::with(|resolver| resolver.is_some())
222}
223
224fn with_context_resolver<R>(
225    context: &crate::context::RuntimeContext,
226    callback: impl FnOnce(&WorkspaceResolver) -> Result<R, String>,
227) -> Result<R, String> {
228    let resolver = context.state().workspace.borrow();
229    callback(
230        resolver
231            .as_ref()
232            .ok_or_else(|| "workspace state unavailable".to_string())?,
233    )
234}
235
236#[cfg(test)]
237pub(crate) fn test_guard() -> std::sync::MutexGuard<'static, ()> {
238    TEST_WORKSPACE_LOCK
239        .lock()
240        .unwrap_or_else(|poison| poison.into_inner())
241}