1mod format;
2mod imports;
3pub mod iter;
4mod methods;
5mod ops;
6
7use std::cell::RefCell;
8use std::collections::{BTreeMap, HashSet};
9use std::future::Future;
10use std::pin::Pin;
11use std::rc::Rc;
12use std::time::Instant;
13
14use crate::chunk::{Chunk, CompiledFunction, Constant};
15use crate::value::{
16 ErrorCategory, ModuleFunctionRegistry, VmAsyncBuiltinFn, VmBuiltinFn, VmClosure, VmEnv,
17 VmError, VmTaskHandle, VmValue,
18};
19
20thread_local! {
21 static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> = const { RefCell::new(Vec::new()) };
22}
23
24struct ScopeSpan(u64);
26
27impl ScopeSpan {
28 fn new(kind: crate::tracing::SpanKind, name: String) -> Self {
29 Self(crate::tracing::span_start(kind, name))
30 }
31}
32
33impl Drop for ScopeSpan {
34 fn drop(&mut self) {
35 crate::tracing::span_end(self.0);
36 }
37}
38
39pub(crate) struct CallFrame {
41 pub(crate) chunk: Chunk,
42 pub(crate) ip: usize,
43 pub(crate) stack_base: usize,
44 pub(crate) saved_env: VmEnv,
45 pub(crate) initial_env: Option<VmEnv>,
53 pub(crate) saved_iterator_depth: usize,
55 pub(crate) fn_name: String,
57 pub(crate) argc: usize,
59 pub(crate) saved_source_dir: Option<std::path::PathBuf>,
62 pub(crate) module_functions: Option<ModuleFunctionRegistry>,
64 pub(crate) module_state: Option<crate::value::ModuleState>,
70}
71
72pub(crate) struct ExceptionHandler {
74 pub(crate) catch_ip: usize,
75 pub(crate) stack_depth: usize,
76 pub(crate) frame_depth: usize,
77 pub(crate) env_scope_depth: usize,
78 pub(crate) error_type: String,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub enum DebugAction {
85 Continue,
87 Stop,
89}
90
91#[derive(Debug, Clone)]
93pub struct DebugState {
94 pub line: usize,
95 pub variables: BTreeMap<String, VmValue>,
96 pub frame_name: String,
97 pub frame_depth: usize,
98}
99
100type DebugHook = dyn FnMut(&DebugState) -> DebugAction;
101
102pub(crate) enum IterState {
104 Vec {
105 items: Vec<VmValue>,
106 idx: usize,
107 },
108 Channel {
109 receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
110 closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
111 },
112 Generator {
113 gen: crate::value::VmGenerator,
114 },
115 Range {
119 next: i64,
120 stop: i64,
121 },
122 VmIter {
123 handle: std::rc::Rc<std::cell::RefCell<crate::vm::iter::VmIter>>,
124 },
125}
126
127#[derive(Clone)]
128pub(crate) struct LoadedModule {
129 pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
130 pub(crate) public_names: HashSet<String>,
131}
132
133pub struct Vm {
135 pub(crate) stack: Vec<VmValue>,
136 pub(crate) env: VmEnv,
137 pub(crate) output: String,
138 pub(crate) builtins: BTreeMap<String, VmBuiltinFn>,
139 pub(crate) async_builtins: BTreeMap<String, VmAsyncBuiltinFn>,
140 pub(crate) iterators: Vec<IterState>,
142 pub(crate) frames: Vec<CallFrame>,
144 pub(crate) exception_handlers: Vec<ExceptionHandler>,
146 pub(crate) spawned_tasks: BTreeMap<String, VmTaskHandle>,
148 pub(crate) task_counter: u64,
150 pub(crate) deadlines: Vec<(Instant, usize)>,
152 pub(crate) breakpoints: BTreeMap<String, std::collections::BTreeSet<usize>>,
157 pub(crate) function_breakpoints: std::collections::BTreeSet<String>,
163 pub(crate) pending_function_bp: Option<String>,
168 pub(crate) step_mode: bool,
170 pub(crate) step_frame_depth: usize,
172 pub(crate) stopped: bool,
174 pub(crate) last_line: usize,
176 pub(crate) source_dir: Option<std::path::PathBuf>,
178 pub(crate) imported_paths: Vec<std::path::PathBuf>,
180 pub(crate) module_cache: BTreeMap<std::path::PathBuf, LoadedModule>,
182 pub(crate) source_file: Option<String>,
184 pub(crate) source_text: Option<String>,
186 pub(crate) bridge: Option<Rc<crate::bridge::HostBridge>>,
188 pub(crate) denied_builtins: HashSet<String>,
190 pub(crate) cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
192 pub(crate) error_stack_trace: Vec<(String, usize, usize, Option<String>)>,
194 pub(crate) yield_sender: Option<tokio::sync::mpsc::Sender<VmValue>>,
197 pub(crate) project_root: Option<std::path::PathBuf>,
200 pub(crate) globals: BTreeMap<String, VmValue>,
203 pub(crate) debug_hook: Option<Box<DebugHook>>,
205}
206
207impl Vm {
208 pub fn new() -> Self {
209 Self {
210 stack: Vec::with_capacity(256),
211 env: VmEnv::new(),
212 output: String::new(),
213 builtins: BTreeMap::new(),
214 async_builtins: BTreeMap::new(),
215 iterators: Vec::new(),
216 frames: Vec::new(),
217 exception_handlers: Vec::new(),
218 spawned_tasks: BTreeMap::new(),
219 task_counter: 0,
220 deadlines: Vec::new(),
221 breakpoints: BTreeMap::new(),
222 function_breakpoints: std::collections::BTreeSet::new(),
223 pending_function_bp: None,
224 step_mode: false,
225 step_frame_depth: 0,
226 stopped: false,
227 last_line: 0,
228 source_dir: None,
229 imported_paths: Vec::new(),
230 module_cache: BTreeMap::new(),
231 source_file: None,
232 source_text: None,
233 bridge: None,
234 denied_builtins: HashSet::new(),
235 cancel_token: None,
236 error_stack_trace: Vec::new(),
237 yield_sender: None,
238 project_root: None,
239 globals: BTreeMap::new(),
240 debug_hook: None,
241 }
242 }
243
244 pub fn set_bridge(&mut self, bridge: Rc<crate::bridge::HostBridge>) {
246 self.bridge = Some(bridge);
247 }
248
249 pub fn set_denied_builtins(&mut self, denied: HashSet<String>) {
252 self.denied_builtins = denied;
253 }
254
255 pub fn set_source_info(&mut self, file: &str, text: &str) {
257 self.source_file = Some(file.to_string());
258 self.source_text = Some(text.to_string());
259 }
260
261 pub fn set_breakpoints_for_file(&mut self, file: &str, lines: Vec<usize>) {
266 if lines.is_empty() {
267 self.breakpoints.remove(file);
268 return;
269 }
270 self.breakpoints
271 .insert(file.to_string(), lines.into_iter().collect());
272 }
273
274 pub fn set_breakpoints(&mut self, lines: Vec<usize>) {
278 self.set_breakpoints_for_file("", lines);
279 }
280
281 pub fn set_function_breakpoints(&mut self, names: Vec<String>) {
285 self.function_breakpoints = names.into_iter().collect();
286 self.pending_function_bp = None;
289 }
290
291 pub fn function_breakpoint_names(&self) -> Vec<String> {
295 self.function_breakpoints.iter().cloned().collect()
296 }
297
298 pub fn take_pending_function_bp(&mut self) -> Option<String> {
302 self.pending_function_bp.take()
303 }
304
305 pub(crate) fn current_source_file(&self) -> Option<&str> {
307 self.frames
308 .last()
309 .and_then(|f| f.chunk.source_file.as_deref())
310 }
311
312 pub(crate) fn breakpoint_matches(&self, line: usize) -> bool {
315 if let Some(wild) = self.breakpoints.get("") {
316 if wild.contains(&line) {
317 return true;
318 }
319 }
320 if let Some(file) = self.current_source_file() {
321 if let Some(set) = self.breakpoints.get(file) {
322 if set.contains(&line) {
323 return true;
324 }
325 }
326 for (key, set) in &self.breakpoints {
330 if key.is_empty() {
331 continue;
332 }
333 if (file.ends_with(key.as_str()) || key.ends_with(file)) && set.contains(&line) {
334 return true;
335 }
336 }
337 }
338 false
339 }
340
341 pub fn set_step_mode(&mut self, step: bool) {
344 self.step_mode = step;
345 self.step_frame_depth = usize::MAX;
346 }
347
348 pub fn set_step_over(&mut self) {
351 self.step_mode = true;
352 self.step_frame_depth = self.frames.len();
353 }
354
355 pub fn set_debug_hook<F>(&mut self, hook: F)
357 where
358 F: FnMut(&DebugState) -> DebugAction + 'static,
359 {
360 self.debug_hook = Some(Box::new(hook));
361 }
362
363 pub fn clear_debug_hook(&mut self) {
365 self.debug_hook = None;
366 }
367
368 pub fn set_step_out(&mut self) {
372 self.step_mode = true;
373 self.step_frame_depth = self.frames.len().saturating_sub(1);
379 }
380
381 pub fn is_stopped(&self) -> bool {
383 self.stopped
384 }
385
386 pub fn debug_state(&self) -> DebugState {
388 let line = self.current_line();
389 let variables = self.env.all_variables();
390 let frame_name = if self.frames.len() > 1 {
391 format!("frame_{}", self.frames.len() - 1)
392 } else {
393 "pipeline".to_string()
394 };
395 DebugState {
396 line,
397 variables,
398 frame_name,
399 frame_depth: self.frames.len(),
400 }
401 }
402
403 pub fn call_sites_on_line(&self, line: u32) -> Vec<(u32, String)> {
409 let Some(frame) = self.frames.last() else {
410 return Vec::new();
411 };
412 let chunk = &frame.chunk;
413 let mut out = Vec::new();
414 let code = &chunk.code;
415 let lines = &chunk.lines;
416 let mut ip: usize = 0;
417 while ip < code.len() {
418 let op = code[ip];
419 if ip < lines.len() && lines[ip] == line {
420 if matches!(op, 0x40..=0x44) {
426 let label = Self::label_preceding_call(chunk, ip);
429 out.push((ip as u32, label));
430 }
431 }
432 ip += 1;
433 }
434 out
435 }
436
437 fn label_preceding_call(chunk: &crate::chunk::Chunk, call_ip: usize) -> String {
438 let mut back = call_ip.saturating_sub(6);
443 while back < call_ip {
444 let op = chunk.code[back];
445 if (op == 0x01 || op == 0x02) && back + 2 < chunk.code.len() {
448 let idx = (u16::from(chunk.code[back + 1]) << 8) | u16::from(chunk.code[back + 2]);
449 if let Some(crate::chunk::Constant::String(s)) = chunk.constants.get(idx as usize) {
450 return s.clone();
451 }
452 }
453 back += 1;
454 }
455 "call".to_string()
456 }
457
458 pub fn install_cancel_token(&mut self, token: std::sync::Arc<std::sync::atomic::AtomicBool>) {
464 self.cancel_token = Some(token);
465 }
466
467 pub fn signal_cancel(&mut self) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
473 let token = self.cancel_token.clone().unwrap_or_else(|| {
474 let t = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
475 self.cancel_token = Some(t.clone());
476 t
477 });
478 token.store(true, std::sync::atomic::Ordering::SeqCst);
479 token
480 }
481
482 pub fn is_cancel_requested(&self) -> bool {
484 self.cancel_token
485 .as_ref()
486 .map(|t| t.load(std::sync::atomic::Ordering::SeqCst))
487 .unwrap_or(false)
488 }
489
490 pub fn identifiers_in_scope(&self, _frame_id: usize) -> Vec<String> {
495 let mut out: Vec<String> = self.env.all_variables().keys().cloned().collect();
496 out.extend(self.builtins.keys().cloned());
497 out.extend(self.async_builtins.keys().cloned());
498 out.sort();
499 out.dedup();
500 out
501 }
502
503 pub fn debug_stack_frames(&self) -> Vec<(String, usize)> {
505 let mut frames = Vec::new();
506 for (i, frame) in self.frames.iter().enumerate() {
507 let line = if frame.ip > 0 && frame.ip - 1 < frame.chunk.lines.len() {
508 frame.chunk.lines[frame.ip - 1] as usize
509 } else {
510 0
511 };
512 let name = if frame.fn_name.is_empty() {
513 if i == 0 {
514 "pipeline".to_string()
515 } else {
516 format!("fn_{}", i)
517 }
518 } else {
519 frame.fn_name.clone()
520 };
521 frames.push((name, line));
522 }
523 frames
524 }
525
526 fn current_line(&self) -> usize {
528 if let Some(frame) = self.frames.last() {
529 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
530 if ip < frame.chunk.lines.len() {
531 return frame.chunk.lines[ip] as usize;
532 }
533 }
534 0
535 }
536
537 pub async fn step_execute(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
549 if self.is_cancel_requested() {
554 return Err(VmError::Thrown(VmValue::String(std::rc::Rc::from(
555 "kind:cancelled:VM cancelled by host",
556 ))));
557 }
558 let current_line = self.upcoming_line();
559 let line_changed = current_line != self.last_line && current_line > 0;
560
561 if line_changed {
562 self.last_line = current_line;
563
564 let state = self.debug_state();
565 if let Some(hook) = self.debug_hook.as_mut() {
566 if matches!(hook(&state), DebugAction::Stop) {
567 self.stopped = true;
568 return Ok(Some((VmValue::Nil, true)));
569 }
570 }
571
572 if self.breakpoint_matches(current_line) {
573 self.stopped = true;
574 return Ok(Some((VmValue::Nil, true)));
575 }
576
577 if self.pending_function_bp.is_some() {
582 self.stopped = true;
583 return Ok(Some((VmValue::Nil, true)));
584 }
585
586 if self.step_mode && self.frames.len() <= self.step_frame_depth {
592 self.step_mode = false;
593 self.stopped = true;
594 return Ok(Some((VmValue::Nil, true)));
595 }
596 }
597
598 self.stopped = false;
599 self.execute_one_cycle().await
600 }
601
602 fn upcoming_line(&self) -> usize {
606 if let Some(frame) = self.frames.last() {
607 if frame.ip < frame.chunk.lines.len() {
608 return frame.chunk.lines[frame.ip] as usize;
609 }
610 }
611 0
612 }
613
614 pub fn frame_count(&self) -> usize {
619 self.frames.len()
620 }
621
622 pub fn restart_frame(&mut self, frame_id: usize) -> Result<(), VmError> {
634 if frame_id >= self.frames.len() {
635 return Err(VmError::Runtime(format!(
636 "restartFrame: frame id {frame_id} out of range (have {} frames)",
637 self.frames.len()
638 )));
639 }
640 let Some(initial_env) = self.frames[frame_id].initial_env.clone() else {
641 return Err(VmError::Runtime(
642 "restartFrame: target frame was not captured for restart (scratch / evaluator frame)".into(),
643 ));
644 };
645 while self.frames.len() > frame_id + 1 {
649 let popped = self.frames.pop().expect("bounds checked above");
650 self.iterators.truncate(popped.saved_iterator_depth);
651 }
652 let frame = self
654 .frames
655 .last_mut()
656 .expect("frame_id within bounds guarantees a frame");
657 frame.ip = 0;
658 let stack_base = frame.stack_base;
659 let saved_iter_depth = frame.saved_iterator_depth;
660 self.stack.truncate(stack_base);
661 self.iterators.truncate(saved_iter_depth);
662 self.env = initial_env;
663 self.last_line = 0;
664 self.stopped = false;
665 Ok(())
666 }
667
668 pub async fn set_variable_in_frame(
678 &mut self,
679 name: &str,
680 value_expr: &str,
681 frame_id: usize,
682 ) -> Result<VmValue, VmError> {
683 let value = self.evaluate_in_frame(value_expr, frame_id).await?;
684 self.env
690 .assign_debug(name, value.clone())
691 .map_err(|e| match e {
692 VmError::UndefinedVariable(n) => {
693 VmError::Runtime(format!("setVariable: '{n}' is not in the current scope"))
694 }
695 other => other,
696 })?;
697 Ok(value)
698 }
699
700 pub async fn evaluate_in_frame(
721 &mut self,
722 expr: &str,
723 _frame_id: usize,
724 ) -> Result<VmValue, VmError> {
725 let trimmed = expr.trim();
726 if trimmed.is_empty() {
727 return Err(VmError::Runtime("evaluate: empty expression".into()));
728 }
729
730 let wrapped = format!("pipeline default() {{\n return ({trimmed})\n}}\n");
738 let program = harn_parser::check_source_strict(&wrapped)
739 .map_err(|e| VmError::Runtime(format!("evaluate: parse error: {e}")))?;
740 let mut chunk = crate::compiler::Compiler::new()
741 .compile(&program)
742 .map_err(|e| VmError::Runtime(format!("evaluate: compile error: {e}")))?;
743 if let Some(current) = self.frames.last() {
746 chunk.source_file = current.chunk.source_file.clone();
747 }
748
749 let saved_stack_len = self.stack.len();
755 let saved_frame_count = self.frames.len();
756 let saved_iter_depth = self.iterators.len();
757 let saved_scope_depth = self.env.scope_depth();
758 let saved_last_line = self.last_line;
759 let saved_step_mode = self.step_mode;
760 let saved_step_frame_depth = self.step_frame_depth;
761 let saved_stopped = self.stopped;
762 let saved_env = self.env.clone();
763
764 self.step_mode = false;
767 self.stopped = false;
768
769 self.frames.push(CallFrame {
770 chunk,
771 ip: 0,
772 stack_base: saved_stack_len,
773 saved_env,
774 initial_env: None,
778 saved_iterator_depth: saved_iter_depth,
779 fn_name: "<eval>".to_string(),
780 argc: 0,
781 saved_source_dir: self.source_dir.clone(),
782 module_functions: None,
783 module_state: None,
784 });
785
786 const MAX_EVAL_STEPS: usize = 10_000;
791 let mut err: Option<VmError> = None;
792 for _ in 0..MAX_EVAL_STEPS {
793 if self.frames.len() <= saved_frame_count {
794 break;
795 }
796 match self.execute_one_cycle().await {
797 Ok(_) => {
798 if self.frames.len() <= saved_frame_count {
799 break;
800 }
801 }
802 Err(e) => {
803 err = Some(e);
804 break;
805 }
806 }
807 }
808
809 let result = if self.stack.len() > saved_stack_len {
813 Some(self.stack[saved_stack_len].clone())
814 } else {
815 None
816 };
817
818 self.frames.truncate(saved_frame_count);
821 self.stack.truncate(saved_stack_len);
822 self.iterators.truncate(saved_iter_depth);
823 self.env.truncate_scopes(saved_scope_depth);
824 self.last_line = saved_last_line;
825 self.step_mode = saved_step_mode;
826 self.step_frame_depth = saved_step_frame_depth;
827 self.stopped = saved_stopped;
828
829 if let Some(e) = err {
830 return Err(e);
831 }
832 result.ok_or_else(|| {
833 VmError::Runtime(
834 "evaluate: step budget exceeded before the expression produced a value".into(),
835 )
836 })
837 }
838
839 async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
840 if let Some(&(deadline, _)) = self.deadlines.last() {
841 if Instant::now() > deadline {
842 self.deadlines.pop();
843 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
844 match self.handle_error(err) {
845 Ok(None) => return Ok(None),
846 Ok(Some(val)) => return Ok(Some((val, false))),
847 Err(e) => return Err(e),
848 }
849 }
850 }
851
852 let frame = match self.frames.last_mut() {
853 Some(f) => f,
854 None => {
855 let val = self.stack.pop().unwrap_or(VmValue::Nil);
856 return Ok(Some((val, false)));
857 }
858 };
859
860 if frame.ip >= frame.chunk.code.len() {
861 let val = self.stack.pop().unwrap_or(VmValue::Nil);
862 let popped_frame = self.frames.pop().unwrap();
863 if self.frames.is_empty() {
864 return Ok(Some((val, false)));
865 } else {
866 self.iterators.truncate(popped_frame.saved_iterator_depth);
867 self.env = popped_frame.saved_env;
868 self.stack.truncate(popped_frame.stack_base);
869 self.stack.push(val);
870 return Ok(None);
871 }
872 }
873
874 let op = frame.chunk.code[frame.ip];
875 frame.ip += 1;
876
877 match self.execute_op(op).await {
878 Ok(Some(val)) => Ok(Some((val, false))),
879 Ok(None) => Ok(None),
880 Err(VmError::Return(val)) => {
881 if let Some(popped_frame) = self.frames.pop() {
882 if let Some(ref dir) = popped_frame.saved_source_dir {
883 crate::stdlib::set_thread_source_dir(dir);
884 }
885 let current_depth = self.frames.len();
886 self.exception_handlers
887 .retain(|h| h.frame_depth <= current_depth);
888 if self.frames.is_empty() {
889 return Ok(Some((val, false)));
890 }
891 self.iterators.truncate(popped_frame.saved_iterator_depth);
892 self.env = popped_frame.saved_env;
893 self.stack.truncate(popped_frame.stack_base);
894 self.stack.push(val);
895 Ok(None)
896 } else {
897 Ok(Some((val, false)))
898 }
899 }
900 Err(e) => {
901 if self.error_stack_trace.is_empty() {
902 self.error_stack_trace = self.capture_stack_trace();
903 }
904 match self.handle_error(e) {
905 Ok(None) => {
906 self.error_stack_trace.clear();
907 Ok(None)
908 }
909 Ok(Some(val)) => Ok(Some((val, false))),
910 Err(e) => Err(self.enrich_error_with_line(e)),
911 }
912 }
913 }
914 }
915
916 pub fn start(&mut self, chunk: &Chunk) {
918 let initial_env = self.env.clone();
919 self.frames.push(CallFrame {
920 chunk: chunk.clone(),
921 ip: 0,
922 stack_base: self.stack.len(),
923 saved_env: self.env.clone(),
924 initial_env: Some(initial_env),
929 saved_iterator_depth: self.iterators.len(),
930 fn_name: String::new(),
931 argc: 0,
932 saved_source_dir: None,
933 module_functions: None,
934 module_state: None,
935 });
936 }
937
938 pub fn register_builtin<F>(&mut self, name: &str, f: F)
940 where
941 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
942 {
943 self.builtins.insert(name.to_string(), Rc::new(f));
944 }
945
946 pub fn unregister_builtin(&mut self, name: &str) {
948 self.builtins.remove(name);
949 }
950
951 pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
953 where
954 F: Fn(Vec<VmValue>) -> Fut + 'static,
955 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
956 {
957 self.async_builtins
958 .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
959 }
960
961 fn child_vm(&self) -> Vm {
964 Vm {
965 stack: Vec::with_capacity(64),
966 env: self.env.clone(),
967 output: String::new(),
968 builtins: self.builtins.clone(),
969 async_builtins: self.async_builtins.clone(),
970 iterators: Vec::new(),
971 frames: Vec::new(),
972 exception_handlers: Vec::new(),
973 spawned_tasks: BTreeMap::new(),
974 task_counter: 0,
975 deadlines: self.deadlines.clone(),
976 breakpoints: BTreeMap::new(),
977 function_breakpoints: std::collections::BTreeSet::new(),
978 pending_function_bp: None,
979 step_mode: false,
980 step_frame_depth: 0,
981 stopped: false,
982 last_line: 0,
983 source_dir: self.source_dir.clone(),
984 imported_paths: Vec::new(),
985 module_cache: self.module_cache.clone(),
986 source_file: self.source_file.clone(),
987 source_text: self.source_text.clone(),
988 bridge: self.bridge.clone(),
989 denied_builtins: self.denied_builtins.clone(),
990 cancel_token: None,
991 error_stack_trace: Vec::new(),
992 yield_sender: None,
993 project_root: self.project_root.clone(),
994 globals: self.globals.clone(),
995 debug_hook: None,
996 }
997 }
998
999 pub(crate) fn child_vm_for_host(&self) -> Vm {
1002 self.child_vm()
1003 }
1004
1005 pub fn set_source_dir(&mut self, dir: &std::path::Path) {
1008 self.source_dir = Some(dir.to_path_buf());
1009 crate::stdlib::set_thread_source_dir(dir);
1010 if self.project_root.is_none() {
1012 self.project_root = crate::stdlib::process::find_project_root(dir);
1013 }
1014 }
1015
1016 pub fn set_project_root(&mut self, root: &std::path::Path) {
1019 self.project_root = Some(root.to_path_buf());
1020 }
1021
1022 pub fn project_root(&self) -> Option<&std::path::Path> {
1024 self.project_root.as_deref().or(self.source_dir.as_deref())
1025 }
1026
1027 pub fn builtin_names(&self) -> Vec<String> {
1029 let mut names: Vec<String> = self.builtins.keys().cloned().collect();
1030 names.extend(self.async_builtins.keys().cloned());
1031 names
1032 }
1033
1034 pub fn set_global(&mut self, name: &str, value: VmValue) {
1037 self.globals.insert(name.to_string(), value);
1038 }
1039
1040 pub fn output(&self) -> &str {
1042 &self.output
1043 }
1044
1045 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
1047 let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
1048 let result = self.run_chunk(chunk).await;
1049 crate::tracing::span_end(span_id);
1050 result
1051 }
1052
1053 fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
1055 let thrown_value = match &error {
1056 VmError::Thrown(v) => v.clone(),
1057 other => VmValue::String(Rc::from(other.to_string())),
1058 };
1059
1060 if let Some(handler) = self.exception_handlers.pop() {
1061 if !handler.error_type.is_empty() {
1062 let matches = match &thrown_value {
1064 VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
1065 _ => false,
1066 };
1067 if !matches {
1068 return self.handle_error(error);
1069 }
1070 }
1071
1072 while self.frames.len() > handler.frame_depth {
1073 if let Some(frame) = self.frames.pop() {
1074 if let Some(ref dir) = frame.saved_source_dir {
1075 crate::stdlib::set_thread_source_dir(dir);
1076 }
1077 self.iterators.truncate(frame.saved_iterator_depth);
1078 self.env = frame.saved_env;
1079 }
1080 }
1081
1082 while self
1084 .deadlines
1085 .last()
1086 .is_some_and(|d| d.1 > handler.frame_depth)
1087 {
1088 self.deadlines.pop();
1089 }
1090
1091 self.env.truncate_scopes(handler.env_scope_depth);
1092
1093 self.stack.truncate(handler.stack_depth);
1094 self.stack.push(thrown_value);
1095
1096 if let Some(frame) = self.frames.last_mut() {
1097 frame.ip = handler.catch_ip;
1098 }
1099
1100 Ok(None)
1101 } else {
1102 Err(error)
1103 }
1104 }
1105
1106 async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
1107 self.run_chunk_entry(chunk, 0, None, None, None).await
1108 }
1109
1110 async fn run_chunk_entry(
1111 &mut self,
1112 chunk: &Chunk,
1113 argc: usize,
1114 saved_source_dir: Option<std::path::PathBuf>,
1115 module_functions: Option<ModuleFunctionRegistry>,
1116 module_state: Option<crate::value::ModuleState>,
1117 ) -> Result<VmValue, VmError> {
1118 let initial_env = self.env.clone();
1119 self.frames.push(CallFrame {
1120 chunk: chunk.clone(),
1121 ip: 0,
1122 stack_base: self.stack.len(),
1123 saved_env: self.env.clone(),
1124 initial_env: Some(initial_env),
1125 saved_iterator_depth: self.iterators.len(),
1126 fn_name: String::new(),
1127 argc,
1128 saved_source_dir,
1129 module_functions,
1130 module_state,
1131 });
1132
1133 loop {
1134 if let Some(&(deadline, _)) = self.deadlines.last() {
1135 if Instant::now() > deadline {
1136 self.deadlines.pop();
1137 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
1138 match self.handle_error(err) {
1139 Ok(None) => continue,
1140 Ok(Some(val)) => return Ok(val),
1141 Err(e) => return Err(e),
1142 }
1143 }
1144 }
1145
1146 let frame = match self.frames.last_mut() {
1147 Some(f) => f,
1148 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
1149 };
1150
1151 if frame.ip >= frame.chunk.code.len() {
1152 let val = self.stack.pop().unwrap_or(VmValue::Nil);
1153 let popped_frame = self.frames.pop().unwrap();
1154 if let Some(ref dir) = popped_frame.saved_source_dir {
1155 crate::stdlib::set_thread_source_dir(dir);
1156 }
1157
1158 if self.frames.is_empty() {
1159 return Ok(val);
1160 } else {
1161 self.iterators.truncate(popped_frame.saved_iterator_depth);
1162 self.env = popped_frame.saved_env;
1163 self.stack.truncate(popped_frame.stack_base);
1164 self.stack.push(val);
1165 continue;
1166 }
1167 }
1168
1169 let op = frame.chunk.code[frame.ip];
1170 frame.ip += 1;
1171
1172 match self.execute_op(op).await {
1173 Ok(Some(val)) => return Ok(val),
1174 Ok(None) => continue,
1175 Err(VmError::Return(val)) => {
1176 if let Some(popped_frame) = self.frames.pop() {
1177 if let Some(ref dir) = popped_frame.saved_source_dir {
1178 crate::stdlib::set_thread_source_dir(dir);
1179 }
1180 let current_depth = self.frames.len();
1181 self.exception_handlers
1182 .retain(|h| h.frame_depth <= current_depth);
1183
1184 if self.frames.is_empty() {
1185 return Ok(val);
1186 }
1187 self.iterators.truncate(popped_frame.saved_iterator_depth);
1188 self.env = popped_frame.saved_env;
1189 self.stack.truncate(popped_frame.stack_base);
1190 self.stack.push(val);
1191 } else {
1192 return Ok(val);
1193 }
1194 }
1195 Err(e) => {
1196 if self.error_stack_trace.is_empty() {
1198 self.error_stack_trace = self.capture_stack_trace();
1199 }
1200 match self.handle_error(e) {
1201 Ok(None) => {
1202 self.error_stack_trace.clear();
1203 continue;
1204 }
1205 Ok(Some(val)) => return Ok(val),
1206 Err(e) => return Err(self.enrich_error_with_line(e)),
1207 }
1208 }
1209 }
1210 }
1211 }
1212
1213 fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
1215 self.frames
1216 .iter()
1217 .map(|f| {
1218 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
1219 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
1220 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
1221 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
1222 })
1223 .collect()
1224 }
1225
1226 fn enrich_error_with_line(&self, error: VmError) -> VmError {
1230 let line = self
1232 .error_stack_trace
1233 .last()
1234 .map(|(_, l, _, _)| *l)
1235 .unwrap_or_else(|| self.current_line());
1236 if line == 0 {
1237 return error;
1238 }
1239 let suffix = format!(" (line {line})");
1240 match error {
1241 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
1242 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
1243 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
1244 VmError::UndefinedVariable(name) => {
1245 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
1246 }
1247 VmError::UndefinedBuiltin(name) => {
1248 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
1249 }
1250 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
1251 "Cannot assign to immutable binding: {name}{suffix}"
1252 )),
1253 VmError::StackOverflow => {
1254 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
1255 }
1256 other => other,
1262 }
1263 }
1264
1265 const MAX_FRAMES: usize = 512;
1266
1267 fn closure_call_env(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
1295 if closure.module_state.is_some() {
1296 return closure.env.clone();
1297 }
1298 let mut call_env = closure.env.clone();
1299 for scope in &caller_env.scopes {
1303 for (name, (val, mutable)) in &scope.vars {
1304 if matches!(val, VmValue::Closure(_)) && call_env.get(name).is_none() {
1305 let _ = call_env.define(name, val.clone(), *mutable);
1306 }
1307 }
1308 }
1309 call_env
1310 }
1311
1312 fn resolve_named_closure(&self, name: &str) -> Option<Rc<VmClosure>> {
1313 if let Some(VmValue::Closure(closure)) = self.env.get(name) {
1314 return Some(closure);
1315 }
1316 self.frames
1317 .last()
1318 .and_then(|frame| frame.module_functions.as_ref())
1319 .and_then(|registry| registry.borrow().get(name).cloned())
1320 }
1321
1322 fn push_closure_frame(
1324 &mut self,
1325 closure: &VmClosure,
1326 args: &[VmValue],
1327 _parent_functions: &[CompiledFunction],
1328 ) -> Result<(), VmError> {
1329 if self.frames.len() >= Self::MAX_FRAMES {
1330 return Err(VmError::StackOverflow);
1331 }
1332 let saved_env = self.env.clone();
1333
1334 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
1338 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
1339 crate::stdlib::set_thread_source_dir(dir);
1340 prev
1341 } else {
1342 None
1343 };
1344
1345 let mut call_env = Self::closure_call_env(&saved_env, closure);
1346 call_env.push_scope();
1347
1348 let default_start = closure
1349 .func
1350 .default_start
1351 .unwrap_or(closure.func.params.len());
1352 let param_count = closure.func.params.len();
1353 for (i, param) in closure.func.params.iter().enumerate() {
1354 if closure.func.has_rest_param && i == param_count - 1 {
1355 let rest_args = if i < args.len() {
1357 args[i..].to_vec()
1358 } else {
1359 Vec::new()
1360 };
1361 let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
1362 } else if i < args.len() {
1363 let _ = call_env.define(param, args[i].clone(), false);
1364 } else if i < default_start {
1365 let _ = call_env.define(param, VmValue::Nil, false);
1366 }
1367 }
1368
1369 let initial_env = call_env.clone();
1373 self.env = call_env;
1374
1375 if self.function_breakpoints.contains(&closure.func.name) {
1382 self.pending_function_bp = Some(closure.func.name.clone());
1383 }
1384
1385 self.frames.push(CallFrame {
1386 chunk: closure.func.chunk.clone(),
1387 ip: 0,
1388 stack_base: self.stack.len(),
1389 saved_env,
1390 initial_env: Some(initial_env),
1391 saved_iterator_depth: self.iterators.len(),
1392 fn_name: closure.func.name.clone(),
1393 argc: args.len(),
1394 saved_source_dir,
1395 module_functions: closure.module_functions.clone(),
1396 module_state: closure.module_state.clone(),
1397 });
1398
1399 Ok(())
1400 }
1401
1402 pub(crate) fn create_generator(&self, closure: &VmClosure, args: &[VmValue]) -> VmValue {
1405 use crate::value::VmGenerator;
1406
1407 let (tx, rx) = tokio::sync::mpsc::channel::<VmValue>(1);
1409
1410 let mut child = self.child_vm();
1411 child.yield_sender = Some(tx);
1412
1413 let parent_env = self.env.clone();
1419 let mut call_env = Self::closure_call_env(&parent_env, closure);
1420 call_env.push_scope();
1421
1422 let default_start = closure
1423 .func
1424 .default_start
1425 .unwrap_or(closure.func.params.len());
1426 let param_count = closure.func.params.len();
1427 for (i, param) in closure.func.params.iter().enumerate() {
1428 if closure.func.has_rest_param && i == param_count - 1 {
1429 let rest_args = if i < args.len() {
1430 args[i..].to_vec()
1431 } else {
1432 Vec::new()
1433 };
1434 let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
1435 } else if i < args.len() {
1436 let _ = call_env.define(param, args[i].clone(), false);
1437 } else if i < default_start {
1438 let _ = call_env.define(param, VmValue::Nil, false);
1439 }
1440 }
1441 child.env = call_env;
1442
1443 let chunk = closure.func.chunk.clone();
1444 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
1445 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
1446 crate::stdlib::set_thread_source_dir(dir);
1447 prev
1448 } else {
1449 None
1450 };
1451 let module_functions = closure.module_functions.clone();
1452 let module_state = closure.module_state.clone();
1453 let argc = args.len();
1454 tokio::task::spawn_local(async move {
1457 let _ = child
1458 .run_chunk_entry(
1459 &chunk,
1460 argc,
1461 saved_source_dir,
1462 module_functions,
1463 module_state,
1464 )
1465 .await;
1466 });
1469
1470 VmValue::Generator(VmGenerator {
1471 done: Rc::new(std::cell::Cell::new(false)),
1472 receiver: Rc::new(tokio::sync::Mutex::new(rx)),
1473 })
1474 }
1475
1476 fn pop(&mut self) -> Result<VmValue, VmError> {
1477 self.stack.pop().ok_or(VmError::StackUnderflow)
1478 }
1479
1480 fn peek(&self) -> Result<&VmValue, VmError> {
1481 self.stack.last().ok_or(VmError::StackUnderflow)
1482 }
1483
1484 fn const_string(c: &Constant) -> Result<String, VmError> {
1485 match c {
1486 Constant::String(s) => Ok(s.clone()),
1487 _ => Err(VmError::TypeError("expected string constant".into())),
1488 }
1489 }
1490
1491 fn call_closure<'a>(
1494 &'a mut self,
1495 closure: &'a VmClosure,
1496 args: &'a [VmValue],
1497 _parent_functions: &'a [CompiledFunction],
1498 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
1499 Box::pin(async move {
1500 let saved_env = self.env.clone();
1501 let saved_frames = std::mem::take(&mut self.frames);
1502 let saved_handlers = std::mem::take(&mut self.exception_handlers);
1503 let saved_iterators = std::mem::take(&mut self.iterators);
1504 let saved_deadlines = std::mem::take(&mut self.deadlines);
1505
1506 let mut call_env = Self::closure_call_env(&saved_env, closure);
1507 call_env.push_scope();
1508
1509 let default_start = closure
1510 .func
1511 .default_start
1512 .unwrap_or(closure.func.params.len());
1513 let param_count = closure.func.params.len();
1514 for (i, param) in closure.func.params.iter().enumerate() {
1515 if closure.func.has_rest_param && i == param_count - 1 {
1516 let rest_args = if i < args.len() {
1517 args[i..].to_vec()
1518 } else {
1519 Vec::new()
1520 };
1521 let _ =
1522 call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
1523 } else if i < args.len() {
1524 let _ = call_env.define(param, args[i].clone(), false);
1525 } else if i < default_start {
1526 let _ = call_env.define(param, VmValue::Nil, false);
1527 }
1528 }
1529
1530 self.env = call_env;
1531 let argc = args.len();
1532 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
1533 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
1534 crate::stdlib::set_thread_source_dir(dir);
1535 prev
1536 } else {
1537 None
1538 };
1539 let result = self
1540 .run_chunk_entry(
1541 &closure.func.chunk,
1542 argc,
1543 saved_source_dir,
1544 closure.module_functions.clone(),
1545 closure.module_state.clone(),
1546 )
1547 .await;
1548
1549 self.env = saved_env;
1550 self.frames = saved_frames;
1551 self.exception_handlers = saved_handlers;
1552 self.iterators = saved_iterators;
1553 self.deadlines = saved_deadlines;
1554
1555 result
1556 })
1557 }
1558
1559 #[allow(clippy::manual_async_fn)]
1564 fn call_callable_value<'a>(
1565 &'a mut self,
1566 callable: &'a VmValue,
1567 args: &'a [VmValue],
1568 functions: &'a [CompiledFunction],
1569 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
1570 Box::pin(async move {
1571 match callable {
1572 VmValue::Closure(closure) => self.call_closure(closure, args, functions).await,
1573 VmValue::BuiltinRef(name) => {
1574 let name_owned = name.to_string();
1575 self.call_named_builtin(&name_owned, args.to_vec()).await
1576 }
1577 other => Err(VmError::TypeError(format!(
1578 "expected callable, got {}",
1579 other.type_name()
1580 ))),
1581 }
1582 })
1583 }
1584
1585 fn is_callable_value(v: &VmValue) -> bool {
1587 matches!(v, VmValue::Closure(_) | VmValue::BuiltinRef(_))
1588 }
1589
1590 pub async fn call_closure_pub(
1593 &mut self,
1594 closure: &VmClosure,
1595 args: &[VmValue],
1596 functions: &[CompiledFunction],
1597 ) -> Result<VmValue, VmError> {
1598 self.call_closure(closure, args, functions).await
1599 }
1600
1601 async fn call_named_builtin(
1604 &mut self,
1605 name: &str,
1606 args: Vec<VmValue>,
1607 ) -> Result<VmValue, VmError> {
1608 let span_kind = match name {
1610 "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
1611 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
1612 _ => None,
1613 };
1614 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
1615
1616 if self.denied_builtins.contains(name) {
1618 return Err(VmError::CategorizedError {
1619 message: format!("Tool '{}' is not permitted.", name),
1620 category: ErrorCategory::ToolRejected,
1621 });
1622 }
1623 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
1624 if let Some(builtin) = self.builtins.get(name).cloned() {
1625 builtin(&args, &mut self.output)
1626 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
1627 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1628 slot.borrow_mut().push(self.child_vm());
1629 });
1630 let result = async_builtin(args).await;
1631 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1632 slot.borrow_mut().pop();
1633 });
1634 result
1635 } else if let Some(bridge) = &self.bridge {
1636 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
1637 let args_json: Vec<serde_json::Value> =
1638 args.iter().map(crate::llm::vm_value_to_json).collect();
1639 let result = bridge
1640 .call(
1641 "builtin_call",
1642 serde_json::json!({"name": name, "args": args_json}),
1643 )
1644 .await?;
1645 Ok(crate::bridge::json_result_to_vm_value(&result))
1646 } else {
1647 let all_builtins = self
1648 .builtins
1649 .keys()
1650 .chain(self.async_builtins.keys())
1651 .map(|s| s.as_str());
1652 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
1653 return Err(VmError::Runtime(format!(
1654 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
1655 )));
1656 }
1657 Err(VmError::UndefinedBuiltin(name.to_string()))
1658 }
1659 }
1660}
1661
1662pub fn clone_async_builtin_child_vm() -> Option<Vm> {
1670 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
1671}
1672
1673#[deprecated(
1677 note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
1678)]
1679pub fn take_async_builtin_child_vm() -> Option<Vm> {
1680 clone_async_builtin_child_vm()
1681}
1682
1683#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
1685pub fn restore_async_builtin_child_vm(_vm: Vm) {
1686 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1687 let _ = slot;
1688 });
1689}
1690
1691impl Default for Vm {
1692 fn default() -> Self {
1693 Self::new()
1694 }
1695}
1696
1697#[cfg(test)]
1698mod tests {
1699 use super::*;
1700 use crate::compiler::Compiler;
1701 use crate::stdlib::register_vm_stdlib;
1702 use crate::values_equal;
1703 use harn_lexer::Lexer;
1704 use harn_parser::Parser;
1705
1706 fn run_harn(source: &str) -> (String, VmValue) {
1707 let rt = tokio::runtime::Builder::new_current_thread()
1708 .enable_all()
1709 .build()
1710 .unwrap();
1711 rt.block_on(async {
1712 let local = tokio::task::LocalSet::new();
1713 local
1714 .run_until(async {
1715 let mut lexer = Lexer::new(source);
1716 let tokens = lexer.tokenize().unwrap();
1717 let mut parser = Parser::new(tokens);
1718 let program = parser.parse().unwrap();
1719 let chunk = Compiler::new().compile(&program).unwrap();
1720
1721 let mut vm = Vm::new();
1722 register_vm_stdlib(&mut vm);
1723 let result = vm.execute(&chunk).await.unwrap();
1724 (vm.output().to_string(), result)
1725 })
1726 .await
1727 })
1728 }
1729
1730 fn run_output(source: &str) -> String {
1731 run_harn(source).0.trim_end().to_string()
1732 }
1733
1734 fn run_harn_result(source: &str) -> 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 let result = vm.execute(&chunk).await?;
1752 Ok((vm.output().to_string(), result))
1753 })
1754 .await
1755 })
1756 }
1757
1758 fn run_until_paused(vm: &mut Vm, chunk: &Chunk) {
1764 vm.start(chunk);
1765 let rt = tokio::runtime::Builder::new_current_thread()
1766 .enable_all()
1767 .build()
1768 .unwrap();
1769 rt.block_on(async {
1770 let local = tokio::task::LocalSet::new();
1771 local
1772 .run_until(async {
1773 for _ in 0..10_000 {
1774 if vm.is_stopped() {
1775 return;
1776 }
1777 match vm.step_execute().await {
1778 Ok(Some((_, true))) => return,
1779 Ok(_) => continue,
1780 Err(e) => panic!("step_execute failed: {e}"),
1781 }
1782 }
1783 panic!("run_until_paused: step budget exceeded");
1784 })
1785 .await
1786 })
1787 }
1788
1789 fn eval(vm: &mut Vm, expr: &str) -> Result<VmValue, VmError> {
1793 let rt = tokio::runtime::Builder::new_current_thread()
1794 .enable_all()
1795 .build()
1796 .unwrap();
1797 rt.block_on(async {
1798 let local = tokio::task::LocalSet::new();
1799 local.run_until(vm.evaluate_in_frame(expr, 0)).await
1800 })
1801 }
1802
1803 #[test]
1804 fn test_evaluate_in_frame_literal() {
1805 let mut vm = Vm::new();
1810 register_vm_stdlib(&mut vm);
1811 vm.set_breakpoints(vec![2]);
1812 let chunk = crate::compile_source("let __seed__: int = 0\nlog(__seed__)\n").unwrap();
1813 run_until_paused(&mut vm, &chunk);
1814
1815 assert!(values_equal(
1816 &eval(&mut vm, "1 + 2").unwrap(),
1817 &VmValue::Int(3)
1818 ));
1819 assert!(values_equal(
1820 &eval(&mut vm, "\"hi\" + \" there\"").unwrap(),
1821 &VmValue::String(Rc::from("hi there"))
1822 ));
1823 assert!(values_equal(
1824 &eval(&mut vm, "5 > 3 && 2 < 4").unwrap(),
1825 &VmValue::Bool(true)
1826 ));
1827 }
1828
1829 #[test]
1830 fn test_evaluate_in_frame_sees_locals() {
1831 let mut vm = Vm::new();
1832 register_vm_stdlib(&mut vm);
1833 vm.set_breakpoints(vec![3]);
1834 let chunk = crate::compile_source(
1835 "let user: string = \"alice\"\nlet count: int = 42\nlog(count)\n",
1836 )
1837 .unwrap();
1838 run_until_paused(&mut vm, &chunk);
1839
1840 assert!(values_equal(
1841 &eval(&mut vm, "user").unwrap(),
1842 &VmValue::String(Rc::from("alice"))
1843 ));
1844 assert!(values_equal(
1845 &eval(&mut vm, "count * 2").unwrap(),
1846 &VmValue::Int(84)
1847 ));
1848 assert!(values_equal(
1849 &eval(&mut vm, "user + \" has \" + to_string(count)").unwrap(),
1850 &VmValue::String(Rc::from("alice has 42"))
1851 ));
1852 }
1853
1854 #[test]
1855 fn test_evaluate_in_frame_does_not_leak_state() {
1856 let mut vm = Vm::new();
1859 register_vm_stdlib(&mut vm);
1860 vm.set_breakpoints(vec![2]);
1861 let chunk = crate::compile_source("let x: int = 7\nlog(x)\n").unwrap();
1862 run_until_paused(&mut vm, &chunk);
1863
1864 let pre_stack = vm.stack.len();
1865 let pre_frames = vm.frames.len();
1866 let pre_scope = vm.env.scope_depth();
1867 let _ = eval(&mut vm, "x + 100").unwrap();
1868 let _ = eval(&mut vm, "x * x").unwrap();
1869 assert_eq!(vm.stack.len(), pre_stack);
1870 assert_eq!(vm.frames.len(), pre_frames);
1871 assert_eq!(vm.env.scope_depth(), pre_scope);
1872 assert!(vm.env.get("__burin_eval_result__").is_none());
1875 }
1876
1877 #[test]
1878 fn test_set_variable_in_frame_updates_let_binding() {
1879 let mut vm = Vm::new();
1882 register_vm_stdlib(&mut vm);
1883 vm.set_breakpoints(vec![3]);
1884 let chunk = crate::compile_source(
1885 "let count: int = 7\nlet label: string = \"before\"\nlog(count)\n",
1886 )
1887 .unwrap();
1888 run_until_paused(&mut vm, &chunk);
1889
1890 let rt = tokio::runtime::Builder::new_current_thread()
1891 .enable_all()
1892 .build()
1893 .unwrap();
1894 let stored = rt.block_on(async {
1895 let local = tokio::task::LocalSet::new();
1896 local
1897 .run_until(vm.set_variable_in_frame("count", "42", 0))
1898 .await
1899 });
1900 assert!(values_equal(&stored.unwrap(), &VmValue::Int(42)));
1901 assert!(values_equal(
1902 &eval(&mut vm, "count").unwrap(),
1903 &VmValue::Int(42)
1904 ));
1905
1906 let rt = tokio::runtime::Builder::new_current_thread()
1908 .enable_all()
1909 .build()
1910 .unwrap();
1911 rt.block_on(async {
1912 let local = tokio::task::LocalSet::new();
1913 local
1914 .run_until(vm.set_variable_in_frame("label", "\"x\" + to_string(count)", 0))
1915 .await
1916 .unwrap()
1917 });
1918 assert!(values_equal(
1919 &eval(&mut vm, "label").unwrap(),
1920 &VmValue::String(Rc::from("x42"))
1921 ));
1922 }
1923
1924 #[test]
1925 fn test_set_variable_in_frame_rejects_undefined() {
1926 let mut vm = Vm::new();
1927 register_vm_stdlib(&mut vm);
1928 vm.set_breakpoints(vec![2]);
1929 let chunk = crate::compile_source("let x: int = 1\nlog(x)\n").unwrap();
1930 run_until_paused(&mut vm, &chunk);
1931
1932 let rt = tokio::runtime::Builder::new_current_thread()
1933 .enable_all()
1934 .build()
1935 .unwrap();
1936 let err = rt
1937 .block_on(async {
1938 let local = tokio::task::LocalSet::new();
1939 local
1940 .run_until(vm.set_variable_in_frame("ghost", "0", 0))
1941 .await
1942 })
1943 .unwrap_err();
1944 let msg = err.to_string();
1945 assert!(
1946 msg.contains("ghost"),
1947 "expected 'ghost' in error, got {msg}"
1948 );
1949 }
1950
1951 #[test]
1952 fn test_restart_frame_rewinds_ip_and_rebinds_args() {
1953 let mut vm = Vm::new();
1957 register_vm_stdlib(&mut vm);
1958 vm.set_breakpoints(vec![3]);
1959 let chunk = crate::compile_source(
1960 "fn inner(n: int) -> int { \n let doubled: int = n * 2\n log(doubled)\n return doubled\n}\nlog(inner(21))\n",
1961 )
1962 .unwrap();
1963 run_until_paused(&mut vm, &chunk);
1964
1965 let rt = tokio::runtime::Builder::new_current_thread()
1968 .enable_all()
1969 .build()
1970 .unwrap();
1971 rt.block_on(async {
1972 let local = tokio::task::LocalSet::new();
1973 local
1974 .run_until(vm.set_variable_in_frame("doubled", "999", 0))
1975 .await
1976 .unwrap()
1977 });
1978 assert!(values_equal(
1979 &eval(&mut vm, "doubled").unwrap(),
1980 &VmValue::Int(999)
1981 ));
1982
1983 let top = vm.frame_count() - 1;
1985 vm.restart_frame(top).unwrap();
1986
1987 assert!(values_equal(
1991 &eval(&mut vm, "n").unwrap(),
1992 &VmValue::Int(21)
1993 ));
1994 }
1995
1996 #[test]
1997 fn test_restart_frame_rejects_scratch_frames() {
1998 let mut vm = Vm::new();
1999 register_vm_stdlib(&mut vm);
2000 vm.set_breakpoints(vec![2]);
2001 let chunk = crate::compile_source("let x: int = 1\nlog(x)\n").unwrap();
2002 run_until_paused(&mut vm, &chunk);
2003 let err = vm.restart_frame(99).unwrap_err();
2009 assert!(err.to_string().contains("out of range"));
2010 }
2011
2012 #[test]
2013 fn test_signal_cancel_unwinds_step_loop() {
2014 let mut vm = Vm::new();
2015 register_vm_stdlib(&mut vm);
2016 let chunk = crate::compile_source(
2021 "pipeline t(task) { var i = 0\n while i < 1000000 { i = i + 1 } }\n",
2022 )
2023 .unwrap();
2024 vm.start(&chunk);
2025 vm.signal_cancel();
2026 let rt = tokio::runtime::Builder::new_current_thread()
2027 .enable_all()
2028 .build()
2029 .unwrap();
2030 let result = rt.block_on(async {
2031 let local = tokio::task::LocalSet::new();
2032 local.run_until(vm.step_execute()).await
2033 });
2034 match result {
2035 Err(VmError::Thrown(VmValue::String(s))) => {
2036 assert!(
2037 s.contains("kind:cancelled:"),
2038 "cancellation must surface as a kind-tagged Thrown error"
2039 );
2040 }
2041 other => panic!("expected cancelled Thrown, got {other:?}"),
2042 }
2043 }
2044
2045 #[test]
2046 fn test_function_breakpoint_stops_on_entry() {
2047 let mut vm = Vm::new();
2048 register_vm_stdlib(&mut vm);
2049 vm.set_function_breakpoints(vec!["do_work".to_string()]);
2050 let chunk = crate::compile_source(
2051 "fn do_work(n: int) -> int { return n + 1 }\npipeline t(task) { let x = do_work(41)\nlog(x) }\n",
2052 )
2053 .unwrap();
2054 run_until_paused(&mut vm, &chunk);
2055 let hit = vm.take_pending_function_bp().expect("must latch a hit");
2058 assert_eq!(hit, "do_work");
2059 assert!(vm.take_pending_function_bp().is_none(), "one-shot");
2060
2061 let frames = vm.debug_stack_frames();
2063 let top = frames.last().expect("callee frame on stack");
2064 assert_eq!(top.0, "do_work");
2065 }
2066
2067 #[test]
2068 fn test_function_breakpoint_unknown_name_does_not_fire() {
2069 let mut vm = Vm::new();
2070 register_vm_stdlib(&mut vm);
2071 vm.set_function_breakpoints(vec!["nonexistent".to_string()]);
2072 let chunk = crate::compile_source("pipeline t(task) { let x = 1\nlog(x) }\n").unwrap();
2073 vm.start(&chunk);
2078 let rt = tokio::runtime::Builder::new_current_thread()
2079 .enable_all()
2080 .build()
2081 .unwrap();
2082 rt.block_on(async {
2083 let local = tokio::task::LocalSet::new();
2084 local
2085 .run_until(async {
2086 for _ in 0..10_000 {
2087 match vm.step_execute().await {
2088 Ok(Some((_, false))) => return,
2089 Ok(_) => continue,
2090 Err(e) => panic!("step_execute failed: {e}"),
2091 }
2092 }
2093 panic!("step budget exceeded");
2094 })
2095 .await
2096 });
2097 assert!(vm.take_pending_function_bp().is_none());
2098 }
2099
2100 #[test]
2101 fn test_evaluate_in_frame_parse_error_is_surfaced_standalone() {
2102 let mut vm = Vm::new();
2103 register_vm_stdlib(&mut vm);
2104 vm.set_breakpoints(vec![1]);
2105 let chunk = crate::compile_source("log(0)\n").unwrap();
2106 run_until_paused(&mut vm, &chunk);
2107
2108 let err = eval(&mut vm, "(\"unterminated").unwrap_err();
2109 let msg = err.to_string();
2110 assert!(
2111 msg.contains("evaluate:"),
2112 "expected evaluate error prefix, got: {msg}"
2113 );
2114 }
2115
2116 #[test]
2117 fn test_breakpoints_wildcard_matches_any_file() {
2118 let mut vm = Vm::new();
2119 vm.set_breakpoints(vec![3, 7]);
2120 assert!(vm.breakpoint_matches(3));
2121 assert!(vm.breakpoint_matches(7));
2122 assert!(!vm.breakpoint_matches(4));
2123 }
2124
2125 #[test]
2126 fn test_breakpoints_per_file_does_not_leak_to_wildcard() {
2127 let mut vm = Vm::new();
2128 vm.set_breakpoints_for_file("auto.harn", vec![10]);
2129 assert!(!vm.breakpoint_matches(10));
2132 }
2133
2134 #[test]
2135 fn test_breakpoints_per_file_clear_on_empty() {
2136 let mut vm = Vm::new();
2137 vm.set_breakpoints_for_file("a.harn", vec![1, 2]);
2138 vm.set_breakpoints_for_file("a.harn", vec![]);
2139 assert!(!vm.breakpoints.contains_key("a.harn"));
2140 }
2141
2142 #[test]
2143 fn test_arithmetic() {
2144 let out =
2145 run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
2146 assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
2147 }
2148
2149 #[test]
2150 fn test_mixed_arithmetic() {
2151 let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
2152 assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
2153 }
2154
2155 #[test]
2156 fn test_exponentiation() {
2157 let out = run_output(
2158 "pipeline t(task) { log(2 ** 8)\nlog(2 * 3 ** 2)\nlog(2 ** 3 ** 2)\nlog(2 ** -1) }",
2159 );
2160 assert_eq!(out, "[harn] 256\n[harn] 18\n[harn] 512\n[harn] 0.5");
2161 }
2162
2163 #[test]
2164 fn test_comparisons() {
2165 let out =
2166 run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
2167 assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
2168 }
2169
2170 #[test]
2171 fn test_let_var() {
2172 let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
2173 assert_eq!(out, "[harn] 42\n[harn] 2");
2174 }
2175
2176 #[test]
2177 fn test_if_else() {
2178 let out = run_output(
2179 r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
2180 );
2181 assert_eq!(out, "[harn] yes\n[harn] no");
2182 }
2183
2184 #[test]
2185 fn test_while_loop() {
2186 let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
2187 assert_eq!(out, "[harn] 5");
2188 }
2189
2190 #[test]
2191 fn test_for_in() {
2192 let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
2193 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
2194 }
2195
2196 #[test]
2197 fn test_inner_for_return_does_not_leak_iterator_into_caller() {
2198 let out = run_output(
2199 r#"pipeline t(task) {
2200 fn first_match() {
2201 for pattern in ["a", "b"] {
2202 return pattern
2203 }
2204 return ""
2205 }
2206
2207 var seen = []
2208 for path in ["outer"] {
2209 seen = seen + [path + ":" + first_match()]
2210 }
2211 log(join(seen, ","))
2212}"#,
2213 );
2214 assert_eq!(out, "[harn] outer:a");
2215 }
2216
2217 #[test]
2218 fn test_fn_decl_and_call() {
2219 let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
2220 assert_eq!(out, "[harn] 7");
2221 }
2222
2223 #[test]
2224 fn test_closure() {
2225 let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
2226 assert_eq!(out, "[harn] 10");
2227 }
2228
2229 #[test]
2230 fn test_closure_capture() {
2231 let out = run_output(
2232 "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
2233 );
2234 assert_eq!(out, "[harn] 15");
2235 }
2236
2237 #[test]
2238 fn test_string_concat() {
2239 let out = run_output(
2240 r#"pipeline t(task) { let a = "hello" + " " + "world"
2241log(a) }"#,
2242 );
2243 assert_eq!(out, "[harn] hello world");
2244 }
2245
2246 #[test]
2247 fn test_list_map() {
2248 let out = run_output(
2249 "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
2250 );
2251 assert_eq!(out, "[harn] [2, 4, 6]");
2252 }
2253
2254 #[test]
2255 fn test_list_filter() {
2256 let out = run_output(
2257 "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
2258 );
2259 assert_eq!(out, "[harn] [4, 5]");
2260 }
2261
2262 #[test]
2263 fn test_list_reduce() {
2264 let out = run_output(
2265 "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
2266 );
2267 assert_eq!(out, "[harn] 10");
2268 }
2269
2270 #[test]
2271 fn test_dict_access() {
2272 let out = run_output(
2273 r#"pipeline t(task) { let d = {name: "test", value: 42}
2274log(d.name)
2275log(d.value) }"#,
2276 );
2277 assert_eq!(out, "[harn] test\n[harn] 42");
2278 }
2279
2280 #[test]
2281 fn test_dict_methods() {
2282 let out = run_output(
2283 r#"pipeline t(task) { let d = {a: 1, b: 2}
2284log(d.keys())
2285log(d.values())
2286log(d.has("a"))
2287log(d.has("z")) }"#,
2288 );
2289 assert_eq!(
2290 out,
2291 "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
2292 );
2293 }
2294
2295 #[test]
2296 fn test_pipe_operator() {
2297 let out = run_output(
2298 "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
2299 );
2300 assert_eq!(out, "[harn] 10");
2301 }
2302
2303 #[test]
2304 fn test_pipe_with_closure() {
2305 let out = run_output(
2306 r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
2307log(r) }"#,
2308 );
2309 assert_eq!(out, "[harn] [hello, world]");
2310 }
2311
2312 #[test]
2313 fn test_nil_coalescing() {
2314 let out = run_output(
2315 r#"pipeline t(task) { let a = nil ?? "fallback"
2316log(a)
2317let b = "present" ?? "fallback"
2318log(b) }"#,
2319 );
2320 assert_eq!(out, "[harn] fallback\n[harn] present");
2321 }
2322
2323 #[test]
2324 fn test_logical_operators() {
2325 let out =
2326 run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
2327 assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
2328 }
2329
2330 #[test]
2331 fn test_match() {
2332 let out = run_output(
2333 r#"pipeline t(task) { let x = "b"
2334match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
2335 );
2336 assert_eq!(out, "[harn] second");
2337 }
2338
2339 #[test]
2340 fn test_subscript() {
2341 let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
2342 assert_eq!(out, "[harn] 20");
2343 }
2344
2345 #[test]
2346 fn test_string_methods() {
2347 let out = run_output(
2348 r#"pipeline t(task) { log("hello world".replace("world", "harn"))
2349log("a,b,c".split(","))
2350log(" hello ".trim())
2351log("hello".starts_with("hel"))
2352log("hello".ends_with("lo"))
2353log("hello".substring(1, 3)) }"#,
2354 );
2355 assert_eq!(
2356 out,
2357 "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
2358 );
2359 }
2360
2361 #[test]
2362 fn test_list_properties() {
2363 let out = run_output(
2364 "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
2365 );
2366 assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
2367 }
2368
2369 #[test]
2370 fn test_recursive_function() {
2371 let out = run_output(
2372 "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
2373 );
2374 assert_eq!(out, "[harn] 55");
2375 }
2376
2377 #[test]
2378 fn test_ternary() {
2379 let out = run_output(
2380 r#"pipeline t(task) { let x = 5
2381let r = x > 0 ? "positive" : "non-positive"
2382log(r) }"#,
2383 );
2384 assert_eq!(out, "[harn] positive");
2385 }
2386
2387 #[test]
2388 fn test_for_in_dict() {
2389 let out = run_output(
2390 "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
2391 );
2392 assert_eq!(out, "[harn] a\n[harn] b");
2393 }
2394
2395 #[test]
2396 fn test_list_any_all() {
2397 let out = run_output(
2398 "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 })) }",
2399 );
2400 assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
2401 }
2402
2403 #[test]
2404 fn test_disassembly() {
2405 let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
2406 let tokens = lexer.tokenize().unwrap();
2407 let mut parser = Parser::new(tokens);
2408 let program = parser.parse().unwrap();
2409 let chunk = Compiler::new().compile(&program).unwrap();
2410 let disasm = chunk.disassemble("test");
2411 assert!(disasm.contains("CONSTANT"));
2412 assert!(disasm.contains("ADD"));
2413 assert!(disasm.contains("CALL"));
2414 }
2415
2416 #[test]
2419 fn test_try_catch_basic() {
2420 let out = run_output(
2421 r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
2422 );
2423 assert_eq!(out, "[harn] caught: oops");
2424 }
2425
2426 #[test]
2427 fn test_try_no_error() {
2428 let out = run_output(
2429 r#"pipeline t(task) {
2430var result = 0
2431try { result = 42 } catch(e) { result = 0 }
2432log(result)
2433}"#,
2434 );
2435 assert_eq!(out, "[harn] 42");
2436 }
2437
2438 #[test]
2439 fn test_throw_uncaught() {
2440 let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
2441 assert!(result.is_err());
2442 }
2443
2444 fn run_vm(source: &str) -> String {
2447 let rt = tokio::runtime::Builder::new_current_thread()
2448 .enable_all()
2449 .build()
2450 .unwrap();
2451 rt.block_on(async {
2452 let local = tokio::task::LocalSet::new();
2453 local
2454 .run_until(async {
2455 let mut lexer = Lexer::new(source);
2456 let tokens = lexer.tokenize().unwrap();
2457 let mut parser = Parser::new(tokens);
2458 let program = parser.parse().unwrap();
2459 let chunk = Compiler::new().compile(&program).unwrap();
2460 let mut vm = Vm::new();
2461 register_vm_stdlib(&mut vm);
2462 vm.execute(&chunk).await.unwrap();
2463 vm.output().to_string()
2464 })
2465 .await
2466 })
2467 }
2468
2469 fn run_vm_err(source: &str) -> String {
2470 let rt = tokio::runtime::Builder::new_current_thread()
2471 .enable_all()
2472 .build()
2473 .unwrap();
2474 rt.block_on(async {
2475 let local = tokio::task::LocalSet::new();
2476 local
2477 .run_until(async {
2478 let mut lexer = Lexer::new(source);
2479 let tokens = lexer.tokenize().unwrap();
2480 let mut parser = Parser::new(tokens);
2481 let program = parser.parse().unwrap();
2482 let chunk = Compiler::new().compile(&program).unwrap();
2483 let mut vm = Vm::new();
2484 register_vm_stdlib(&mut vm);
2485 match vm.execute(&chunk).await {
2486 Err(e) => format!("{}", e),
2487 Ok(_) => panic!("Expected error"),
2488 }
2489 })
2490 .await
2491 })
2492 }
2493
2494 #[test]
2495 fn test_hello_world() {
2496 let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
2497 assert_eq!(out, "[harn] hello\n");
2498 }
2499
2500 #[test]
2501 fn test_arithmetic_new() {
2502 let out = run_vm("pipeline default(task) { log(2 + 3) }");
2503 assert_eq!(out, "[harn] 5\n");
2504 }
2505
2506 #[test]
2507 fn test_string_concat_new() {
2508 let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
2509 assert_eq!(out, "[harn] ab\n");
2510 }
2511
2512 #[test]
2513 fn test_if_else_new() {
2514 let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
2515 assert_eq!(out, "[harn] 1\n");
2516 }
2517
2518 #[test]
2519 fn test_for_loop_new() {
2520 let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
2521 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
2522 }
2523
2524 #[test]
2525 fn test_while_loop_new() {
2526 let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
2527 assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
2528 }
2529
2530 #[test]
2531 fn test_function_call_new() {
2532 let out =
2533 run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
2534 assert_eq!(out, "[harn] 5\n");
2535 }
2536
2537 #[test]
2538 fn test_closure_new() {
2539 let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
2540 assert_eq!(out, "[harn] 10\n");
2541 }
2542
2543 #[test]
2544 fn test_recursion() {
2545 let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
2546 assert_eq!(out, "[harn] 120\n");
2547 }
2548
2549 #[test]
2550 fn test_try_catch_new() {
2551 let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
2552 assert_eq!(out, "[harn] err\n");
2553 }
2554
2555 #[test]
2556 fn test_try_no_error_new() {
2557 let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
2558 assert_eq!(out, "[harn] 1\n");
2559 }
2560
2561 #[test]
2562 fn test_list_map_new() {
2563 let out =
2564 run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
2565 assert_eq!(out, "[harn] [2, 4, 6]\n");
2566 }
2567
2568 #[test]
2569 fn test_list_filter_new() {
2570 let out = run_vm(
2571 "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
2572 );
2573 assert_eq!(out, "[harn] [3, 4]\n");
2574 }
2575
2576 #[test]
2577 fn test_dict_access_new() {
2578 let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
2579 assert_eq!(out, "[harn] Alice\n");
2580 }
2581
2582 #[test]
2583 fn test_string_interpolation() {
2584 let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
2585 assert_eq!(out, "[harn] val=42\n");
2586 }
2587
2588 #[test]
2589 fn test_match_new() {
2590 let out = run_vm(
2591 "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
2592 );
2593 assert_eq!(out, "[harn] 2\n");
2594 }
2595
2596 #[test]
2597 fn test_json_roundtrip() {
2598 let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
2599 assert!(out.contains("\"a\""));
2600 assert!(out.contains("1"));
2601 }
2602
2603 #[test]
2604 fn test_type_of() {
2605 let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
2606 assert_eq!(out, "[harn] int\n[harn] string\n");
2607 }
2608
2609 #[test]
2610 fn test_stack_overflow() {
2611 let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
2612 assert!(
2613 err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
2614 "Expected stack overflow error, got: {}",
2615 err
2616 );
2617 }
2618
2619 #[test]
2620 fn test_division_by_zero() {
2621 let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
2622 assert!(
2623 err.contains("Division by zero") || err.contains("division"),
2624 "Expected division by zero error, got: {}",
2625 err
2626 );
2627 }
2628
2629 #[test]
2630 fn test_float_division_by_zero_uses_ieee_values() {
2631 let out = run_vm(
2632 "pipeline default(task) { log(is_nan(0.0 / 0.0))\nlog(is_infinite(1.0 / 0.0))\nlog(is_infinite(-1.0 / 0.0)) }",
2633 );
2634 assert_eq!(out, "[harn] true\n[harn] true\n[harn] true\n");
2635 }
2636
2637 #[test]
2638 fn test_reusing_catch_binding_name_in_same_block() {
2639 let out = run_vm(
2640 r#"pipeline default(task) {
2641try {
2642 throw "a"
2643} catch e {
2644 log(e)
2645}
2646try {
2647 throw "b"
2648} catch e {
2649 log(e)
2650}
2651}"#,
2652 );
2653 assert_eq!(out, "[harn] a\n[harn] b\n");
2654 }
2655
2656 #[test]
2657 fn test_try_catch_nested() {
2658 let out = run_output(
2659 r#"pipeline t(task) {
2660try {
2661 try {
2662 throw "inner"
2663 } catch(e) {
2664 log("inner caught: " + e)
2665 throw "outer"
2666 }
2667} catch(e2) {
2668 log("outer caught: " + e2)
2669}
2670}"#,
2671 );
2672 assert_eq!(
2673 out,
2674 "[harn] inner caught: inner\n[harn] outer caught: outer"
2675 );
2676 }
2677
2678 #[test]
2681 fn test_parallel_basic() {
2682 let out = run_output(
2683 "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
2684 );
2685 assert_eq!(out, "[harn] [0, 10, 20]");
2686 }
2687
2688 #[test]
2689 fn test_parallel_no_variable() {
2690 let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
2691 assert_eq!(out, "[harn] [42, 42, 42]");
2692 }
2693
2694 #[test]
2695 fn test_parallel_each_basic() {
2696 let out = run_output(
2697 "pipeline t(task) { let results = parallel each [1, 2, 3] { x -> x * x }\nlog(results) }",
2698 );
2699 assert_eq!(out, "[harn] [1, 4, 9]");
2700 }
2701
2702 #[test]
2703 fn test_spawn_await() {
2704 let out = run_output(
2705 r#"pipeline t(task) {
2706let handle = spawn { log("spawned") }
2707let result = await(handle)
2708log("done")
2709}"#,
2710 );
2711 assert_eq!(out, "[harn] spawned\n[harn] done");
2712 }
2713
2714 #[test]
2715 fn test_spawn_cancel() {
2716 let out = run_output(
2717 r#"pipeline t(task) {
2718let handle = spawn { log("should be cancelled") }
2719cancel(handle)
2720log("cancelled")
2721}"#,
2722 );
2723 assert_eq!(out, "[harn] cancelled");
2724 }
2725
2726 #[test]
2727 fn test_spawn_returns_value() {
2728 let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
2729 assert_eq!(out, "[harn] 42");
2730 }
2731
2732 #[test]
2735 fn test_deadline_success() {
2736 let out = run_output(
2737 r#"pipeline t(task) {
2738let result = deadline 5s { log("within deadline")
273942 }
2740log(result)
2741}"#,
2742 );
2743 assert_eq!(out, "[harn] within deadline\n[harn] 42");
2744 }
2745
2746 #[test]
2747 fn test_deadline_exceeded() {
2748 let result = run_harn_result(
2749 r#"pipeline t(task) {
2750deadline 1ms {
2751 var i = 0
2752 while i < 1000000 { i = i + 1 }
2753}
2754}"#,
2755 );
2756 assert!(result.is_err());
2757 }
2758
2759 #[test]
2760 fn test_deadline_caught_by_try() {
2761 let out = run_output(
2762 r#"pipeline t(task) {
2763try {
2764 deadline 1ms {
2765 var i = 0
2766 while i < 1000000 { i = i + 1 }
2767 }
2768} catch(e) {
2769 log("caught")
2770}
2771}"#,
2772 );
2773 assert_eq!(out, "[harn] caught");
2774 }
2775
2776 fn run_harn_with_denied(
2778 source: &str,
2779 denied: HashSet<String>,
2780 ) -> Result<(String, VmValue), VmError> {
2781 let rt = tokio::runtime::Builder::new_current_thread()
2782 .enable_all()
2783 .build()
2784 .unwrap();
2785 rt.block_on(async {
2786 let local = tokio::task::LocalSet::new();
2787 local
2788 .run_until(async {
2789 let mut lexer = Lexer::new(source);
2790 let tokens = lexer.tokenize().unwrap();
2791 let mut parser = Parser::new(tokens);
2792 let program = parser.parse().unwrap();
2793 let chunk = Compiler::new().compile(&program).unwrap();
2794
2795 let mut vm = Vm::new();
2796 register_vm_stdlib(&mut vm);
2797 vm.set_denied_builtins(denied);
2798 let result = vm.execute(&chunk).await?;
2799 Ok((vm.output().to_string(), result))
2800 })
2801 .await
2802 })
2803 }
2804
2805 #[test]
2806 fn test_sandbox_deny_builtin() {
2807 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2808 let result = run_harn_with_denied(
2809 r#"pipeline t(task) {
2810let xs = [1, 2]
2811push(xs, 3)
2812}"#,
2813 denied,
2814 );
2815 let err = result.unwrap_err();
2816 let msg = format!("{err}");
2817 assert!(
2818 msg.contains("not permitted"),
2819 "expected not permitted, got: {msg}"
2820 );
2821 assert!(
2822 msg.contains("push"),
2823 "expected builtin name in error, got: {msg}"
2824 );
2825 }
2826
2827 #[test]
2828 fn test_sandbox_allowed_builtin_works() {
2829 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2831 let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
2832 let (output, _) = result.unwrap();
2833 assert_eq!(output.trim(), "[harn] hello");
2834 }
2835
2836 #[test]
2837 fn test_sandbox_empty_denied_set() {
2838 let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
2840 let (output, _) = result.unwrap();
2841 assert_eq!(output.trim(), "[harn] ok");
2842 }
2843
2844 #[test]
2845 fn test_sandbox_propagates_to_spawn() {
2846 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2848 let result = run_harn_with_denied(
2849 r#"pipeline t(task) {
2850let handle = spawn {
2851 let xs = [1, 2]
2852 push(xs, 3)
2853}
2854await(handle)
2855}"#,
2856 denied,
2857 );
2858 let err = result.unwrap_err();
2859 let msg = format!("{err}");
2860 assert!(
2861 msg.contains("not permitted"),
2862 "expected not permitted in spawned VM, got: {msg}"
2863 );
2864 }
2865
2866 #[test]
2867 fn test_sandbox_propagates_to_parallel() {
2868 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2870 let result = run_harn_with_denied(
2871 r#"pipeline t(task) {
2872let results = parallel(2) { i ->
2873 let xs = [1, 2]
2874 push(xs, 3)
2875}
2876}"#,
2877 denied,
2878 );
2879 let err = result.unwrap_err();
2880 let msg = format!("{err}");
2881 assert!(
2882 msg.contains("not permitted"),
2883 "expected not permitted in parallel VM, got: {msg}"
2884 );
2885 }
2886
2887 #[test]
2888 fn test_if_else_has_lexical_block_scope() {
2889 let out = run_output(
2890 r#"pipeline t(task) {
2891let x = "outer"
2892if true {
2893 let x = "inner"
2894 log(x)
2895} else {
2896 let x = "other"
2897 log(x)
2898}
2899log(x)
2900}"#,
2901 );
2902 assert_eq!(out, "[harn] inner\n[harn] outer");
2903 }
2904
2905 #[test]
2906 fn test_loop_and_catch_bindings_are_block_scoped() {
2907 let out = run_output(
2908 r#"pipeline t(task) {
2909let label = "outer"
2910for item in [1, 2] {
2911 let label = "loop ${item}"
2912 log(label)
2913}
2914try {
2915 throw("boom")
2916} catch (label) {
2917 log(label)
2918}
2919log(label)
2920}"#,
2921 );
2922 assert_eq!(
2923 out,
2924 "[harn] loop 1\n[harn] loop 2\n[harn] boom\n[harn] outer"
2925 );
2926 }
2927}