Skip to main content

sema_core/
context.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, HashMap};
3use std::path::PathBuf;
4
5use crate::{CallFrame, Env, Sandbox, SemaError, Span, SpanMap, StackTrace, Value};
6
7const MAX_SPAN_TABLE_ENTRIES: usize = 200_000;
8
9/// Function-pointer type for the full evaluator callback: (ctx, expr, env) -> Result<Value, SemaError>
10pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
11
12/// Function-pointer type for calling a function value with evaluated arguments: (ctx, func, args) -> Result<Value, SemaError>
13pub type CallCallbackFn = fn(&EvalContext, &Value, &[Value]) -> Result<Value, SemaError>;
14
15pub struct EvalContext {
16    pub module_cache: RefCell<BTreeMap<PathBuf, BTreeMap<String, Value>>>,
17    pub current_file: RefCell<Vec<PathBuf>>,
18    pub module_exports: RefCell<Vec<Option<Vec<String>>>>,
19    pub module_load_stack: RefCell<Vec<PathBuf>>,
20    pub call_stack: RefCell<Vec<CallFrame>>,
21    pub span_table: RefCell<HashMap<usize, Span>>,
22    pub eval_depth: Cell<usize>,
23    pub max_eval_depth: Cell<usize>,
24    pub eval_step_limit: Cell<usize>,
25    pub eval_steps: Cell<usize>,
26    pub sandbox: Sandbox,
27    pub user_context: RefCell<Vec<BTreeMap<Value, Value>>>,
28    pub hidden_context: RefCell<Vec<BTreeMap<Value, Value>>>,
29    pub context_stacks: RefCell<BTreeMap<Value, Vec<Value>>>,
30    pub eval_fn: Cell<Option<EvalCallbackFn>>,
31    pub call_fn: Cell<Option<CallCallbackFn>>,
32    pub interactive: Cell<bool>,
33}
34
35impl EvalContext {
36    pub fn new() -> Self {
37        EvalContext {
38            module_cache: RefCell::new(BTreeMap::new()),
39            current_file: RefCell::new(Vec::new()),
40            module_exports: RefCell::new(Vec::new()),
41            module_load_stack: RefCell::new(Vec::new()),
42            call_stack: RefCell::new(Vec::new()),
43            span_table: RefCell::new(HashMap::new()),
44            eval_depth: Cell::new(0),
45            max_eval_depth: Cell::new(0),
46            eval_step_limit: Cell::new(0),
47            eval_steps: Cell::new(0),
48            sandbox: Sandbox::allow_all(),
49            user_context: RefCell::new(vec![BTreeMap::new()]),
50            hidden_context: RefCell::new(vec![BTreeMap::new()]),
51            context_stacks: RefCell::new(BTreeMap::new()),
52            eval_fn: Cell::new(None),
53            call_fn: Cell::new(None),
54            interactive: Cell::new(false),
55        }
56    }
57
58    pub fn new_with_sandbox(sandbox: Sandbox) -> Self {
59        EvalContext {
60            module_cache: RefCell::new(BTreeMap::new()),
61            current_file: RefCell::new(Vec::new()),
62            module_exports: RefCell::new(Vec::new()),
63            module_load_stack: RefCell::new(Vec::new()),
64            call_stack: RefCell::new(Vec::new()),
65            span_table: RefCell::new(HashMap::new()),
66            eval_depth: Cell::new(0),
67            max_eval_depth: Cell::new(0),
68            eval_step_limit: Cell::new(0),
69            eval_steps: Cell::new(0),
70            sandbox,
71            user_context: RefCell::new(vec![BTreeMap::new()]),
72            hidden_context: RefCell::new(vec![BTreeMap::new()]),
73            context_stacks: RefCell::new(BTreeMap::new()),
74            eval_fn: Cell::new(None),
75            call_fn: Cell::new(None),
76            interactive: Cell::new(false),
77        }
78    }
79
80    pub fn push_file_path(&self, path: PathBuf) {
81        self.current_file.borrow_mut().push(path);
82    }
83
84    pub fn pop_file_path(&self) {
85        self.current_file.borrow_mut().pop();
86    }
87
88    pub fn current_file_dir(&self) -> Option<PathBuf> {
89        self.current_file
90            .borrow()
91            .last()
92            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
93    }
94
95    pub fn current_file_path(&self) -> Option<PathBuf> {
96        self.current_file.borrow().last().cloned()
97    }
98
99    pub fn get_cached_module(&self, path: &PathBuf) -> Option<BTreeMap<String, Value>> {
100        self.module_cache.borrow().get(path).cloned()
101    }
102
103    pub fn cache_module(&self, path: PathBuf, exports: BTreeMap<String, Value>) {
104        self.module_cache.borrow_mut().insert(path, exports);
105    }
106
107    pub fn set_module_exports(&self, names: Vec<String>) {
108        let mut stack = self.module_exports.borrow_mut();
109        if let Some(top) = stack.last_mut() {
110            *top = Some(names);
111        }
112    }
113
114    pub fn clear_module_exports(&self) {
115        self.module_exports.borrow_mut().push(None);
116    }
117
118    pub fn take_module_exports(&self) -> Option<Vec<String>> {
119        self.module_exports.borrow_mut().pop().flatten()
120    }
121
122    pub fn begin_module_load(&self, path: &PathBuf) -> Result<(), SemaError> {
123        let mut stack = self.module_load_stack.borrow_mut();
124        if let Some(pos) = stack.iter().position(|p| p == path) {
125            let mut cycle: Vec<String> = stack[pos..]
126                .iter()
127                .map(|p| p.display().to_string())
128                .collect();
129            cycle.push(path.display().to_string());
130            return Err(SemaError::eval(format!(
131                "cyclic import detected: {}",
132                cycle.join(" -> ")
133            )));
134        }
135        stack.push(path.clone());
136        Ok(())
137    }
138
139    pub fn end_module_load(&self, path: &PathBuf) {
140        let mut stack = self.module_load_stack.borrow_mut();
141        if matches!(stack.last(), Some(last) if last == path) {
142            stack.pop();
143        } else if let Some(pos) = stack.iter().rposition(|p| p == path) {
144            stack.remove(pos);
145        }
146    }
147
148    pub fn push_call_frame(&self, frame: CallFrame) {
149        self.call_stack.borrow_mut().push(frame);
150    }
151
152    pub fn call_stack_depth(&self) -> usize {
153        self.call_stack.borrow().len()
154    }
155
156    pub fn truncate_call_stack(&self, depth: usize) {
157        self.call_stack.borrow_mut().truncate(depth);
158    }
159
160    pub fn capture_stack_trace(&self) -> StackTrace {
161        let stack = self.call_stack.borrow();
162        StackTrace(stack.iter().rev().cloned().collect())
163    }
164
165    pub fn merge_span_table(&self, spans: SpanMap) {
166        let mut table = self.span_table.borrow_mut();
167        if table.len() < MAX_SPAN_TABLE_ENTRIES {
168            table.extend(spans);
169        }
170        // If table is full, skip merging new spans (preserves existing error locations)
171    }
172
173    pub fn lookup_span(&self, ptr: usize) -> Option<Span> {
174        self.span_table.borrow().get(&ptr).cloned()
175    }
176
177    pub fn set_eval_step_limit(&self, limit: usize) {
178        self.eval_step_limit.set(limit);
179    }
180
181    // --- User context methods ---
182
183    pub fn context_get(&self, key: &Value) -> Option<Value> {
184        let frames = self.user_context.borrow();
185        for frame in frames.iter().rev() {
186            if let Some(v) = frame.get(key) {
187                return Some(v.clone());
188            }
189        }
190        None
191    }
192
193    pub fn context_set(&self, key: Value, value: Value) {
194        let mut frames = self.user_context.borrow_mut();
195        if let Some(top) = frames.last_mut() {
196            top.insert(key, value);
197        }
198    }
199
200    pub fn context_has(&self, key: &Value) -> bool {
201        let frames = self.user_context.borrow();
202        frames.iter().any(|frame| frame.contains_key(key))
203    }
204
205    pub fn context_remove(&self, key: &Value) -> Option<Value> {
206        let mut frames = self.user_context.borrow_mut();
207        let mut first_found = None;
208        for frame in frames.iter_mut().rev() {
209            if let Some(v) = frame.remove(key) {
210                if first_found.is_none() {
211                    first_found = Some(v);
212                }
213            }
214        }
215        first_found
216    }
217
218    pub fn context_all(&self) -> BTreeMap<Value, Value> {
219        let frames = self.user_context.borrow();
220        let mut merged = BTreeMap::new();
221        for frame in frames.iter() {
222            for (k, v) in frame {
223                merged.insert(k.clone(), v.clone());
224            }
225        }
226        merged
227    }
228
229    pub fn context_push_frame(&self) {
230        self.user_context.borrow_mut().push(BTreeMap::new());
231    }
232
233    pub fn context_push_frame_with(&self, bindings: BTreeMap<Value, Value>) {
234        self.user_context.borrow_mut().push(bindings);
235    }
236
237    pub fn context_pop_frame(&self) {
238        let mut frames = self.user_context.borrow_mut();
239        if frames.len() > 1 {
240            frames.pop();
241        }
242    }
243
244    pub fn context_clear(&self) {
245        let mut frames = self.user_context.borrow_mut();
246        frames.clear();
247        frames.push(BTreeMap::new());
248    }
249
250    // --- Hidden context methods ---
251
252    pub fn hidden_get(&self, key: &Value) -> Option<Value> {
253        let frames = self.hidden_context.borrow();
254        for frame in frames.iter().rev() {
255            if let Some(v) = frame.get(key) {
256                return Some(v.clone());
257            }
258        }
259        None
260    }
261
262    pub fn hidden_set(&self, key: Value, value: Value) {
263        let mut frames = self.hidden_context.borrow_mut();
264        if let Some(top) = frames.last_mut() {
265            top.insert(key, value);
266        }
267    }
268
269    pub fn hidden_has(&self, key: &Value) -> bool {
270        let frames = self.hidden_context.borrow();
271        frames.iter().any(|frame| frame.contains_key(key))
272    }
273
274    pub fn hidden_push_frame(&self) {
275        self.hidden_context.borrow_mut().push(BTreeMap::new());
276    }
277
278    pub fn hidden_pop_frame(&self) {
279        let mut frames = self.hidden_context.borrow_mut();
280        if frames.len() > 1 {
281            frames.pop();
282        }
283    }
284
285    // --- Stack methods ---
286
287    pub fn context_stack_push(&self, key: Value, value: Value) {
288        self.context_stacks
289            .borrow_mut()
290            .entry(key)
291            .or_default()
292            .push(value);
293    }
294
295    pub fn context_stack_get(&self, key: &Value) -> Vec<Value> {
296        self.context_stacks
297            .borrow()
298            .get(key)
299            .cloned()
300            .unwrap_or_default()
301    }
302
303    pub fn context_stack_pop(&self, key: &Value) -> Option<Value> {
304        let mut stacks = self.context_stacks.borrow_mut();
305        let stack = stacks.get_mut(key)?;
306        let val = stack.pop();
307        if stack.is_empty() {
308            stacks.remove(key);
309        }
310        val
311    }
312}
313
314impl Default for EvalContext {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320thread_local! {
321    static STDLIB_CTX: EvalContext = EvalContext::new();
322}
323
324/// Get a reference to the shared stdlib EvalContext.
325/// Use this for stdlib callback invocations instead of creating throwaway contexts.
326pub fn with_stdlib_ctx<F, R>(f: F) -> R
327where
328    F: FnOnce(&EvalContext) -> R,
329{
330    STDLIB_CTX.with(f)
331}
332
333/// Register the full evaluator callback. Called by `sema-eval` during interpreter init.
334/// Stores into both `ctx` and the shared `STDLIB_CTX` so that stdlib simple-fn closures
335/// (which lack a ctx parameter) can still invoke the evaluator.
336pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
337    ctx.eval_fn.set(Some(f));
338    STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
339}
340
341/// Register the call-value callback. Called by `sema-eval` during interpreter init.
342/// Stores into both `ctx` and the shared `STDLIB_CTX`.
343pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
344    ctx.call_fn.set(Some(f));
345    STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
346}
347
348/// Evaluate an expression using the registered evaluator.
349/// Panics if no evaluator has been registered (programming error).
350pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
351    let f = ctx
352        .eval_fn
353        .get()
354        .expect("eval callback not registered — Interpreter::new() must be called first");
355    f(ctx, expr, env)
356}
357
358/// Call a function value with arguments using the registered callback.
359/// Panics if no callback has been registered (programming error).
360pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
361    let f = ctx
362        .call_fn
363        .get()
364        .expect("call callback not registered — Interpreter::new() must be called first");
365    f(ctx, func, args)
366}