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 #[inline]
227 pub fn check_loop_interrupt(&self) -> Result<(), SemaError> {
228 let steps = self.eval_steps.get().wrapping_add(1);
229 self.eval_steps.set(steps);
230 let limit = self.eval_step_limit.get();
231 if limit != 0 && steps > limit {
232 return Err(SemaError::eval(
233 "evaluation exceeded step limit (looks like an infinite loop?)".to_string(),
234 ));
235 }
236 if steps & 0x3FFF == 0 {
237 if self.deadline_exceeded() {
238 return Err(SemaError::eval(
239 "evaluation exceeded time budget (looks like an infinite loop?)".to_string(),
240 ));
241 }
242 if crate::async_signal::check_interrupt() {
243 return Err(SemaError::eval("evaluation cancelled".to_string()));
244 }
245 }
246 Ok(())
247 }
248
249 pub fn context_get(&self, key: &Value) -> Option<Value> {
252 let frames = self.user_context.borrow();
253 for frame in frames.iter().rev() {
254 if let Some(v) = frame.get(key) {
255 return Some(v.clone());
256 }
257 }
258 None
259 }
260
261 pub fn context_set(&self, key: Value, value: Value) {
262 let mut frames = self.user_context.borrow_mut();
263 if let Some(top) = frames.last_mut() {
264 top.insert(key, value);
265 }
266 }
267
268 pub fn context_has(&self, key: &Value) -> bool {
269 let frames = self.user_context.borrow();
270 frames.iter().any(|frame| frame.contains_key(key))
271 }
272
273 pub fn context_remove(&self, key: &Value) -> Option<Value> {
274 let mut frames = self.user_context.borrow_mut();
275 let mut first_found = None;
276 for frame in frames.iter_mut().rev() {
277 if let Some(v) = frame.remove(key) {
278 if first_found.is_none() {
279 first_found = Some(v);
280 }
281 }
282 }
283 first_found
284 }
285
286 pub fn context_all(&self) -> BTreeMap<Value, Value> {
287 let frames = self.user_context.borrow();
288 let mut merged = BTreeMap::new();
289 for frame in frames.iter() {
290 for (k, v) in frame {
291 merged.insert(k.clone(), v.clone());
292 }
293 }
294 merged
295 }
296
297 pub fn context_push_frame(&self) {
298 self.user_context.borrow_mut().push(BTreeMap::new());
299 }
300
301 pub fn context_push_frame_with(&self, bindings: BTreeMap<Value, Value>) {
302 self.user_context.borrow_mut().push(bindings);
303 }
304
305 pub fn context_pop_frame(&self) {
306 let mut frames = self.user_context.borrow_mut();
307 if frames.len() > 1 {
308 frames.pop();
309 }
310 }
311
312 pub fn context_clear(&self) {
313 let mut frames = self.user_context.borrow_mut();
314 frames.clear();
315 frames.push(BTreeMap::new());
316 }
317
318 pub fn hidden_get(&self, key: &Value) -> Option<Value> {
321 let frames = self.hidden_context.borrow();
322 for frame in frames.iter().rev() {
323 if let Some(v) = frame.get(key) {
324 return Some(v.clone());
325 }
326 }
327 None
328 }
329
330 pub fn hidden_set(&self, key: Value, value: Value) {
331 let mut frames = self.hidden_context.borrow_mut();
332 if let Some(top) = frames.last_mut() {
333 top.insert(key, value);
334 }
335 }
336
337 pub fn hidden_has(&self, key: &Value) -> bool {
338 let frames = self.hidden_context.borrow();
339 frames.iter().any(|frame| frame.contains_key(key))
340 }
341
342 pub fn hidden_push_frame(&self) {
343 self.hidden_context.borrow_mut().push(BTreeMap::new());
344 }
345
346 pub fn hidden_pop_frame(&self) {
347 let mut frames = self.hidden_context.borrow_mut();
348 if frames.len() > 1 {
349 frames.pop();
350 }
351 }
352
353 pub fn context_stack_push(&self, key: Value, value: Value) {
356 self.context_stacks
357 .borrow_mut()
358 .entry(key)
359 .or_default()
360 .push(value);
361 }
362
363 pub fn context_stack_get(&self, key: &Value) -> Vec<Value> {
364 self.context_stacks
365 .borrow()
366 .get(key)
367 .cloned()
368 .unwrap_or_default()
369 }
370
371 pub fn context_stack_pop(&self, key: &Value) -> Option<Value> {
372 let mut stacks = self.context_stacks.borrow_mut();
373 let stack = stacks.get_mut(key)?;
374 let val = stack.pop();
375 if stack.is_empty() {
376 stacks.remove(key);
377 }
378 val
379 }
380}
381
382impl Default for EvalContext {
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use std::collections::BTreeMap;
392 use std::path::PathBuf;
393
394 use crate::{Caps, Sandbox, Value};
395
396 #[test]
399 fn test_push_pop_file_path() {
400 let ctx = EvalContext::new();
401 let path = PathBuf::from("/foo/bar/baz.sema");
402 ctx.push_file_path(path.clone());
403 assert_eq!(ctx.current_file_path(), Some(path));
404 ctx.pop_file_path();
405 assert_eq!(ctx.current_file_path(), None);
406 }
407
408 #[test]
409 fn test_current_file_dir() {
410 let ctx = EvalContext::new();
411 ctx.push_file_path(PathBuf::from("/foo/bar/baz.sema"));
412 assert_eq!(ctx.current_file_dir(), Some(PathBuf::from("/foo/bar")));
413 }
414
415 #[test]
416 fn test_current_file_dir_empty() {
417 let ctx = EvalContext::new();
418 assert_eq!(ctx.current_file_dir(), None);
419 }
420
421 #[test]
422 fn test_nested_file_paths() {
423 let ctx = EvalContext::new();
424 let first = PathBuf::from("/a/first.sema");
425 let second = PathBuf::from("/b/second.sema");
426 ctx.push_file_path(first.clone());
427 ctx.push_file_path(second.clone());
428 assert_eq!(ctx.current_file_path(), Some(second));
429 ctx.pop_file_path();
430 assert_eq!(ctx.current_file_path(), Some(first));
431 }
432
433 #[test]
436 fn test_cache_module() {
437 let ctx = EvalContext::new();
438 let path = PathBuf::from("/lib/math.sema");
439 let mut exports = BTreeMap::new();
440 exports.insert("add".to_string(), Value::int(1));
441 ctx.cache_module(path.clone(), exports.clone());
442 let cached = ctx.get_cached_module(&path).unwrap();
443 assert_eq!(cached.len(), 1);
444 assert_eq!(cached.get("add"), Some(&Value::int(1)));
445 }
446
447 #[test]
448 fn test_get_cached_module_miss() {
449 let ctx = EvalContext::new();
450 let path = PathBuf::from("/nonexistent.sema");
451 assert_eq!(ctx.get_cached_module(&path), None);
452 }
453
454 #[test]
455 fn test_cache_module_overwrites() {
456 let ctx = EvalContext::new();
457 let path = PathBuf::from("/lib/math.sema");
458
459 let mut first = BTreeMap::new();
460 first.insert("old".to_string(), Value::int(1));
461 ctx.cache_module(path.clone(), first);
462
463 let mut second = BTreeMap::new();
464 second.insert("new".to_string(), Value::int(2));
465 ctx.cache_module(path.clone(), second);
466
467 let cached = ctx.get_cached_module(&path).unwrap();
468 assert!(!cached.contains_key("old"));
469 assert_eq!(cached.get("new"), Some(&Value::int(2)));
470 }
471
472 #[test]
475 fn test_module_exports_roundtrip() {
476 let ctx = EvalContext::new();
477 ctx.clear_module_exports(); ctx.set_module_exports(vec!["foo".to_string(), "bar".to_string()]);
479 let taken = ctx.take_module_exports();
480 assert_eq!(taken, Some(vec!["foo".to_string(), "bar".to_string()]));
481 }
482
483 #[test]
484 fn test_take_module_exports_empty() {
485 let ctx = EvalContext::new();
486 assert_eq!(ctx.take_module_exports(), None);
488 }
489
490 #[test]
493 fn test_begin_module_load_ok() {
494 let ctx = EvalContext::new();
495 let path = PathBuf::from("/lib/a.sema");
496 assert!(ctx.begin_module_load(&path).is_ok());
497 }
498
499 #[test]
500 fn test_begin_module_load_cycle() {
501 let ctx = EvalContext::new();
502 let path = PathBuf::from("/lib/a.sema");
503 ctx.begin_module_load(&path).unwrap();
504 let result = ctx.begin_module_load(&path);
505 assert!(result.is_err());
506 let err = result.unwrap_err();
507 let msg = err.to_string();
508 assert!(
509 msg.contains("cyclic import"),
510 "error should mention cyclic import: {msg}"
511 );
512 }
513
514 #[test]
515 fn test_end_module_load() {
516 let ctx = EvalContext::new();
517 let path = PathBuf::from("/lib/a.sema");
518 ctx.begin_module_load(&path).unwrap();
519 ctx.end_module_load(&path);
520 assert!(ctx.begin_module_load(&path).is_ok());
522 }
523
524 #[test]
525 fn test_nested_module_loads() {
526 let ctx = EvalContext::new();
527 let a = PathBuf::from("/lib/a.sema");
528 let b = PathBuf::from("/lib/b.sema");
529 ctx.begin_module_load(&a).unwrap();
530 ctx.begin_module_load(&b).unwrap();
531 ctx.end_module_load(&b);
532 let result = ctx.begin_module_load(&a);
534 assert!(result.is_err());
535 let msg = result.unwrap_err().to_string();
536 assert!(
537 msg.contains("cyclic import"),
538 "A should still be loading: {msg}"
539 );
540 }
541
542 #[test]
545 fn test_new_with_sandbox() {
546 let sandbox = Sandbox::deny(Caps::NETWORK);
547 let ctx = EvalContext::new_with_sandbox(sandbox);
548 let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
550 assert!(result.is_err());
551 let result = ctx.sandbox.check(Caps::FS_READ, "file/read");
553 assert!(result.is_ok());
554 }
555}
556
557thread_local! {
558 static STDLIB_CTX: EvalContext = EvalContext::new();
559}
560
561pub fn with_stdlib_ctx<F, R>(f: F) -> R
564where
565 F: FnOnce(&EvalContext) -> R,
566{
567 STDLIB_CTX.with(f)
568}
569
570pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
574 ctx.eval_fn.set(Some(f));
575 STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
576}
577
578pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
581 ctx.call_fn.set(Some(f));
582 STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
583}
584
585pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
588 let f = ctx.eval_fn.get().ok_or_else(|| {
589 SemaError::eval("eval callback not registered — Interpreter::new() must be called first")
590 })?;
591 f(ctx, expr, env)
592}
593
594pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
597 let f = ctx.call_fn.get().ok_or_else(|| {
598 SemaError::eval("call callback not registered — Interpreter::new() must be called first")
599 })?;
600 f(ctx, func, args)
601}