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