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 pub vm_backend: Cell<bool>,
47}
48
49impl EvalContext {
50 pub fn new() -> Self {
51 EvalContext {
52 module_cache: RefCell::new(BTreeMap::new()),
53 current_file: RefCell::new(Vec::new()),
54 module_exports: RefCell::new(Vec::new()),
55 module_load_stack: RefCell::new(Vec::new()),
56 call_stack: RefCell::new(Vec::new()),
57 span_table: RefCell::new(HashMap::new()),
58 eval_depth: Cell::new(0),
59 max_eval_depth: Cell::new(0),
60 eval_step_limit: Cell::new(0),
61 eval_steps: Cell::new(0),
62 eval_deadline: Cell::new(None),
63 sandbox: Sandbox::allow_all(),
64 user_context: RefCell::new(vec![BTreeMap::new()]),
65 hidden_context: RefCell::new(vec![BTreeMap::new()]),
66 context_stacks: RefCell::new(BTreeMap::new()),
67 eval_fn: Cell::new(None),
68 call_fn: Cell::new(None),
69 interactive: Cell::new(false),
70 vm_backend: Cell::new(false),
71 }
72 }
73
74 pub fn new_with_sandbox(sandbox: Sandbox) -> Self {
75 EvalContext {
76 module_cache: RefCell::new(BTreeMap::new()),
77 current_file: RefCell::new(Vec::new()),
78 module_exports: RefCell::new(Vec::new()),
79 module_load_stack: RefCell::new(Vec::new()),
80 call_stack: RefCell::new(Vec::new()),
81 span_table: RefCell::new(HashMap::new()),
82 eval_depth: Cell::new(0),
83 max_eval_depth: Cell::new(0),
84 eval_step_limit: Cell::new(0),
85 eval_steps: Cell::new(0),
86 eval_deadline: Cell::new(None),
87 sandbox,
88 user_context: RefCell::new(vec![BTreeMap::new()]),
89 hidden_context: RefCell::new(vec![BTreeMap::new()]),
90 context_stacks: RefCell::new(BTreeMap::new()),
91 eval_fn: Cell::new(None),
92 call_fn: Cell::new(None),
93 interactive: Cell::new(false),
94 vm_backend: Cell::new(false),
95 }
96 }
97
98 pub fn set_vm_backend(&self, on: bool) {
100 self.vm_backend.set(on);
101 }
102
103 pub fn vm_backend(&self) -> bool {
105 self.vm_backend.get()
106 }
107
108 pub fn push_file_path(&self, path: PathBuf) {
109 self.current_file.borrow_mut().push(path);
110 }
111
112 pub fn pop_file_path(&self) {
113 self.current_file.borrow_mut().pop();
114 }
115
116 pub fn current_file_dir(&self) -> Option<PathBuf> {
117 self.current_file
118 .borrow()
119 .last()
120 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
121 }
122
123 pub fn current_file_path(&self) -> Option<PathBuf> {
124 self.current_file.borrow().last().cloned()
125 }
126
127 pub fn get_cached_module(&self, path: &PathBuf) -> Option<BTreeMap<String, Value>> {
128 self.module_cache.borrow().get(path).cloned()
129 }
130
131 pub fn cache_module(&self, path: PathBuf, exports: BTreeMap<String, Value>) {
132 self.module_cache.borrow_mut().insert(path, exports);
133 }
134
135 pub fn set_module_exports(&self, names: Vec<String>) {
136 let mut stack = self.module_exports.borrow_mut();
137 if let Some(top) = stack.last_mut() {
138 *top = Some(names);
139 }
140 }
141
142 pub fn clear_module_exports(&self) {
143 self.module_exports.borrow_mut().push(None);
144 }
145
146 pub fn take_module_exports(&self) -> Option<Vec<String>> {
147 self.module_exports.borrow_mut().pop().flatten()
148 }
149
150 pub fn begin_module_load(&self, path: &PathBuf) -> Result<(), SemaError> {
151 let mut stack = self.module_load_stack.borrow_mut();
152 if let Some(pos) = stack.iter().position(|p| p == path) {
153 let mut cycle: Vec<String> = stack[pos..]
154 .iter()
155 .map(|p| p.display().to_string())
156 .collect();
157 cycle.push(path.display().to_string());
158 return Err(SemaError::eval(format!(
159 "cyclic import detected: {}",
160 cycle.join(" -> ")
161 )));
162 }
163 stack.push(path.clone());
164 Ok(())
165 }
166
167 pub fn end_module_load(&self, path: &PathBuf) {
168 let mut stack = self.module_load_stack.borrow_mut();
169 if matches!(stack.last(), Some(last) if last == path) {
170 stack.pop();
171 } else if let Some(pos) = stack.iter().rposition(|p| p == path) {
172 stack.remove(pos);
173 }
174 }
175
176 pub fn push_call_frame(&self, frame: CallFrame) {
177 self.call_stack.borrow_mut().push(frame);
178 }
179
180 pub fn call_stack_depth(&self) -> usize {
181 self.call_stack.borrow().len()
182 }
183
184 pub fn truncate_call_stack(&self, depth: usize) {
185 self.call_stack.borrow_mut().truncate(depth);
186 }
187
188 pub fn capture_stack_trace(&self) -> StackTrace {
189 let stack = self.call_stack.borrow();
190 StackTrace(stack.iter().rev().cloned().collect())
191 }
192
193 pub fn merge_span_table(&self, spans: SpanMap) {
194 let mut table = self.span_table.borrow_mut();
195 if table.len() < MAX_SPAN_TABLE_ENTRIES {
196 table.extend(spans);
197 }
198 }
200
201 pub fn lookup_span(&self, ptr: usize) -> Option<Span> {
202 self.span_table.borrow().get(&ptr).cloned()
203 }
204
205 pub fn set_eval_step_limit(&self, limit: usize) {
206 self.eval_step_limit.set(limit);
207 }
208
209 pub fn set_eval_deadline(&self, deadline: Option<Instant>) {
212 self.eval_deadline.set(deadline);
213 }
214
215 #[inline]
217 pub fn deadline_exceeded(&self) -> bool {
218 match self.eval_deadline.get() {
219 Some(d) => Instant::now() >= d,
220 None => false,
221 }
222 }
223
224 #[inline]
226 pub fn check_deadline(&self) -> Result<(), SemaError> {
227 if self.deadline_exceeded() {
228 Err(SemaError::eval(
229 "evaluation exceeded time budget (looks like an infinite loop?)".to_string(),
230 ))
231 } else {
232 Ok(())
233 }
234 }
235
236 pub fn context_get(&self, key: &Value) -> Option<Value> {
239 let frames = self.user_context.borrow();
240 for frame in frames.iter().rev() {
241 if let Some(v) = frame.get(key) {
242 return Some(v.clone());
243 }
244 }
245 None
246 }
247
248 pub fn context_set(&self, key: Value, value: Value) {
249 let mut frames = self.user_context.borrow_mut();
250 if let Some(top) = frames.last_mut() {
251 top.insert(key, value);
252 }
253 }
254
255 pub fn context_has(&self, key: &Value) -> bool {
256 let frames = self.user_context.borrow();
257 frames.iter().any(|frame| frame.contains_key(key))
258 }
259
260 pub fn context_remove(&self, key: &Value) -> Option<Value> {
261 let mut frames = self.user_context.borrow_mut();
262 let mut first_found = None;
263 for frame in frames.iter_mut().rev() {
264 if let Some(v) = frame.remove(key) {
265 if first_found.is_none() {
266 first_found = Some(v);
267 }
268 }
269 }
270 first_found
271 }
272
273 pub fn context_all(&self) -> BTreeMap<Value, Value> {
274 let frames = self.user_context.borrow();
275 let mut merged = BTreeMap::new();
276 for frame in frames.iter() {
277 for (k, v) in frame {
278 merged.insert(k.clone(), v.clone());
279 }
280 }
281 merged
282 }
283
284 pub fn context_push_frame(&self) {
285 self.user_context.borrow_mut().push(BTreeMap::new());
286 }
287
288 pub fn context_push_frame_with(&self, bindings: BTreeMap<Value, Value>) {
289 self.user_context.borrow_mut().push(bindings);
290 }
291
292 pub fn context_pop_frame(&self) {
293 let mut frames = self.user_context.borrow_mut();
294 if frames.len() > 1 {
295 frames.pop();
296 }
297 }
298
299 pub fn context_clear(&self) {
300 let mut frames = self.user_context.borrow_mut();
301 frames.clear();
302 frames.push(BTreeMap::new());
303 }
304
305 pub fn hidden_get(&self, key: &Value) -> Option<Value> {
308 let frames = self.hidden_context.borrow();
309 for frame in frames.iter().rev() {
310 if let Some(v) = frame.get(key) {
311 return Some(v.clone());
312 }
313 }
314 None
315 }
316
317 pub fn hidden_set(&self, key: Value, value: Value) {
318 let mut frames = self.hidden_context.borrow_mut();
319 if let Some(top) = frames.last_mut() {
320 top.insert(key, value);
321 }
322 }
323
324 pub fn hidden_has(&self, key: &Value) -> bool {
325 let frames = self.hidden_context.borrow();
326 frames.iter().any(|frame| frame.contains_key(key))
327 }
328
329 pub fn hidden_push_frame(&self) {
330 self.hidden_context.borrow_mut().push(BTreeMap::new());
331 }
332
333 pub fn hidden_pop_frame(&self) {
334 let mut frames = self.hidden_context.borrow_mut();
335 if frames.len() > 1 {
336 frames.pop();
337 }
338 }
339
340 pub fn context_stack_push(&self, key: Value, value: Value) {
343 self.context_stacks
344 .borrow_mut()
345 .entry(key)
346 .or_default()
347 .push(value);
348 }
349
350 pub fn context_stack_get(&self, key: &Value) -> Vec<Value> {
351 self.context_stacks
352 .borrow()
353 .get(key)
354 .cloned()
355 .unwrap_or_default()
356 }
357
358 pub fn context_stack_pop(&self, key: &Value) -> Option<Value> {
359 let mut stacks = self.context_stacks.borrow_mut();
360 let stack = stacks.get_mut(key)?;
361 let val = stack.pop();
362 if stack.is_empty() {
363 stacks.remove(key);
364 }
365 val
366 }
367}
368
369impl Default for EvalContext {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use std::collections::BTreeMap;
379 use std::path::PathBuf;
380
381 use crate::{Caps, Sandbox, Value};
382
383 #[test]
386 fn test_push_pop_file_path() {
387 let ctx = EvalContext::new();
388 let path = PathBuf::from("/foo/bar/baz.sema");
389 ctx.push_file_path(path.clone());
390 assert_eq!(ctx.current_file_path(), Some(path));
391 ctx.pop_file_path();
392 assert_eq!(ctx.current_file_path(), None);
393 }
394
395 #[test]
396 fn test_current_file_dir() {
397 let ctx = EvalContext::new();
398 ctx.push_file_path(PathBuf::from("/foo/bar/baz.sema"));
399 assert_eq!(ctx.current_file_dir(), Some(PathBuf::from("/foo/bar")));
400 }
401
402 #[test]
403 fn test_current_file_dir_empty() {
404 let ctx = EvalContext::new();
405 assert_eq!(ctx.current_file_dir(), None);
406 }
407
408 #[test]
409 fn test_nested_file_paths() {
410 let ctx = EvalContext::new();
411 let first = PathBuf::from("/a/first.sema");
412 let second = PathBuf::from("/b/second.sema");
413 ctx.push_file_path(first.clone());
414 ctx.push_file_path(second.clone());
415 assert_eq!(ctx.current_file_path(), Some(second));
416 ctx.pop_file_path();
417 assert_eq!(ctx.current_file_path(), Some(first));
418 }
419
420 #[test]
423 fn test_cache_module() {
424 let ctx = EvalContext::new();
425 let path = PathBuf::from("/lib/math.sema");
426 let mut exports = BTreeMap::new();
427 exports.insert("add".to_string(), Value::int(1));
428 ctx.cache_module(path.clone(), exports.clone());
429 let cached = ctx.get_cached_module(&path).unwrap();
430 assert_eq!(cached.len(), 1);
431 assert_eq!(cached.get("add"), Some(&Value::int(1)));
432 }
433
434 #[test]
435 fn test_get_cached_module_miss() {
436 let ctx = EvalContext::new();
437 let path = PathBuf::from("/nonexistent.sema");
438 assert_eq!(ctx.get_cached_module(&path), None);
439 }
440
441 #[test]
442 fn test_cache_module_overwrites() {
443 let ctx = EvalContext::new();
444 let path = PathBuf::from("/lib/math.sema");
445
446 let mut first = BTreeMap::new();
447 first.insert("old".to_string(), Value::int(1));
448 ctx.cache_module(path.clone(), first);
449
450 let mut second = BTreeMap::new();
451 second.insert("new".to_string(), Value::int(2));
452 ctx.cache_module(path.clone(), second);
453
454 let cached = ctx.get_cached_module(&path).unwrap();
455 assert!(!cached.contains_key("old"));
456 assert_eq!(cached.get("new"), Some(&Value::int(2)));
457 }
458
459 #[test]
462 fn test_module_exports_roundtrip() {
463 let ctx = EvalContext::new();
464 ctx.clear_module_exports(); ctx.set_module_exports(vec!["foo".to_string(), "bar".to_string()]);
466 let taken = ctx.take_module_exports();
467 assert_eq!(taken, Some(vec!["foo".to_string(), "bar".to_string()]));
468 }
469
470 #[test]
471 fn test_take_module_exports_empty() {
472 let ctx = EvalContext::new();
473 assert_eq!(ctx.take_module_exports(), None);
475 }
476
477 #[test]
480 fn test_begin_module_load_ok() {
481 let ctx = EvalContext::new();
482 let path = PathBuf::from("/lib/a.sema");
483 assert!(ctx.begin_module_load(&path).is_ok());
484 }
485
486 #[test]
487 fn test_begin_module_load_cycle() {
488 let ctx = EvalContext::new();
489 let path = PathBuf::from("/lib/a.sema");
490 ctx.begin_module_load(&path).unwrap();
491 let result = ctx.begin_module_load(&path);
492 assert!(result.is_err());
493 let err = result.unwrap_err();
494 let msg = err.to_string();
495 assert!(
496 msg.contains("cyclic import"),
497 "error should mention cyclic import: {msg}"
498 );
499 }
500
501 #[test]
502 fn test_end_module_load() {
503 let ctx = EvalContext::new();
504 let path = PathBuf::from("/lib/a.sema");
505 ctx.begin_module_load(&path).unwrap();
506 ctx.end_module_load(&path);
507 assert!(ctx.begin_module_load(&path).is_ok());
509 }
510
511 #[test]
512 fn test_nested_module_loads() {
513 let ctx = EvalContext::new();
514 let a = PathBuf::from("/lib/a.sema");
515 let b = PathBuf::from("/lib/b.sema");
516 ctx.begin_module_load(&a).unwrap();
517 ctx.begin_module_load(&b).unwrap();
518 ctx.end_module_load(&b);
519 let result = ctx.begin_module_load(&a);
521 assert!(result.is_err());
522 let msg = result.unwrap_err().to_string();
523 assert!(
524 msg.contains("cyclic import"),
525 "A should still be loading: {msg}"
526 );
527 }
528
529 #[test]
532 fn test_new_with_sandbox() {
533 let sandbox = Sandbox::deny(Caps::NETWORK);
534 let ctx = EvalContext::new_with_sandbox(sandbox);
535 let result = ctx.sandbox.check(Caps::NETWORK, "http/get");
537 assert!(result.is_err());
538 let result = ctx.sandbox.check(Caps::FS_READ, "file/read");
540 assert!(result.is_ok());
541 }
542}
543
544thread_local! {
545 static STDLIB_CTX: EvalContext = EvalContext::new();
546}
547
548pub fn with_stdlib_ctx<F, R>(f: F) -> R
551where
552 F: FnOnce(&EvalContext) -> R,
553{
554 STDLIB_CTX.with(f)
555}
556
557pub fn set_eval_callback(ctx: &EvalContext, f: EvalCallbackFn) {
561 ctx.eval_fn.set(Some(f));
562 STDLIB_CTX.with(|stdlib| stdlib.eval_fn.set(Some(f)));
563}
564
565pub fn set_call_callback(ctx: &EvalContext, f: CallCallbackFn) {
568 ctx.call_fn.set(Some(f));
569 STDLIB_CTX.with(|stdlib| stdlib.call_fn.set(Some(f)));
570}
571
572pub fn eval_callback(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Value, SemaError> {
575 let f = ctx.eval_fn.get().ok_or_else(|| {
576 SemaError::eval("eval callback not registered — Interpreter::new() must be called first")
577 })?;
578 f(ctx, expr, env)
579}
580
581pub fn call_callback(ctx: &EvalContext, func: &Value, args: &[Value]) -> Result<Value, SemaError> {
584 let f = ctx.call_fn.get().ok_or_else(|| {
585 SemaError::eval("call callback not registered — Interpreter::new() must be called first")
586 })?;
587 f(ctx, func, args)
588}