Skip to main content

runmat_runtime/context/
scope.rs

1use super::RuntimeContext;
2use runmat_thread_local::runmat_thread_local;
3use std::cell::RefCell;
4use std::future::Future;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8runmat_thread_local! {
9    static ACTIVE_CONTEXTS: RefCell<Vec<RuntimeContext>> = const { RefCell::new(Vec::new()) };
10}
11
12pub(super) fn active_runtime_context() -> Option<RuntimeContext> {
13    ACTIVE_CONTEXTS.with(|contexts| contexts.borrow().last().cloned())
14}
15
16#[must_use]
17pub struct RuntimeContextGuard {
18    state_identity: *const super::RuntimeContextState,
19}
20
21impl RuntimeContextGuard {
22    pub(crate) fn enter(context: RuntimeContext) -> Self {
23        let state_identity = context.state_identity();
24        ACTIVE_CONTEXTS.with(|contexts| contexts.borrow_mut().push(context));
25        Self { state_identity }
26    }
27}
28
29impl Drop for RuntimeContextGuard {
30    fn drop(&mut self) {
31        ACTIVE_CONTEXTS.with(|contexts| {
32            let popped = contexts.borrow_mut().pop();
33            debug_assert_eq!(
34                popped.as_ref().map(RuntimeContext::state_identity),
35                Some(self.state_identity),
36                "runtime contexts must unwind in stack order"
37            );
38        });
39    }
40}
41
42/// Future adapter that activates the same context for every poll and while the
43/// inner future is destroyed. It never keeps ambient state installed across a
44/// yield, so independently polled sessions cannot observe one another.
45pub struct ContextFuture<F> {
46    context: RuntimeContext,
47    future: Option<Pin<Box<F>>>,
48}
49
50impl<F> ContextFuture<F> {
51    pub(super) fn new(context: RuntimeContext, future: F) -> Self {
52        Self {
53            context,
54            future: Some(Box::pin(future)),
55        }
56    }
57}
58
59impl<F: Future> Future for ContextFuture<F> {
60    type Output = F::Output;
61
62    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
63        let this = self.as_mut().get_mut();
64        let _guard = RuntimeContextGuard::enter(this.context.clone());
65        this.future
66            .as_mut()
67            .expect("context future polled after completion")
68            .as_mut()
69            .poll(cx)
70    }
71}
72
73impl<F> Drop for ContextFuture<F> {
74    fn drop(&mut self) {
75        if let Some(future) = self.future.take() {
76            let _guard = RuntimeContextGuard::enter(self.context.clone());
77            drop(future);
78        }
79    }
80}
81
82impl<F> Unpin for ContextFuture<F> {}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::execution::RuntimeExecutionService;
88    use runmat_types::SourceId;
89    use std::rc::Rc;
90    use std::sync::Arc;
91    use std::task::Wake;
92
93    struct NoopWake;
94
95    impl Wake for NoopWake {
96        fn wake(self: Arc<Self>) {}
97    }
98
99    #[test]
100    fn synchronous_context_guard_restores_the_previous_context() {
101        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
102        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
103        let first_scope = first.execution().scope_id();
104        let second_scope = second.execution().scope_id();
105        {
106            let _first = first.enter();
107            assert_eq!(
108                active_runtime_context().unwrap().execution().scope_id(),
109                first_scope
110            );
111            {
112                let _second = second.enter();
113                assert_eq!(
114                    active_runtime_context().unwrap().execution().scope_id(),
115                    second_scope
116                );
117            }
118            assert_eq!(
119                active_runtime_context().unwrap().execution().scope_id(),
120                first_scope
121            );
122        }
123        assert!(active_runtime_context().is_none());
124    }
125
126    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
127    #[cfg_attr(not(target_arch = "wasm32"), test)]
128    fn interleaved_context_futures_restore_the_active_session() {
129        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
130        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
131        let first_scope = first.execution().scope_id();
132        let second_scope = second.execution().scope_id();
133
134        let mut first_future = Box::pin(first.scope(std::future::poll_fn(move |_| {
135            assert_eq!(
136                active_runtime_context()
137                    .expect("first context active")
138                    .execution()
139                    .scope_id(),
140                first_scope
141            );
142            Poll::<()>::Pending
143        })));
144        let mut second_future = Box::pin(second.scope(std::future::poll_fn(move |_| {
145            assert_eq!(
146                active_runtime_context()
147                    .expect("second context active")
148                    .execution()
149                    .scope_id(),
150                second_scope
151            );
152            Poll::<()>::Pending
153        })));
154        let waker = std::task::Waker::from(Arc::new(NoopWake));
155        let mut cx = Context::from_waker(&waker);
156
157        assert!(first_future.as_mut().poll(&mut cx).is_pending());
158        assert!(active_runtime_context().is_none());
159        assert!(second_future.as_mut().poll(&mut cx).is_pending());
160        assert!(active_runtime_context().is_none());
161    }
162
163    #[test]
164    fn interleaved_futures_isolate_source_output_and_cancellation_state() {
165        async fn session_future(source_id: SourceId, source: &'static str, outputs: usize) {
166            let _catalog = crate::source_context::replace_source_catalog(vec![(
167                source_id,
168                format!("session-{}.m", source_id.0),
169                source.to_string(),
170            )]);
171            let _source = crate::source_context::replace_current_source_id(Some(source_id));
172            let _outputs = crate::output_count::push_output_count(Some(outputs));
173            let mut yielded = false;
174            std::future::poll_fn(move |_| {
175                if yielded {
176                    Poll::Ready(())
177                } else {
178                    yielded = true;
179                    Poll::Pending
180                }
181            })
182            .await;
183            assert_eq!(
184                crate::source_context::current_source().as_deref(),
185                Some(source)
186            );
187            assert_eq!(crate::output_count::current_output_count(), Some(outputs));
188            assert!(!crate::interrupt::is_cancelled());
189        }
190
191        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
192        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
193        first
194            .cancellation()
195            .store(true, std::sync::atomic::Ordering::Relaxed);
196        let mut first_future = Box::pin(first.scope(session_future(SourceId(1), "first", 1)));
197        let mut second_future = Box::pin(second.scope(session_future(SourceId(2), "second", 2)));
198        let waker = std::task::Waker::from(Arc::new(NoopWake));
199        let mut cx = Context::from_waker(&waker);
200
201        assert!(first_future.as_mut().poll(&mut cx).is_pending());
202        assert!(second_future.as_mut().poll(&mut cx).is_pending());
203        first
204            .cancellation()
205            .store(false, std::sync::atomic::Ordering::Relaxed);
206        assert!(first_future.as_mut().poll(&mut cx).is_ready());
207        assert!(second_future.as_mut().poll(&mut cx).is_ready());
208        assert!(crate::source_context::current_source().is_none());
209        assert_eq!(crate::output_count::current_output_count(), None);
210    }
211
212    #[test]
213    fn interleaved_futures_isolate_host_interaction_handlers() {
214        async fn session_future(label: &'static str) {
215            let handler: Arc<crate::interaction::AsyncInteractionHandler> = Arc::new(move |_| {
216                Box::pin(async move {
217                    Ok(crate::interaction::InteractionResponse::Line(
218                        label.to_string(),
219                    ))
220                })
221            });
222            let _handler = crate::interaction::replace_async_handler(Some(handler));
223            let mut yielded = false;
224            std::future::poll_fn(move |_| {
225                if yielded {
226                    Poll::Ready(())
227                } else {
228                    yielded = true;
229                    Poll::Pending
230                }
231            })
232            .await;
233            assert_eq!(
234                crate::interaction::request_line_async("", false)
235                    .await
236                    .unwrap(),
237                label
238            );
239        }
240
241        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
242        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
243        let mut first_future = Box::pin(first.scope(session_future("first")));
244        let mut second_future = Box::pin(second.scope(session_future("second")));
245        let waker = std::task::Waker::from(Arc::new(NoopWake));
246        let mut cx = Context::from_waker(&waker);
247        assert!(first_future.as_mut().poll(&mut cx).is_pending());
248        assert!(second_future.as_mut().poll(&mut cx).is_pending());
249        assert!(first_future.as_mut().poll(&mut cx).is_ready());
250        assert!(second_future.as_mut().poll(&mut cx).is_ready());
251    }
252
253    #[test]
254    fn dropping_scoped_future_activates_its_context_for_guard_cleanup() {
255        struct DropProbe(runmat_execution::ExecutionScopeId);
256
257        impl Drop for DropProbe {
258            fn drop(&mut self) {
259                assert_eq!(
260                    active_runtime_context()
261                        .expect("context active while inner future drops")
262                        .execution()
263                        .scope_id(),
264                    self.0
265                );
266            }
267        }
268
269        let runtime = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
270        let scope_id = runtime.execution().scope_id();
271        let future = async move {
272            let _probe = DropProbe(scope_id);
273            std::future::pending::<()>().await;
274        };
275        let mut future = Box::pin(runtime.scope(future));
276        let waker = std::task::Waker::from(Arc::new(NoopWake));
277        let mut cx = Context::from_waker(&waker);
278        assert!(future.as_mut().poll(&mut cx).is_pending());
279        drop(future);
280        assert!(active_runtime_context().is_none());
281    }
282
283    #[test]
284    fn workspace_resolver_registration_is_context_scoped_in_production_shape() {
285        fn first_lookup(name: &str) -> Option<runmat_value::Value> {
286            (name == "x").then_some(runmat_value::Value::Num(1.0))
287        }
288        fn second_lookup(name: &str) -> Option<runmat_value::Value> {
289            (name == "x").then_some(runmat_value::Value::Num(2.0))
290        }
291        fn empty_snapshot() -> Vec<(String, runmat_value::Value)> {
292            Vec::new()
293        }
294        fn empty_globals() -> Vec<String> {
295            Vec::new()
296        }
297
298        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
299        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
300        {
301            let _scope = RuntimeContextGuard::enter(first.clone());
302            crate::workspace::register_workspace_resolver(crate::workspace::WorkspaceResolver {
303                lookup: first_lookup,
304                snapshot: empty_snapshot,
305                globals: empty_globals,
306                assign: None,
307                clear: None,
308                remove: None,
309            });
310        }
311        {
312            let _scope = RuntimeContextGuard::enter(second.clone());
313            crate::workspace::register_workspace_resolver(crate::workspace::WorkspaceResolver {
314                lookup: second_lookup,
315                snapshot: empty_snapshot,
316                globals: empty_globals,
317                assign: None,
318                clear: None,
319                remove: None,
320            });
321        }
322        {
323            let _scope = RuntimeContextGuard::enter(first);
324            assert_eq!(
325                crate::workspace::lookup("x"),
326                Some(runmat_value::Value::Num(1.0))
327            );
328        }
329        {
330            let _scope = RuntimeContextGuard::enter(second);
331            assert_eq!(
332                crate::workspace::lookup("x"),
333                Some(runmat_value::Value::Num(2.0))
334            );
335        }
336    }
337
338    #[test]
339    fn class_and_static_property_state_is_session_owned() {
340        fn class(name: &str) -> crate::class_registry::RuntimeClass {
341            crate::class_registry::RuntimeClass {
342                name: name.to_string(),
343                parent: None,
344                properties: std::collections::HashMap::from([(
345                    "answer".to_string(),
346                    crate::class_registry::RuntimeProperty {
347                        name: "answer".to_string(),
348                        is_static: true,
349                        is_constant: false,
350                        is_dependent: false,
351                        get_access: runmat_types::MemberAccess::Public,
352                        set_access: runmat_types::MemberAccess::Public,
353                        default_value: None,
354                    },
355                )]),
356                methods: std::collections::HashMap::new(),
357            }
358        }
359
360        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
361        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
362        {
363            let _scope = RuntimeContextGuard::enter(first.clone());
364            crate::class_registry::register_class(class("OnlyFirst"));
365            crate::class_registry::set_static_property_value(
366                "OnlyFirst",
367                "answer",
368                runmat_value::Value::Num(1.0),
369            );
370        }
371        {
372            let _scope = RuntimeContextGuard::enter(second);
373            assert!(crate::class_registry::get_class("OnlyFirst").is_none());
374            assert!(
375                crate::class_registry::get_static_property_value("OnlyFirst", "answer").is_none()
376            );
377        }
378        {
379            let _scope = RuntimeContextGuard::enter(first);
380            assert!(crate::class_registry::get_class("OnlyFirst").is_some());
381            assert_eq!(
382                crate::class_registry::get_static_property_value("OnlyFirst", "answer"),
383                Some(runmat_value::Value::Num(1.0))
384            );
385        }
386    }
387
388    #[test]
389    fn lazy_builtin_class_registration_is_session_owned() {
390        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
391        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
392        {
393            let _scope = RuntimeContextGuard::enter(first.clone());
394            crate::testing::ensure_testing_classes();
395            assert!(crate::class_registry::get_class(crate::testing::TEST_CASE_CLASS).is_some());
396        }
397        {
398            let _scope = RuntimeContextGuard::enter(second.clone());
399            assert!(crate::class_registry::get_class(crate::testing::TEST_CASE_CLASS).is_none());
400            crate::testing::ensure_testing_classes();
401            assert!(crate::class_registry::get_class(crate::testing::TEST_CASE_CLASS).is_some());
402        }
403        {
404            let _scope = RuntimeContextGuard::enter(first);
405            assert!(crate::class_registry::get_class(crate::testing::TEST_CASE_CLASS).is_some());
406        }
407    }
408
409    #[test]
410    fn error_and_compatibility_policy_is_session_owned() {
411        let first = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
412        let second = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
413        first.set_error_namespace("First");
414        first.set_callstack_limit(11);
415        second.set_error_namespace("Second");
416        second.set_callstack_limit(22);
417        {
418            let _scope = RuntimeContextGuard::enter(first);
419            crate::compatibility::set_runmat_extensions_enabled(true);
420            assert_eq!(active_runtime_context().unwrap().error_namespace(), "First");
421            assert_eq!(active_runtime_context().unwrap().callstack_limit(), 11);
422            assert!(crate::compatibility::runmat_extensions_enabled());
423        }
424        {
425            let _scope = RuntimeContextGuard::enter(second);
426            assert_eq!(
427                active_runtime_context().unwrap().error_namespace(),
428                "Second"
429            );
430            assert_eq!(active_runtime_context().unwrap().callstack_limit(), 22);
431            assert!(!crate::compatibility::runmat_extensions_enabled());
432        }
433    }
434}