runmat_runtime/context/
scope.rs1use 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
42pub 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}