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
10pub type EvalCallbackFn = fn(&EvalContext, &Value, &Env) -> Result<Value, SemaError>;
12
13pub 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 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 }
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 pub fn set_eval_deadline(&self, deadline: Option<Instant>) {
192 self.eval_deadline.set(deadline);
193 }
194
195 #[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 #[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 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 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 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 #[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 #[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.contains_key("old"));
436 assert_eq!(cached.get("new"), Some(&Value::int(2)));
437 }
438
439 #[test]
442 fn test_module_exports_roundtrip() {
443 let ctx = EvalContext::new();
444 ctx.clear_module_exports(); 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 assert_eq!(ctx.take_module_exports(), None);
455 }
456
457 #[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 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 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 #[test]
512 fn test_new_with_sandbox() {
513 let sandbox = Sandbox::deny(Caps::NETWORK);
514 let ctx = EvalContext::new_with_sandbox(sandbox);
515 let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
517 assert!(result.is_err());
518 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
528pub fn with_stdlib_ctx<F, R>(f: F) -> R
531where
532 F: FnOnce(&EvalContext) -> R,
533{
534 STDLIB_CTX.with(f)
535}
536
537pub 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
545pub 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
552pub 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
561pub 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}