Skip to main content

runmat_runtime/
workspace.rs

1use runmat_builtins::Value;
2
3#[cfg(test)]
4use once_cell::sync::Lazy;
5#[cfg(test)]
6use std::sync::Mutex;
7
8/// Resolver used by the runtime to access the caller workspace when builtins
9/// (such as `save`) need to look up variables by name.
10type AssignFn = fn(&str, Value) -> Result<(), String>;
11type ClearFn = fn() -> Result<(), String>;
12type RemoveFn = fn(&str) -> Result<(), String>;
13
14pub struct WorkspaceResolver {
15    pub lookup: fn(&str) -> Option<Value>,
16    pub snapshot: fn() -> Vec<(String, Value)>,
17    pub globals: fn() -> Vec<String>,
18    pub assign: Option<AssignFn>,
19    pub clear: Option<ClearFn>,
20    pub remove: Option<RemoveFn>,
21}
22
23mod resolver_storage {
24    use super::WorkspaceResolver;
25
26    pub(super) fn set(resolver: WorkspaceResolver) {
27        imp::set(resolver)
28    }
29
30    pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
31        imp::with(f)
32    }
33
34    #[cfg(test)]
35    mod imp {
36        use super::WorkspaceResolver;
37        use std::cell::RefCell;
38
39        // In tests, the resolver is frequently swapped by many modules. Using a global resolver
40        // makes tests flaky under the default parallel test runner.
41        // Thread-local storage matches the "resolver is tied to an executing context" model and
42        // avoids cross-test interference.
43        thread_local! {
44            static RESOLVER: RefCell<Option<WorkspaceResolver>> = const { RefCell::new(None) };
45        }
46
47        pub(super) fn set(resolver: WorkspaceResolver) {
48            RESOLVER.with(|slot| {
49                *slot.borrow_mut() = Some(resolver);
50            });
51        }
52
53        pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
54            RESOLVER.with(|slot| {
55                let guard = slot.borrow();
56                f(guard.as_ref())
57            })
58        }
59    }
60
61    #[cfg(not(test))]
62    mod imp {
63        use super::WorkspaceResolver;
64        use once_cell::sync::Lazy;
65        use std::sync::RwLock;
66
67        static RESOLVER: Lazy<RwLock<Option<WorkspaceResolver>>> = Lazy::new(|| RwLock::new(None));
68
69        pub(super) fn set(resolver: WorkspaceResolver) {
70            let mut guard = RESOLVER
71                .write()
72                .unwrap_or_else(|poison| poison.into_inner());
73            *guard = Some(resolver);
74        }
75
76        pub(super) fn with<R>(f: impl FnOnce(Option<&WorkspaceResolver>) -> R) -> R {
77            let guard = RESOLVER.read().unwrap_or_else(|poison| poison.into_inner());
78            f(guard.as_ref())
79        }
80    }
81}
82
83#[cfg(test)]
84static TEST_WORKSPACE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
85
86/// Register the workspace resolver. Ignition installs this once during
87/// initialization so that language builtins can query variables lazily.
88pub fn register_workspace_resolver(resolver: WorkspaceResolver) {
89    resolver_storage::set(resolver);
90}
91
92/// Lookup a variable by name in the active workspace.
93pub fn lookup(name: &str) -> Option<Value> {
94    resolver_storage::with(|resolver| resolver.and_then(|r| (r.lookup)(name)))
95}
96
97/// Snapshot the active workspace into a vector of `(name, value)` pairs.
98/// Returns `None` when no resolver/workspace is active.
99pub fn snapshot() -> Option<Vec<(String, Value)>> {
100    resolver_storage::with(|resolver| resolver.map(|r| (r.snapshot)()))
101}
102
103/// Return the list of global variable names visible to the active workspace.
104pub fn global_names() -> Vec<String> {
105    resolver_storage::with(|resolver| resolver.map(|r| (r.globals)()).unwrap_or_default())
106}
107
108pub fn assign(name: &str, value: Value) -> Result<(), String> {
109    resolver_storage::with(|resolver| {
110        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
111        let assign = resolver
112            .assign
113            .ok_or_else(|| "workspace assignment unavailable".to_string())?;
114        (assign)(name, value)
115    })
116}
117
118pub fn clear() -> Result<(), String> {
119    resolver_storage::with(|resolver| {
120        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
121        let clear = resolver
122            .clear
123            .ok_or_else(|| "workspace clearing unavailable".to_string())?;
124        (clear)()
125    })
126}
127
128pub fn remove(name: &str) -> Result<(), String> {
129    resolver_storage::with(|resolver| {
130        let resolver = resolver.ok_or_else(|| "workspace state unavailable".to_string())?;
131        let remove = resolver
132            .remove
133            .ok_or_else(|| "workspace removal unavailable".to_string())?;
134        (remove)(name)
135    })
136}
137
138/// Returns true when a resolver has been registered.
139pub fn is_available() -> bool {
140    resolver_storage::with(|resolver| resolver.is_some())
141}
142
143#[cfg(test)]
144pub(crate) fn test_guard() -> std::sync::MutexGuard<'static, ()> {
145    TEST_WORKSPACE_LOCK
146        .lock()
147        .unwrap_or_else(|poison| poison.into_inner())
148}