Skip to main content

sema_core/
context.rs

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
17/// Function-pointer type for the full evaluator callback: (ctx, expr, env) -> Result<Value, SemaError>
18pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
19
20/// Function-pointer type for macro expansion with an explicit runtime-native
21/// context. The expansion implementation may run bounded restricted evaluators,
22/// but it must not drive the scheduler or retain any borrowed input.
23pub type MacroExpandCallbackFn =
24    for<'a> fn(&NativeCallContext<'a>, &Value, &Env) -> Result<Value, SemaError>;
25
26/// Function-pointer type for calling a function value with evaluated arguments: (ctx, func, args) -> Result<Value, SemaError>
27pub type CallCallbackFn = fn(&EvalContext, &Value, &[Value]) -> Result<Value, SemaError>;
28
29/// Function-pointer type for the owned-args variant of the call callback: the
30/// caller passes a buffer it owns and will not reuse, and the callee may move
31/// values out of it (leaving nil behind). See [`call_callback_owned`].
32pub 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/// Legacy-compatible dynamic stack storage whose mutable guard renews opaque
39/// entry identities on drop. Direct mutation therefore cannot bypass the ABA
40/// protection used by task-root snapshots.
41#[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    /// Optional wall-clock deadline for evaluation. When set,
115    /// the bytecode VM periodically checks whether the current
116    /// time has passed this instant and, if so, abort with an error. Used by
117    /// the notebook engine to bound how long a single cell evaluation can run.
118    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    /// Interpreter-wide roots for callbacks registered by `sys/on-signal`.
124    /// This store is intentionally separate from task-local dynamic context:
125    /// any root may dispatch a subscription installed by another root.
126    signal_callbacks: RefCell<[Vec<Value>; 3]>,
127    /// Weak, non-Value hooks that release host resources at interpreter
128    /// teardown even when an embedder retains the global environment.
129    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    // Cache the typed Rcs at installation so EvalContext compatibility methods
144    // do not need to re-borrow the task-local extension map during a native's
145    // own short, scoped task-context operation.
146    // The canonical state is still the extension owned by `handle`; every
147    // installation refreshes this cached clone of the same Rc.
148    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
164/// RAII guard for a module-load scope: pops the load stack when dropped, so the
165/// stack stays balanced on every exit path (early return, `?`, panic). Created
166/// by [`EvalContext::enter_module_load`].
167pub 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/// Restores the value-ABI native call environment when a VM dispatch returns
187/// or unwinds. The weak handle avoids making `EvalContext` an owner of an
188/// interpreter or isolated module environment.
189#[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    /// Whether `handle` is the task context already installed on this thread.
279    /// A pointer compare, so callers can skip a scoped reinstall (and its
280    /// extension-map refresh) when the context they would install is the one
281    /// currently live — the common case for natives invoked inside the task's
282    /// own runtime quantum.
283    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    /// Installs the globals of the VM invoking a native through its synchronous
306    /// value ABI. Nested VM dispatches replace this weak handle for their scope
307    /// and the guard restores the previous environment on every exit path.
308    #[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    /// Returns the synchronous VM environment installed for the current native
317    /// invocation. Direct host calls have no installed environment.
318    #[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        // Mirror the per-context flag into a thread-local so context-free
347        // host-adapter guards reject synchronous or blocking entry during any
348        // active runtime quantum, including one owned by a different context.
349        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    /// Compatibility name for hosts built against the signal-specific hook API.
398    #[doc(hidden)]
399    pub fn register_signal_teardown_hook(&self, hook: impl Fn() + 'static) {
400        self.register_interpreter_teardown_hook(hook);
401    }
402
403    /// Compatibility name for hosts built against the signal-specific hook API.
404    #[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    /// When enabled, `import` and `load` may only resolve host-provided embedded files.
475    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    /// Enter a module-load scope, guarding against import/load cycles. The
512    /// returned [`ModuleLoadGuard`] pops the load stack when dropped, keeping it
513    /// balanced on any exit path. Errors if `path` is already being loaded.
514    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        // If table is full, skip merging new spans (preserves existing error locations)
564    }
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    /// Set a wall-clock deadline after which evaluation should abort.
575    /// Passing `None` clears any existing deadline.
576    pub fn set_eval_deadline(&self, deadline: Option<Instant>) {
577        self.eval_deadline.set(deadline);
578    }
579
580    /// Returns true if a deadline is set and has been exceeded.
581    #[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    /// Returns an `eval` error if a deadline is set and exceeded; otherwise Ok(()).
590    #[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    /// Per-iteration loop/recursion guard, called by the VM at loop back-edges
602    /// and frame transitions. Counts a step and aborts when:
603    ///   - the step limit is exceeded (wasm-safe runaway-loop guard — the wall
604    ///     clock is unavailable in wasm, so the step counter is the guard there);
605    ///   - the wall-clock deadline is exceeded (native);
606    ///   - a cancellation has been requested (e.g. the playground Stop button).
607    ///
608    /// The step compare runs every call (cheap); the clock read and the
609    /// cancellation thread-local read run only periodically to keep tight loops
610    /// fast. `eval_steps` is reset per top-level eval by the evaluator.
611    #[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    // --- User context methods ---
635
636    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    // --- Hidden context methods ---
740
741    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    // --- Stack methods ---
795
796    /// Capture an independent root-authority snapshot of the interpreter-wide
797    /// dynamic state. Stack entry identities are shared with the publication
798    /// baseline so a stale root cannot remove an equal replacement entry.
799    #[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    /// Capture the transient module-resolution scopes inherited by a new root.
813    /// These scopes are task-private and never publish back at settlement.
814    #[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    /// Publish and drain one root snapshot's mutation journal. Child snapshots
824    /// carry no journal and return `false` without changing interpreter state.
825    #[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    // --- File path tracking ---
1491
1492    #[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    // --- Module caching ---
1528
1529    #[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    // --- Module exports ---
1567
1568    #[test]
1569    fn test_module_exports_roundtrip() {
1570        let ctx = EvalContext::new();
1571        ctx.clear_module_exports(); // pushes None onto stack
1572        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        // Nothing has been pushed, so take should return None
1581        assert_eq!(ctx.take_module_exports(), None);
1582    }
1583
1584    // --- Cyclic import detection ---
1585
1586    #[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        // Stack is now empty, so beginning the same path again should succeed
1615        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        // A should still be in the stack — beginning A again should fail
1627        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    // --- Sandbox integration ---
1637
1638    #[test]
1639    fn test_new_with_sandbox() {
1640        let sandbox = Sandbox::deny(Caps::NETWORK);
1641        let ctx = EvalContext::new_with_sandbox(sandbox);
1642        // Verify the sandbox is set by checking a denied capability
1643        let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
1644        assert!(result.is_err());
1645        // Allowed capability should pass
1646        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
1655/// Get a reference to the shared stdlib EvalContext.
1656/// Use this for host-side stdlib callback invocations instead of creating
1657/// throwaway contexts. Panics before invoking `f` during a runtime quantum.
1658pub 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
1671/// Register the full evaluator callback for host compatibility.
1672/// Stores it on both `ctx` and the shared host-only `STDLIB_CTX`.
1673pub 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
1678/// Register this evaluator context's runtime-aware macro expander.
1679///
1680/// Unlike the legacy evaluator and call callbacks, this is deliberately not
1681/// copied into `STDLIB_CTX`: every invocation must carry an explicit task,
1682/// cancellation snapshot, and call environment in [`NativeCallContext`].
1683pub fn set_macro_expand_callback(ctx: &EvalContext, f: MacroExpandCallbackFn) {
1684    ctx.macro_expand_fn.set(Some(f));
1685}
1686
1687/// Invoke the macro expander registered on the exact evaluator context.
1688///
1689/// `None` means no expansion provider is installed. Low-level VM embedders use
1690/// that to retain their direct-compilation fallback without acquiring ambient
1691/// evaluator state.
1692pub 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
1704/// Register the call-value callback. Called by `sema-eval` during interpreter init.
1705/// Stores into both `ctx` and the shared `STDLIB_CTX`.
1706pub 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
1711/// Evaluate an expression using the registered host-compatibility evaluator.
1712/// Runtime code must use the restricted structural evaluator instead.
1713pub 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
1721/// Call a function value with arguments using the registered callback.
1722/// Returns an error if no callback has been registered.
1723pub 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
1731/// Register the owned-args call callback. Called by `sema-eval` during
1732/// interpreter init, alongside [`set_call_callback`].
1733pub 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
1738/// Call a function value with an args buffer the CALLER owns and will not
1739/// reuse after the call: the callee may move values out of it (leaving nil
1740/// behind). This is the refcount-shedding variant of [`call_callback`] — a
1741/// fold-style accumulator handed off this way stays uniquely owned across the
1742/// callback boundary, so the stdlib's `strong_count == 1` in-place fast paths
1743/// (e.g. `assoc` on a map) can fire inside the callback. Falls back to the
1744/// borrowed protocol (args intact) when no owned callback is registered.
1745pub 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}