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