1mod format;
2mod imports;
3mod methods;
4mod ops;
5
6use std::cell::RefCell;
7use std::collections::{BTreeMap, HashSet};
8use std::future::Future;
9use std::pin::Pin;
10use std::rc::Rc;
11use std::time::Instant;
12
13use crate::chunk::{Chunk, CompiledFunction, Constant};
14use crate::value::{
15 ErrorCategory, ModuleFunctionRegistry, VmAsyncBuiltinFn, VmBuiltinFn, VmClosure, VmEnv,
16 VmError, VmTaskHandle, VmValue,
17};
18
19thread_local! {
20 static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> = const { RefCell::new(Vec::new()) };
21}
22
23struct ScopeSpan(u64);
25
26impl ScopeSpan {
27 fn new(kind: crate::tracing::SpanKind, name: String) -> Self {
28 Self(crate::tracing::span_start(kind, name))
29 }
30}
31
32impl Drop for ScopeSpan {
33 fn drop(&mut self) {
34 crate::tracing::span_end(self.0);
35 }
36}
37
38pub(crate) struct CallFrame {
40 pub(crate) chunk: Chunk,
41 pub(crate) ip: usize,
42 pub(crate) stack_base: usize,
43 pub(crate) saved_env: VmEnv,
44 pub(crate) fn_name: String,
46 pub(crate) argc: usize,
48 pub(crate) saved_source_dir: Option<std::path::PathBuf>,
51 pub(crate) module_functions: Option<ModuleFunctionRegistry>,
53}
54
55pub(crate) struct ExceptionHandler {
57 pub(crate) catch_ip: usize,
58 pub(crate) stack_depth: usize,
59 pub(crate) frame_depth: usize,
60 pub(crate) env_scope_depth: usize,
61 pub(crate) error_type: String,
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub enum DebugAction {
68 Continue,
70 Stop,
72}
73
74#[derive(Debug, Clone)]
76pub struct DebugState {
77 pub line: usize,
78 pub variables: BTreeMap<String, VmValue>,
79 pub frame_name: String,
80 pub frame_depth: usize,
81}
82
83pub(crate) enum IterState {
85 Vec {
86 items: Vec<VmValue>,
87 idx: usize,
88 },
89 Channel {
90 receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
91 closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
92 },
93 Generator {
94 gen: crate::value::VmGenerator,
95 },
96}
97
98#[derive(Clone)]
99pub(crate) struct LoadedModule {
100 pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
101 pub(crate) public_names: HashSet<String>,
102}
103
104pub struct Vm {
106 pub(crate) stack: Vec<VmValue>,
107 pub(crate) env: VmEnv,
108 pub(crate) output: String,
109 pub(crate) builtins: BTreeMap<String, VmBuiltinFn>,
110 pub(crate) async_builtins: BTreeMap<String, VmAsyncBuiltinFn>,
111 pub(crate) iterators: Vec<IterState>,
113 pub(crate) frames: Vec<CallFrame>,
115 pub(crate) exception_handlers: Vec<ExceptionHandler>,
117 pub(crate) spawned_tasks: BTreeMap<String, VmTaskHandle>,
119 pub(crate) task_counter: u64,
121 pub(crate) deadlines: Vec<(Instant, usize)>,
123 pub(crate) breakpoints: Vec<usize>,
125 pub(crate) step_mode: bool,
127 pub(crate) step_frame_depth: usize,
129 pub(crate) stopped: bool,
131 pub(crate) last_line: usize,
133 pub(crate) source_dir: Option<std::path::PathBuf>,
135 pub(crate) imported_paths: Vec<std::path::PathBuf>,
137 pub(crate) module_cache: BTreeMap<std::path::PathBuf, LoadedModule>,
139 pub(crate) source_file: Option<String>,
141 pub(crate) source_text: Option<String>,
143 pub(crate) bridge: Option<Rc<crate::bridge::HostBridge>>,
145 pub(crate) denied_builtins: HashSet<String>,
147 pub(crate) cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
149 pub(crate) error_stack_trace: Vec<(String, usize, usize, Option<String>)>,
151 pub(crate) yield_sender: Option<tokio::sync::mpsc::Sender<VmValue>>,
154 pub(crate) project_root: Option<std::path::PathBuf>,
157 pub(crate) globals: BTreeMap<String, VmValue>,
160}
161
162impl Vm {
163 pub fn new() -> Self {
164 Self {
165 stack: Vec::with_capacity(256),
166 env: VmEnv::new(),
167 output: String::new(),
168 builtins: BTreeMap::new(),
169 async_builtins: BTreeMap::new(),
170 iterators: Vec::new(),
171 frames: Vec::new(),
172 exception_handlers: Vec::new(),
173 spawned_tasks: BTreeMap::new(),
174 task_counter: 0,
175 deadlines: Vec::new(),
176 breakpoints: Vec::new(),
177 step_mode: false,
178 step_frame_depth: 0,
179 stopped: false,
180 last_line: 0,
181 source_dir: None,
182 imported_paths: Vec::new(),
183 module_cache: BTreeMap::new(),
184 source_file: None,
185 source_text: None,
186 bridge: None,
187 denied_builtins: HashSet::new(),
188 cancel_token: None,
189 error_stack_trace: Vec::new(),
190 yield_sender: None,
191 project_root: None,
192 globals: BTreeMap::new(),
193 }
194 }
195
196 pub fn set_bridge(&mut self, bridge: Rc<crate::bridge::HostBridge>) {
198 self.bridge = Some(bridge);
199 }
200
201 pub fn set_denied_builtins(&mut self, denied: HashSet<String>) {
204 self.denied_builtins = denied;
205 }
206
207 pub fn set_source_info(&mut self, file: &str, text: &str) {
209 self.source_file = Some(file.to_string());
210 self.source_text = Some(text.to_string());
211 }
212
213 pub fn set_breakpoints(&mut self, lines: Vec<usize>) {
215 self.breakpoints = lines;
216 }
217
218 pub fn set_step_mode(&mut self, step: bool) {
220 self.step_mode = step;
221 self.step_frame_depth = self.frames.len();
222 }
223
224 pub fn set_step_over(&mut self) {
226 self.step_mode = true;
227 self.step_frame_depth = self.frames.len();
228 }
229
230 pub fn set_step_out(&mut self) {
232 self.step_mode = true;
233 self.step_frame_depth = self.frames.len().saturating_sub(1);
234 }
235
236 pub fn is_stopped(&self) -> bool {
238 self.stopped
239 }
240
241 pub fn debug_state(&self) -> DebugState {
243 let line = self.current_line();
244 let variables = self.env.all_variables();
245 let frame_name = if self.frames.len() > 1 {
246 format!("frame_{}", self.frames.len() - 1)
247 } else {
248 "pipeline".to_string()
249 };
250 DebugState {
251 line,
252 variables,
253 frame_name,
254 frame_depth: self.frames.len(),
255 }
256 }
257
258 pub fn debug_stack_frames(&self) -> Vec<(String, usize)> {
260 let mut frames = Vec::new();
261 for (i, frame) in self.frames.iter().enumerate() {
262 let line = if frame.ip > 0 && frame.ip - 1 < frame.chunk.lines.len() {
263 frame.chunk.lines[frame.ip - 1] as usize
264 } else {
265 0
266 };
267 let name = if frame.fn_name.is_empty() {
268 if i == 0 {
269 "pipeline".to_string()
270 } else {
271 format!("fn_{}", i)
272 }
273 } else {
274 frame.fn_name.clone()
275 };
276 frames.push((name, line));
277 }
278 frames
279 }
280
281 fn current_line(&self) -> usize {
283 if let Some(frame) = self.frames.last() {
284 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
285 if ip < frame.chunk.lines.len() {
286 return frame.chunk.lines[ip] as usize;
287 }
288 }
289 0
290 }
291
292 pub async fn step_execute(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
295 let current_line = self.current_line();
297 let line_changed = current_line != self.last_line && current_line > 0;
298
299 if line_changed {
300 self.last_line = current_line;
301
302 if self.breakpoints.contains(¤t_line) {
304 self.stopped = true;
305 return Ok(Some((VmValue::Nil, true))); }
307
308 if self.step_mode && self.frames.len() <= self.step_frame_depth + 1 {
310 self.step_mode = false;
311 self.stopped = true;
312 return Ok(Some((VmValue::Nil, true))); }
314 }
315
316 self.stopped = false;
318 self.execute_one_cycle().await
319 }
320
321 async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
323 if let Some(&(deadline, _)) = self.deadlines.last() {
325 if Instant::now() > deadline {
326 self.deadlines.pop();
327 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
328 match self.handle_error(err) {
329 Ok(None) => return Ok(None),
330 Ok(Some(val)) => return Ok(Some((val, false))),
331 Err(e) => return Err(e),
332 }
333 }
334 }
335
336 let frame = match self.frames.last_mut() {
338 Some(f) => f,
339 None => {
340 let val = self.stack.pop().unwrap_or(VmValue::Nil);
341 return Ok(Some((val, false)));
342 }
343 };
344
345 if frame.ip >= frame.chunk.code.len() {
347 let val = self.stack.pop().unwrap_or(VmValue::Nil);
348 let popped_frame = self.frames.pop().unwrap();
349 if self.frames.is_empty() {
350 return Ok(Some((val, false)));
351 } else {
352 self.env = popped_frame.saved_env;
353 self.stack.truncate(popped_frame.stack_base);
354 self.stack.push(val);
355 return Ok(None);
356 }
357 }
358
359 let op = frame.chunk.code[frame.ip];
360 frame.ip += 1;
361
362 match self.execute_op(op).await {
363 Ok(Some(val)) => Ok(Some((val, false))),
364 Ok(None) => Ok(None),
365 Err(VmError::Return(val)) => {
366 if let Some(popped_frame) = self.frames.pop() {
367 if let Some(ref dir) = popped_frame.saved_source_dir {
368 crate::stdlib::set_thread_source_dir(dir);
369 }
370 let current_depth = self.frames.len();
371 self.exception_handlers
372 .retain(|h| h.frame_depth <= current_depth);
373 if self.frames.is_empty() {
374 return Ok(Some((val, false)));
375 }
376 self.env = popped_frame.saved_env;
377 self.stack.truncate(popped_frame.stack_base);
378 self.stack.push(val);
379 Ok(None)
380 } else {
381 Ok(Some((val, false)))
382 }
383 }
384 Err(e) => {
385 if self.error_stack_trace.is_empty() {
386 self.error_stack_trace = self.capture_stack_trace();
387 }
388 match self.handle_error(e) {
389 Ok(None) => {
390 self.error_stack_trace.clear();
391 Ok(None)
392 }
393 Ok(Some(val)) => Ok(Some((val, false))),
394 Err(e) => Err(self.enrich_error_with_line(e)),
395 }
396 }
397 }
398 }
399
400 pub fn start(&mut self, chunk: &Chunk) {
402 self.frames.push(CallFrame {
403 chunk: chunk.clone(),
404 ip: 0,
405 stack_base: self.stack.len(),
406 saved_env: self.env.clone(),
407 fn_name: String::new(),
408 argc: 0,
409 saved_source_dir: None,
410 module_functions: None,
411 });
412 }
413
414 pub fn register_builtin<F>(&mut self, name: &str, f: F)
416 where
417 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
418 {
419 self.builtins.insert(name.to_string(), Rc::new(f));
420 }
421
422 pub fn unregister_builtin(&mut self, name: &str) {
424 self.builtins.remove(name);
425 }
426
427 pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
429 where
430 F: Fn(Vec<VmValue>) -> Fut + 'static,
431 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
432 {
433 self.async_builtins
434 .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
435 }
436
437 fn child_vm(&self) -> Vm {
440 Vm {
441 stack: Vec::with_capacity(64),
442 env: self.env.clone(),
443 output: String::new(),
444 builtins: self.builtins.clone(),
445 async_builtins: self.async_builtins.clone(),
446 iterators: Vec::new(),
447 frames: Vec::new(),
448 exception_handlers: Vec::new(),
449 spawned_tasks: BTreeMap::new(),
450 task_counter: 0,
451 deadlines: self.deadlines.clone(),
452 breakpoints: Vec::new(),
453 step_mode: false,
454 step_frame_depth: 0,
455 stopped: false,
456 last_line: 0,
457 source_dir: self.source_dir.clone(),
458 imported_paths: Vec::new(),
459 module_cache: self.module_cache.clone(),
460 source_file: self.source_file.clone(),
461 source_text: self.source_text.clone(),
462 bridge: self.bridge.clone(),
463 denied_builtins: self.denied_builtins.clone(),
464 cancel_token: None,
465 error_stack_trace: Vec::new(),
466 yield_sender: None,
467 project_root: self.project_root.clone(),
468 globals: self.globals.clone(),
469 }
470 }
471
472 pub fn set_source_dir(&mut self, dir: &std::path::Path) {
475 self.source_dir = Some(dir.to_path_buf());
476 crate::stdlib::set_thread_source_dir(dir);
477 if self.project_root.is_none() {
479 self.project_root = crate::stdlib::process::find_project_root(dir);
480 }
481 }
482
483 pub fn set_project_root(&mut self, root: &std::path::Path) {
486 self.project_root = Some(root.to_path_buf());
487 }
488
489 pub fn project_root(&self) -> Option<&std::path::Path> {
491 self.project_root.as_deref().or(self.source_dir.as_deref())
492 }
493
494 pub fn builtin_names(&self) -> Vec<String> {
496 let mut names: Vec<String> = self.builtins.keys().cloned().collect();
497 names.extend(self.async_builtins.keys().cloned());
498 names
499 }
500
501 pub fn set_global(&mut self, name: &str, value: VmValue) {
504 self.globals.insert(name.to_string(), value);
505 }
506
507 pub fn output(&self) -> &str {
509 &self.output
510 }
511
512 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
514 let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
515 let result = self.run_chunk(chunk).await;
516 crate::tracing::span_end(span_id);
517 result
518 }
519
520 fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
522 let thrown_value = match &error {
524 VmError::Thrown(v) => v.clone(),
525 other => VmValue::String(Rc::from(other.to_string())),
526 };
527
528 if let Some(handler) = self.exception_handlers.pop() {
529 if !handler.error_type.is_empty() {
531 let matches = match &thrown_value {
532 VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
533 _ => false,
534 };
535 if !matches {
536 return self.handle_error(error);
538 }
539 }
540
541 while self.frames.len() > handler.frame_depth {
543 if let Some(frame) = self.frames.pop() {
544 if let Some(ref dir) = frame.saved_source_dir {
545 crate::stdlib::set_thread_source_dir(dir);
546 }
547 self.env = frame.saved_env;
548 }
549 }
550
551 while self
553 .deadlines
554 .last()
555 .is_some_and(|d| d.1 > handler.frame_depth)
556 {
557 self.deadlines.pop();
558 }
559
560 self.env.truncate_scopes(handler.env_scope_depth);
561
562 self.stack.truncate(handler.stack_depth);
564
565 self.stack.push(thrown_value);
567
568 if let Some(frame) = self.frames.last_mut() {
570 frame.ip = handler.catch_ip;
571 }
572
573 Ok(None) } else {
575 Err(error) }
577 }
578
579 async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
580 self.run_chunk_entry(chunk, 0, None, None).await
581 }
582
583 async fn run_chunk_entry(
584 &mut self,
585 chunk: &Chunk,
586 argc: usize,
587 saved_source_dir: Option<std::path::PathBuf>,
588 module_functions: Option<ModuleFunctionRegistry>,
589 ) -> Result<VmValue, VmError> {
590 self.frames.push(CallFrame {
591 chunk: chunk.clone(),
592 ip: 0,
593 stack_base: self.stack.len(),
594 saved_env: self.env.clone(),
595 fn_name: String::new(),
596 argc,
597 saved_source_dir,
598 module_functions,
599 });
600
601 loop {
602 if let Some(&(deadline, _)) = self.deadlines.last() {
604 if Instant::now() > deadline {
605 self.deadlines.pop();
606 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
607 match self.handle_error(err) {
608 Ok(None) => continue,
609 Ok(Some(val)) => return Ok(val),
610 Err(e) => return Err(e),
611 }
612 }
613 }
614
615 let frame = match self.frames.last_mut() {
617 Some(f) => f,
618 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
619 };
620
621 if frame.ip >= frame.chunk.code.len() {
623 let val = self.stack.pop().unwrap_or(VmValue::Nil);
624 let popped_frame = self.frames.pop().unwrap();
625 if let Some(ref dir) = popped_frame.saved_source_dir {
626 crate::stdlib::set_thread_source_dir(dir);
627 }
628
629 if self.frames.is_empty() {
630 return Ok(val);
632 } else {
633 self.env = popped_frame.saved_env;
635 self.stack.truncate(popped_frame.stack_base);
636 self.stack.push(val);
637 continue;
638 }
639 }
640
641 let op = frame.chunk.code[frame.ip];
642 frame.ip += 1;
643
644 match self.execute_op(op).await {
645 Ok(Some(val)) => return Ok(val),
646 Ok(None) => continue,
647 Err(VmError::Return(val)) => {
648 if let Some(popped_frame) = self.frames.pop() {
650 if let Some(ref dir) = popped_frame.saved_source_dir {
651 crate::stdlib::set_thread_source_dir(dir);
652 }
653 let current_depth = self.frames.len();
655 self.exception_handlers
656 .retain(|h| h.frame_depth <= current_depth);
657
658 if self.frames.is_empty() {
659 return Ok(val);
660 }
661 self.env = popped_frame.saved_env;
662 self.stack.truncate(popped_frame.stack_base);
663 self.stack.push(val);
664 } else {
665 return Ok(val);
666 }
667 }
668 Err(e) => {
669 if self.error_stack_trace.is_empty() {
671 self.error_stack_trace = self.capture_stack_trace();
672 }
673 match self.handle_error(e) {
674 Ok(None) => {
675 self.error_stack_trace.clear();
676 continue; }
678 Ok(Some(val)) => return Ok(val),
679 Err(e) => return Err(self.enrich_error_with_line(e)),
680 }
681 }
682 }
683 }
684 }
685
686 fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
688 self.frames
689 .iter()
690 .map(|f| {
691 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
692 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
693 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
694 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
695 })
696 .collect()
697 }
698
699 fn enrich_error_with_line(&self, error: VmError) -> VmError {
703 let line = self
705 .error_stack_trace
706 .last()
707 .map(|(_, l, _, _)| *l)
708 .unwrap_or_else(|| self.current_line());
709 if line == 0 {
710 return error;
711 }
712 let suffix = format!(" (line {line})");
713 match error {
714 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
715 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
716 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
717 VmError::UndefinedVariable(name) => {
718 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
719 }
720 VmError::UndefinedBuiltin(name) => {
721 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
722 }
723 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
724 "Cannot assign to immutable binding: {name}{suffix}"
725 )),
726 VmError::StackOverflow => {
727 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
728 }
729 other => other,
735 }
736 }
737
738 const MAX_FRAMES: usize = 512;
739
740 fn merge_env_into_closure(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
742 let mut call_env = closure.env.clone();
743 for scope in &caller_env.scopes {
744 for (name, (val, mutable)) in &scope.vars {
745 if call_env.get(name).is_none() {
746 let _ = call_env.define(name, val.clone(), *mutable);
747 }
748 }
749 }
750 call_env
751 }
752
753 fn resolve_named_closure(&self, name: &str) -> Option<Rc<VmClosure>> {
754 if let Some(VmValue::Closure(closure)) = self.env.get(name) {
755 return Some(closure);
756 }
757 self.frames
758 .last()
759 .and_then(|frame| frame.module_functions.as_ref())
760 .and_then(|registry| registry.borrow().get(name).cloned())
761 }
762
763 fn push_closure_frame(
765 &mut self,
766 closure: &VmClosure,
767 args: &[VmValue],
768 _parent_functions: &[CompiledFunction],
769 ) -> Result<(), VmError> {
770 if self.frames.len() >= Self::MAX_FRAMES {
771 return Err(VmError::StackOverflow);
772 }
773 let saved_env = self.env.clone();
774
775 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
779 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
780 crate::stdlib::set_thread_source_dir(dir);
781 prev
782 } else {
783 None
784 };
785
786 let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
787 call_env.push_scope();
788
789 let default_start = closure
790 .func
791 .default_start
792 .unwrap_or(closure.func.params.len());
793 for (i, param) in closure.func.params.iter().enumerate() {
794 if i < args.len() {
795 let _ = call_env.define(param, args[i].clone(), false);
796 } else if i < default_start {
797 let _ = call_env.define(param, VmValue::Nil, false);
798 }
799 }
800
801 self.env = call_env;
802
803 self.frames.push(CallFrame {
804 chunk: closure.func.chunk.clone(),
805 ip: 0,
806 stack_base: self.stack.len(),
807 saved_env,
808 fn_name: closure.func.name.clone(),
809 argc: args.len(),
810 saved_source_dir,
811 module_functions: closure.module_functions.clone(),
812 });
813
814 Ok(())
815 }
816
817 pub(crate) fn create_generator(&self, closure: &VmClosure, args: &[VmValue]) -> VmValue {
820 use crate::value::VmGenerator;
821
822 let (tx, rx) = tokio::sync::mpsc::channel::<VmValue>(1);
824
825 let mut child = self.child_vm();
826 child.yield_sender = Some(tx);
827
828 let saved_env = child.env.clone();
830 let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
831 call_env.push_scope();
832
833 let default_start = closure
834 .func
835 .default_start
836 .unwrap_or(closure.func.params.len());
837 for (i, param) in closure.func.params.iter().enumerate() {
838 if i < args.len() {
839 let _ = call_env.define(param, args[i].clone(), false);
840 } else if i < default_start {
841 let _ = call_env.define(param, VmValue::Nil, false);
842 }
843 }
844 child.env = call_env;
845
846 let chunk = closure.func.chunk.clone();
847 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
848 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
849 crate::stdlib::set_thread_source_dir(dir);
850 prev
851 } else {
852 None
853 };
854 let module_functions = closure.module_functions.clone();
855 let argc = args.len();
856 tokio::task::spawn_local(async move {
859 let _ = child
860 .run_chunk_entry(&chunk, argc, saved_source_dir, module_functions)
861 .await;
862 });
865
866 VmValue::Generator(VmGenerator {
867 done: Rc::new(std::cell::Cell::new(false)),
868 receiver: Rc::new(tokio::sync::Mutex::new(rx)),
869 })
870 }
871
872 fn pop(&mut self) -> Result<VmValue, VmError> {
873 self.stack.pop().ok_or(VmError::StackUnderflow)
874 }
875
876 fn peek(&self) -> Result<&VmValue, VmError> {
877 self.stack.last().ok_or(VmError::StackUnderflow)
878 }
879
880 fn const_string(c: &Constant) -> Result<String, VmError> {
881 match c {
882 Constant::String(s) => Ok(s.clone()),
883 _ => Err(VmError::TypeError("expected string constant".into())),
884 }
885 }
886
887 fn call_closure<'a>(
890 &'a mut self,
891 closure: &'a VmClosure,
892 args: &'a [VmValue],
893 _parent_functions: &'a [CompiledFunction],
894 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
895 Box::pin(async move {
896 let saved_env = self.env.clone();
897 let saved_frames = std::mem::take(&mut self.frames);
898 let saved_handlers = std::mem::take(&mut self.exception_handlers);
899 let saved_iterators = std::mem::take(&mut self.iterators);
900 let saved_deadlines = std::mem::take(&mut self.deadlines);
901
902 let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
903 call_env.push_scope();
904
905 let default_start = closure
906 .func
907 .default_start
908 .unwrap_or(closure.func.params.len());
909 for (i, param) in closure.func.params.iter().enumerate() {
910 if i < args.len() {
911 let _ = call_env.define(param, args[i].clone(), false);
912 } else if i < default_start {
913 let _ = call_env.define(param, VmValue::Nil, false);
914 }
915 }
916
917 self.env = call_env;
918 let argc = args.len();
919 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
920 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
921 crate::stdlib::set_thread_source_dir(dir);
922 prev
923 } else {
924 None
925 };
926 let result = self
927 .run_chunk_entry(
928 &closure.func.chunk,
929 argc,
930 saved_source_dir,
931 closure.module_functions.clone(),
932 )
933 .await;
934
935 self.env = saved_env;
936 self.frames = saved_frames;
937 self.exception_handlers = saved_handlers;
938 self.iterators = saved_iterators;
939 self.deadlines = saved_deadlines;
940
941 result
942 })
943 }
944
945 pub async fn call_closure_pub(
948 &mut self,
949 closure: &VmClosure,
950 args: &[VmValue],
951 functions: &[CompiledFunction],
952 ) -> Result<VmValue, VmError> {
953 self.call_closure(closure, args, functions).await
954 }
955
956 async fn call_named_builtin(
959 &mut self,
960 name: &str,
961 args: Vec<VmValue>,
962 ) -> Result<VmValue, VmError> {
963 let span_kind = match name {
965 "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
966 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
967 _ => None,
968 };
969 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
970
971 if self.denied_builtins.contains(name) {
973 return Err(VmError::CategorizedError {
974 message: format!("Tool '{}' is not permitted.", name),
975 category: ErrorCategory::ToolRejected,
976 });
977 }
978 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
979 if let Some(builtin) = self.builtins.get(name).cloned() {
980 builtin(&args, &mut self.output)
981 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
982 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
983 slot.borrow_mut().push(self.child_vm());
984 });
985 let result = async_builtin(args).await;
986 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
987 slot.borrow_mut().pop();
988 });
989 result
990 } else if let Some(bridge) = &self.bridge {
991 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
992 let args_json: Vec<serde_json::Value> =
993 args.iter().map(crate::llm::vm_value_to_json).collect();
994 let result = bridge
995 .call(
996 "builtin_call",
997 serde_json::json!({"name": name, "args": args_json}),
998 )
999 .await?;
1000 Ok(crate::bridge::json_result_to_vm_value(&result))
1001 } else {
1002 let all_builtins = self
1003 .builtins
1004 .keys()
1005 .chain(self.async_builtins.keys())
1006 .map(|s| s.as_str());
1007 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
1008 return Err(VmError::Runtime(format!(
1009 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
1010 )));
1011 }
1012 Err(VmError::UndefinedBuiltin(name.to_string()))
1013 }
1014 }
1015}
1016
1017pub fn clone_async_builtin_child_vm() -> Option<Vm> {
1029 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
1030}
1031
1032#[deprecated(
1039 note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
1040)]
1041pub fn take_async_builtin_child_vm() -> Option<Vm> {
1042 clone_async_builtin_child_vm()
1043}
1044
1045#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
1049pub fn restore_async_builtin_child_vm(_vm: Vm) {
1050 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1053 let _ = slot;
1056 });
1057}
1058
1059impl Default for Vm {
1060 fn default() -> Self {
1061 Self::new()
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068 use crate::compiler::Compiler;
1069 use crate::stdlib::register_vm_stdlib;
1070 use harn_lexer::Lexer;
1071 use harn_parser::Parser;
1072
1073 fn run_harn(source: &str) -> (String, VmValue) {
1074 let rt = tokio::runtime::Builder::new_current_thread()
1075 .enable_all()
1076 .build()
1077 .unwrap();
1078 rt.block_on(async {
1079 let local = tokio::task::LocalSet::new();
1080 local
1081 .run_until(async {
1082 let mut lexer = Lexer::new(source);
1083 let tokens = lexer.tokenize().unwrap();
1084 let mut parser = Parser::new(tokens);
1085 let program = parser.parse().unwrap();
1086 let chunk = Compiler::new().compile(&program).unwrap();
1087
1088 let mut vm = Vm::new();
1089 register_vm_stdlib(&mut vm);
1090 let result = vm.execute(&chunk).await.unwrap();
1091 (vm.output().to_string(), result)
1092 })
1093 .await
1094 })
1095 }
1096
1097 fn run_output(source: &str) -> String {
1098 run_harn(source).0.trim_end().to_string()
1099 }
1100
1101 fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
1102 let rt = tokio::runtime::Builder::new_current_thread()
1103 .enable_all()
1104 .build()
1105 .unwrap();
1106 rt.block_on(async {
1107 let local = tokio::task::LocalSet::new();
1108 local
1109 .run_until(async {
1110 let mut lexer = Lexer::new(source);
1111 let tokens = lexer.tokenize().unwrap();
1112 let mut parser = Parser::new(tokens);
1113 let program = parser.parse().unwrap();
1114 let chunk = Compiler::new().compile(&program).unwrap();
1115
1116 let mut vm = Vm::new();
1117 register_vm_stdlib(&mut vm);
1118 let result = vm.execute(&chunk).await?;
1119 Ok((vm.output().to_string(), result))
1120 })
1121 .await
1122 })
1123 }
1124
1125 #[test]
1126 fn test_arithmetic() {
1127 let out =
1128 run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
1129 assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
1130 }
1131
1132 #[test]
1133 fn test_mixed_arithmetic() {
1134 let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
1135 assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
1136 }
1137
1138 #[test]
1139 fn test_comparisons() {
1140 let out =
1141 run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
1142 assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
1143 }
1144
1145 #[test]
1146 fn test_let_var() {
1147 let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
1148 assert_eq!(out, "[harn] 42\n[harn] 2");
1149 }
1150
1151 #[test]
1152 fn test_if_else() {
1153 let out = run_output(
1154 r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
1155 );
1156 assert_eq!(out, "[harn] yes\n[harn] no");
1157 }
1158
1159 #[test]
1160 fn test_while_loop() {
1161 let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
1162 assert_eq!(out, "[harn] 5");
1163 }
1164
1165 #[test]
1166 fn test_for_in() {
1167 let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
1168 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
1169 }
1170
1171 #[test]
1172 fn test_fn_decl_and_call() {
1173 let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
1174 assert_eq!(out, "[harn] 7");
1175 }
1176
1177 #[test]
1178 fn test_closure() {
1179 let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
1180 assert_eq!(out, "[harn] 10");
1181 }
1182
1183 #[test]
1184 fn test_closure_capture() {
1185 let out = run_output(
1186 "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
1187 );
1188 assert_eq!(out, "[harn] 15");
1189 }
1190
1191 #[test]
1192 fn test_string_concat() {
1193 let out = run_output(
1194 r#"pipeline t(task) { let a = "hello" + " " + "world"
1195log(a) }"#,
1196 );
1197 assert_eq!(out, "[harn] hello world");
1198 }
1199
1200 #[test]
1201 fn test_list_map() {
1202 let out = run_output(
1203 "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1204 );
1205 assert_eq!(out, "[harn] [2, 4, 6]");
1206 }
1207
1208 #[test]
1209 fn test_list_filter() {
1210 let out = run_output(
1211 "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1212 );
1213 assert_eq!(out, "[harn] [4, 5]");
1214 }
1215
1216 #[test]
1217 fn test_list_reduce() {
1218 let out = run_output(
1219 "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1220 );
1221 assert_eq!(out, "[harn] 10");
1222 }
1223
1224 #[test]
1225 fn test_dict_access() {
1226 let out = run_output(
1227 r#"pipeline t(task) { let d = {name: "test", value: 42}
1228log(d.name)
1229log(d.value) }"#,
1230 );
1231 assert_eq!(out, "[harn] test\n[harn] 42");
1232 }
1233
1234 #[test]
1235 fn test_dict_methods() {
1236 let out = run_output(
1237 r#"pipeline t(task) { let d = {a: 1, b: 2}
1238log(d.keys())
1239log(d.values())
1240log(d.has("a"))
1241log(d.has("z")) }"#,
1242 );
1243 assert_eq!(
1244 out,
1245 "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1246 );
1247 }
1248
1249 #[test]
1250 fn test_pipe_operator() {
1251 let out = run_output(
1252 "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1253 );
1254 assert_eq!(out, "[harn] 10");
1255 }
1256
1257 #[test]
1258 fn test_pipe_with_closure() {
1259 let out = run_output(
1260 r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1261log(r) }"#,
1262 );
1263 assert_eq!(out, "[harn] [hello, world]");
1264 }
1265
1266 #[test]
1267 fn test_nil_coalescing() {
1268 let out = run_output(
1269 r#"pipeline t(task) { let a = nil ?? "fallback"
1270log(a)
1271let b = "present" ?? "fallback"
1272log(b) }"#,
1273 );
1274 assert_eq!(out, "[harn] fallback\n[harn] present");
1275 }
1276
1277 #[test]
1278 fn test_logical_operators() {
1279 let out =
1280 run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1281 assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1282 }
1283
1284 #[test]
1285 fn test_match() {
1286 let out = run_output(
1287 r#"pipeline t(task) { let x = "b"
1288match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1289 );
1290 assert_eq!(out, "[harn] second");
1291 }
1292
1293 #[test]
1294 fn test_subscript() {
1295 let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1296 assert_eq!(out, "[harn] 20");
1297 }
1298
1299 #[test]
1300 fn test_string_methods() {
1301 let out = run_output(
1302 r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1303log("a,b,c".split(","))
1304log(" hello ".trim())
1305log("hello".starts_with("hel"))
1306log("hello".ends_with("lo"))
1307log("hello".substring(1, 3)) }"#,
1308 );
1309 assert_eq!(
1310 out,
1311 "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_list_properties() {
1317 let out = run_output(
1318 "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1319 );
1320 assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1321 }
1322
1323 #[test]
1324 fn test_recursive_function() {
1325 let out = run_output(
1326 "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1327 );
1328 assert_eq!(out, "[harn] 55");
1329 }
1330
1331 #[test]
1332 fn test_ternary() {
1333 let out = run_output(
1334 r#"pipeline t(task) { let x = 5
1335let r = x > 0 ? "positive" : "non-positive"
1336log(r) }"#,
1337 );
1338 assert_eq!(out, "[harn] positive");
1339 }
1340
1341 #[test]
1342 fn test_for_in_dict() {
1343 let out = run_output(
1344 "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1345 );
1346 assert_eq!(out, "[harn] a\n[harn] b");
1347 }
1348
1349 #[test]
1350 fn test_list_any_all() {
1351 let out = run_output(
1352 "pipeline t(task) { let nums = [2, 4, 6]\nlog(nums.any({ x -> x > 5 }))\nlog(nums.all({ x -> x > 0 }))\nlog(nums.all({ x -> x > 3 })) }",
1353 );
1354 assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1355 }
1356
1357 #[test]
1358 fn test_disassembly() {
1359 let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1360 let tokens = lexer.tokenize().unwrap();
1361 let mut parser = Parser::new(tokens);
1362 let program = parser.parse().unwrap();
1363 let chunk = Compiler::new().compile(&program).unwrap();
1364 let disasm = chunk.disassemble("test");
1365 assert!(disasm.contains("CONSTANT"));
1366 assert!(disasm.contains("ADD"));
1367 assert!(disasm.contains("CALL"));
1368 }
1369
1370 #[test]
1373 fn test_try_catch_basic() {
1374 let out = run_output(
1375 r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1376 );
1377 assert_eq!(out, "[harn] caught: oops");
1378 }
1379
1380 #[test]
1381 fn test_try_no_error() {
1382 let out = run_output(
1383 r#"pipeline t(task) {
1384var result = 0
1385try { result = 42 } catch(e) { result = 0 }
1386log(result)
1387}"#,
1388 );
1389 assert_eq!(out, "[harn] 42");
1390 }
1391
1392 #[test]
1393 fn test_throw_uncaught() {
1394 let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1395 assert!(result.is_err());
1396 }
1397
1398 fn run_vm(source: &str) -> String {
1401 let rt = tokio::runtime::Builder::new_current_thread()
1402 .enable_all()
1403 .build()
1404 .unwrap();
1405 rt.block_on(async {
1406 let local = tokio::task::LocalSet::new();
1407 local
1408 .run_until(async {
1409 let mut lexer = Lexer::new(source);
1410 let tokens = lexer.tokenize().unwrap();
1411 let mut parser = Parser::new(tokens);
1412 let program = parser.parse().unwrap();
1413 let chunk = Compiler::new().compile(&program).unwrap();
1414 let mut vm = Vm::new();
1415 register_vm_stdlib(&mut vm);
1416 vm.execute(&chunk).await.unwrap();
1417 vm.output().to_string()
1418 })
1419 .await
1420 })
1421 }
1422
1423 fn run_vm_err(source: &str) -> String {
1424 let rt = tokio::runtime::Builder::new_current_thread()
1425 .enable_all()
1426 .build()
1427 .unwrap();
1428 rt.block_on(async {
1429 let local = tokio::task::LocalSet::new();
1430 local
1431 .run_until(async {
1432 let mut lexer = Lexer::new(source);
1433 let tokens = lexer.tokenize().unwrap();
1434 let mut parser = Parser::new(tokens);
1435 let program = parser.parse().unwrap();
1436 let chunk = Compiler::new().compile(&program).unwrap();
1437 let mut vm = Vm::new();
1438 register_vm_stdlib(&mut vm);
1439 match vm.execute(&chunk).await {
1440 Err(e) => format!("{}", e),
1441 Ok(_) => panic!("Expected error"),
1442 }
1443 })
1444 .await
1445 })
1446 }
1447
1448 #[test]
1449 fn test_hello_world() {
1450 let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1451 assert_eq!(out, "[harn] hello\n");
1452 }
1453
1454 #[test]
1455 fn test_arithmetic_new() {
1456 let out = run_vm("pipeline default(task) { log(2 + 3) }");
1457 assert_eq!(out, "[harn] 5\n");
1458 }
1459
1460 #[test]
1461 fn test_string_concat_new() {
1462 let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1463 assert_eq!(out, "[harn] ab\n");
1464 }
1465
1466 #[test]
1467 fn test_if_else_new() {
1468 let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1469 assert_eq!(out, "[harn] 1\n");
1470 }
1471
1472 #[test]
1473 fn test_for_loop_new() {
1474 let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1475 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1476 }
1477
1478 #[test]
1479 fn test_while_loop_new() {
1480 let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1481 assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1482 }
1483
1484 #[test]
1485 fn test_function_call_new() {
1486 let out =
1487 run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1488 assert_eq!(out, "[harn] 5\n");
1489 }
1490
1491 #[test]
1492 fn test_closure_new() {
1493 let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1494 assert_eq!(out, "[harn] 10\n");
1495 }
1496
1497 #[test]
1498 fn test_recursion() {
1499 let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1500 assert_eq!(out, "[harn] 120\n");
1501 }
1502
1503 #[test]
1504 fn test_try_catch_new() {
1505 let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1506 assert_eq!(out, "[harn] err\n");
1507 }
1508
1509 #[test]
1510 fn test_try_no_error_new() {
1511 let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1512 assert_eq!(out, "[harn] 1\n");
1513 }
1514
1515 #[test]
1516 fn test_list_map_new() {
1517 let out =
1518 run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1519 assert_eq!(out, "[harn] [2, 4, 6]\n");
1520 }
1521
1522 #[test]
1523 fn test_list_filter_new() {
1524 let out = run_vm(
1525 "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1526 );
1527 assert_eq!(out, "[harn] [3, 4]\n");
1528 }
1529
1530 #[test]
1531 fn test_dict_access_new() {
1532 let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1533 assert_eq!(out, "[harn] Alice\n");
1534 }
1535
1536 #[test]
1537 fn test_string_interpolation() {
1538 let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1539 assert_eq!(out, "[harn] val=42\n");
1540 }
1541
1542 #[test]
1543 fn test_match_new() {
1544 let out = run_vm(
1545 "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1546 );
1547 assert_eq!(out, "[harn] 2\n");
1548 }
1549
1550 #[test]
1551 fn test_json_roundtrip() {
1552 let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1553 assert!(out.contains("\"a\""));
1554 assert!(out.contains("1"));
1555 }
1556
1557 #[test]
1558 fn test_type_of() {
1559 let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1560 assert_eq!(out, "[harn] int\n[harn] string\n");
1561 }
1562
1563 #[test]
1564 fn test_stack_overflow() {
1565 let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1566 assert!(
1567 err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1568 "Expected stack overflow error, got: {}",
1569 err
1570 );
1571 }
1572
1573 #[test]
1574 fn test_division_by_zero() {
1575 let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1576 assert!(
1577 err.contains("Division by zero") || err.contains("division"),
1578 "Expected division by zero error, got: {}",
1579 err
1580 );
1581 }
1582
1583 #[test]
1584 fn test_float_division_by_zero_uses_ieee_values() {
1585 let out = run_vm(
1586 "pipeline default(task) { log(is_nan(0.0 / 0.0))\nlog(is_infinite(1.0 / 0.0))\nlog(is_infinite(-1.0 / 0.0)) }",
1587 );
1588 assert_eq!(out, "[harn] true\n[harn] true\n[harn] true\n");
1589 }
1590
1591 #[test]
1592 fn test_reusing_catch_binding_name_in_same_block() {
1593 let out = run_vm(
1594 r#"pipeline default(task) {
1595try {
1596 throw "a"
1597} catch e {
1598 log(e)
1599}
1600try {
1601 throw "b"
1602} catch e {
1603 log(e)
1604}
1605}"#,
1606 );
1607 assert_eq!(out, "[harn] a\n[harn] b\n");
1608 }
1609
1610 #[test]
1611 fn test_try_catch_nested() {
1612 let out = run_output(
1613 r#"pipeline t(task) {
1614try {
1615 try {
1616 throw "inner"
1617 } catch(e) {
1618 log("inner caught: " + e)
1619 throw "outer"
1620 }
1621} catch(e2) {
1622 log("outer caught: " + e2)
1623}
1624}"#,
1625 );
1626 assert_eq!(
1627 out,
1628 "[harn] inner caught: inner\n[harn] outer caught: outer"
1629 );
1630 }
1631
1632 #[test]
1635 fn test_parallel_basic() {
1636 let out = run_output(
1637 "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1638 );
1639 assert_eq!(out, "[harn] [0, 10, 20]");
1640 }
1641
1642 #[test]
1643 fn test_parallel_no_variable() {
1644 let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1645 assert_eq!(out, "[harn] [42, 42, 42]");
1646 }
1647
1648 #[test]
1649 fn test_parallel_map_basic() {
1650 let out = run_output(
1651 "pipeline t(task) { let results = parallel_map([1, 2, 3]) { x -> x * x }\nlog(results) }",
1652 );
1653 assert_eq!(out, "[harn] [1, 4, 9]");
1654 }
1655
1656 #[test]
1657 fn test_spawn_await() {
1658 let out = run_output(
1659 r#"pipeline t(task) {
1660let handle = spawn { log("spawned") }
1661let result = await(handle)
1662log("done")
1663}"#,
1664 );
1665 assert_eq!(out, "[harn] spawned\n[harn] done");
1666 }
1667
1668 #[test]
1669 fn test_spawn_cancel() {
1670 let out = run_output(
1671 r#"pipeline t(task) {
1672let handle = spawn { log("should be cancelled") }
1673cancel(handle)
1674log("cancelled")
1675}"#,
1676 );
1677 assert_eq!(out, "[harn] cancelled");
1678 }
1679
1680 #[test]
1681 fn test_spawn_returns_value() {
1682 let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1683 assert_eq!(out, "[harn] 42");
1684 }
1685
1686 #[test]
1689 fn test_deadline_success() {
1690 let out = run_output(
1691 r#"pipeline t(task) {
1692let result = deadline 5s { log("within deadline")
169342 }
1694log(result)
1695}"#,
1696 );
1697 assert_eq!(out, "[harn] within deadline\n[harn] 42");
1698 }
1699
1700 #[test]
1701 fn test_deadline_exceeded() {
1702 let result = run_harn_result(
1703 r#"pipeline t(task) {
1704deadline 1ms {
1705 var i = 0
1706 while i < 1000000 { i = i + 1 }
1707}
1708}"#,
1709 );
1710 assert!(result.is_err());
1711 }
1712
1713 #[test]
1714 fn test_deadline_caught_by_try() {
1715 let out = run_output(
1716 r#"pipeline t(task) {
1717try {
1718 deadline 1ms {
1719 var i = 0
1720 while i < 1000000 { i = i + 1 }
1721 }
1722} catch(e) {
1723 log("caught")
1724}
1725}"#,
1726 );
1727 assert_eq!(out, "[harn] caught");
1728 }
1729
1730 fn run_harn_with_denied(
1732 source: &str,
1733 denied: HashSet<String>,
1734 ) -> Result<(String, VmValue), VmError> {
1735 let rt = tokio::runtime::Builder::new_current_thread()
1736 .enable_all()
1737 .build()
1738 .unwrap();
1739 rt.block_on(async {
1740 let local = tokio::task::LocalSet::new();
1741 local
1742 .run_until(async {
1743 let mut lexer = Lexer::new(source);
1744 let tokens = lexer.tokenize().unwrap();
1745 let mut parser = Parser::new(tokens);
1746 let program = parser.parse().unwrap();
1747 let chunk = Compiler::new().compile(&program).unwrap();
1748
1749 let mut vm = Vm::new();
1750 register_vm_stdlib(&mut vm);
1751 vm.set_denied_builtins(denied);
1752 let result = vm.execute(&chunk).await?;
1753 Ok((vm.output().to_string(), result))
1754 })
1755 .await
1756 })
1757 }
1758
1759 #[test]
1760 fn test_sandbox_deny_builtin() {
1761 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1762 let result = run_harn_with_denied(
1763 r#"pipeline t(task) {
1764let xs = [1, 2]
1765push(xs, 3)
1766}"#,
1767 denied,
1768 );
1769 let err = result.unwrap_err();
1770 let msg = format!("{err}");
1771 assert!(
1772 msg.contains("not permitted"),
1773 "expected not permitted, got: {msg}"
1774 );
1775 assert!(
1776 msg.contains("push"),
1777 "expected builtin name in error, got: {msg}"
1778 );
1779 }
1780
1781 #[test]
1782 fn test_sandbox_allowed_builtin_works() {
1783 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1785 let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
1786 let (output, _) = result.unwrap();
1787 assert_eq!(output.trim(), "[harn] hello");
1788 }
1789
1790 #[test]
1791 fn test_sandbox_empty_denied_set() {
1792 let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
1794 let (output, _) = result.unwrap();
1795 assert_eq!(output.trim(), "[harn] ok");
1796 }
1797
1798 #[test]
1799 fn test_sandbox_propagates_to_spawn() {
1800 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1802 let result = run_harn_with_denied(
1803 r#"pipeline t(task) {
1804let handle = spawn {
1805 let xs = [1, 2]
1806 push(xs, 3)
1807}
1808await(handle)
1809}"#,
1810 denied,
1811 );
1812 let err = result.unwrap_err();
1813 let msg = format!("{err}");
1814 assert!(
1815 msg.contains("not permitted"),
1816 "expected not permitted in spawned VM, got: {msg}"
1817 );
1818 }
1819
1820 #[test]
1821 fn test_sandbox_propagates_to_parallel() {
1822 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1824 let result = run_harn_with_denied(
1825 r#"pipeline t(task) {
1826let results = parallel(2) { i ->
1827 let xs = [1, 2]
1828 push(xs, 3)
1829}
1830}"#,
1831 denied,
1832 );
1833 let err = result.unwrap_err();
1834 let msg = format!("{err}");
1835 assert!(
1836 msg.contains("not permitted"),
1837 "expected not permitted in parallel VM, got: {msg}"
1838 );
1839 }
1840
1841 #[test]
1842 fn test_if_else_has_lexical_block_scope() {
1843 let out = run_output(
1844 r#"pipeline t(task) {
1845let x = "outer"
1846if true {
1847 let x = "inner"
1848 log(x)
1849} else {
1850 let x = "other"
1851 log(x)
1852}
1853log(x)
1854}"#,
1855 );
1856 assert_eq!(out, "[harn] inner\n[harn] outer");
1857 }
1858
1859 #[test]
1860 fn test_loop_and_catch_bindings_are_block_scoped() {
1861 let out = run_output(
1862 r#"pipeline t(task) {
1863let label = "outer"
1864for item in [1, 2] {
1865 let label = "loop " + item
1866 log(label)
1867}
1868try {
1869 throw("boom")
1870} catch (label) {
1871 log(label)
1872}
1873log(label)
1874}"#,
1875 );
1876 assert_eq!(
1877 out,
1878 "[harn] loop 1\n[harn] loop 2\n[harn] boom\n[harn] outer"
1879 );
1880 }
1881}