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