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
9pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
11
12pub 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 }
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 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 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 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
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use std::collections::BTreeMap;
324 use std::path::PathBuf;
325
326 use crate::{Caps, Sandbox, Value};
327
328 #[test]
331 fn test_push_pop_file_path() {
332 let ctx = EvalContext::new();
333 let path = PathBuf::from("/foo/bar/baz.sema");
334 ctx.push_file_path(path.clone());
335 assert_eq!(ctx.current_file_path(), Some(path));
336 ctx.pop_file_path();
337 assert_eq!(ctx.current_file_path(), None);
338 }
339
340 #[test]
341 fn test_current_file_dir() {
342 let ctx = EvalContext::new();
343 ctx.push_file_path(PathBuf::from("/foo/bar/baz.sema"));
344 assert_eq!(ctx.current_file_dir(), Some(PathBuf::from("/foo/bar")));
345 }
346
347 #[test]
348 fn test_current_file_dir_empty() {
349 let ctx = EvalContext::new();
350 assert_eq!(ctx.current_file_dir(), None);
351 }
352
353 #[test]
354 fn test_nested_file_paths() {
355 let ctx = EvalContext::new();
356 let first = PathBuf::from("/a/first.sema");
357 let second = PathBuf::from("/b/second.sema");
358 ctx.push_file_path(first.clone());
359 ctx.push_file_path(second.clone());
360 assert_eq!(ctx.current_file_path(), Some(second));
361 ctx.pop_file_path();
362 assert_eq!(ctx.current_file_path(), Some(first));
363 }
364
365 #[test]
368 fn test_cache_module() {
369 let ctx = EvalContext::new();
370 let path = PathBuf::from("/lib/math.sema");
371 let mut exports = BTreeMap::new();
372 exports.insert("add".to_string(), Value::int(1));
373 ctx.cache_module(path.clone(), exports.clone());
374 let cached = ctx.get_cached_module(&path).unwrap();
375 assert_eq!(cached.len(), 1);
376 assert_eq!(cached.get("add"), Some(&Value::int(1)));
377 }
378
379 #[test]
380 fn test_get_cached_module_miss() {
381 let ctx = EvalContext::new();
382 let path = PathBuf::from("/nonexistent.sema");
383 assert_eq!(ctx.get_cached_module(&path), None);
384 }
385
386 #[test]
387 fn test_cache_module_overwrites() {
388 let ctx = EvalContext::new();
389 let path = PathBuf::from("/lib/math.sema");
390
391 let mut first = BTreeMap::new();
392 first.insert("old".to_string(), Value::int(1));
393 ctx.cache_module(path.clone(), first);
394
395 let mut second = BTreeMap::new();
396 second.insert("new".to_string(), Value::int(2));
397 ctx.cache_module(path.clone(), second);
398
399 let cached = ctx.get_cached_module(&path).unwrap();
400 assert!(cached.get("old").is_none());
401 assert_eq!(cached.get("new"), Some(&Value::int(2)));
402 }
403
404 #[test]
407 fn test_module_exports_roundtrip() {
408 let ctx = EvalContext::new();
409 ctx.clear_module_exports(); ctx.set_module_exports(vec!["foo".to_string(), "bar".to_string()]);
411 let taken = ctx.take_module_exports();
412 assert_eq!(taken, Some(vec!["foo".to_string(), "bar".to_string()]));
413 }
414
415 #[test]
416 fn test_take_module_exports_empty() {
417 let ctx = EvalContext::new();
418 assert_eq!(ctx.take_module_exports(), None);
420 }
421
422 #[test]
425 fn test_begin_module_load_ok() {
426 let ctx = EvalContext::new();
427 let path = PathBuf::from("/lib/a.sema");
428 assert!(ctx.begin_module_load(&path).is_ok());
429 }
430
431 #[test]
432 fn test_begin_module_load_cycle() {
433 let ctx = EvalContext::new();
434 let path = PathBuf::from("/lib/a.sema");
435 ctx.begin_module_load(&path).unwrap();
436 let result = ctx.begin_module_load(&path);
437 assert!(result.is_err());
438 let err = result.unwrap_err();
439 let msg = err.to_string();
440 assert!(
441 msg.contains("cyclic import"),
442 "error should mention cyclic import: {msg}"
443 );
444 }
445
446 #[test]
447 fn test_end_module_load() {
448 let ctx = EvalContext::new();
449 let path = PathBuf::from("/lib/a.sema");
450 ctx.begin_module_load(&path).unwrap();
451 ctx.end_module_load(&path);
452 assert!(ctx.begin_module_load(&path).is_ok());
454 }
455
456 #[test]
457 fn test_nested_module_loads() {
458 let ctx = EvalContext::new();
459 let a = PathBuf::from("/lib/a.sema");
460 let b = PathBuf::from("/lib/b.sema");
461 ctx.begin_module_load(&a).unwrap();
462 ctx.begin_module_load(&b).unwrap();
463 ctx.end_module_load(&b);
464 let result = ctx.begin_module_load(&a);
466 assert!(result.is_err());
467 let msg = result.unwrap_err().to_string();
468 assert!(
469 msg.contains("cyclic import"),
470 "A should still be loading: {msg}"
471 );
472 }
473
474 #[test]
477 fn test_new_with_sandbox() {
478 let sandbox = Sandbox::deny(Caps::NETWORK);
479 let ctx = EvalContext::new_with_sandbox(sandbox);
480 let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
482 assert!(result.is_err());
483 let result = ctx.sandbox.check(Caps::FS_READ, "file/read");
485 assert!(result.is_ok());
486 }
487}
488
489thread_local! {
490 static STDLIB_CTX: EvalContext = EvalContext::new();
491}
492
493pub fn with_stdlib_ctx<F, R>(f: F) -> R
496where
497 F: FnOnce(&EvalContext) -> R,
498{
499 STDLIB_CTX.with(f)
500}
501
502pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
506 ctx.eval_fn.set(Some(f));
507 STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
508}
509
510pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
513 ctx.call_fn.set(Some(f));
514 STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
515}
516
517pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
520 let f = ctx.eval_fn.get().ok_or_else(|| {
521 SemaError::eval("eval callback not registered — Interpreter::new() must be called first")
522 })?;
523 f(ctx, expr, env)
524}
525
526pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
529 let f = ctx.call_fn.get().ok_or_else(|| {
530 SemaError::eval("call callback not registered — Interpreter::new() must be called first")
531 })?;
532 f(ctx, func, args)
533}