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