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