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