1use std::cell::{Cell, Ref, RefCell, RefMut};
2use std::collections::{BTreeMap, HashMap};
3use std::ops::{Deref, DerefMut};
4use std::path::PathBuf;
5use std::rc::{Rc, Weak};
6
7use web_time::Instant;
8
9use crate::runtime::{
10 apply_dynamic_mutations, DynamicStackIdentities, DynamicTaskState, ModuleTaskState,
11 NativeCallContext, ScopeId, TaskContextHandle,
12};
13use crate::{CallFrame, Env, Sandbox, SemaError, Span, SpanMap, StackTrace, Value};
14
15const MAX_SPAN_TABLE_ENTRIES: usize = 200_000;
16
17pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
19
20pub type MacroExpandCallbackFn =
24 for<'a> fn(&NativeCallContext<'a>, &Value, &Env) -> Result<Value, SemaError>;
25
26pub type CallCallbackFn = fn(&EvalContext, &Value, &[Value]) -> Result<Value, SemaError>;
28
29pub type CallOwnedCallbackFn = fn(&EvalContext, &Value, &mut [Value]) -> Result<Value, SemaError>;
33
34type InterpreterTeardownHook = Box<dyn Fn()>;
35
36pub type ContextStackMap = BTreeMap<Value, Vec<Value>>;
37
38#[derive(Default)]
42pub struct ContextStacks {
43 values: RefCell<ContextStackMap>,
44 identities: RefCell<DynamicStackIdentities>,
45}
46
47impl ContextStacks {
48 pub fn borrow(&self) -> Ref<'_, ContextStackMap> {
49 self.values.borrow()
50 }
51
52 pub fn borrow_mut(&self) -> ContextStacksMut<'_> {
53 ContextStacksMut {
54 values: self.values.borrow_mut(),
55 identities: self.identities.borrow_mut(),
56 }
57 }
58
59 fn snapshot(&self) -> (ContextStackMap, DynamicStackIdentities) {
60 (
61 self.values.borrow().clone(),
62 self.identities.borrow().clone(),
63 )
64 }
65
66 fn borrow_parts_mut(
67 &self,
68 ) -> (
69 RefMut<'_, ContextStackMap>,
70 RefMut<'_, DynamicStackIdentities>,
71 ) {
72 (self.values.borrow_mut(), self.identities.borrow_mut())
73 }
74}
75
76pub struct ContextStacksMut<'a> {
77 values: RefMut<'a, ContextStackMap>,
78 identities: RefMut<'a, DynamicStackIdentities>,
79}
80
81impl Deref for ContextStacksMut<'_> {
82 type Target = ContextStackMap;
83
84 fn deref(&self) -> &Self::Target {
85 &self.values
86 }
87}
88
89impl DerefMut for ContextStacksMut<'_> {
90 fn deref_mut(&mut self) -> &mut Self::Target {
91 &mut self.values
92 }
93}
94
95impl Drop for ContextStacksMut<'_> {
96 fn drop(&mut self) {
97 *self.identities = DynamicStackIdentities::from_stacks(&self.values);
98 }
99}
100
101pub struct EvalContext {
102 pub module_cache: RefCell<BTreeMap<PathBuf, BTreeMap<String, Value>>>,
103 pub embedded_files: RefCell<BTreeMap<PathBuf, Vec<u8>>>,
104 embedded_files_only: Cell<bool>,
105 pub current_file: RefCell<Vec<PathBuf>>,
106 pub module_exports: RefCell<Vec<Option<Vec<String>>>>,
107 pub module_load_stack: RefCell<Vec<PathBuf>>,
108 pub call_stack: RefCell<Vec<CallFrame>>,
109 pub span_table: RefCell<HashMap<usize, Span>>,
110 pub eval_depth: Cell<usize>,
111 pub max_eval_depth: Cell<usize>,
112 pub eval_step_limit: Cell<usize>,
113 pub eval_steps: Cell<usize>,
114 pub eval_deadline: Cell<Option<Instant>>,
119 pub sandbox: Sandbox,
120 pub user_context: RefCell<Vec<BTreeMap<Value, Value>>>,
121 pub hidden_context: RefCell<Vec<BTreeMap<Value, Value>>>,
122 pub context_stacks: ContextStacks,
123 signal_callbacks: RefCell<[Vec<Value>; 3]>,
127 interpreter_teardown_hooks: RefCell<Vec<InterpreterTeardownHook>>,
130 pub eval_fn: Cell<Option<EvalCallbackFn>>,
131 macro_expand_fn: Cell<Option<MacroExpandCallbackFn>>,
132 pub call_fn: Cell<Option<CallCallbackFn>>,
133 pub call_owned_fn: Cell<Option<CallOwnedCallbackFn>>,
134 pub interactive: Cell<bool>,
135 task_context: RefCell<Option<InstalledTaskContext>>,
136 legacy_call_env: RefCell<Option<Weak<Env>>>,
137 runtime_quantum_active: Cell<bool>,
138}
139
140#[derive(Clone)]
141struct InstalledTaskContext {
142 handle: TaskContextHandle,
143 dynamic: Option<Rc<DynamicTaskState>>,
149 module: Option<Rc<ModuleTaskState>>,
150}
151
152impl InstalledTaskContext {
153 fn new(handle: TaskContextHandle) -> Self {
154 let dynamic = handle.get_rc::<DynamicTaskState>();
155 let module = handle.get_rc::<ModuleTaskState>();
156 Self {
157 handle,
158 dynamic,
159 module,
160 }
161 }
162}
163
164pub struct ModuleLoadGuard<'a> {
168 ctx: &'a EvalContext,
169 scope: ModuleLoadScope,
170}
171
172#[derive(Debug)]
173enum ModuleLoadScope {
174 Ambient(PathBuf),
175 Task {
176 state: Rc<ModuleTaskState>,
177 scope: ScopeId,
178 },
179}
180
181pub struct TaskContextGuard<'a> {
182 ctx: &'a EvalContext,
183 previous: Option<InstalledTaskContext>,
184}
185
186#[doc(hidden)]
190#[must_use = "the call environment guard must live for the native invocation"]
191pub struct LegacyCallEnvGuard<'a> {
192 ctx: &'a EvalContext,
193 previous: Option<Weak<Env>>,
194}
195
196impl Drop for TaskContextGuard<'_> {
197 fn drop(&mut self) {
198 self.ctx.task_context.replace(self.previous.take());
199 }
200}
201
202impl Drop for LegacyCallEnvGuard<'_> {
203 fn drop(&mut self) {
204 self.ctx.legacy_call_env.replace(self.previous.take());
205 }
206}
207
208impl Drop for ModuleLoadGuard<'_> {
209 fn drop(&mut self) {
210 match &self.scope {
211 ModuleLoadScope::Ambient(path) => self.ctx.end_module_load(path),
212 ModuleLoadScope::Task { state, scope } => {
213 state.remove_loading(*scope);
214 }
215 }
216 }
217}
218
219fn check_module_cycle(stack: &[PathBuf], path: &PathBuf) -> Result<(), SemaError> {
220 let Some(pos) = stack.iter().position(|candidate| candidate == path) else {
221 return Ok(());
222 };
223 let mut cycle: Vec<String> = stack[pos..]
224 .iter()
225 .map(|entry| entry.display().to_string())
226 .collect();
227 cycle.push(path.display().to_string());
228 Err(SemaError::eval(format!(
229 "cyclic import detected: {}",
230 cycle.join(" -> ")
231 )))
232}
233
234impl EvalContext {
235 pub fn new() -> Self {
236 Self::new_with_sandbox(Sandbox::allow_all())
237 }
238
239 pub fn new_with_sandbox(sandbox: Sandbox) -> Self {
240 EvalContext {
241 module_cache: RefCell::new(BTreeMap::new()),
242 embedded_files: RefCell::new(BTreeMap::new()),
243 embedded_files_only: Cell::new(false),
244 current_file: RefCell::new(Vec::new()),
245 module_exports: RefCell::new(Vec::new()),
246 module_load_stack: RefCell::new(Vec::new()),
247 call_stack: RefCell::new(Vec::new()),
248 span_table: RefCell::new(HashMap::new()),
249 eval_depth: Cell::new(0),
250 max_eval_depth: Cell::new(0),
251 eval_step_limit: Cell::new(0),
252 eval_steps: Cell::new(0),
253 eval_deadline: Cell::new(None),
254 sandbox,
255 user_context: RefCell::new(vec![BTreeMap::new()]),
256 hidden_context: RefCell::new(vec![BTreeMap::new()]),
257 context_stacks: ContextStacks::default(),
258 signal_callbacks: RefCell::default(),
259 interpreter_teardown_hooks: RefCell::default(),
260 eval_fn: Cell::new(None),
261 macro_expand_fn: Cell::new(None),
262 call_fn: Cell::new(None),
263 call_owned_fn: Cell::new(None),
264 interactive: Cell::new(false),
265 task_context: RefCell::new(None),
266 legacy_call_env: RefCell::new(None),
267 runtime_quantum_active: Cell::new(false),
268 }
269 }
270
271 pub fn task_context(&self) -> Option<TaskContextHandle> {
272 self.task_context
273 .borrow()
274 .as_ref()
275 .map(|installed| installed.handle.clone())
276 }
277
278 pub fn task_context_installed_is(&self, handle: &TaskContextHandle) -> bool {
284 match self.task_context.borrow().as_ref() {
285 Some(installed) => installed.handle.ptr_eq(handle),
286 None => false,
287 }
288 }
289
290 pub fn install_task_context(&self, handle: TaskContextHandle) -> Option<TaskContextHandle> {
291 self.task_context
292 .replace(Some(InstalledTaskContext::new(handle)))
293 .map(|installed| installed.handle)
294 }
295
296 pub fn scope_task_context(&self, handle: TaskContextHandle) -> TaskContextGuard<'_> {
297 TaskContextGuard {
298 ctx: self,
299 previous: self
300 .task_context
301 .replace(Some(InstalledTaskContext::new(handle))),
302 }
303 }
304
305 #[doc(hidden)]
309 pub fn scope_legacy_call_env(&self, env: &Rc<Env>) -> LegacyCallEnvGuard<'_> {
310 LegacyCallEnvGuard {
311 ctx: self,
312 previous: self.legacy_call_env.replace(Some(Rc::downgrade(env))),
313 }
314 }
315
316 #[doc(hidden)]
319 pub fn legacy_call_env(&self) -> Option<Rc<Env>> {
320 self.legacy_call_env
321 .borrow()
322 .as_ref()
323 .and_then(Weak::upgrade)
324 }
325
326 fn dynamic_task_state(&self) -> Option<Rc<DynamicTaskState>> {
327 self.task_context
328 .borrow()
329 .as_ref()
330 .and_then(|installed| installed.dynamic.clone())
331 }
332
333 fn module_task_state(&self) -> Option<Rc<ModuleTaskState>> {
334 self.task_context
335 .borrow()
336 .as_ref()
337 .and_then(|installed| installed.module.clone())
338 }
339
340 pub fn enter_runtime_quantum(&self) -> Result<RuntimeQuantumGuard<'_>, SemaError> {
341 if self.runtime_quantum_active.replace(true) {
342 return Err(SemaError::eval(
343 "internal error: runtime VM quantum is already active",
344 ));
345 }
346 let previous_thread_local = crate::in_runtime_quantum();
350 crate::set_runtime_quantum(true);
351 Ok(RuntimeQuantumGuard {
352 ctx: self,
353 previous_thread_local,
354 })
355 }
356
357 pub fn runtime_quantum_active(&self) -> bool {
358 self.runtime_quantum_active.get()
359 }
360
361 pub fn take_task_context(&self) -> Option<TaskContextHandle> {
362 self.task_context
363 .borrow_mut()
364 .take()
365 .map(|installed| installed.handle)
366 }
367
368 #[doc(hidden)]
369 pub fn register_signal_callback(&self, signal_index: usize, callback: Value) {
370 self.signal_callbacks.borrow_mut()[signal_index].push(callback);
371 }
372
373 #[doc(hidden)]
374 pub fn signal_callbacks(&self, signal_index: usize) -> Vec<Value> {
375 self.signal_callbacks.borrow()[signal_index].clone()
376 }
377
378 #[doc(hidden)]
379 pub fn register_interpreter_teardown_hook(&self, hook: impl Fn() + 'static) {
380 self.interpreter_teardown_hooks
381 .borrow_mut()
382 .push(Box::new(hook));
383 }
384
385 #[doc(hidden)]
386 pub fn try_run_interpreter_teardown_hooks(&self) -> bool {
387 let hooks = match self.interpreter_teardown_hooks.try_borrow_mut() {
388 Ok(mut hooks) => std::mem::take(&mut *hooks),
389 Err(_) => return false,
390 };
391 for hook in hooks {
392 hook();
393 }
394 true
395 }
396
397 #[doc(hidden)]
399 pub fn register_signal_teardown_hook(&self, hook: impl Fn() + 'static) {
400 self.register_interpreter_teardown_hook(hook);
401 }
402
403 #[doc(hidden)]
405 pub fn try_run_signal_teardown_hooks(&self) -> bool {
406 self.try_run_interpreter_teardown_hooks()
407 }
408
409 #[doc(hidden)]
410 pub fn clear_signal_callbacks(&self) {
411 for callbacks in self.signal_callbacks.borrow_mut().iter_mut() {
412 callbacks.clear();
413 }
414 }
415
416 pub fn push_file_path(&self, path: PathBuf) {
417 if let Some(state) = self.module_task_state() {
418 state
419 .push_current_file(path)
420 .expect("module current-file scope identity exhausted");
421 return;
422 }
423 self.current_file.borrow_mut().push(path);
424 }
425
426 pub fn pop_file_path(&self) {
427 if let Some(state) = self.module_task_state() {
428 state.pop_current_file();
429 return;
430 }
431 self.current_file.borrow_mut().pop();
432 }
433
434 pub fn current_file_dir(&self) -> Option<PathBuf> {
435 self.current_file_path()
436 .and_then(|path| path.parent().map(|dir| dir.to_path_buf()))
437 }
438
439 pub fn current_file_path(&self) -> Option<PathBuf> {
440 if let Some(state) = self.module_task_state() {
441 return state.current_file();
442 }
443 self.current_file.borrow().last().cloned()
444 }
445
446 pub fn get_cached_module(&self, path: &PathBuf) -> Option<BTreeMap<String, Value>> {
447 self.module_cache.borrow().get(path).cloned()
448 }
449
450 pub fn cache_module(&self, path: PathBuf, exports: BTreeMap<String, Value>) {
451 self.module_cache.borrow_mut().insert(path, exports);
452 }
453
454 pub fn clear_module_cache(&self) {
455 self.module_cache.borrow_mut().clear();
456 }
457
458 pub fn embedded_file_exists(&self, path: &PathBuf) -> bool {
459 self.embedded_files.borrow().contains_key(path)
460 }
461
462 pub fn get_embedded_file(&self, path: &PathBuf) -> Option<Vec<u8>> {
463 self.embedded_files.borrow().get(path).cloned()
464 }
465
466 pub fn set_embedded_file(&self, path: PathBuf, bytes: Vec<u8>) {
467 self.embedded_files.borrow_mut().insert(path, bytes);
468 }
469
470 pub fn clear_embedded_files(&self) {
471 self.embedded_files.borrow_mut().clear();
472 }
473
474 pub fn set_embedded_files_only(&self, enabled: bool) {
476 self.embedded_files_only.set(enabled);
477 }
478
479 pub fn embedded_files_only(&self) -> bool {
480 self.embedded_files_only.get()
481 }
482
483 pub fn set_module_exports(&self, names: Vec<String>) {
484 if let Some(state) = self.module_task_state() {
485 state.set_current_exports(names);
486 return;
487 }
488 let mut stack = self.module_exports.borrow_mut();
489 if let Some(top) = stack.last_mut() {
490 *top = Some(names);
491 }
492 }
493
494 pub fn clear_module_exports(&self) {
495 if let Some(state) = self.module_task_state() {
496 state
497 .push_exports(None)
498 .expect("module export scope identity exhausted");
499 return;
500 }
501 self.module_exports.borrow_mut().push(None);
502 }
503
504 pub fn take_module_exports(&self) -> Option<Vec<String>> {
505 if let Some(state) = self.module_task_state() {
506 return state.pop_exports().flatten();
507 }
508 self.module_exports.borrow_mut().pop().flatten()
509 }
510
511 pub fn enter_module_load(&self, path: PathBuf) -> Result<ModuleLoadGuard<'_>, SemaError> {
515 let scope = self.begin_module_load(&path)?;
516 Ok(ModuleLoadGuard { ctx: self, scope })
517 }
518
519 fn begin_module_load(&self, path: &PathBuf) -> Result<ModuleLoadScope, SemaError> {
520 if let Some(state) = self.module_task_state() {
521 check_module_cycle(&state.loading(), path)?;
522 let scope = state
523 .push_loading(path.clone())
524 .map_err(|error| SemaError::eval(format!("module load scope: {error}")))?;
525 return Ok(ModuleLoadScope::Task { state, scope });
526 }
527 check_module_cycle(&self.module_load_stack.borrow(), path)?;
528 self.module_load_stack.borrow_mut().push(path.clone());
529 Ok(ModuleLoadScope::Ambient(path.clone()))
530 }
531
532 fn end_module_load(&self, path: &PathBuf) {
533 let mut stack = self.module_load_stack.borrow_mut();
534 if matches!(stack.last(), Some(last) if last == path) {
535 stack.pop();
536 } else if let Some(pos) = stack.iter().rposition(|p| p == path) {
537 stack.remove(pos);
538 }
539 }
540
541 pub fn push_call_frame(&self, frame: CallFrame) {
542 self.call_stack.borrow_mut().push(frame);
543 }
544
545 pub fn call_stack_depth(&self) -> usize {
546 self.call_stack.borrow().len()
547 }
548
549 pub fn truncate_call_stack(&self, depth: usize) {
550 self.call_stack.borrow_mut().truncate(depth);
551 }
552
553 pub fn capture_stack_trace(&self) -> StackTrace {
554 let stack = self.call_stack.borrow();
555 StackTrace(stack.iter().rev().cloned().collect())
556 }
557
558 pub fn merge_span_table(&self, spans: SpanMap) {
559 let mut table = self.span_table.borrow_mut();
560 if table.len() < MAX_SPAN_TABLE_ENTRIES {
561 table.extend(spans);
562 }
563 }
565
566 pub fn lookup_span(&self, ptr: usize) -> Option<Span> {
567 self.span_table.borrow().get(&ptr).cloned()
568 }
569
570 pub fn set_eval_step_limit(&self, limit: usize) {
571 self.eval_step_limit.set(limit);
572 }
573
574 pub fn set_eval_deadline(&self, deadline: Option<Instant>) {
577 self.eval_deadline.set(deadline);
578 }
579
580 #[inline]
582 pub fn deadline_exceeded(&self) -> bool {
583 match self.eval_deadline.get() {
584 Some(d) => Instant::now() >= d,
585 None => false,
586 }
587 }
588
589 #[inline]
591 pub fn check_deadline(&self) -> Result<(), SemaError> {
592 if self.deadline_exceeded() {
593 Err(SemaError::eval(
594 "evaluation exceeded time budget (looks like an infinite loop?)".to_string(),
595 ))
596 } else {
597 Ok(())
598 }
599 }
600
601 #[inline]
612 pub fn check_loop_interrupt(&self) -> Result<(), SemaError> {
613 let steps = self.eval_steps.get().wrapping_add(1);
614 self.eval_steps.set(steps);
615 let limit = self.eval_step_limit.get();
616 if limit != 0 && steps > limit {
617 return Err(SemaError::eval(
618 "evaluation exceeded step limit (looks like an infinite loop?)".to_string(),
619 ));
620 }
621 if steps & 0x3FFF == 0 {
622 if self.deadline_exceeded() {
623 return Err(SemaError::eval(
624 "evaluation exceeded time budget (looks like an infinite loop?)".to_string(),
625 ));
626 }
627 if crate::async_signal::check_interrupt() {
628 return Err(SemaError::eval("evaluation cancelled".to_string()));
629 }
630 }
631 Ok(())
632 }
633
634 pub fn context_get(&self, key: &Value) -> Option<Value> {
637 if let Some(state) = self.dynamic_task_state() {
638 return state.user_get(key);
639 }
640 let frames = self.user_context.borrow();
641 for frame in frames.iter().rev() {
642 if let Some(v) = frame.get(key) {
643 return Some(v.clone());
644 }
645 }
646 None
647 }
648
649 pub fn context_set(&self, key: Value, value: Value) {
650 if let Some(state) = self.dynamic_task_state() {
651 state.user_set(key, value);
652 return;
653 }
654 let mut frames = self.user_context.borrow_mut();
655 if let Some(top) = frames.last_mut() {
656 top.insert(key, value);
657 }
658 }
659
660 pub fn context_has(&self, key: &Value) -> bool {
661 if let Some(state) = self.dynamic_task_state() {
662 return state.user_get(key).is_some();
663 }
664 let frames = self.user_context.borrow();
665 frames.iter().any(|frame| frame.contains_key(key))
666 }
667
668 pub fn context_remove(&self, key: &Value) -> Option<Value> {
669 if let Some(state) = self.dynamic_task_state() {
670 return state.user_remove(key);
671 }
672 let mut frames = self.user_context.borrow_mut();
673 let mut first_found = None;
674 for frame in frames.iter_mut().rev() {
675 if let Some(v) = frame.remove(key) {
676 if first_found.is_none() {
677 first_found = Some(v);
678 }
679 }
680 }
681 first_found
682 }
683
684 pub fn context_all(&self) -> BTreeMap<Value, Value> {
685 if let Some(state) = self.dynamic_task_state() {
686 return state.user_all();
687 }
688 let frames = self.user_context.borrow();
689 let mut merged = BTreeMap::new();
690 for frame in frames.iter() {
691 for (k, v) in frame {
692 merged.insert(k.clone(), v.clone());
693 }
694 }
695 merged
696 }
697
698 pub fn context_push_frame(&self) {
699 if let Some(state) = self.dynamic_task_state() {
700 state
701 .push_user_frame(BTreeMap::new())
702 .expect("dynamic user-context scope identity exhausted");
703 return;
704 }
705 self.user_context.borrow_mut().push(BTreeMap::new());
706 }
707
708 pub fn context_push_frame_with(&self, bindings: BTreeMap<Value, Value>) {
709 if let Some(state) = self.dynamic_task_state() {
710 state
711 .push_user_frame(bindings)
712 .expect("dynamic user-context scope identity exhausted");
713 return;
714 }
715 self.user_context.borrow_mut().push(bindings);
716 }
717
718 pub fn context_pop_frame(&self) {
719 if let Some(state) = self.dynamic_task_state() {
720 state.pop_user_frame();
721 return;
722 }
723 let mut frames = self.user_context.borrow_mut();
724 if frames.len() > 1 {
725 frames.pop();
726 }
727 }
728
729 pub fn context_clear(&self) {
730 if let Some(state) = self.dynamic_task_state() {
731 state.user_clear();
732 return;
733 }
734 let mut frames = self.user_context.borrow_mut();
735 frames.clear();
736 frames.push(BTreeMap::new());
737 }
738
739 pub fn hidden_get(&self, key: &Value) -> Option<Value> {
742 if let Some(state) = self.dynamic_task_state() {
743 return state.hidden_get(key);
744 }
745 let frames = self.hidden_context.borrow();
746 for frame in frames.iter().rev() {
747 if let Some(v) = frame.get(key) {
748 return Some(v.clone());
749 }
750 }
751 None
752 }
753
754 pub fn hidden_set(&self, key: Value, value: Value) {
755 if let Some(state) = self.dynamic_task_state() {
756 state.hidden_set(key, value);
757 return;
758 }
759 let mut frames = self.hidden_context.borrow_mut();
760 if let Some(top) = frames.last_mut() {
761 top.insert(key, value);
762 }
763 }
764
765 pub fn hidden_has(&self, key: &Value) -> bool {
766 if let Some(state) = self.dynamic_task_state() {
767 return state.hidden_get(key).is_some();
768 }
769 let frames = self.hidden_context.borrow();
770 frames.iter().any(|frame| frame.contains_key(key))
771 }
772
773 pub fn hidden_push_frame(&self) {
774 if let Some(state) = self.dynamic_task_state() {
775 state
776 .push_hidden_frame(BTreeMap::new())
777 .expect("dynamic hidden-context scope identity exhausted");
778 return;
779 }
780 self.hidden_context.borrow_mut().push(BTreeMap::new());
781 }
782
783 pub fn hidden_pop_frame(&self) {
784 if let Some(state) = self.dynamic_task_state() {
785 state.pop_hidden_frame();
786 return;
787 }
788 let mut frames = self.hidden_context.borrow_mut();
789 if frames.len() > 1 {
790 frames.pop();
791 }
792 }
793
794 #[doc(hidden)]
800 pub fn snapshot_dynamic_task_state(&self) -> DynamicTaskState {
801 let user_frames = self.user_context.borrow().clone();
802 let hidden_frames = self.hidden_context.borrow().clone();
803 let (stacks, identities) = self.context_stacks.snapshot();
804 DynamicTaskState::root_with_stack_identities(
805 user_frames,
806 hidden_frames,
807 stacks,
808 &identities,
809 )
810 }
811
812 #[doc(hidden)]
815 pub fn snapshot_module_task_state(&self) -> ModuleTaskState {
816 ModuleTaskState::from_snapshot(
817 self.current_file.borrow().clone(),
818 self.module_load_stack.borrow().clone(),
819 self.module_exports.borrow().clone(),
820 )
821 }
822
823 #[doc(hidden)]
826 pub fn publish_dynamic_task_state(&self, state: &DynamicTaskState) -> bool {
827 let mut user_frames = self.user_context.borrow_mut();
828 let mut hidden_frames = self.hidden_context.borrow_mut();
829 let (mut stacks, mut identities) = self.context_stacks.borrow_parts_mut();
830 assert!(
831 identities.matches_stacks(&stacks),
832 "dynamic stack identities must match their value entries"
833 );
834 let Some(mutations) = state.drain_mutations() else {
835 return false;
836 };
837 apply_dynamic_mutations(
838 &mut user_frames,
839 &mut hidden_frames,
840 &mut stacks,
841 &mut identities,
842 &mutations,
843 );
844 true
845 }
846
847 pub fn context_stack_push(&self, key: Value, value: Value) {
848 if let Some(state) = self.dynamic_task_state() {
849 state
850 .stack_push(key, value)
851 .expect("dynamic context-stack scope identity exhausted");
852 return;
853 }
854 let (mut stacks, mut identities) = self.context_stacks.borrow_parts_mut();
855 stacks.entry(key.clone()).or_default().push(value);
856 identities.push(key);
857 }
858
859 pub fn context_stack_get(&self, key: &Value) -> Vec<Value> {
860 if let Some(state) = self.dynamic_task_state() {
861 return state.stack_get(key);
862 }
863 self.context_stacks
864 .borrow()
865 .get(key)
866 .cloned()
867 .unwrap_or_default()
868 }
869
870 pub fn context_stack_pop(&self, key: &Value) -> Option<Value> {
871 if let Some(state) = self.dynamic_task_state() {
872 return state.stack_pop(key);
873 }
874 let (mut stacks, mut identities) = self.context_stacks.borrow_parts_mut();
875 let stack = stacks.get_mut(key)?;
876 let val = stack.pop();
877 if val.is_some() {
878 assert!(
879 identities.pop(key),
880 "dynamic stack identity must have a matching value entry"
881 );
882 }
883 if stack.is_empty() {
884 stacks.remove(key);
885 identities.remove(key);
886 }
887 val
888 }
889}
890
891pub struct RuntimeQuantumGuard<'a> {
892 ctx: &'a EvalContext,
893 previous_thread_local: bool,
894}
895
896impl Drop for RuntimeQuantumGuard<'_> {
897 fn drop(&mut self) {
898 self.ctx.runtime_quantum_active.set(false);
899 crate::set_runtime_quantum(self.previous_thread_local);
900 }
901}
902
903impl Default for EvalContext {
904 fn default() -> Self {
905 Self::new()
906 }
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use std::cell::Cell;
913 use std::collections::BTreeMap;
914 use std::panic::{catch_unwind, AssertUnwindSafe};
915 use std::path::PathBuf;
916
917 use crate::{Caps, Sandbox, Value};
918
919 #[test]
920 fn scoped_legacy_call_env_restores_nested_environments() {
921 let context = EvalContext::new();
922 let outer = Rc::new(Env::new());
923 let inner = Rc::new(Env::new());
924
925 assert!(context.legacy_call_env().is_none());
926 let outer_guard = context.scope_legacy_call_env(&outer);
927 assert!(Rc::ptr_eq(
928 &context.legacy_call_env().expect("outer call env"),
929 &outer
930 ));
931 {
932 let _inner_guard = context.scope_legacy_call_env(&inner);
933 assert!(Rc::ptr_eq(
934 &context.legacy_call_env().expect("inner call env"),
935 &inner
936 ));
937 }
938 assert!(Rc::ptr_eq(
939 &context.legacy_call_env().expect("restored outer call env"),
940 &outer
941 ));
942 drop(outer_guard);
943 assert!(context.legacy_call_env().is_none());
944 }
945
946 #[test]
947 fn scoped_legacy_call_env_restores_after_panic() {
948 let context = EvalContext::new();
949 let call_env = Rc::new(Env::new());
950
951 let result = catch_unwind(AssertUnwindSafe(|| {
952 let _guard = context.scope_legacy_call_env(&call_env);
953 panic!("expected test panic");
954 }));
955
956 assert!(result.is_err());
957 assert!(context.legacy_call_env().is_none());
958 }
959
960 #[test]
961 fn task_context_handle_lifecycle_and_child_inheritance() {
962 let context = EvalContext::new();
963 assert!(context.task_context().is_none());
964 assert!(EvalContext::default().task_context().is_none());
965 assert!(EvalContext::new_with_sandbox(Sandbox::deny(Caps::FS_READ))
966 .task_context()
967 .is_none());
968
969 let handle = crate::runtime::TaskContextHandle::default();
970 context.install_task_context(handle.clone());
971 let clone = context.task_context().unwrap();
972 clone
973 .borrow_mut()
974 .insert(std::rc::Rc::new(TestTaskLocal(4)));
975 assert_eq!(handle.borrow().get::<TestTaskLocal>().unwrap().0, 4);
976
977 let child = handle.inherit_for_child();
978 assert_eq!(child.borrow().get::<TestTaskLocal>().unwrap().0, 0);
979 assert_eq!(handle.borrow().get::<TestTaskLocal>().unwrap().0, 4);
980 assert!(context.take_task_context().is_some());
981 assert!(context.task_context().is_none());
982 }
983
984 #[test]
985 fn installed_dynamic_state_is_accessible_while_task_context_is_borrowed() {
986 let context = EvalContext::new();
987 let handle = crate::runtime::TaskContextHandle::default();
988 let dynamic = Rc::new(DynamicTaskState::root(
989 vec![BTreeMap::new()],
990 vec![BTreeMap::new()],
991 BTreeMap::new(),
992 ));
993 handle.borrow_mut().insert(Rc::clone(&dynamic));
994 let _scope = context.scope_task_context(handle.clone());
995
996 let held_by_native_call = handle.borrow_mut();
997 context.context_set(Value::keyword("key"), Value::int(42));
998 assert_eq!(
999 context.context_get(&Value::keyword("key")),
1000 Some(Value::int(42))
1001 );
1002 drop(held_by_native_call);
1003
1004 assert_eq!(
1005 dynamic.user_get(&Value::keyword("key")),
1006 Some(Value::int(42))
1007 );
1008 }
1009
1010 #[test]
1011 fn installed_module_state_is_accessible_while_task_context_is_borrowed() {
1012 let context = EvalContext::new();
1013 context.push_file_path(PathBuf::from("ambient/entry.sema"));
1014 let handle = crate::runtime::TaskContextHandle::default();
1015 let module = Rc::new(context.snapshot_module_task_state());
1016 handle.borrow_mut().insert(Rc::clone(&module));
1017 let _scope = context.scope_task_context(handle.clone());
1018
1019 let held_by_native_call = handle.borrow_mut();
1020 assert_eq!(
1021 context.current_file_path(),
1022 Some(PathBuf::from("ambient/entry.sema"))
1023 );
1024 context.push_file_path(PathBuf::from("task/module.sema"));
1025 context.clear_module_exports();
1026 context.set_module_exports(vec!["answer".to_string()]);
1027 let load = context
1028 .enter_module_load(PathBuf::from("task/module.sema"))
1029 .expect("task-local module-load scope");
1030
1031 assert_eq!(
1032 context.current_file_path(),
1033 Some(PathBuf::from("task/module.sema"))
1034 );
1035 assert_eq!(
1036 context.take_module_exports(),
1037 Some(vec!["answer".to_string()])
1038 );
1039 assert_eq!(module.loading(), vec![PathBuf::from("task/module.sema")]);
1040 drop(load);
1041 assert!(module.loading().is_empty());
1042 context.pop_file_path();
1043 assert_eq!(
1044 context.current_file_path(),
1045 Some(PathBuf::from("ambient/entry.sema"))
1046 );
1047 drop(held_by_native_call);
1048
1049 assert_eq!(
1050 context.current_file.borrow().as_slice(),
1051 &[PathBuf::from("ambient/entry.sema")]
1052 );
1053 assert!(context.module_exports.borrow().is_empty());
1054 assert!(context.module_load_stack.borrow().is_empty());
1055 }
1056
1057 #[test]
1058 fn module_load_guards_do_not_cross_task_states_with_colliding_scope_ids() {
1059 let context_a = EvalContext::new();
1060 let context_b = EvalContext::new();
1061 let state_a = Rc::new(ModuleTaskState::default());
1062 let state_b = Rc::new(ModuleTaskState::default());
1063 let handle_a = crate::runtime::TaskContextHandle::default();
1064 let handle_b = crate::runtime::TaskContextHandle::default();
1065 handle_a.borrow_mut().insert(Rc::clone(&state_a));
1066 handle_b.borrow_mut().insert(Rc::clone(&state_b));
1067 let _scope_a = context_a.scope_task_context(handle_a);
1068 let _scope_b = context_b.scope_task_context(handle_b);
1069
1070 let guard_a = context_a
1071 .enter_module_load(PathBuf::from("same.sema"))
1072 .expect("task A load scope");
1073 let guard_b = context_b
1074 .enter_module_load(PathBuf::from("same.sema"))
1075 .expect("task B load scope");
1076 assert_eq!(state_a.loading(), state_b.loading());
1077
1078 drop(guard_a);
1079 assert!(state_a.loading().is_empty());
1080 assert_eq!(state_b.loading(), vec![PathBuf::from("same.sema")]);
1081 drop(guard_b);
1082 assert!(state_b.loading().is_empty());
1083 }
1084
1085 #[test]
1086 fn dynamic_snapshot_publication_rejects_an_equal_value_aba_pop() {
1087 let context = EvalContext::new();
1088 let key = Value::keyword("stack");
1089 let value = Value::keyword("same");
1090 context.context_stack_push(key.clone(), value.clone());
1091 let stale = context.snapshot_dynamic_task_state();
1092 let recreating = context.snapshot_dynamic_task_state();
1093
1094 assert_eq!(stale.stack_pop(&key), Some(value.clone()));
1095 assert_eq!(recreating.stack_pop(&key), Some(value.clone()));
1096 recreating
1097 .stack_push(key.clone(), value.clone())
1098 .expect("scope ID available");
1099
1100 assert!(context.publish_dynamic_task_state(&recreating));
1101 assert!(context.publish_dynamic_task_state(&stale));
1102 assert_eq!(context.context_stack_get(&key), vec![value]);
1103 }
1104
1105 #[test]
1106 fn legacy_stack_mutation_renews_identity_seen_by_later_snapshots() {
1107 let context = EvalContext::new();
1108 let key = Value::keyword("stack");
1109 let value = Value::keyword("same");
1110 context.context_stack_push(key.clone(), value.clone());
1111 let stale = context.snapshot_dynamic_task_state();
1112 assert_eq!(stale.stack_pop(&key), Some(value.clone()));
1113
1114 assert_eq!(context.context_stack_pop(&key), Some(value.clone()));
1115 context.context_stack_push(key.clone(), value.clone());
1116 assert!(context.publish_dynamic_task_state(&stale));
1117
1118 assert_eq!(context.context_stack_get(&key), vec![value]);
1119 }
1120
1121 #[test]
1122 fn direct_equal_stack_replacement_invalidates_stale_snapshot_identity() {
1123 let context = EvalContext::new();
1124 let key = Value::keyword("stack");
1125 let value = Value::keyword("same");
1126 context.context_stack_push(key.clone(), value.clone());
1127 let stale = context.snapshot_dynamic_task_state();
1128 assert_eq!(stale.stack_pop(&key), Some(value.clone()));
1129
1130 context
1131 .context_stacks
1132 .borrow_mut()
1133 .insert(key.clone(), vec![value.clone()]);
1134 assert!(context.publish_dynamic_task_state(&stale));
1135
1136 assert_eq!(context.context_stack_get(&key), vec![value]);
1137 }
1138
1139 #[test]
1140 fn popping_a_direct_empty_stack_keeps_publication_sidecar_aligned() {
1141 let context = EvalContext::new();
1142 let key = Value::keyword("empty-stack");
1143 context
1144 .context_stacks
1145 .borrow_mut()
1146 .insert(key.clone(), Vec::new());
1147 let root = context.snapshot_dynamic_task_state();
1148 root.user_set(Value::keyword("published"), Value::int(1));
1149
1150 assert_eq!(context.context_stack_pop(&key), None);
1151 assert!(context.publish_dynamic_task_state(&root));
1152 assert_eq!(
1153 context.context_get(&Value::keyword("published")),
1154 Some(Value::int(1))
1155 );
1156 }
1157
1158 #[test]
1159 fn publication_borrow_conflict_leaves_the_root_journal_retryable() {
1160 let context = EvalContext::new();
1161 let key = Value::keyword("retry");
1162 let root = context.snapshot_dynamic_task_state();
1163 root.user_set(key.clone(), Value::int(42));
1164 let held = context.user_context.borrow_mut();
1165
1166 let conflicted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1167 context.publish_dynamic_task_state(&root)
1168 }));
1169 assert!(conflicted.is_err());
1170 drop(held);
1171
1172 assert!(context.publish_dynamic_task_state(&root));
1173 assert_eq!(context.context_get(&key), Some(Value::int(42)));
1174 }
1175
1176 #[test]
1177 fn scoped_task_context_restores_the_exact_handle_after_panic() {
1178 let context = EvalContext::new();
1179 let outer = crate::runtime::TaskContextHandle::default();
1180 let inner = crate::runtime::TaskContextHandle::default();
1181 outer
1182 .borrow_mut()
1183 .insert(std::rc::Rc::new(TestTaskLocal(1)));
1184 inner
1185 .borrow_mut()
1186 .insert(std::rc::Rc::new(TestTaskLocal(2)));
1187 context.install_task_context(outer);
1188
1189 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1190 let _scope = context.scope_task_context(inner.clone());
1191 assert_eq!(
1192 context
1193 .task_context()
1194 .unwrap()
1195 .borrow()
1196 .get::<TestTaskLocal>()
1197 .unwrap()
1198 .0,
1199 2
1200 );
1201 panic!("test unwind");
1202 }));
1203
1204 assert!(result.is_err());
1205 assert_eq!(
1206 context
1207 .task_context()
1208 .unwrap()
1209 .borrow()
1210 .get::<TestTaskLocal>()
1211 .unwrap()
1212 .0,
1213 1
1214 );
1215 }
1216
1217 #[test]
1218 fn runtime_quantum_guard_restores_outer_context_thread_local() {
1219 let outer = EvalContext::new();
1220 let inner = EvalContext::new();
1221 assert!(!crate::in_runtime_quantum());
1222
1223 let outer_guard = outer.enter_runtime_quantum().unwrap();
1224 assert!(outer.runtime_quantum_active());
1225 assert!(crate::in_runtime_quantum());
1226
1227 {
1228 let _inner_guard = inner.enter_runtime_quantum().unwrap();
1229 assert!(inner.runtime_quantum_active());
1230 assert!(crate::in_runtime_quantum());
1231 }
1232
1233 assert!(outer.runtime_quantum_active());
1234 assert!(crate::in_runtime_quantum());
1235 drop(outer_guard);
1236 assert!(!outer.runtime_quantum_active());
1237 assert!(!crate::in_runtime_quantum());
1238 }
1239
1240 thread_local! {
1241 static EVAL_CALLS: Cell<usize> = const { Cell::new(0) };
1242 static BORROWED_CALLS: Cell<usize> = const { Cell::new(0) };
1243 static OWNED_CALLS: Cell<usize> = const { Cell::new(0) };
1244 }
1245
1246 fn eval_probe(
1247 _context: &EvalContext,
1248 expression: &Value,
1249 _environment: &Env,
1250 ) -> Result<Value, SemaError> {
1251 EVAL_CALLS.set(EVAL_CALLS.get() + 1);
1252 Ok(expression.clone())
1253 }
1254
1255 fn borrowed_call_probe(
1256 _context: &EvalContext,
1257 _function: &Value,
1258 args: &[Value],
1259 ) -> Result<Value, SemaError> {
1260 BORROWED_CALLS.set(BORROWED_CALLS.get() + 1);
1261 Ok(args.first().cloned().unwrap_or_else(Value::nil))
1262 }
1263
1264 fn owned_call_probe(
1265 _context: &EvalContext,
1266 _function: &Value,
1267 args: &mut [Value],
1268 ) -> Result<Value, SemaError> {
1269 OWNED_CALLS.set(OWNED_CALLS.get() + 1);
1270 Ok(args
1271 .first_mut()
1272 .map_or_else(Value::nil, |value| std::mem::replace(value, Value::nil())))
1273 }
1274
1275 #[test]
1276 fn call_callbacks_remain_available_to_host_code() {
1277 BORROWED_CALLS.set(0);
1278 OWNED_CALLS.set(0);
1279 let context = EvalContext::new();
1280 set_call_callback(&context, borrowed_call_probe);
1281 set_call_owned_callback(&context, owned_call_probe);
1282 let function = Value::int(1);
1283
1284 assert_eq!(
1285 call_callback(&context, &function, &[Value::int(21)]).expect("borrowed host call"),
1286 Value::int(21)
1287 );
1288 let mut owned_args = [Value::int(22)];
1289 assert_eq!(
1290 call_callback_owned(&context, &function, &mut owned_args).expect("owned host call"),
1291 Value::int(22)
1292 );
1293 assert_eq!(owned_args, [Value::nil()]);
1294 assert_eq!(BORROWED_CALLS.get(), 1);
1295 assert_eq!(OWNED_CALLS.get(), 1);
1296 }
1297
1298 #[test]
1299 fn eval_callback_remains_available_to_host_code() {
1300 EVAL_CALLS.set(0);
1301 let context = EvalContext::new();
1302 let environment = Env::new();
1303 set_eval_callback(&context, eval_probe);
1304
1305 assert_eq!(
1306 eval_callback(&context, &Value::int(23), &environment).expect("host evaluation"),
1307 Value::int(23)
1308 );
1309 assert_eq!(EVAL_CALLS.get(), 1);
1310 }
1311
1312 #[test]
1313 fn eval_callback_rejects_same_and_cross_context_runtime_quantums() {
1314 EVAL_CALLS.set(0);
1315 let runtime_context = EvalContext::new();
1316 let callback_context = EvalContext::new();
1317 let environment = Env::new();
1318 set_eval_callback(&runtime_context, eval_probe);
1319 set_eval_callback(&callback_context, eval_probe);
1320
1321 let quantum = runtime_context
1322 .enter_runtime_quantum()
1323 .expect("enter runtime quantum");
1324 let same_context = eval_callback(&runtime_context, &Value::int(1), &environment)
1325 .expect_err("same-context runtime evaluation must be rejected");
1326 let cross_context = eval_callback(&callback_context, &Value::int(2), &environment)
1327 .expect_err("cross-context runtime evaluation must be rejected");
1328 drop(quantum);
1329
1330 assert!(same_context.to_string().contains("host-only"));
1331 assert!(cross_context.to_string().contains("host-only"));
1332 assert_eq!(EVAL_CALLS.get(), 0);
1333 }
1334
1335 #[test]
1336 fn call_callbacks_reject_an_active_runtime_quantum_before_invocation() {
1337 BORROWED_CALLS.set(0);
1338 OWNED_CALLS.set(0);
1339 let context = EvalContext::new();
1340 set_call_callback(&context, borrowed_call_probe);
1341 set_call_owned_callback(&context, owned_call_probe);
1342 let function = Value::int(1);
1343 let mut owned_args = [Value::int(22)];
1344 let _quantum = context
1345 .enter_runtime_quantum()
1346 .expect("enter runtime quantum");
1347
1348 let borrowed_error = call_callback(&context, &function, &[Value::int(21)])
1349 .expect_err("borrowed callback must be host-only");
1350 let owned_error = call_callback_owned(&context, &function, &mut owned_args)
1351 .expect_err("owned callback must be host-only");
1352
1353 assert!(borrowed_error.to_string().contains("host-only"));
1354 assert!(owned_error.to_string().contains("host-only"));
1355 assert_eq!(owned_args, [Value::int(22)]);
1356 assert_eq!(BORROWED_CALLS.get(), 0);
1357 assert_eq!(OWNED_CALLS.get(), 0);
1358 }
1359
1360 #[test]
1361 fn call_callback_rejects_another_context_during_a_thread_runtime_quantum() {
1362 BORROWED_CALLS.set(0);
1363 let runtime_context = EvalContext::new();
1364 let callback_context = EvalContext::new();
1365 set_call_callback(&callback_context, borrowed_call_probe);
1366 let _quantum = runtime_context
1367 .enter_runtime_quantum()
1368 .expect("enter runtime quantum");
1369
1370 let error = call_callback(&callback_context, &Value::int(1), &[])
1371 .expect_err("thread-local runtime quantum must reject ambient callbacks");
1372
1373 assert!(error.to_string().contains("host-only"));
1374 assert_eq!(BORROWED_CALLS.get(), 0);
1375 }
1376
1377 #[test]
1378 fn stdlib_context_rejects_an_active_runtime_quantum_before_invocation() {
1379 let runtime_context = EvalContext::new();
1380 let invoked = Cell::new(false);
1381 let _quantum = runtime_context
1382 .enter_runtime_quantum()
1383 .expect("enter runtime quantum");
1384
1385 let rejected = catch_unwind(AssertUnwindSafe(|| {
1386 with_stdlib_ctx(|_| invoked.set(true));
1387 }));
1388
1389 assert!(rejected.is_err(), "stdlib context must be host-only");
1390 assert!(!invoked.get(), "stdlib closure must not be invoked");
1391 }
1392
1393 struct TestTaskLocal(u32);
1394
1395 impl crate::runtime::Trace for TestTaskLocal {
1396 fn trace(&self, _sink: &mut dyn FnMut(crate::cycle::GcEdge<'_>)) -> bool {
1397 true
1398 }
1399 }
1400
1401 impl crate::runtime::TaskLocalValue for TestTaskLocal {
1402 fn inherit(&self) -> std::rc::Rc<dyn crate::runtime::TaskLocalValue> {
1403 std::rc::Rc::new(Self(0))
1404 }
1405
1406 fn as_any(&self) -> &dyn std::any::Any {
1407 self
1408 }
1409 }
1410
1411 fn macro_expand_probe(
1412 context: &crate::runtime::NativeCallContext<'_>,
1413 expr: &Value,
1414 env: &Env,
1415 ) -> Result<Value, SemaError> {
1416 let env_marker = env
1417 .get(crate::intern("macro-expand-env"))
1418 .expect("expansion env marker");
1419 let call_env_marker = context
1420 .call_env
1421 .as_ref()
1422 .and_then(|call_env| call_env.get(crate::intern("macro-expand-call-env")))
1423 .expect("call env marker");
1424 let task_marker = context
1425 .task_context
1426 .get_rc::<TestTaskLocal>()
1427 .expect("task context marker");
1428 let eval_marker = context
1429 .eval_context
1430 .context_get(&Value::keyword("macro-expand-context"))
1431 .expect("eval context marker");
1432 Ok(Value::list(vec![
1433 expr.clone(),
1434 env_marker,
1435 call_env_marker,
1436 Value::int(i64::from(task_marker.0)),
1437 eval_marker,
1438 Value::bool(context.cancellation.is_requested()),
1439 ]))
1440 }
1441
1442 #[test]
1443 fn macro_expand_callback_is_optional_per_context_and_receives_exact_inputs() {
1444 let eval_context = EvalContext::new();
1445 eval_context.context_set(Value::keyword("macro-expand-context"), Value::int(44));
1446 let env = Env::new();
1447 env.set(crate::intern("macro-expand-env"), Value::int(11));
1448 let call_env = Rc::new(Env::new());
1449 call_env.set(crate::intern("macro-expand-call-env"), Value::int(22));
1450 let task_context = TaskContextHandle::default();
1451 task_context.borrow_mut().insert(Rc::new(TestTaskLocal(33)));
1452 let native_context = crate::runtime::NativeCallContext {
1453 hof_host: None,
1454 eval_context: &eval_context,
1455 task_context,
1456 call_env: Some(call_env),
1457 cancellation: crate::runtime::CancellationView::new(true, None),
1458 };
1459 let expr = Value::int(55);
1460
1461 assert!(try_macro_expand_callback(&native_context, &expr, &env).is_none());
1462
1463 set_macro_expand_callback(&eval_context, macro_expand_probe);
1464 let expanded = try_macro_expand_callback(&native_context, &expr, &env)
1465 .expect("callback registered")
1466 .expect("callback succeeds");
1467 assert_eq!(
1468 expanded,
1469 Value::list(vec![
1470 Value::int(55),
1471 Value::int(11),
1472 Value::int(22),
1473 Value::int(33),
1474 Value::int(44),
1475 Value::bool(true),
1476 ])
1477 );
1478
1479 let other_context = EvalContext::new();
1480 let other_native_context = crate::runtime::NativeCallContext {
1481 hof_host: None,
1482 eval_context: &other_context,
1483 task_context: TaskContextHandle::default(),
1484 call_env: None,
1485 cancellation: crate::runtime::CancellationView::default(),
1486 };
1487 assert!(try_macro_expand_callback(&other_native_context, &expr, &env).is_none());
1488 }
1489
1490 #[test]
1493 fn test_push_pop_file_path() {
1494 let ctx = EvalContext::new();
1495 let path = PathBuf::from("/foo/bar/baz.sema");
1496 ctx.push_file_path(path.clone());
1497 assert_eq!(ctx.current_file_path(), Some(path));
1498 ctx.pop_file_path();
1499 assert_eq!(ctx.current_file_path(), None);
1500 }
1501
1502 #[test]
1503 fn test_current_file_dir() {
1504 let ctx = EvalContext::new();
1505 ctx.push_file_path(PathBuf::from("/foo/bar/baz.sema"));
1506 assert_eq!(ctx.current_file_dir(), Some(PathBuf::from("/foo/bar")));
1507 }
1508
1509 #[test]
1510 fn test_current_file_dir_empty() {
1511 let ctx = EvalContext::new();
1512 assert_eq!(ctx.current_file_dir(), None);
1513 }
1514
1515 #[test]
1516 fn test_nested_file_paths() {
1517 let ctx = EvalContext::new();
1518 let first = PathBuf::from("/a/first.sema");
1519 let second = PathBuf::from("/b/second.sema");
1520 ctx.push_file_path(first.clone());
1521 ctx.push_file_path(second.clone());
1522 assert_eq!(ctx.current_file_path(), Some(second));
1523 ctx.pop_file_path();
1524 assert_eq!(ctx.current_file_path(), Some(first));
1525 }
1526
1527 #[test]
1530 fn test_cache_module() {
1531 let ctx = EvalContext::new();
1532 let path = PathBuf::from("/lib/math.sema");
1533 let mut exports = BTreeMap::new();
1534 exports.insert("add".to_string(), Value::int(1));
1535 ctx.cache_module(path.clone(), exports.clone());
1536 let cached = ctx.get_cached_module(&path).unwrap();
1537 assert_eq!(cached.len(), 1);
1538 assert_eq!(cached.get("add"), Some(&Value::int(1)));
1539 }
1540
1541 #[test]
1542 fn test_get_cached_module_miss() {
1543 let ctx = EvalContext::new();
1544 let path = PathBuf::from("/nonexistent.sema");
1545 assert_eq!(ctx.get_cached_module(&path), None);
1546 }
1547
1548 #[test]
1549 fn test_cache_module_overwrites() {
1550 let ctx = EvalContext::new();
1551 let path = PathBuf::from("/lib/math.sema");
1552
1553 let mut first = BTreeMap::new();
1554 first.insert("old".to_string(), Value::int(1));
1555 ctx.cache_module(path.clone(), first);
1556
1557 let mut second = BTreeMap::new();
1558 second.insert("new".to_string(), Value::int(2));
1559 ctx.cache_module(path.clone(), second);
1560
1561 let cached = ctx.get_cached_module(&path).unwrap();
1562 assert!(!cached.contains_key("old"));
1563 assert_eq!(cached.get("new"), Some(&Value::int(2)));
1564 }
1565
1566 #[test]
1569 fn test_module_exports_roundtrip() {
1570 let ctx = EvalContext::new();
1571 ctx.clear_module_exports(); ctx.set_module_exports(vec!["foo".to_string(), "bar".to_string()]);
1573 let taken = ctx.take_module_exports();
1574 assert_eq!(taken, Some(vec!["foo".to_string(), "bar".to_string()]));
1575 }
1576
1577 #[test]
1578 fn test_take_module_exports_empty() {
1579 let ctx = EvalContext::new();
1580 assert_eq!(ctx.take_module_exports(), None);
1582 }
1583
1584 #[test]
1587 fn test_begin_module_load_ok() {
1588 let ctx = EvalContext::new();
1589 let path = PathBuf::from("/lib/a.sema");
1590 assert!(ctx.begin_module_load(&path).is_ok());
1591 }
1592
1593 #[test]
1594 fn test_begin_module_load_cycle() {
1595 let ctx = EvalContext::new();
1596 let path = PathBuf::from("/lib/a.sema");
1597 ctx.begin_module_load(&path).unwrap();
1598 let result = ctx.begin_module_load(&path);
1599 assert!(result.is_err());
1600 let err = result.unwrap_err();
1601 let msg = err.to_string();
1602 assert!(
1603 msg.contains("cyclic import"),
1604 "error should mention cyclic import: {msg}"
1605 );
1606 }
1607
1608 #[test]
1609 fn test_end_module_load() {
1610 let ctx = EvalContext::new();
1611 let path = PathBuf::from("/lib/a.sema");
1612 ctx.begin_module_load(&path).unwrap();
1613 ctx.end_module_load(&path);
1614 assert!(ctx.begin_module_load(&path).is_ok());
1616 }
1617
1618 #[test]
1619 fn test_nested_module_loads() {
1620 let ctx = EvalContext::new();
1621 let a = PathBuf::from("/lib/a.sema");
1622 let b = PathBuf::from("/lib/b.sema");
1623 ctx.begin_module_load(&a).unwrap();
1624 ctx.begin_module_load(&b).unwrap();
1625 ctx.end_module_load(&b);
1626 let result = ctx.begin_module_load(&a);
1628 assert!(result.is_err());
1629 let msg = result.unwrap_err().to_string();
1630 assert!(
1631 msg.contains("cyclic import"),
1632 "A should still be loading: {msg}"
1633 );
1634 }
1635
1636 #[test]
1639 fn test_new_with_sandbox() {
1640 let sandbox = Sandbox::deny(Caps::NETWORK);
1641 let ctx = EvalContext::new_with_sandbox(sandbox);
1642 let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
1644 assert!(result.is_err());
1645 let result = ctx.sandbox.check(Caps::FS_READ, "file/read");
1647 assert!(result.is_ok());
1648 }
1649}
1650
1651thread_local! {
1652 static STDLIB_CTX: EvalContext = EvalContext::new();
1653}
1654
1655pub fn with_stdlib_ctx<F, R>(f: F) -> R
1659where
1660 F: FnOnce(&EvalContext) -> R,
1661{
1662 STDLIB_CTX.with(|context| {
1663 assert!(
1664 !context.runtime_quantum_active() && !crate::in_runtime_quantum(),
1665 "with_stdlib_ctx is a host-only adapter; runtime code must carry its EvalContext explicitly"
1666 );
1667 f(context)
1668 })
1669}
1670
1671pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
1674 ctx.eval_fn.set(Some(f));
1675 STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
1676}
1677
1678pub fn set_macro_expand_callback(ctx: &EvalContext, f: MacroExpandCallbackFn) {
1684 ctx.macro_expand_fn.set(Some(f));
1685}
1686
1687pub fn try_macro_expand_callback(
1693 context: &NativeCallContext<'_>,
1694 expr: &Value,
1695 env: &Env,
1696) -> Option<Result<Value, SemaError>> {
1697 context
1698 .eval_context
1699 .macro_expand_fn
1700 .get()
1701 .map(|expand| expand(context, expr, env))
1702}
1703
1704pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
1707 ctx.call_fn.set(Some(f));
1708 STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
1709}
1710
1711pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
1714 reject_callback_during_runtime_quantum(ctx, "eval")?;
1715 let f = ctx.eval_fn.get().ok_or_else(|| {
1716 SemaError::eval("eval callback not registered — Interpreter::new() must be called first")
1717 })?;
1718 f(ctx, expr, env)
1719}
1720
1721pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
1724 reject_callback_during_runtime_quantum(ctx, "call")?;
1725 let f = ctx.call_fn.get().ok_or_else(|| {
1726 SemaError::eval("call callback not registered — Interpreter::new() must be called first")
1727 })?;
1728 f(ctx, func, args)
1729}
1730
1731pub fn set_call_owned_callback(ctx: &EvalContext, f: CallOwnedCallbackFn) {
1734 ctx.call_owned_fn.set(Some(f));
1735 STDLIB_CTX.with(|stdlib| stdlib.call_owned_fn.set(Some(f)));
1736}
1737
1738pub fn call_callback_owned(
1746 ctx: &EvalContext,
1747 func: &Value,
1748 args: &mut [Value],
1749) -> Result<Value, SemaError> {
1750 reject_callback_during_runtime_quantum(ctx, "call")?;
1751 if let Some(f) = ctx.call_owned_fn.get() {
1752 return f(ctx, func, args);
1753 }
1754 call_callback(ctx, func, args)
1755}
1756
1757fn reject_callback_during_runtime_quantum(
1758 ctx: &EvalContext,
1759 adapter: &str,
1760) -> Result<(), SemaError> {
1761 if ctx.runtime_quantum_active() || crate::in_runtime_quantum() {
1762 return Err(SemaError::eval(format!(
1763 "internal error: {adapter} callback is a host-only adapter; runtime code must use structural evaluation"
1764 )));
1765 }
1766 Ok(())
1767}