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