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 crate::stdlib::set_thread_source_dir(dir);
407 }
408
409 pub fn builtin_names(&self) -> Vec<String> {
411 let mut names: Vec<String> = self.builtins.keys().cloned().collect();
412 names.extend(self.async_builtins.keys().cloned());
413 names
414 }
415
416 pub fn set_global(&mut self, name: &str, value: VmValue) {
418 self.env.define(name, value, false);
419 }
420
421 fn execute_import<'a>(
423 &'a mut self,
424 path: &'a str,
425 selected_names: Option<&'a [String]>,
426 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
427 Box::pin(async move {
428 use std::path::PathBuf;
429
430 if let Some(module) = path.strip_prefix("std/") {
432 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
433 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
434 if self.imported_paths.contains(&synthetic) {
435 return Ok(());
436 }
437 self.imported_paths.push(synthetic);
438
439 let mut lexer = harn_lexer::Lexer::new(source);
440 let tokens = lexer.tokenize().map_err(|e| {
441 VmError::Runtime(format!("stdlib lex error in std/{module}: {e}"))
442 })?;
443 let mut parser = harn_parser::Parser::new(tokens);
444 let program = parser.parse().map_err(|e| {
445 VmError::Runtime(format!("stdlib parse error in std/{module}: {e}"))
446 })?;
447
448 self.import_declarations(&program, selected_names, None)
449 .await?;
450 return Ok(());
451 }
452 return Err(VmError::Runtime(format!(
453 "Unknown stdlib module: std/{module}"
454 )));
455 }
456
457 let base = self
459 .source_dir
460 .clone()
461 .unwrap_or_else(|| PathBuf::from("."));
462 let mut file_path = base.join(path);
463
464 if !file_path.exists() && file_path.extension().is_none() {
466 file_path.set_extension("harn");
467 }
468
469 if !file_path.exists() {
471 for pkg_dir in [".harn/packages", ".burin/packages"] {
472 let pkg_path = base.join(pkg_dir).join(path);
473 if pkg_path.exists() {
474 file_path = if pkg_path.is_dir() {
475 let lib = pkg_path.join("lib.harn");
476 if lib.exists() {
477 lib
478 } else {
479 pkg_path
480 }
481 } else {
482 pkg_path
483 };
484 break;
485 }
486 let mut pkg_harn = pkg_path.clone();
487 pkg_harn.set_extension("harn");
488 if pkg_harn.exists() {
489 file_path = pkg_harn;
490 break;
491 }
492 }
493 }
494
495 let canonical = file_path
497 .canonicalize()
498 .unwrap_or_else(|_| file_path.clone());
499 if self.imported_paths.contains(&canonical) {
500 return Ok(()); }
502 self.imported_paths.push(canonical);
503
504 let source = std::fs::read_to_string(&file_path).map_err(|e| {
506 VmError::Runtime(format!(
507 "Import error: cannot read '{}': {e}",
508 file_path.display()
509 ))
510 })?;
511
512 let mut lexer = harn_lexer::Lexer::new(&source);
513 let tokens = lexer
514 .tokenize()
515 .map_err(|e| VmError::Runtime(format!("Import lex error: {e}")))?;
516 let mut parser = harn_parser::Parser::new(tokens);
517 let program = parser
518 .parse()
519 .map_err(|e| VmError::Runtime(format!("Import parse error: {e}")))?;
520
521 self.import_declarations(&program, selected_names, Some(&file_path))
522 .await?;
523
524 Ok(())
525 })
526 }
527
528 fn import_declarations<'a>(
531 &'a mut self,
532 program: &'a [harn_parser::SNode],
533 selected_names: Option<&'a [String]>,
534 file_path: Option<&'a std::path::Path>,
535 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + 'a>> {
536 Box::pin(async move {
537 let has_pub = program
538 .iter()
539 .any(|n| matches!(&n.node, harn_parser::Node::FnDecl { is_pub: true, .. }));
540
541 for node in program {
542 match &node.node {
543 harn_parser::Node::FnDecl {
544 name,
545 params,
546 body,
547 is_pub,
548 ..
549 } => {
550 if selected_names.is_none() && has_pub && !is_pub {
554 continue;
555 }
556 if let Some(names) = selected_names {
557 if !names.contains(name) {
558 continue;
559 }
560 }
561 let mut compiler = crate::Compiler::new();
563 let func_chunk = compiler
564 .compile_fn_body(params, body)
565 .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
566 let closure = VmClosure {
567 func: func_chunk,
568 env: self.env.clone(),
569 };
570 self.env
571 .define(name, VmValue::Closure(Rc::new(closure)), false);
572 }
573 harn_parser::Node::ImportDecl { path: sub_path } => {
574 let old_dir = self.source_dir.clone();
575 if let Some(fp) = file_path {
576 if let Some(parent) = fp.parent() {
577 self.source_dir = Some(parent.to_path_buf());
578 }
579 }
580 self.execute_import(sub_path, None).await?;
581 self.source_dir = old_dir;
582 }
583 harn_parser::Node::SelectiveImport {
584 names,
585 path: sub_path,
586 } => {
587 let old_dir = self.source_dir.clone();
588 if let Some(fp) = file_path {
589 if let Some(parent) = fp.parent() {
590 self.source_dir = Some(parent.to_path_buf());
591 }
592 }
593 self.execute_import(sub_path, Some(names)).await?;
594 self.source_dir = old_dir;
595 }
596 _ => {} }
598 }
599
600 Ok(())
601 })
602 }
603
604 pub fn output(&self) -> &str {
606 &self.output
607 }
608
609 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
611 self.run_chunk(chunk).await
612 }
613
614 fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
616 let thrown_value = match &error {
618 VmError::Thrown(v) => v.clone(),
619 other => VmValue::String(Rc::from(other.to_string())),
620 };
621
622 if let Some(handler) = self.exception_handlers.pop() {
623 if !handler.error_type.is_empty() {
625 let matches = match &thrown_value {
626 VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
627 _ => false,
628 };
629 if !matches {
630 return self.handle_error(error);
632 }
633 }
634
635 while self.frames.len() > handler.frame_depth {
637 if let Some(frame) = self.frames.pop() {
638 self.env = frame.saved_env;
639 }
640 }
641
642 while self
644 .deadlines
645 .last()
646 .is_some_and(|d| d.1 > handler.frame_depth)
647 {
648 self.deadlines.pop();
649 }
650
651 self.stack.truncate(handler.stack_depth);
653
654 self.stack.push(thrown_value);
656
657 if let Some(frame) = self.frames.last_mut() {
659 frame.ip = handler.catch_ip;
660 }
661
662 Ok(None) } else {
664 Err(error) }
666 }
667
668 async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
669 self.run_chunk_with_argc(chunk, 0).await
670 }
671
672 async fn run_chunk_with_argc(
673 &mut self,
674 chunk: &Chunk,
675 argc: usize,
676 ) -> Result<VmValue, VmError> {
677 self.frames.push(CallFrame {
678 chunk: chunk.clone(),
679 ip: 0,
680 stack_base: self.stack.len(),
681 saved_env: self.env.clone(),
682 fn_name: String::new(),
683 argc,
684 });
685
686 loop {
687 if let Some(&(deadline, _)) = self.deadlines.last() {
689 if Instant::now() > deadline {
690 self.deadlines.pop();
691 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
692 match self.handle_error(err) {
693 Ok(None) => continue,
694 Ok(Some(val)) => return Ok(val),
695 Err(e) => return Err(e),
696 }
697 }
698 }
699
700 let frame = match self.frames.last_mut() {
702 Some(f) => f,
703 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
704 };
705
706 if frame.ip >= frame.chunk.code.len() {
708 let val = self.stack.pop().unwrap_or(VmValue::Nil);
709 let popped_frame = self.frames.pop().unwrap();
710
711 if self.frames.is_empty() {
712 return Ok(val);
714 } else {
715 self.env = popped_frame.saved_env;
717 self.stack.truncate(popped_frame.stack_base);
718 self.stack.push(val);
719 continue;
720 }
721 }
722
723 let op = frame.chunk.code[frame.ip];
724 frame.ip += 1;
725
726 match self.execute_op(op).await {
727 Ok(Some(val)) => return Ok(val),
728 Ok(None) => continue,
729 Err(VmError::Return(val)) => {
730 if let Some(popped_frame) = self.frames.pop() {
732 let current_depth = self.frames.len();
734 self.exception_handlers
735 .retain(|h| h.frame_depth <= current_depth);
736
737 if self.frames.is_empty() {
738 return Ok(val);
739 }
740 self.env = popped_frame.saved_env;
741 self.stack.truncate(popped_frame.stack_base);
742 self.stack.push(val);
743 } else {
744 return Ok(val);
745 }
746 }
747 Err(e) => {
748 if self.error_stack_trace.is_empty() {
750 self.error_stack_trace = self.capture_stack_trace();
751 }
752 match self.handle_error(e) {
753 Ok(None) => {
754 self.error_stack_trace.clear();
755 continue; }
757 Ok(Some(val)) => return Ok(val),
758 Err(e) => return Err(e), }
760 }
761 }
762 }
763 }
764
765 fn capture_stack_trace(&self) -> Vec<(String, usize, usize)> {
767 self.frames
768 .iter()
769 .map(|f| {
770 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
771 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
772 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
773 (f.fn_name.clone(), line, col)
774 })
775 .collect()
776 }
777
778 const MAX_FRAMES: usize = 512;
779
780 fn merge_env_into_closure(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
782 let mut call_env = closure.env.clone();
783 for scope in &caller_env.scopes {
784 for (name, (val, mutable)) in &scope.vars {
785 if call_env.get(name).is_none() {
786 call_env.define(name, val.clone(), *mutable);
787 }
788 }
789 }
790 call_env
791 }
792
793 fn push_closure_frame(
795 &mut self,
796 closure: &VmClosure,
797 args: &[VmValue],
798 _parent_functions: &[CompiledFunction],
799 ) -> Result<(), VmError> {
800 if self.frames.len() >= Self::MAX_FRAMES {
801 return Err(VmError::StackOverflow);
802 }
803 let saved_env = self.env.clone();
804
805 let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
806 call_env.push_scope();
807
808 let default_start = closure
809 .func
810 .default_start
811 .unwrap_or(closure.func.params.len());
812 for (i, param) in closure.func.params.iter().enumerate() {
813 if i < args.len() {
814 call_env.define(param, args[i].clone(), false);
815 } else if i < default_start {
816 call_env.define(param, VmValue::Nil, false);
818 }
819 }
821
822 self.env = call_env;
823
824 self.frames.push(CallFrame {
825 chunk: closure.func.chunk.clone(),
826 ip: 0,
827 stack_base: self.stack.len(),
828 saved_env,
829 fn_name: closure.func.name.clone(),
830 argc: args.len(),
831 });
832
833 Ok(())
834 }
835
836 fn pop(&mut self) -> Result<VmValue, VmError> {
837 self.stack.pop().ok_or(VmError::StackUnderflow)
838 }
839
840 fn peek(&self) -> Result<&VmValue, VmError> {
841 self.stack.last().ok_or(VmError::StackUnderflow)
842 }
843
844 fn const_string(c: &Constant) -> Result<String, VmError> {
845 match c {
846 Constant::String(s) => Ok(s.clone()),
847 _ => Err(VmError::TypeError("expected string constant".into())),
848 }
849 }
850
851 fn call_closure<'a>(
854 &'a mut self,
855 closure: &'a VmClosure,
856 args: &'a [VmValue],
857 _parent_functions: &'a [CompiledFunction],
858 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
859 Box::pin(async move {
860 let saved_env = self.env.clone();
861 let saved_frames = std::mem::take(&mut self.frames);
862 let saved_handlers = std::mem::take(&mut self.exception_handlers);
863 let saved_iterators = std::mem::take(&mut self.iterators);
864 let saved_deadlines = std::mem::take(&mut self.deadlines);
865
866 let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
867 call_env.push_scope();
868
869 let default_start = closure
870 .func
871 .default_start
872 .unwrap_or(closure.func.params.len());
873 for (i, param) in closure.func.params.iter().enumerate() {
874 if i < args.len() {
875 call_env.define(param, args[i].clone(), false);
876 } else if i < default_start {
877 call_env.define(param, VmValue::Nil, false);
878 }
879 }
881
882 self.env = call_env;
883 let argc = args.len();
884 let result = self.run_chunk_with_argc(&closure.func.chunk, argc).await;
885
886 self.env = saved_env;
887 self.frames = saved_frames;
888 self.exception_handlers = saved_handlers;
889 self.iterators = saved_iterators;
890 self.deadlines = saved_deadlines;
891
892 result
893 })
894 }
895
896 async fn call_named_builtin(
899 &mut self,
900 name: &str,
901 args: Vec<VmValue>,
902 ) -> Result<VmValue, VmError> {
903 if self.denied_builtins.contains(name) {
905 return Err(VmError::Runtime(format!(
906 "Permission denied: builtin '{}' is not allowed in sandbox mode (use --allow {} to permit)",
907 name, name
908 )));
909 }
910 if let Some(builtin) = self.builtins.get(name).cloned() {
911 builtin(&args, &mut self.output)
912 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
913 async_builtin(args).await
914 } else if let Some(bridge) = &self.bridge {
915 let args_json: Vec<serde_json::Value> =
916 args.iter().map(crate::llm::vm_value_to_json).collect();
917 let result = bridge
918 .call(
919 "builtin_call",
920 serde_json::json!({"name": name, "args": args_json}),
921 )
922 .await?;
923 Ok(crate::bridge::json_result_to_vm_value(&result))
924 } else {
925 let all_builtins = self
926 .builtins
927 .keys()
928 .chain(self.async_builtins.keys())
929 .map(|s| s.as_str());
930 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
931 return Err(VmError::Runtime(format!(
932 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
933 )));
934 }
935 Err(VmError::UndefinedBuiltin(name.to_string()))
936 }
937 }
938}
939
940impl Default for Vm {
941 fn default() -> Self {
942 Self::new()
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use crate::compiler::Compiler;
950 use crate::stdlib::register_vm_stdlib;
951 use harn_lexer::Lexer;
952 use harn_parser::Parser;
953
954 fn run_harn(source: &str) -> (String, VmValue) {
955 let rt = tokio::runtime::Builder::new_current_thread()
956 .enable_all()
957 .build()
958 .unwrap();
959 rt.block_on(async {
960 let local = tokio::task::LocalSet::new();
961 local
962 .run_until(async {
963 let mut lexer = Lexer::new(source);
964 let tokens = lexer.tokenize().unwrap();
965 let mut parser = Parser::new(tokens);
966 let program = parser.parse().unwrap();
967 let chunk = Compiler::new().compile(&program).unwrap();
968
969 let mut vm = Vm::new();
970 register_vm_stdlib(&mut vm);
971 let result = vm.execute(&chunk).await.unwrap();
972 (vm.output().to_string(), result)
973 })
974 .await
975 })
976 }
977
978 fn run_output(source: &str) -> String {
979 run_harn(source).0.trim_end().to_string()
980 }
981
982 fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
983 let rt = tokio::runtime::Builder::new_current_thread()
984 .enable_all()
985 .build()
986 .unwrap();
987 rt.block_on(async {
988 let local = tokio::task::LocalSet::new();
989 local
990 .run_until(async {
991 let mut lexer = Lexer::new(source);
992 let tokens = lexer.tokenize().unwrap();
993 let mut parser = Parser::new(tokens);
994 let program = parser.parse().unwrap();
995 let chunk = Compiler::new().compile(&program).unwrap();
996
997 let mut vm = Vm::new();
998 register_vm_stdlib(&mut vm);
999 let result = vm.execute(&chunk).await?;
1000 Ok((vm.output().to_string(), result))
1001 })
1002 .await
1003 })
1004 }
1005
1006 #[test]
1007 fn test_arithmetic() {
1008 let out =
1009 run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
1010 assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
1011 }
1012
1013 #[test]
1014 fn test_mixed_arithmetic() {
1015 let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
1016 assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
1017 }
1018
1019 #[test]
1020 fn test_comparisons() {
1021 let out =
1022 run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
1023 assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
1024 }
1025
1026 #[test]
1027 fn test_let_var() {
1028 let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
1029 assert_eq!(out, "[harn] 42\n[harn] 2");
1030 }
1031
1032 #[test]
1033 fn test_if_else() {
1034 let out = run_output(
1035 r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
1036 );
1037 assert_eq!(out, "[harn] yes\n[harn] no");
1038 }
1039
1040 #[test]
1041 fn test_while_loop() {
1042 let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
1043 assert_eq!(out, "[harn] 5");
1044 }
1045
1046 #[test]
1047 fn test_for_in() {
1048 let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
1049 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
1050 }
1051
1052 #[test]
1053 fn test_fn_decl_and_call() {
1054 let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
1055 assert_eq!(out, "[harn] 7");
1056 }
1057
1058 #[test]
1059 fn test_closure() {
1060 let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
1061 assert_eq!(out, "[harn] 10");
1062 }
1063
1064 #[test]
1065 fn test_closure_capture() {
1066 let out = run_output(
1067 "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
1068 );
1069 assert_eq!(out, "[harn] 15");
1070 }
1071
1072 #[test]
1073 fn test_string_concat() {
1074 let out = run_output(
1075 r#"pipeline t(task) { let a = "hello" + " " + "world"
1076log(a) }"#,
1077 );
1078 assert_eq!(out, "[harn] hello world");
1079 }
1080
1081 #[test]
1082 fn test_list_map() {
1083 let out = run_output(
1084 "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1085 );
1086 assert_eq!(out, "[harn] [2, 4, 6]");
1087 }
1088
1089 #[test]
1090 fn test_list_filter() {
1091 let out = run_output(
1092 "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1093 );
1094 assert_eq!(out, "[harn] [4, 5]");
1095 }
1096
1097 #[test]
1098 fn test_list_reduce() {
1099 let out = run_output(
1100 "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1101 );
1102 assert_eq!(out, "[harn] 10");
1103 }
1104
1105 #[test]
1106 fn test_dict_access() {
1107 let out = run_output(
1108 r#"pipeline t(task) { let d = {name: "test", value: 42}
1109log(d.name)
1110log(d.value) }"#,
1111 );
1112 assert_eq!(out, "[harn] test\n[harn] 42");
1113 }
1114
1115 #[test]
1116 fn test_dict_methods() {
1117 let out = run_output(
1118 r#"pipeline t(task) { let d = {a: 1, b: 2}
1119log(d.keys())
1120log(d.values())
1121log(d.has("a"))
1122log(d.has("z")) }"#,
1123 );
1124 assert_eq!(
1125 out,
1126 "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1127 );
1128 }
1129
1130 #[test]
1131 fn test_pipe_operator() {
1132 let out = run_output(
1133 "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1134 );
1135 assert_eq!(out, "[harn] 10");
1136 }
1137
1138 #[test]
1139 fn test_pipe_with_closure() {
1140 let out = run_output(
1141 r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1142log(r) }"#,
1143 );
1144 assert_eq!(out, "[harn] [hello, world]");
1145 }
1146
1147 #[test]
1148 fn test_nil_coalescing() {
1149 let out = run_output(
1150 r#"pipeline t(task) { let a = nil ?? "fallback"
1151log(a)
1152let b = "present" ?? "fallback"
1153log(b) }"#,
1154 );
1155 assert_eq!(out, "[harn] fallback\n[harn] present");
1156 }
1157
1158 #[test]
1159 fn test_logical_operators() {
1160 let out =
1161 run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1162 assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1163 }
1164
1165 #[test]
1166 fn test_match() {
1167 let out = run_output(
1168 r#"pipeline t(task) { let x = "b"
1169match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1170 );
1171 assert_eq!(out, "[harn] second");
1172 }
1173
1174 #[test]
1175 fn test_subscript() {
1176 let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1177 assert_eq!(out, "[harn] 20");
1178 }
1179
1180 #[test]
1181 fn test_string_methods() {
1182 let out = run_output(
1183 r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1184log("a,b,c".split(","))
1185log(" hello ".trim())
1186log("hello".starts_with("hel"))
1187log("hello".ends_with("lo"))
1188log("hello".substring(1, 3)) }"#,
1189 );
1190 assert_eq!(
1191 out,
1192 "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_list_properties() {
1198 let out = run_output(
1199 "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1200 );
1201 assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1202 }
1203
1204 #[test]
1205 fn test_recursive_function() {
1206 let out = run_output(
1207 "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1208 );
1209 assert_eq!(out, "[harn] 55");
1210 }
1211
1212 #[test]
1213 fn test_ternary() {
1214 let out = run_output(
1215 r#"pipeline t(task) { let x = 5
1216let r = x > 0 ? "positive" : "non-positive"
1217log(r) }"#,
1218 );
1219 assert_eq!(out, "[harn] positive");
1220 }
1221
1222 #[test]
1223 fn test_for_in_dict() {
1224 let out = run_output(
1225 "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1226 );
1227 assert_eq!(out, "[harn] a\n[harn] b");
1228 }
1229
1230 #[test]
1231 fn test_list_any_all() {
1232 let out = run_output(
1233 "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 })) }",
1234 );
1235 assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1236 }
1237
1238 #[test]
1239 fn test_disassembly() {
1240 let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1241 let tokens = lexer.tokenize().unwrap();
1242 let mut parser = Parser::new(tokens);
1243 let program = parser.parse().unwrap();
1244 let chunk = Compiler::new().compile(&program).unwrap();
1245 let disasm = chunk.disassemble("test");
1246 assert!(disasm.contains("CONSTANT"));
1247 assert!(disasm.contains("ADD"));
1248 assert!(disasm.contains("CALL"));
1249 }
1250
1251 #[test]
1254 fn test_try_catch_basic() {
1255 let out = run_output(
1256 r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1257 );
1258 assert_eq!(out, "[harn] caught: oops");
1259 }
1260
1261 #[test]
1262 fn test_try_no_error() {
1263 let out = run_output(
1264 r#"pipeline t(task) {
1265var result = 0
1266try { result = 42 } catch(e) { result = 0 }
1267log(result)
1268}"#,
1269 );
1270 assert_eq!(out, "[harn] 42");
1271 }
1272
1273 #[test]
1274 fn test_throw_uncaught() {
1275 let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1276 assert!(result.is_err());
1277 }
1278
1279 fn run_vm(source: &str) -> String {
1282 let rt = tokio::runtime::Builder::new_current_thread()
1283 .enable_all()
1284 .build()
1285 .unwrap();
1286 rt.block_on(async {
1287 let local = tokio::task::LocalSet::new();
1288 local
1289 .run_until(async {
1290 let mut lexer = Lexer::new(source);
1291 let tokens = lexer.tokenize().unwrap();
1292 let mut parser = Parser::new(tokens);
1293 let program = parser.parse().unwrap();
1294 let chunk = Compiler::new().compile(&program).unwrap();
1295 let mut vm = Vm::new();
1296 register_vm_stdlib(&mut vm);
1297 vm.execute(&chunk).await.unwrap();
1298 vm.output().to_string()
1299 })
1300 .await
1301 })
1302 }
1303
1304 fn run_vm_err(source: &str) -> String {
1305 let rt = tokio::runtime::Builder::new_current_thread()
1306 .enable_all()
1307 .build()
1308 .unwrap();
1309 rt.block_on(async {
1310 let local = tokio::task::LocalSet::new();
1311 local
1312 .run_until(async {
1313 let mut lexer = Lexer::new(source);
1314 let tokens = lexer.tokenize().unwrap();
1315 let mut parser = Parser::new(tokens);
1316 let program = parser.parse().unwrap();
1317 let chunk = Compiler::new().compile(&program).unwrap();
1318 let mut vm = Vm::new();
1319 register_vm_stdlib(&mut vm);
1320 match vm.execute(&chunk).await {
1321 Err(e) => format!("{}", e),
1322 Ok(_) => panic!("Expected error"),
1323 }
1324 })
1325 .await
1326 })
1327 }
1328
1329 #[test]
1330 fn test_hello_world() {
1331 let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1332 assert_eq!(out, "[harn] hello\n");
1333 }
1334
1335 #[test]
1336 fn test_arithmetic_new() {
1337 let out = run_vm("pipeline default(task) { log(2 + 3) }");
1338 assert_eq!(out, "[harn] 5\n");
1339 }
1340
1341 #[test]
1342 fn test_string_concat_new() {
1343 let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1344 assert_eq!(out, "[harn] ab\n");
1345 }
1346
1347 #[test]
1348 fn test_if_else_new() {
1349 let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1350 assert_eq!(out, "[harn] 1\n");
1351 }
1352
1353 #[test]
1354 fn test_for_loop_new() {
1355 let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1356 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1357 }
1358
1359 #[test]
1360 fn test_while_loop_new() {
1361 let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1362 assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1363 }
1364
1365 #[test]
1366 fn test_function_call_new() {
1367 let out =
1368 run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1369 assert_eq!(out, "[harn] 5\n");
1370 }
1371
1372 #[test]
1373 fn test_closure_new() {
1374 let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1375 assert_eq!(out, "[harn] 10\n");
1376 }
1377
1378 #[test]
1379 fn test_recursion() {
1380 let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1381 assert_eq!(out, "[harn] 120\n");
1382 }
1383
1384 #[test]
1385 fn test_try_catch_new() {
1386 let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1387 assert_eq!(out, "[harn] err\n");
1388 }
1389
1390 #[test]
1391 fn test_try_no_error_new() {
1392 let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1393 assert_eq!(out, "[harn] 1\n");
1394 }
1395
1396 #[test]
1397 fn test_list_map_new() {
1398 let out =
1399 run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1400 assert_eq!(out, "[harn] [2, 4, 6]\n");
1401 }
1402
1403 #[test]
1404 fn test_list_filter_new() {
1405 let out = run_vm(
1406 "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1407 );
1408 assert_eq!(out, "[harn] [3, 4]\n");
1409 }
1410
1411 #[test]
1412 fn test_dict_access_new() {
1413 let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1414 assert_eq!(out, "[harn] Alice\n");
1415 }
1416
1417 #[test]
1418 fn test_string_interpolation() {
1419 let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1420 assert_eq!(out, "[harn] val=42\n");
1421 }
1422
1423 #[test]
1424 fn test_match_new() {
1425 let out = run_vm(
1426 "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1427 );
1428 assert_eq!(out, "[harn] 2\n");
1429 }
1430
1431 #[test]
1432 fn test_json_roundtrip() {
1433 let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1434 assert!(out.contains("\"a\""));
1435 assert!(out.contains("1"));
1436 }
1437
1438 #[test]
1439 fn test_type_of() {
1440 let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1441 assert_eq!(out, "[harn] int\n[harn] string\n");
1442 }
1443
1444 #[test]
1445 fn test_stack_overflow() {
1446 let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1447 assert!(
1448 err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1449 "Expected stack overflow error, got: {}",
1450 err
1451 );
1452 }
1453
1454 #[test]
1455 fn test_division_by_zero() {
1456 let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1457 assert!(
1458 err.contains("Division by zero") || err.contains("division"),
1459 "Expected division by zero error, got: {}",
1460 err
1461 );
1462 }
1463
1464 #[test]
1465 fn test_try_catch_nested() {
1466 let out = run_output(
1467 r#"pipeline t(task) {
1468try {
1469 try {
1470 throw "inner"
1471 } catch(e) {
1472 log("inner caught: " + e)
1473 throw "outer"
1474 }
1475} catch(e) {
1476 log("outer caught: " + e)
1477}
1478}"#,
1479 );
1480 assert_eq!(
1481 out,
1482 "[harn] inner caught: inner\n[harn] outer caught: outer"
1483 );
1484 }
1485
1486 #[test]
1489 fn test_parallel_basic() {
1490 let out = run_output(
1491 "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1492 );
1493 assert_eq!(out, "[harn] [0, 10, 20]");
1494 }
1495
1496 #[test]
1497 fn test_parallel_no_variable() {
1498 let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1499 assert_eq!(out, "[harn] [42, 42, 42]");
1500 }
1501
1502 #[test]
1503 fn test_parallel_map_basic() {
1504 let out = run_output(
1505 "pipeline t(task) { let results = parallel_map([1, 2, 3]) { x -> x * x }\nlog(results) }",
1506 );
1507 assert_eq!(out, "[harn] [1, 4, 9]");
1508 }
1509
1510 #[test]
1511 fn test_spawn_await() {
1512 let out = run_output(
1513 r#"pipeline t(task) {
1514let handle = spawn { log("spawned") }
1515let result = await(handle)
1516log("done")
1517}"#,
1518 );
1519 assert_eq!(out, "[harn] spawned\n[harn] done");
1520 }
1521
1522 #[test]
1523 fn test_spawn_cancel() {
1524 let out = run_output(
1525 r#"pipeline t(task) {
1526let handle = spawn { log("should be cancelled") }
1527cancel(handle)
1528log("cancelled")
1529}"#,
1530 );
1531 assert_eq!(out, "[harn] cancelled");
1532 }
1533
1534 #[test]
1535 fn test_spawn_returns_value() {
1536 let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1537 assert_eq!(out, "[harn] 42");
1538 }
1539
1540 #[test]
1543 fn test_deadline_success() {
1544 let out = run_output(
1545 r#"pipeline t(task) {
1546let result = deadline 5s { log("within deadline")
154742 }
1548log(result)
1549}"#,
1550 );
1551 assert_eq!(out, "[harn] within deadline\n[harn] 42");
1552 }
1553
1554 #[test]
1555 fn test_deadline_exceeded() {
1556 let result = run_harn_result(
1557 r#"pipeline t(task) {
1558deadline 1ms {
1559 var i = 0
1560 while i < 1000000 { i = i + 1 }
1561}
1562}"#,
1563 );
1564 assert!(result.is_err());
1565 }
1566
1567 #[test]
1568 fn test_deadline_caught_by_try() {
1569 let out = run_output(
1570 r#"pipeline t(task) {
1571try {
1572 deadline 1ms {
1573 var i = 0
1574 while i < 1000000 { i = i + 1 }
1575 }
1576} catch(e) {
1577 log("caught")
1578}
1579}"#,
1580 );
1581 assert_eq!(out, "[harn] caught");
1582 }
1583
1584 fn run_harn_with_denied(
1586 source: &str,
1587 denied: HashSet<String>,
1588 ) -> Result<(String, VmValue), VmError> {
1589 let rt = tokio::runtime::Builder::new_current_thread()
1590 .enable_all()
1591 .build()
1592 .unwrap();
1593 rt.block_on(async {
1594 let local = tokio::task::LocalSet::new();
1595 local
1596 .run_until(async {
1597 let mut lexer = Lexer::new(source);
1598 let tokens = lexer.tokenize().unwrap();
1599 let mut parser = Parser::new(tokens);
1600 let program = parser.parse().unwrap();
1601 let chunk = Compiler::new().compile(&program).unwrap();
1602
1603 let mut vm = Vm::new();
1604 register_vm_stdlib(&mut vm);
1605 vm.set_denied_builtins(denied);
1606 let result = vm.execute(&chunk).await?;
1607 Ok((vm.output().to_string(), result))
1608 })
1609 .await
1610 })
1611 }
1612
1613 #[test]
1614 fn test_sandbox_deny_builtin() {
1615 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1616 let result = run_harn_with_denied(
1617 r#"pipeline t(task) {
1618let xs = [1, 2]
1619push(xs, 3)
1620}"#,
1621 denied,
1622 );
1623 let err = result.unwrap_err();
1624 let msg = format!("{err}");
1625 assert!(
1626 msg.contains("Permission denied"),
1627 "expected permission denied, got: {msg}"
1628 );
1629 assert!(
1630 msg.contains("push"),
1631 "expected builtin name in error, got: {msg}"
1632 );
1633 }
1634
1635 #[test]
1636 fn test_sandbox_allowed_builtin_works() {
1637 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1639 let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
1640 let (output, _) = result.unwrap();
1641 assert_eq!(output.trim(), "[harn] hello");
1642 }
1643
1644 #[test]
1645 fn test_sandbox_empty_denied_set() {
1646 let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
1648 let (output, _) = result.unwrap();
1649 assert_eq!(output.trim(), "[harn] ok");
1650 }
1651
1652 #[test]
1653 fn test_sandbox_propagates_to_spawn() {
1654 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1656 let result = run_harn_with_denied(
1657 r#"pipeline t(task) {
1658let handle = spawn {
1659 let xs = [1, 2]
1660 push(xs, 3)
1661}
1662await(handle)
1663}"#,
1664 denied,
1665 );
1666 let err = result.unwrap_err();
1667 let msg = format!("{err}");
1668 assert!(
1669 msg.contains("Permission denied"),
1670 "expected permission denied in spawned VM, got: {msg}"
1671 );
1672 }
1673
1674 #[test]
1675 fn test_sandbox_propagates_to_parallel() {
1676 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1678 let result = run_harn_with_denied(
1679 r#"pipeline t(task) {
1680let results = parallel(2) { i ->
1681 let xs = [1, 2]
1682 push(xs, 3)
1683}
1684}"#,
1685 denied,
1686 );
1687 let err = result.unwrap_err();
1688 let msg = format!("{err}");
1689 assert!(
1690 msg.contains("Permission denied"),
1691 "expected permission denied in parallel VM, got: {msg}"
1692 );
1693 }
1694}