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