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