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