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