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