Skip to main content

sema_core/
context.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, HashMap};
3use std::path::PathBuf;
4use std::time::Instant;
5
6use crate::{CallFrame, Env, Sandbox, SemaError, Span, SpanMap, StackTrace, Value};
7
8const MAX_SPAN_TABLE_ENTRIES: usize = 200_000;
9
10/// Function-pointer type for the full evaluator callback: (ctx, expr, env) -> Result<Value, SemaError>
11pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
12
13/// Function-pointer type for calling a function value with evaluated arguments: (ctx, func, args) -> Result<Value, SemaError>
14pub type CallCallbackFn = fn(&EvalContext, &Value, &[Value]) -> Result<Value, SemaError>;
15
16pub struct EvalContext {
17    pub module_cache: RefCell<BTreeMap<PathBuf, BTreeMap<String, Value>>>,
18    pub current_file: RefCell<Vec<PathBuf>>,
19    pub module_exports: RefCell<Vec<Option<Vec<String>>>>,
20    pub module_load_stack: RefCell<Vec<PathBuf>>,
21    pub call_stack: RefCell<Vec<CallFrame>>,
22    pub span_table: RefCell<HashMap<usize, Span>>,
23    pub eval_depth: Cell<usize>,
24    pub max_eval_depth: Cell<usize>,
25    pub eval_step_limit: Cell<usize>,
26    pub eval_steps: Cell<usize>,
27    /// Optional wall-clock deadline for evaluation. When set, both the
28    /// tree-walker and the bytecode VM periodically check whether the current
29    /// time has passed this instant and, if so, abort with an error. Used by
30    /// the notebook engine to bound how long a single cell evaluation can run.
31    pub eval_deadline: Cell<Option<Instant>>,
32    pub sandbox: Sandbox,
33    pub user_context: RefCell<Vec<BTreeMap<Value, Value>>>,
34    pub hidden_context: RefCell<Vec<BTreeMap<Value, Value>>>,
35    pub context_stacks: RefCell<BTreeMap<Value, Vec<Value>>>,
36    pub eval_fn: Cell<Option<EvalCallbackFn>>,
37    pub call_fn: Cell<Option<CallCallbackFn>>,
38    pub interactive: Cell<bool>,
39}
40
41impl EvalContext {
42    pub fn new() -> Self {
43        EvalContext {
44            module_cache: RefCell::new(BTreeMap::new()),
45            current_file: RefCell::new(Vec::new()),
46            module_exports: RefCell::new(Vec::new()),
47            module_load_stack: RefCell::new(Vec::new()),
48            call_stack: RefCell::new(Vec::new()),
49            span_table: RefCell::new(HashMap::new()),
50            eval_depth: Cell::new(0),
51            max_eval_depth: Cell::new(0),
52            eval_step_limit: Cell::new(0),
53            eval_steps: Cell::new(0),
54            eval_deadline: Cell::new(None),
55            sandbox: Sandbox::allow_all(),
56            user_context: RefCell::new(vec![BTreeMap::new()]),
57            hidden_context: RefCell::new(vec![BTreeMap::new()]),
58            context_stacks: RefCell::new(BTreeMap::new()),
59            eval_fn: Cell::new(None),
60            call_fn: Cell::new(None),
61            interactive: Cell::new(false),
62        }
63    }
64
65    pub fn new_with_sandbox(sandbox: Sandbox) -> Self {
66        EvalContext {
67            module_cache: RefCell::new(BTreeMap::new()),
68            current_file: RefCell::new(Vec::new()),
69            module_exports: RefCell::new(Vec::new()),
70            module_load_stack: RefCell::new(Vec::new()),
71            call_stack: RefCell::new(Vec::new()),
72            span_table: RefCell::new(HashMap::new()),
73            eval_depth: Cell::new(0),
74            max_eval_depth: Cell::new(0),
75            eval_step_limit: Cell::new(0),
76            eval_steps: Cell::new(0),
77            eval_deadline: Cell::new(None),
78            sandbox,
79            user_context: RefCell::new(vec![BTreeMap::new()]),
80            hidden_context: RefCell::new(vec![BTreeMap::new()]),
81            context_stacks: RefCell::new(BTreeMap::new()),
82            eval_fn: Cell::new(None),
83            call_fn: Cell::new(None),
84            interactive: Cell::new(false),
85        }
86    }
87
88    pub fn push_file_path(&self, path: PathBuf) {
89        self.current_file.borrow_mut().push(path);
90    }
91
92    pub fn pop_file_path(&self) {
93        self.current_file.borrow_mut().pop();
94    }
95
96    pub fn current_file_dir(&self) -> Option<PathBuf> {
97        self.current_file
98            .borrow()
99            .last()
100            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
101    }
102
103    pub fn current_file_path(&self) -> Option<PathBuf> {
104        self.current_file.borrow().last().cloned()
105    }
106
107    pub fn get_cached_module(&self, path: &PathBuf) -> Option<BTreeMap<String, Value>> {
108        self.module_cache.borrow().get(path).cloned()
109    }
110
111    pub fn cache_module(&self, path: PathBuf, exports: BTreeMap<String, Value>) {
112        self.module_cache.borrow_mut().insert(path, exports);
113    }
114
115    pub fn set_module_exports(&self, names: Vec<String>) {
116        let mut stack = self.module_exports.borrow_mut();
117        if let Some(top) = stack.last_mut() {
118            *top = Some(names);
119        }
120    }
121
122    pub fn clear_module_exports(&self) {
123        self.module_exports.borrow_mut().push(None);
124    }
125
126    pub fn take_module_exports(&self) -> Option<Vec<String>> {
127        self.module_exports.borrow_mut().pop().flatten()
128    }
129
130    pub fn begin_module_load(&self, path: &PathBuf) -> Result<(), SemaError> {
131        let mut stack = self.module_load_stack.borrow_mut();
132        if let Some(pos) = stack.iter().position(|p| p == path) {
133            let mut cycle: Vec<String> = stack[pos..]
134                .iter()
135                .map(|p| p.display().to_string())
136                .collect();
137            cycle.push(path.display().to_string());
138            return Err(SemaError::eval(format!(
139                "cyclic import detected: {}",
140                cycle.join(" -> ")
141            )));
142        }
143        stack.push(path.clone());
144        Ok(())
145    }
146
147    pub fn end_module_load(&self, path: &PathBuf) {
148        let mut stack = self.module_load_stack.borrow_mut();
149        if matches!(stack.last(), Some(last) if last == path) {
150            stack.pop();
151        } else if let Some(pos) = stack.iter().rposition(|p| p == path) {
152            stack.remove(pos);
153        }
154    }
155
156    pub fn push_call_frame(&self, frame: CallFrame) {
157        self.call_stack.borrow_mut().push(frame);
158    }
159
160    pub fn call_stack_depth(&self) -> usize {
161        self.call_stack.borrow().len()
162    }
163
164    pub fn truncate_call_stack(&self, depth: usize) {
165        self.call_stack.borrow_mut().truncate(depth);
166    }
167
168    pub fn capture_stack_trace(&self) -> StackTrace {
169        let stack = self.call_stack.borrow();
170        StackTrace(stack.iter().rev().cloned().collect())
171    }
172
173    pub fn merge_span_table(&self, spans: SpanMap) {
174        let mut table = self.span_table.borrow_mut();
175        if table.len() < MAX_SPAN_TABLE_ENTRIES {
176            table.extend(spans);
177        }
178        // If table is full, skip merging new spans (preserves existing error locations)
179    }
180
181    pub fn lookup_span(&self, ptr: usize) -> Option<Span> {
182        self.span_table.borrow().get(&ptr).cloned()
183    }
184
185    pub fn set_eval_step_limit(&self, limit: usize) {
186        self.eval_step_limit.set(limit);
187    }
188
189    /// Set a wall-clock deadline after which evaluation should abort.
190    /// Passing `None` clears any existing deadline.
191    pub fn set_eval_deadline(&self, deadline: Option<Instant>) {
192        self.eval_deadline.set(deadline);
193    }
194
195    /// Returns true if a deadline is set and has been exceeded.
196    #[inline]
197    pub fn deadline_exceeded(&self) -> bool {
198        match self.eval_deadline.get() {
199            Some(d) => Instant::now() >= d,
200            None => false,
201        }
202    }
203
204    /// Returns an `eval` error if a deadline is set and exceeded; otherwise Ok(()).
205    #[inline]
206    pub fn check_deadline(&self) -> Result<(), SemaError> {
207        if self.deadline_exceeded() {
208            Err(SemaError::eval(
209                "evaluation exceeded time budget (looks like an infinite loop?)".to_string(),
210            ))
211        } else {
212            Ok(())
213        }
214    }
215
216    // --- User context methods ---
217
218    pub fn context_get(&self, key: &Value) -> Option<Value> {
219        let frames = self.user_context.borrow();
220        for frame in frames.iter().rev() {
221            if let Some(v) = frame.get(key) {
222                return Some(v.clone());
223            }
224        }
225        None
226    }
227
228    pub fn context_set(&self, key: Value, value: Value) {
229        let mut frames = self.user_context.borrow_mut();
230        if let Some(top) = frames.last_mut() {
231            top.insert(key, value);
232        }
233    }
234
235    pub fn context_has(&self, key: &Value) -> bool {
236        let frames = self.user_context.borrow();
237        frames.iter().any(|frame| frame.contains_key(key))
238    }
239
240    pub fn context_remove(&self, key: &Value) -> Option<Value> {
241        let mut frames = self.user_context.borrow_mut();
242        let mut first_found = None;
243        for frame in frames.iter_mut().rev() {
244            if let Some(v) = frame.remove(key) {
245                if first_found.is_none() {
246                    first_found = Some(v);
247                }
248            }
249        }
250        first_found
251    }
252
253    pub fn context_all(&self) -> BTreeMap<Value, Value> {
254        let frames = self.user_context.borrow();
255        let mut merged = BTreeMap::new();
256        for frame in frames.iter() {
257            for (k, v) in frame {
258                merged.insert(k.clone(), v.clone());
259            }
260        }
261        merged
262    }
263
264    pub fn context_push_frame(&self) {
265        self.user_context.borrow_mut().push(BTreeMap::new());
266    }
267
268    pub fn context_push_frame_with(&self, bindings: BTreeMap<Value, Value>) {
269        self.user_context.borrow_mut().push(bindings);
270    }
271
272    pub fn context_pop_frame(&self) {
273        let mut frames = self.user_context.borrow_mut();
274        if frames.len() > 1 {
275            frames.pop();
276        }
277    }
278
279    pub fn context_clear(&self) {
280        let mut frames = self.user_context.borrow_mut();
281        frames.clear();
282        frames.push(BTreeMap::new());
283    }
284
285    // --- Hidden context methods ---
286
287    pub fn hidden_get(&self, key: &Value) -> Option<Value> {
288        let frames = self.hidden_context.borrow();
289        for frame in frames.iter().rev() {
290            if let Some(v) = frame.get(key) {
291                return Some(v.clone());
292            }
293        }
294        None
295    }
296
297    pub fn hidden_set(&self, key: Value, value: Value) {
298        let mut frames = self.hidden_context.borrow_mut();
299        if let Some(top) = frames.last_mut() {
300            top.insert(key, value);
301        }
302    }
303
304    pub fn hidden_has(&self, key: &Value) -> bool {
305        let frames = self.hidden_context.borrow();
306        frames.iter().any(|frame| frame.contains_key(key))
307    }
308
309    pub fn hidden_push_frame(&self) {
310        self.hidden_context.borrow_mut().push(BTreeMap::new());
311    }
312
313    pub fn hidden_pop_frame(&self) {
314        let mut frames = self.hidden_context.borrow_mut();
315        if frames.len() > 1 {
316            frames.pop();
317        }
318    }
319
320    // --- Stack methods ---
321
322    pub fn context_stack_push(&self, key: Value, value: Value) {
323        self.context_stacks
324            .borrow_mut()
325            .entry(key)
326            .or_default()
327            .push(value);
328    }
329
330    pub fn context_stack_get(&self, key: &Value) -> Vec<Value> {
331        self.context_stacks
332            .borrow()
333            .get(key)
334            .cloned()
335            .unwrap_or_default()
336    }
337
338    pub fn context_stack_pop(&self, key: &Value) -> Option<Value> {
339        let mut stacks = self.context_stacks.borrow_mut();
340        let stack = stacks.get_mut(key)?;
341        let val = stack.pop();
342        if stack.is_empty() {
343            stacks.remove(key);
344        }
345        val
346    }
347}
348
349impl Default for EvalContext {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use std::collections::BTreeMap;
359    use std::path::PathBuf;
360
361    use crate::{Caps, Sandbox, Value};
362
363    // --- File path tracking ---
364
365    #[test]
366    fn test_push_pop_file_path() {
367        let ctx = EvalContext::new();
368        let path = PathBuf::from("/foo/bar/baz.sema");
369        ctx.push_file_path(path.clone());
370        assert_eq!(ctx.current_file_path(), Some(path));
371        ctx.pop_file_path();
372        assert_eq!(ctx.current_file_path(), None);
373    }
374
375    #[test]
376    fn test_current_file_dir() {
377        let ctx = EvalContext::new();
378        ctx.push_file_path(PathBuf::from("/foo/bar/baz.sema"));
379        assert_eq!(ctx.current_file_dir(), Some(PathBuf::from("/foo/bar")));
380    }
381
382    #[test]
383    fn test_current_file_dir_empty() {
384        let ctx = EvalContext::new();
385        assert_eq!(ctx.current_file_dir(), None);
386    }
387
388    #[test]
389    fn test_nested_file_paths() {
390        let ctx = EvalContext::new();
391        let first = PathBuf::from("/a/first.sema");
392        let second = PathBuf::from("/b/second.sema");
393        ctx.push_file_path(first.clone());
394        ctx.push_file_path(second.clone());
395        assert_eq!(ctx.current_file_path(), Some(second));
396        ctx.pop_file_path();
397        assert_eq!(ctx.current_file_path(), Some(first));
398    }
399
400    // --- Module caching ---
401
402    #[test]
403    fn test_cache_module() {
404        let ctx = EvalContext::new();
405        let path = PathBuf::from("/lib/math.sema");
406        let mut exports = BTreeMap::new();
407        exports.insert("add".to_string(), Value::int(1));
408        ctx.cache_module(path.clone(), exports.clone());
409        let cached = ctx.get_cached_module(&path).unwrap();
410        assert_eq!(cached.len(), 1);
411        assert_eq!(cached.get("add"), Some(&Value::int(1)));
412    }
413
414    #[test]
415    fn test_get_cached_module_miss() {
416        let ctx = EvalContext::new();
417        let path = PathBuf::from("/nonexistent.sema");
418        assert_eq!(ctx.get_cached_module(&path), None);
419    }
420
421    #[test]
422    fn test_cache_module_overwrites() {
423        let ctx = EvalContext::new();
424        let path = PathBuf::from("/lib/math.sema");
425
426        let mut first = BTreeMap::new();
427        first.insert("old".to_string(), Value::int(1));
428        ctx.cache_module(path.clone(), first);
429
430        let mut second = BTreeMap::new();
431        second.insert("new".to_string(), Value::int(2));
432        ctx.cache_module(path.clone(), second);
433
434        let cached = ctx.get_cached_module(&path).unwrap();
435        assert!(cached.get("old").is_none());
436        assert_eq!(cached.get("new"), Some(&Value::int(2)));
437    }
438
439    // --- Module exports ---
440
441    #[test]
442    fn test_module_exports_roundtrip() {
443        let ctx = EvalContext::new();
444        ctx.clear_module_exports(); // pushes None onto stack
445        ctx.set_module_exports(vec!["foo".to_string(), "bar".to_string()]);
446        let taken = ctx.take_module_exports();
447        assert_eq!(taken, Some(vec!["foo".to_string(), "bar".to_string()]));
448    }
449
450    #[test]
451    fn test_take_module_exports_empty() {
452        let ctx = EvalContext::new();
453        // Nothing has been pushed, so take should return None
454        assert_eq!(ctx.take_module_exports(), None);
455    }
456
457    // --- Cyclic import detection ---
458
459    #[test]
460    fn test_begin_module_load_ok() {
461        let ctx = EvalContext::new();
462        let path = PathBuf::from("/lib/a.sema");
463        assert!(ctx.begin_module_load(&path).is_ok());
464    }
465
466    #[test]
467    fn test_begin_module_load_cycle() {
468        let ctx = EvalContext::new();
469        let path = PathBuf::from("/lib/a.sema");
470        ctx.begin_module_load(&path).unwrap();
471        let result = ctx.begin_module_load(&path);
472        assert!(result.is_err());
473        let err = result.unwrap_err();
474        let msg = err.to_string();
475        assert!(
476            msg.contains("cyclic import"),
477            "error should mention cyclic import: {msg}"
478        );
479    }
480
481    #[test]
482    fn test_end_module_load() {
483        let ctx = EvalContext::new();
484        let path = PathBuf::from("/lib/a.sema");
485        ctx.begin_module_load(&path).unwrap();
486        ctx.end_module_load(&path);
487        // Stack is now empty, so beginning the same path again should succeed
488        assert!(ctx.begin_module_load(&path).is_ok());
489    }
490
491    #[test]
492    fn test_nested_module_loads() {
493        let ctx = EvalContext::new();
494        let a = PathBuf::from("/lib/a.sema");
495        let b = PathBuf::from("/lib/b.sema");
496        ctx.begin_module_load(&a).unwrap();
497        ctx.begin_module_load(&b).unwrap();
498        ctx.end_module_load(&b);
499        // A should still be in the stack — beginning A again should fail
500        let result = ctx.begin_module_load(&a);
501        assert!(result.is_err());
502        let msg = result.unwrap_err().to_string();
503        assert!(
504            msg.contains("cyclic import"),
505            "A should still be loading: {msg}"
506        );
507    }
508
509    // --- Sandbox integration ---
510
511    #[test]
512    fn test_new_with_sandbox() {
513        let sandbox = Sandbox::deny(Caps::NETWORK);
514        let ctx = EvalContext::new_with_sandbox(sandbox);
515        // Verify the sandbox is set by checking a denied capability
516        let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
517        assert!(result.is_err());
518        // Allowed capability should pass
519        let result = ctx.sandbox.check(Caps::FS_READ, "file/read");
520        assert!(result.is_ok());
521    }
522}
523
524thread_local! {
525    static STDLIB_CTX: EvalContext = EvalContext::new();
526}
527
528/// Get a reference to the shared stdlib EvalContext.
529/// Use this for stdlib callback invocations instead of creating throwaway contexts.
530pub fn with_stdlib_ctx<F, R>(f: F) -> R
531where
532    F: FnOnce(&EvalContext) -> R,
533{
534    STDLIB_CTX.with(f)
535}
536
537/// Register the full evaluator callback. Called by `sema-eval` during interpreter init.
538/// Stores into both `ctx` and the shared `STDLIB_CTX` so that stdlib simple-fn closures
539/// (which lack a ctx parameter) can still invoke the evaluator.
540pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
541    ctx.eval_fn.set(Some(f));
542    STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
543}
544
545/// Register the call-value callback. Called by `sema-eval` during interpreter init.
546/// Stores into both `ctx` and the shared `STDLIB_CTX`.
547pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
548    ctx.call_fn.set(Some(f));
549    STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
550}
551
552/// Evaluate an expression using the registered evaluator.
553/// Returns an error if no evaluator has been registered.
554pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
555    let f = ctx.eval_fn.get().ok_or_else(|| {
556        SemaError::eval("eval callback not registered — Interpreter::new() must be called first")
557    })?;
558    f(ctx, expr, env)
559}
560
561/// Call a function value with arguments using the registered callback.
562/// Returns an error if no callback has been registered.
563pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
564    let f = ctx.call_fn.get().ok_or_else(|| {
565        SemaError::eval("call callback not registered — Interpreter::new() must be called first")
566    })?;
567    f(ctx, func, args)
568}