1mod format;
2mod imports;
3pub mod iter;
4mod methods;
5mod ops;
6
7use std::cell::RefCell;
8use std::collections::{BTreeMap, HashSet};
9use std::future::Future;
10use std::pin::Pin;
11use std::rc::Rc;
12use std::time::Instant;
13
14use crate::chunk::{Chunk, CompiledFunction, Constant};
15use crate::value::{
16 ErrorCategory, ModuleFunctionRegistry, VmAsyncBuiltinFn, VmBuiltinFn, VmClosure, VmEnv,
17 VmError, VmTaskHandle, VmValue,
18};
19
20thread_local! {
21 static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> = const { RefCell::new(Vec::new()) };
22}
23
24struct ScopeSpan(u64);
26
27impl ScopeSpan {
28 fn new(kind: crate::tracing::SpanKind, name: String) -> Self {
29 Self(crate::tracing::span_start(kind, name))
30 }
31}
32
33impl Drop for ScopeSpan {
34 fn drop(&mut self) {
35 crate::tracing::span_end(self.0);
36 }
37}
38
39pub(crate) struct CallFrame {
41 pub(crate) chunk: Chunk,
42 pub(crate) ip: usize,
43 pub(crate) stack_base: usize,
44 pub(crate) saved_env: VmEnv,
45 pub(crate) saved_iterator_depth: usize,
47 pub(crate) fn_name: String,
49 pub(crate) argc: usize,
51 pub(crate) saved_source_dir: Option<std::path::PathBuf>,
54 pub(crate) module_functions: Option<ModuleFunctionRegistry>,
56 pub(crate) module_state: Option<crate::value::ModuleState>,
62}
63
64pub(crate) struct ExceptionHandler {
66 pub(crate) catch_ip: usize,
67 pub(crate) stack_depth: usize,
68 pub(crate) frame_depth: usize,
69 pub(crate) env_scope_depth: usize,
70 pub(crate) error_type: String,
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub enum DebugAction {
77 Continue,
79 Stop,
81}
82
83#[derive(Debug, Clone)]
85pub struct DebugState {
86 pub line: usize,
87 pub variables: BTreeMap<String, VmValue>,
88 pub frame_name: String,
89 pub frame_depth: usize,
90}
91
92type DebugHook = dyn FnMut(&DebugState) -> DebugAction;
93
94pub(crate) enum IterState {
96 Vec {
97 items: Vec<VmValue>,
98 idx: usize,
99 },
100 Channel {
101 receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
102 closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
103 },
104 Generator {
105 gen: crate::value::VmGenerator,
106 },
107 Range {
111 next: i64,
112 stop: i64,
113 },
114 VmIter {
115 handle: std::rc::Rc<std::cell::RefCell<crate::vm::iter::VmIter>>,
116 },
117}
118
119#[derive(Clone)]
120pub(crate) struct LoadedModule {
121 pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
122 pub(crate) public_names: HashSet<String>,
123}
124
125pub struct Vm {
127 pub(crate) stack: Vec<VmValue>,
128 pub(crate) env: VmEnv,
129 pub(crate) output: String,
130 pub(crate) builtins: BTreeMap<String, VmBuiltinFn>,
131 pub(crate) async_builtins: BTreeMap<String, VmAsyncBuiltinFn>,
132 pub(crate) iterators: Vec<IterState>,
134 pub(crate) frames: Vec<CallFrame>,
136 pub(crate) exception_handlers: Vec<ExceptionHandler>,
138 pub(crate) spawned_tasks: BTreeMap<String, VmTaskHandle>,
140 pub(crate) task_counter: u64,
142 pub(crate) deadlines: Vec<(Instant, usize)>,
144 pub(crate) breakpoints: BTreeMap<String, std::collections::BTreeSet<usize>>,
149 pub(crate) step_mode: bool,
151 pub(crate) step_frame_depth: usize,
153 pub(crate) stopped: bool,
155 pub(crate) last_line: usize,
157 pub(crate) source_dir: Option<std::path::PathBuf>,
159 pub(crate) imported_paths: Vec<std::path::PathBuf>,
161 pub(crate) module_cache: BTreeMap<std::path::PathBuf, LoadedModule>,
163 pub(crate) source_file: Option<String>,
165 pub(crate) source_text: Option<String>,
167 pub(crate) bridge: Option<Rc<crate::bridge::HostBridge>>,
169 pub(crate) denied_builtins: HashSet<String>,
171 pub(crate) cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
173 pub(crate) error_stack_trace: Vec<(String, usize, usize, Option<String>)>,
175 pub(crate) yield_sender: Option<tokio::sync::mpsc::Sender<VmValue>>,
178 pub(crate) project_root: Option<std::path::PathBuf>,
181 pub(crate) globals: BTreeMap<String, VmValue>,
184 pub(crate) debug_hook: Option<Box<DebugHook>>,
186}
187
188impl Vm {
189 pub fn new() -> Self {
190 Self {
191 stack: Vec::with_capacity(256),
192 env: VmEnv::new(),
193 output: String::new(),
194 builtins: BTreeMap::new(),
195 async_builtins: BTreeMap::new(),
196 iterators: Vec::new(),
197 frames: Vec::new(),
198 exception_handlers: Vec::new(),
199 spawned_tasks: BTreeMap::new(),
200 task_counter: 0,
201 deadlines: Vec::new(),
202 breakpoints: BTreeMap::new(),
203 step_mode: false,
204 step_frame_depth: 0,
205 stopped: false,
206 last_line: 0,
207 source_dir: None,
208 imported_paths: Vec::new(),
209 module_cache: BTreeMap::new(),
210 source_file: None,
211 source_text: None,
212 bridge: None,
213 denied_builtins: HashSet::new(),
214 cancel_token: None,
215 error_stack_trace: Vec::new(),
216 yield_sender: None,
217 project_root: None,
218 globals: BTreeMap::new(),
219 debug_hook: None,
220 }
221 }
222
223 pub fn set_bridge(&mut self, bridge: Rc<crate::bridge::HostBridge>) {
225 self.bridge = Some(bridge);
226 }
227
228 pub fn set_denied_builtins(&mut self, denied: HashSet<String>) {
231 self.denied_builtins = denied;
232 }
233
234 pub fn set_source_info(&mut self, file: &str, text: &str) {
236 self.source_file = Some(file.to_string());
237 self.source_text = Some(text.to_string());
238 }
239
240 pub fn set_breakpoints_for_file(&mut self, file: &str, lines: Vec<usize>) {
245 if lines.is_empty() {
246 self.breakpoints.remove(file);
247 return;
248 }
249 self.breakpoints
250 .insert(file.to_string(), lines.into_iter().collect());
251 }
252
253 pub fn set_breakpoints(&mut self, lines: Vec<usize>) {
257 self.set_breakpoints_for_file("", lines);
258 }
259
260 pub(crate) fn current_source_file(&self) -> Option<&str> {
262 self.frames
263 .last()
264 .and_then(|f| f.chunk.source_file.as_deref())
265 }
266
267 pub(crate) fn breakpoint_matches(&self, line: usize) -> bool {
270 if let Some(wild) = self.breakpoints.get("") {
271 if wild.contains(&line) {
272 return true;
273 }
274 }
275 if let Some(file) = self.current_source_file() {
276 if let Some(set) = self.breakpoints.get(file) {
277 if set.contains(&line) {
278 return true;
279 }
280 }
281 for (key, set) in &self.breakpoints {
285 if key.is_empty() {
286 continue;
287 }
288 if (file.ends_with(key.as_str()) || key.ends_with(file)) && set.contains(&line) {
289 return true;
290 }
291 }
292 }
293 false
294 }
295
296 pub fn set_step_mode(&mut self, step: bool) {
298 self.step_mode = step;
299 self.step_frame_depth = self.frames.len();
300 }
301
302 pub fn set_step_over(&mut self) {
304 self.step_mode = true;
305 self.step_frame_depth = self.frames.len();
306 }
307
308 pub fn set_debug_hook<F>(&mut self, hook: F)
310 where
311 F: FnMut(&DebugState) -> DebugAction + 'static,
312 {
313 self.debug_hook = Some(Box::new(hook));
314 }
315
316 pub fn clear_debug_hook(&mut self) {
318 self.debug_hook = None;
319 }
320
321 pub fn set_step_out(&mut self) {
323 self.step_mode = true;
324 self.step_frame_depth = self.frames.len().saturating_sub(1);
325 }
326
327 pub fn is_stopped(&self) -> bool {
329 self.stopped
330 }
331
332 pub fn debug_state(&self) -> DebugState {
334 let line = self.current_line();
335 let variables = self.env.all_variables();
336 let frame_name = if self.frames.len() > 1 {
337 format!("frame_{}", self.frames.len() - 1)
338 } else {
339 "pipeline".to_string()
340 };
341 DebugState {
342 line,
343 variables,
344 frame_name,
345 frame_depth: self.frames.len(),
346 }
347 }
348
349 pub fn debug_stack_frames(&self) -> Vec<(String, usize)> {
351 let mut frames = Vec::new();
352 for (i, frame) in self.frames.iter().enumerate() {
353 let line = if frame.ip > 0 && frame.ip - 1 < frame.chunk.lines.len() {
354 frame.chunk.lines[frame.ip - 1] as usize
355 } else {
356 0
357 };
358 let name = if frame.fn_name.is_empty() {
359 if i == 0 {
360 "pipeline".to_string()
361 } else {
362 format!("fn_{}", i)
363 }
364 } else {
365 frame.fn_name.clone()
366 };
367 frames.push((name, line));
368 }
369 frames
370 }
371
372 fn current_line(&self) -> usize {
374 if let Some(frame) = self.frames.last() {
375 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
376 if ip < frame.chunk.lines.len() {
377 return frame.chunk.lines[ip] as usize;
378 }
379 }
380 0
381 }
382
383 pub async fn step_execute(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
386 let current_line = self.current_line();
387 let line_changed = current_line != self.last_line && current_line > 0;
388
389 if line_changed {
390 self.last_line = current_line;
391
392 let state = self.debug_state();
393 if let Some(hook) = self.debug_hook.as_mut() {
394 if matches!(hook(&state), DebugAction::Stop) {
395 self.stopped = true;
396 return Ok(Some((VmValue::Nil, true)));
397 }
398 }
399
400 if self.breakpoint_matches(current_line) {
401 self.stopped = true;
402 return Ok(Some((VmValue::Nil, true)));
403 }
404
405 if self.step_mode && self.frames.len() <= self.step_frame_depth + 1 {
406 self.step_mode = false;
407 self.stopped = true;
408 return Ok(Some((VmValue::Nil, true)));
409 }
410 }
411
412 self.stopped = false;
413 self.execute_one_cycle().await
414 }
415
416 async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
417 if let Some(&(deadline, _)) = self.deadlines.last() {
418 if Instant::now() > deadline {
419 self.deadlines.pop();
420 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
421 match self.handle_error(err) {
422 Ok(None) => return Ok(None),
423 Ok(Some(val)) => return Ok(Some((val, false))),
424 Err(e) => return Err(e),
425 }
426 }
427 }
428
429 let frame = match self.frames.last_mut() {
430 Some(f) => f,
431 None => {
432 let val = self.stack.pop().unwrap_or(VmValue::Nil);
433 return Ok(Some((val, false)));
434 }
435 };
436
437 if frame.ip >= frame.chunk.code.len() {
438 let val = self.stack.pop().unwrap_or(VmValue::Nil);
439 let popped_frame = self.frames.pop().unwrap();
440 if self.frames.is_empty() {
441 return Ok(Some((val, false)));
442 } else {
443 self.iterators.truncate(popped_frame.saved_iterator_depth);
444 self.env = popped_frame.saved_env;
445 self.stack.truncate(popped_frame.stack_base);
446 self.stack.push(val);
447 return Ok(None);
448 }
449 }
450
451 let op = frame.chunk.code[frame.ip];
452 frame.ip += 1;
453
454 match self.execute_op(op).await {
455 Ok(Some(val)) => Ok(Some((val, false))),
456 Ok(None) => Ok(None),
457 Err(VmError::Return(val)) => {
458 if let Some(popped_frame) = self.frames.pop() {
459 if let Some(ref dir) = popped_frame.saved_source_dir {
460 crate::stdlib::set_thread_source_dir(dir);
461 }
462 let current_depth = self.frames.len();
463 self.exception_handlers
464 .retain(|h| h.frame_depth <= current_depth);
465 if self.frames.is_empty() {
466 return Ok(Some((val, false)));
467 }
468 self.iterators.truncate(popped_frame.saved_iterator_depth);
469 self.env = popped_frame.saved_env;
470 self.stack.truncate(popped_frame.stack_base);
471 self.stack.push(val);
472 Ok(None)
473 } else {
474 Ok(Some((val, false)))
475 }
476 }
477 Err(e) => {
478 if self.error_stack_trace.is_empty() {
479 self.error_stack_trace = self.capture_stack_trace();
480 }
481 match self.handle_error(e) {
482 Ok(None) => {
483 self.error_stack_trace.clear();
484 Ok(None)
485 }
486 Ok(Some(val)) => Ok(Some((val, false))),
487 Err(e) => Err(self.enrich_error_with_line(e)),
488 }
489 }
490 }
491 }
492
493 pub fn start(&mut self, chunk: &Chunk) {
495 self.frames.push(CallFrame {
496 chunk: chunk.clone(),
497 ip: 0,
498 stack_base: self.stack.len(),
499 saved_env: self.env.clone(),
500 saved_iterator_depth: self.iterators.len(),
501 fn_name: String::new(),
502 argc: 0,
503 saved_source_dir: None,
504 module_functions: None,
505 module_state: None,
506 });
507 }
508
509 pub fn register_builtin<F>(&mut self, name: &str, f: F)
511 where
512 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
513 {
514 self.builtins.insert(name.to_string(), Rc::new(f));
515 }
516
517 pub fn unregister_builtin(&mut self, name: &str) {
519 self.builtins.remove(name);
520 }
521
522 pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
524 where
525 F: Fn(Vec<VmValue>) -> Fut + 'static,
526 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
527 {
528 self.async_builtins
529 .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
530 }
531
532 fn child_vm(&self) -> Vm {
535 Vm {
536 stack: Vec::with_capacity(64),
537 env: self.env.clone(),
538 output: String::new(),
539 builtins: self.builtins.clone(),
540 async_builtins: self.async_builtins.clone(),
541 iterators: Vec::new(),
542 frames: Vec::new(),
543 exception_handlers: Vec::new(),
544 spawned_tasks: BTreeMap::new(),
545 task_counter: 0,
546 deadlines: self.deadlines.clone(),
547 breakpoints: BTreeMap::new(),
548 step_mode: false,
549 step_frame_depth: 0,
550 stopped: false,
551 last_line: 0,
552 source_dir: self.source_dir.clone(),
553 imported_paths: Vec::new(),
554 module_cache: self.module_cache.clone(),
555 source_file: self.source_file.clone(),
556 source_text: self.source_text.clone(),
557 bridge: self.bridge.clone(),
558 denied_builtins: self.denied_builtins.clone(),
559 cancel_token: None,
560 error_stack_trace: Vec::new(),
561 yield_sender: None,
562 project_root: self.project_root.clone(),
563 globals: self.globals.clone(),
564 debug_hook: None,
565 }
566 }
567
568 pub fn set_source_dir(&mut self, dir: &std::path::Path) {
571 self.source_dir = Some(dir.to_path_buf());
572 crate::stdlib::set_thread_source_dir(dir);
573 if self.project_root.is_none() {
575 self.project_root = crate::stdlib::process::find_project_root(dir);
576 }
577 }
578
579 pub fn set_project_root(&mut self, root: &std::path::Path) {
582 self.project_root = Some(root.to_path_buf());
583 }
584
585 pub fn project_root(&self) -> Option<&std::path::Path> {
587 self.project_root.as_deref().or(self.source_dir.as_deref())
588 }
589
590 pub fn builtin_names(&self) -> Vec<String> {
592 let mut names: Vec<String> = self.builtins.keys().cloned().collect();
593 names.extend(self.async_builtins.keys().cloned());
594 names
595 }
596
597 pub fn set_global(&mut self, name: &str, value: VmValue) {
600 self.globals.insert(name.to_string(), value);
601 }
602
603 pub fn output(&self) -> &str {
605 &self.output
606 }
607
608 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
610 let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
611 let result = self.run_chunk(chunk).await;
612 crate::tracing::span_end(span_id);
613 result
614 }
615
616 fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
618 let thrown_value = match &error {
619 VmError::Thrown(v) => v.clone(),
620 other => VmValue::String(Rc::from(other.to_string())),
621 };
622
623 if let Some(handler) = self.exception_handlers.pop() {
624 if !handler.error_type.is_empty() {
625 let matches = match &thrown_value {
627 VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
628 _ => false,
629 };
630 if !matches {
631 return self.handle_error(error);
632 }
633 }
634
635 while self.frames.len() > handler.frame_depth {
636 if let Some(frame) = self.frames.pop() {
637 if let Some(ref dir) = frame.saved_source_dir {
638 crate::stdlib::set_thread_source_dir(dir);
639 }
640 self.iterators.truncate(frame.saved_iterator_depth);
641 self.env = frame.saved_env;
642 }
643 }
644
645 while self
647 .deadlines
648 .last()
649 .is_some_and(|d| d.1 > handler.frame_depth)
650 {
651 self.deadlines.pop();
652 }
653
654 self.env.truncate_scopes(handler.env_scope_depth);
655
656 self.stack.truncate(handler.stack_depth);
657 self.stack.push(thrown_value);
658
659 if let Some(frame) = self.frames.last_mut() {
660 frame.ip = handler.catch_ip;
661 }
662
663 Ok(None)
664 } else {
665 Err(error)
666 }
667 }
668
669 async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
670 self.run_chunk_entry(chunk, 0, None, None, None).await
671 }
672
673 async fn run_chunk_entry(
674 &mut self,
675 chunk: &Chunk,
676 argc: usize,
677 saved_source_dir: Option<std::path::PathBuf>,
678 module_functions: Option<ModuleFunctionRegistry>,
679 module_state: Option<crate::value::ModuleState>,
680 ) -> Result<VmValue, VmError> {
681 self.frames.push(CallFrame {
682 chunk: chunk.clone(),
683 ip: 0,
684 stack_base: self.stack.len(),
685 saved_env: self.env.clone(),
686 saved_iterator_depth: self.iterators.len(),
687 fn_name: String::new(),
688 argc,
689 saved_source_dir,
690 module_functions,
691 module_state,
692 });
693
694 loop {
695 if let Some(&(deadline, _)) = self.deadlines.last() {
696 if Instant::now() > deadline {
697 self.deadlines.pop();
698 let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
699 match self.handle_error(err) {
700 Ok(None) => continue,
701 Ok(Some(val)) => return Ok(val),
702 Err(e) => return Err(e),
703 }
704 }
705 }
706
707 let frame = match self.frames.last_mut() {
708 Some(f) => f,
709 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
710 };
711
712 if frame.ip >= frame.chunk.code.len() {
713 let val = self.stack.pop().unwrap_or(VmValue::Nil);
714 let popped_frame = self.frames.pop().unwrap();
715 if let Some(ref dir) = popped_frame.saved_source_dir {
716 crate::stdlib::set_thread_source_dir(dir);
717 }
718
719 if self.frames.is_empty() {
720 return Ok(val);
721 } else {
722 self.iterators.truncate(popped_frame.saved_iterator_depth);
723 self.env = popped_frame.saved_env;
724 self.stack.truncate(popped_frame.stack_base);
725 self.stack.push(val);
726 continue;
727 }
728 }
729
730 let op = frame.chunk.code[frame.ip];
731 frame.ip += 1;
732
733 match self.execute_op(op).await {
734 Ok(Some(val)) => return Ok(val),
735 Ok(None) => continue,
736 Err(VmError::Return(val)) => {
737 if let Some(popped_frame) = self.frames.pop() {
738 if let Some(ref dir) = popped_frame.saved_source_dir {
739 crate::stdlib::set_thread_source_dir(dir);
740 }
741 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.iterators.truncate(popped_frame.saved_iterator_depth);
749 self.env = popped_frame.saved_env;
750 self.stack.truncate(popped_frame.stack_base);
751 self.stack.push(val);
752 } else {
753 return Ok(val);
754 }
755 }
756 Err(e) => {
757 if self.error_stack_trace.is_empty() {
759 self.error_stack_trace = self.capture_stack_trace();
760 }
761 match self.handle_error(e) {
762 Ok(None) => {
763 self.error_stack_trace.clear();
764 continue;
765 }
766 Ok(Some(val)) => return Ok(val),
767 Err(e) => return Err(self.enrich_error_with_line(e)),
768 }
769 }
770 }
771 }
772 }
773
774 fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
776 self.frames
777 .iter()
778 .map(|f| {
779 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
780 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
781 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
782 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
783 })
784 .collect()
785 }
786
787 fn enrich_error_with_line(&self, error: VmError) -> VmError {
791 let line = self
793 .error_stack_trace
794 .last()
795 .map(|(_, l, _, _)| *l)
796 .unwrap_or_else(|| self.current_line());
797 if line == 0 {
798 return error;
799 }
800 let suffix = format!(" (line {line})");
801 match error {
802 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
803 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
804 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
805 VmError::UndefinedVariable(name) => {
806 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
807 }
808 VmError::UndefinedBuiltin(name) => {
809 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
810 }
811 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
812 "Cannot assign to immutable binding: {name}{suffix}"
813 )),
814 VmError::StackOverflow => {
815 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
816 }
817 other => other,
823 }
824 }
825
826 const MAX_FRAMES: usize = 512;
827
828 fn closure_call_env(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
856 if closure.module_state.is_some() {
857 return closure.env.clone();
858 }
859 let mut call_env = closure.env.clone();
860 for scope in &caller_env.scopes {
864 for (name, (val, mutable)) in &scope.vars {
865 if matches!(val, VmValue::Closure(_)) && call_env.get(name).is_none() {
866 let _ = call_env.define(name, val.clone(), *mutable);
867 }
868 }
869 }
870 call_env
871 }
872
873 fn resolve_named_closure(&self, name: &str) -> Option<Rc<VmClosure>> {
874 if let Some(VmValue::Closure(closure)) = self.env.get(name) {
875 return Some(closure);
876 }
877 self.frames
878 .last()
879 .and_then(|frame| frame.module_functions.as_ref())
880 .and_then(|registry| registry.borrow().get(name).cloned())
881 }
882
883 fn push_closure_frame(
885 &mut self,
886 closure: &VmClosure,
887 args: &[VmValue],
888 _parent_functions: &[CompiledFunction],
889 ) -> Result<(), VmError> {
890 if self.frames.len() >= Self::MAX_FRAMES {
891 return Err(VmError::StackOverflow);
892 }
893 let saved_env = self.env.clone();
894
895 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
899 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
900 crate::stdlib::set_thread_source_dir(dir);
901 prev
902 } else {
903 None
904 };
905
906 let mut call_env = Self::closure_call_env(&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 let param_count = closure.func.params.len();
914 for (i, param) in closure.func.params.iter().enumerate() {
915 if closure.func.has_rest_param && i == param_count - 1 {
916 let rest_args = if i < args.len() {
918 args[i..].to_vec()
919 } else {
920 Vec::new()
921 };
922 let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
923 } else if i < args.len() {
924 let _ = call_env.define(param, args[i].clone(), false);
925 } else if i < default_start {
926 let _ = call_env.define(param, VmValue::Nil, false);
927 }
928 }
929
930 self.env = call_env;
931
932 self.frames.push(CallFrame {
933 chunk: closure.func.chunk.clone(),
934 ip: 0,
935 stack_base: self.stack.len(),
936 saved_env,
937 saved_iterator_depth: self.iterators.len(),
938 fn_name: closure.func.name.clone(),
939 argc: args.len(),
940 saved_source_dir,
941 module_functions: closure.module_functions.clone(),
942 module_state: closure.module_state.clone(),
943 });
944
945 Ok(())
946 }
947
948 pub(crate) fn create_generator(&self, closure: &VmClosure, args: &[VmValue]) -> VmValue {
951 use crate::value::VmGenerator;
952
953 let (tx, rx) = tokio::sync::mpsc::channel::<VmValue>(1);
955
956 let mut child = self.child_vm();
957 child.yield_sender = Some(tx);
958
959 let parent_env = self.env.clone();
965 let mut call_env = Self::closure_call_env(&parent_env, closure);
966 call_env.push_scope();
967
968 let default_start = closure
969 .func
970 .default_start
971 .unwrap_or(closure.func.params.len());
972 let param_count = closure.func.params.len();
973 for (i, param) in closure.func.params.iter().enumerate() {
974 if closure.func.has_rest_param && i == param_count - 1 {
975 let rest_args = if i < args.len() {
976 args[i..].to_vec()
977 } else {
978 Vec::new()
979 };
980 let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
981 } else if i < args.len() {
982 let _ = call_env.define(param, args[i].clone(), false);
983 } else if i < default_start {
984 let _ = call_env.define(param, VmValue::Nil, false);
985 }
986 }
987 child.env = call_env;
988
989 let chunk = closure.func.chunk.clone();
990 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
991 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
992 crate::stdlib::set_thread_source_dir(dir);
993 prev
994 } else {
995 None
996 };
997 let module_functions = closure.module_functions.clone();
998 let module_state = closure.module_state.clone();
999 let argc = args.len();
1000 tokio::task::spawn_local(async move {
1003 let _ = child
1004 .run_chunk_entry(
1005 &chunk,
1006 argc,
1007 saved_source_dir,
1008 module_functions,
1009 module_state,
1010 )
1011 .await;
1012 });
1015
1016 VmValue::Generator(VmGenerator {
1017 done: Rc::new(std::cell::Cell::new(false)),
1018 receiver: Rc::new(tokio::sync::Mutex::new(rx)),
1019 })
1020 }
1021
1022 fn pop(&mut self) -> Result<VmValue, VmError> {
1023 self.stack.pop().ok_or(VmError::StackUnderflow)
1024 }
1025
1026 fn peek(&self) -> Result<&VmValue, VmError> {
1027 self.stack.last().ok_or(VmError::StackUnderflow)
1028 }
1029
1030 fn const_string(c: &Constant) -> Result<String, VmError> {
1031 match c {
1032 Constant::String(s) => Ok(s.clone()),
1033 _ => Err(VmError::TypeError("expected string constant".into())),
1034 }
1035 }
1036
1037 fn call_closure<'a>(
1040 &'a mut self,
1041 closure: &'a VmClosure,
1042 args: &'a [VmValue],
1043 _parent_functions: &'a [CompiledFunction],
1044 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
1045 Box::pin(async move {
1046 let saved_env = self.env.clone();
1047 let saved_frames = std::mem::take(&mut self.frames);
1048 let saved_handlers = std::mem::take(&mut self.exception_handlers);
1049 let saved_iterators = std::mem::take(&mut self.iterators);
1050 let saved_deadlines = std::mem::take(&mut self.deadlines);
1051
1052 let mut call_env = Self::closure_call_env(&saved_env, closure);
1053 call_env.push_scope();
1054
1055 let default_start = closure
1056 .func
1057 .default_start
1058 .unwrap_or(closure.func.params.len());
1059 let param_count = closure.func.params.len();
1060 for (i, param) in closure.func.params.iter().enumerate() {
1061 if closure.func.has_rest_param && i == param_count - 1 {
1062 let rest_args = if i < args.len() {
1063 args[i..].to_vec()
1064 } else {
1065 Vec::new()
1066 };
1067 let _ =
1068 call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
1069 } else if i < args.len() {
1070 let _ = call_env.define(param, args[i].clone(), false);
1071 } else if i < default_start {
1072 let _ = call_env.define(param, VmValue::Nil, false);
1073 }
1074 }
1075
1076 self.env = call_env;
1077 let argc = args.len();
1078 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
1079 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
1080 crate::stdlib::set_thread_source_dir(dir);
1081 prev
1082 } else {
1083 None
1084 };
1085 let result = self
1086 .run_chunk_entry(
1087 &closure.func.chunk,
1088 argc,
1089 saved_source_dir,
1090 closure.module_functions.clone(),
1091 closure.module_state.clone(),
1092 )
1093 .await;
1094
1095 self.env = saved_env;
1096 self.frames = saved_frames;
1097 self.exception_handlers = saved_handlers;
1098 self.iterators = saved_iterators;
1099 self.deadlines = saved_deadlines;
1100
1101 result
1102 })
1103 }
1104
1105 #[allow(clippy::manual_async_fn)]
1110 fn call_callable_value<'a>(
1111 &'a mut self,
1112 callable: &'a VmValue,
1113 args: &'a [VmValue],
1114 functions: &'a [CompiledFunction],
1115 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
1116 Box::pin(async move {
1117 match callable {
1118 VmValue::Closure(closure) => self.call_closure(closure, args, functions).await,
1119 VmValue::BuiltinRef(name) => {
1120 let name_owned = name.to_string();
1121 self.call_named_builtin(&name_owned, args.to_vec()).await
1122 }
1123 other => Err(VmError::TypeError(format!(
1124 "expected callable, got {}",
1125 other.type_name()
1126 ))),
1127 }
1128 })
1129 }
1130
1131 fn is_callable_value(v: &VmValue) -> bool {
1133 matches!(v, VmValue::Closure(_) | VmValue::BuiltinRef(_))
1134 }
1135
1136 pub async fn call_closure_pub(
1139 &mut self,
1140 closure: &VmClosure,
1141 args: &[VmValue],
1142 functions: &[CompiledFunction],
1143 ) -> Result<VmValue, VmError> {
1144 self.call_closure(closure, args, functions).await
1145 }
1146
1147 async fn call_named_builtin(
1150 &mut self,
1151 name: &str,
1152 args: Vec<VmValue>,
1153 ) -> Result<VmValue, VmError> {
1154 let span_kind = match name {
1156 "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
1157 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
1158 _ => None,
1159 };
1160 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
1161
1162 if self.denied_builtins.contains(name) {
1164 return Err(VmError::CategorizedError {
1165 message: format!("Tool '{}' is not permitted.", name),
1166 category: ErrorCategory::ToolRejected,
1167 });
1168 }
1169 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
1170 if let Some(builtin) = self.builtins.get(name).cloned() {
1171 builtin(&args, &mut self.output)
1172 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
1173 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1174 slot.borrow_mut().push(self.child_vm());
1175 });
1176 let result = async_builtin(args).await;
1177 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1178 slot.borrow_mut().pop();
1179 });
1180 result
1181 } else if let Some(bridge) = &self.bridge {
1182 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
1183 let args_json: Vec<serde_json::Value> =
1184 args.iter().map(crate::llm::vm_value_to_json).collect();
1185 let result = bridge
1186 .call(
1187 "builtin_call",
1188 serde_json::json!({"name": name, "args": args_json}),
1189 )
1190 .await?;
1191 Ok(crate::bridge::json_result_to_vm_value(&result))
1192 } else {
1193 let all_builtins = self
1194 .builtins
1195 .keys()
1196 .chain(self.async_builtins.keys())
1197 .map(|s| s.as_str());
1198 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
1199 return Err(VmError::Runtime(format!(
1200 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
1201 )));
1202 }
1203 Err(VmError::UndefinedBuiltin(name.to_string()))
1204 }
1205 }
1206}
1207
1208pub fn clone_async_builtin_child_vm() -> Option<Vm> {
1216 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
1217}
1218
1219#[deprecated(
1223 note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
1224)]
1225pub fn take_async_builtin_child_vm() -> Option<Vm> {
1226 clone_async_builtin_child_vm()
1227}
1228
1229#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
1231pub fn restore_async_builtin_child_vm(_vm: Vm) {
1232 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1233 let _ = slot;
1234 });
1235}
1236
1237impl Default for Vm {
1238 fn default() -> Self {
1239 Self::new()
1240 }
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245 use super::*;
1246 use crate::compiler::Compiler;
1247 use crate::stdlib::register_vm_stdlib;
1248 use harn_lexer::Lexer;
1249 use harn_parser::Parser;
1250
1251 fn run_harn(source: &str) -> (String, VmValue) {
1252 let rt = tokio::runtime::Builder::new_current_thread()
1253 .enable_all()
1254 .build()
1255 .unwrap();
1256 rt.block_on(async {
1257 let local = tokio::task::LocalSet::new();
1258 local
1259 .run_until(async {
1260 let mut lexer = Lexer::new(source);
1261 let tokens = lexer.tokenize().unwrap();
1262 let mut parser = Parser::new(tokens);
1263 let program = parser.parse().unwrap();
1264 let chunk = Compiler::new().compile(&program).unwrap();
1265
1266 let mut vm = Vm::new();
1267 register_vm_stdlib(&mut vm);
1268 let result = vm.execute(&chunk).await.unwrap();
1269 (vm.output().to_string(), result)
1270 })
1271 .await
1272 })
1273 }
1274
1275 fn run_output(source: &str) -> String {
1276 run_harn(source).0.trim_end().to_string()
1277 }
1278
1279 fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
1280 let rt = tokio::runtime::Builder::new_current_thread()
1281 .enable_all()
1282 .build()
1283 .unwrap();
1284 rt.block_on(async {
1285 let local = tokio::task::LocalSet::new();
1286 local
1287 .run_until(async {
1288 let mut lexer = Lexer::new(source);
1289 let tokens = lexer.tokenize().unwrap();
1290 let mut parser = Parser::new(tokens);
1291 let program = parser.parse().unwrap();
1292 let chunk = Compiler::new().compile(&program).unwrap();
1293
1294 let mut vm = Vm::new();
1295 register_vm_stdlib(&mut vm);
1296 let result = vm.execute(&chunk).await?;
1297 Ok((vm.output().to_string(), result))
1298 })
1299 .await
1300 })
1301 }
1302
1303 #[test]
1304 fn test_breakpoints_wildcard_matches_any_file() {
1305 let mut vm = Vm::new();
1306 vm.set_breakpoints(vec![3, 7]);
1307 assert!(vm.breakpoint_matches(3));
1308 assert!(vm.breakpoint_matches(7));
1309 assert!(!vm.breakpoint_matches(4));
1310 }
1311
1312 #[test]
1313 fn test_breakpoints_per_file_does_not_leak_to_wildcard() {
1314 let mut vm = Vm::new();
1315 vm.set_breakpoints_for_file("auto.harn", vec![10]);
1316 assert!(!vm.breakpoint_matches(10));
1319 }
1320
1321 #[test]
1322 fn test_breakpoints_per_file_clear_on_empty() {
1323 let mut vm = Vm::new();
1324 vm.set_breakpoints_for_file("a.harn", vec![1, 2]);
1325 vm.set_breakpoints_for_file("a.harn", vec![]);
1326 assert!(!vm.breakpoints.contains_key("a.harn"));
1327 }
1328
1329 #[test]
1330 fn test_arithmetic() {
1331 let out =
1332 run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
1333 assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
1334 }
1335
1336 #[test]
1337 fn test_mixed_arithmetic() {
1338 let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
1339 assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
1340 }
1341
1342 #[test]
1343 fn test_exponentiation() {
1344 let out = run_output(
1345 "pipeline t(task) { log(2 ** 8)\nlog(2 * 3 ** 2)\nlog(2 ** 3 ** 2)\nlog(2 ** -1) }",
1346 );
1347 assert_eq!(out, "[harn] 256\n[harn] 18\n[harn] 512\n[harn] 0.5");
1348 }
1349
1350 #[test]
1351 fn test_comparisons() {
1352 let out =
1353 run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
1354 assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
1355 }
1356
1357 #[test]
1358 fn test_let_var() {
1359 let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
1360 assert_eq!(out, "[harn] 42\n[harn] 2");
1361 }
1362
1363 #[test]
1364 fn test_if_else() {
1365 let out = run_output(
1366 r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
1367 );
1368 assert_eq!(out, "[harn] yes\n[harn] no");
1369 }
1370
1371 #[test]
1372 fn test_while_loop() {
1373 let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
1374 assert_eq!(out, "[harn] 5");
1375 }
1376
1377 #[test]
1378 fn test_for_in() {
1379 let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
1380 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
1381 }
1382
1383 #[test]
1384 fn test_inner_for_return_does_not_leak_iterator_into_caller() {
1385 let out = run_output(
1386 r#"pipeline t(task) {
1387 fn first_match() {
1388 for pattern in ["a", "b"] {
1389 return pattern
1390 }
1391 return ""
1392 }
1393
1394 var seen = []
1395 for path in ["outer"] {
1396 seen = seen + [path + ":" + first_match()]
1397 }
1398 log(join(seen, ","))
1399}"#,
1400 );
1401 assert_eq!(out, "[harn] outer:a");
1402 }
1403
1404 #[test]
1405 fn test_fn_decl_and_call() {
1406 let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
1407 assert_eq!(out, "[harn] 7");
1408 }
1409
1410 #[test]
1411 fn test_closure() {
1412 let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
1413 assert_eq!(out, "[harn] 10");
1414 }
1415
1416 #[test]
1417 fn test_closure_capture() {
1418 let out = run_output(
1419 "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
1420 );
1421 assert_eq!(out, "[harn] 15");
1422 }
1423
1424 #[test]
1425 fn test_string_concat() {
1426 let out = run_output(
1427 r#"pipeline t(task) { let a = "hello" + " " + "world"
1428log(a) }"#,
1429 );
1430 assert_eq!(out, "[harn] hello world");
1431 }
1432
1433 #[test]
1434 fn test_list_map() {
1435 let out = run_output(
1436 "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1437 );
1438 assert_eq!(out, "[harn] [2, 4, 6]");
1439 }
1440
1441 #[test]
1442 fn test_list_filter() {
1443 let out = run_output(
1444 "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1445 );
1446 assert_eq!(out, "[harn] [4, 5]");
1447 }
1448
1449 #[test]
1450 fn test_list_reduce() {
1451 let out = run_output(
1452 "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1453 );
1454 assert_eq!(out, "[harn] 10");
1455 }
1456
1457 #[test]
1458 fn test_dict_access() {
1459 let out = run_output(
1460 r#"pipeline t(task) { let d = {name: "test", value: 42}
1461log(d.name)
1462log(d.value) }"#,
1463 );
1464 assert_eq!(out, "[harn] test\n[harn] 42");
1465 }
1466
1467 #[test]
1468 fn test_dict_methods() {
1469 let out = run_output(
1470 r#"pipeline t(task) { let d = {a: 1, b: 2}
1471log(d.keys())
1472log(d.values())
1473log(d.has("a"))
1474log(d.has("z")) }"#,
1475 );
1476 assert_eq!(
1477 out,
1478 "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1479 );
1480 }
1481
1482 #[test]
1483 fn test_pipe_operator() {
1484 let out = run_output(
1485 "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1486 );
1487 assert_eq!(out, "[harn] 10");
1488 }
1489
1490 #[test]
1491 fn test_pipe_with_closure() {
1492 let out = run_output(
1493 r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1494log(r) }"#,
1495 );
1496 assert_eq!(out, "[harn] [hello, world]");
1497 }
1498
1499 #[test]
1500 fn test_nil_coalescing() {
1501 let out = run_output(
1502 r#"pipeline t(task) { let a = nil ?? "fallback"
1503log(a)
1504let b = "present" ?? "fallback"
1505log(b) }"#,
1506 );
1507 assert_eq!(out, "[harn] fallback\n[harn] present");
1508 }
1509
1510 #[test]
1511 fn test_logical_operators() {
1512 let out =
1513 run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1514 assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1515 }
1516
1517 #[test]
1518 fn test_match() {
1519 let out = run_output(
1520 r#"pipeline t(task) { let x = "b"
1521match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1522 );
1523 assert_eq!(out, "[harn] second");
1524 }
1525
1526 #[test]
1527 fn test_subscript() {
1528 let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1529 assert_eq!(out, "[harn] 20");
1530 }
1531
1532 #[test]
1533 fn test_string_methods() {
1534 let out = run_output(
1535 r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1536log("a,b,c".split(","))
1537log(" hello ".trim())
1538log("hello".starts_with("hel"))
1539log("hello".ends_with("lo"))
1540log("hello".substring(1, 3)) }"#,
1541 );
1542 assert_eq!(
1543 out,
1544 "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1545 );
1546 }
1547
1548 #[test]
1549 fn test_list_properties() {
1550 let out = run_output(
1551 "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1552 );
1553 assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1554 }
1555
1556 #[test]
1557 fn test_recursive_function() {
1558 let out = run_output(
1559 "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1560 );
1561 assert_eq!(out, "[harn] 55");
1562 }
1563
1564 #[test]
1565 fn test_ternary() {
1566 let out = run_output(
1567 r#"pipeline t(task) { let x = 5
1568let r = x > 0 ? "positive" : "non-positive"
1569log(r) }"#,
1570 );
1571 assert_eq!(out, "[harn] positive");
1572 }
1573
1574 #[test]
1575 fn test_for_in_dict() {
1576 let out = run_output(
1577 "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1578 );
1579 assert_eq!(out, "[harn] a\n[harn] b");
1580 }
1581
1582 #[test]
1583 fn test_list_any_all() {
1584 let out = run_output(
1585 "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 })) }",
1586 );
1587 assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1588 }
1589
1590 #[test]
1591 fn test_disassembly() {
1592 let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1593 let tokens = lexer.tokenize().unwrap();
1594 let mut parser = Parser::new(tokens);
1595 let program = parser.parse().unwrap();
1596 let chunk = Compiler::new().compile(&program).unwrap();
1597 let disasm = chunk.disassemble("test");
1598 assert!(disasm.contains("CONSTANT"));
1599 assert!(disasm.contains("ADD"));
1600 assert!(disasm.contains("CALL"));
1601 }
1602
1603 #[test]
1606 fn test_try_catch_basic() {
1607 let out = run_output(
1608 r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1609 );
1610 assert_eq!(out, "[harn] caught: oops");
1611 }
1612
1613 #[test]
1614 fn test_try_no_error() {
1615 let out = run_output(
1616 r#"pipeline t(task) {
1617var result = 0
1618try { result = 42 } catch(e) { result = 0 }
1619log(result)
1620}"#,
1621 );
1622 assert_eq!(out, "[harn] 42");
1623 }
1624
1625 #[test]
1626 fn test_throw_uncaught() {
1627 let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1628 assert!(result.is_err());
1629 }
1630
1631 fn run_vm(source: &str) -> String {
1634 let rt = tokio::runtime::Builder::new_current_thread()
1635 .enable_all()
1636 .build()
1637 .unwrap();
1638 rt.block_on(async {
1639 let local = tokio::task::LocalSet::new();
1640 local
1641 .run_until(async {
1642 let mut lexer = Lexer::new(source);
1643 let tokens = lexer.tokenize().unwrap();
1644 let mut parser = Parser::new(tokens);
1645 let program = parser.parse().unwrap();
1646 let chunk = Compiler::new().compile(&program).unwrap();
1647 let mut vm = Vm::new();
1648 register_vm_stdlib(&mut vm);
1649 vm.execute(&chunk).await.unwrap();
1650 vm.output().to_string()
1651 })
1652 .await
1653 })
1654 }
1655
1656 fn run_vm_err(source: &str) -> String {
1657 let rt = tokio::runtime::Builder::new_current_thread()
1658 .enable_all()
1659 .build()
1660 .unwrap();
1661 rt.block_on(async {
1662 let local = tokio::task::LocalSet::new();
1663 local
1664 .run_until(async {
1665 let mut lexer = Lexer::new(source);
1666 let tokens = lexer.tokenize().unwrap();
1667 let mut parser = Parser::new(tokens);
1668 let program = parser.parse().unwrap();
1669 let chunk = Compiler::new().compile(&program).unwrap();
1670 let mut vm = Vm::new();
1671 register_vm_stdlib(&mut vm);
1672 match vm.execute(&chunk).await {
1673 Err(e) => format!("{}", e),
1674 Ok(_) => panic!("Expected error"),
1675 }
1676 })
1677 .await
1678 })
1679 }
1680
1681 #[test]
1682 fn test_hello_world() {
1683 let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1684 assert_eq!(out, "[harn] hello\n");
1685 }
1686
1687 #[test]
1688 fn test_arithmetic_new() {
1689 let out = run_vm("pipeline default(task) { log(2 + 3) }");
1690 assert_eq!(out, "[harn] 5\n");
1691 }
1692
1693 #[test]
1694 fn test_string_concat_new() {
1695 let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1696 assert_eq!(out, "[harn] ab\n");
1697 }
1698
1699 #[test]
1700 fn test_if_else_new() {
1701 let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1702 assert_eq!(out, "[harn] 1\n");
1703 }
1704
1705 #[test]
1706 fn test_for_loop_new() {
1707 let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1708 assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1709 }
1710
1711 #[test]
1712 fn test_while_loop_new() {
1713 let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1714 assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1715 }
1716
1717 #[test]
1718 fn test_function_call_new() {
1719 let out =
1720 run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1721 assert_eq!(out, "[harn] 5\n");
1722 }
1723
1724 #[test]
1725 fn test_closure_new() {
1726 let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1727 assert_eq!(out, "[harn] 10\n");
1728 }
1729
1730 #[test]
1731 fn test_recursion() {
1732 let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1733 assert_eq!(out, "[harn] 120\n");
1734 }
1735
1736 #[test]
1737 fn test_try_catch_new() {
1738 let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1739 assert_eq!(out, "[harn] err\n");
1740 }
1741
1742 #[test]
1743 fn test_try_no_error_new() {
1744 let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1745 assert_eq!(out, "[harn] 1\n");
1746 }
1747
1748 #[test]
1749 fn test_list_map_new() {
1750 let out =
1751 run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1752 assert_eq!(out, "[harn] [2, 4, 6]\n");
1753 }
1754
1755 #[test]
1756 fn test_list_filter_new() {
1757 let out = run_vm(
1758 "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1759 );
1760 assert_eq!(out, "[harn] [3, 4]\n");
1761 }
1762
1763 #[test]
1764 fn test_dict_access_new() {
1765 let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1766 assert_eq!(out, "[harn] Alice\n");
1767 }
1768
1769 #[test]
1770 fn test_string_interpolation() {
1771 let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1772 assert_eq!(out, "[harn] val=42\n");
1773 }
1774
1775 #[test]
1776 fn test_match_new() {
1777 let out = run_vm(
1778 "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1779 );
1780 assert_eq!(out, "[harn] 2\n");
1781 }
1782
1783 #[test]
1784 fn test_json_roundtrip() {
1785 let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1786 assert!(out.contains("\"a\""));
1787 assert!(out.contains("1"));
1788 }
1789
1790 #[test]
1791 fn test_type_of() {
1792 let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1793 assert_eq!(out, "[harn] int\n[harn] string\n");
1794 }
1795
1796 #[test]
1797 fn test_stack_overflow() {
1798 let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1799 assert!(
1800 err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1801 "Expected stack overflow error, got: {}",
1802 err
1803 );
1804 }
1805
1806 #[test]
1807 fn test_division_by_zero() {
1808 let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1809 assert!(
1810 err.contains("Division by zero") || err.contains("division"),
1811 "Expected division by zero error, got: {}",
1812 err
1813 );
1814 }
1815
1816 #[test]
1817 fn test_float_division_by_zero_uses_ieee_values() {
1818 let out = run_vm(
1819 "pipeline default(task) { log(is_nan(0.0 / 0.0))\nlog(is_infinite(1.0 / 0.0))\nlog(is_infinite(-1.0 / 0.0)) }",
1820 );
1821 assert_eq!(out, "[harn] true\n[harn] true\n[harn] true\n");
1822 }
1823
1824 #[test]
1825 fn test_reusing_catch_binding_name_in_same_block() {
1826 let out = run_vm(
1827 r#"pipeline default(task) {
1828try {
1829 throw "a"
1830} catch e {
1831 log(e)
1832}
1833try {
1834 throw "b"
1835} catch e {
1836 log(e)
1837}
1838}"#,
1839 );
1840 assert_eq!(out, "[harn] a\n[harn] b\n");
1841 }
1842
1843 #[test]
1844 fn test_try_catch_nested() {
1845 let out = run_output(
1846 r#"pipeline t(task) {
1847try {
1848 try {
1849 throw "inner"
1850 } catch(e) {
1851 log("inner caught: " + e)
1852 throw "outer"
1853 }
1854} catch(e2) {
1855 log("outer caught: " + e2)
1856}
1857}"#,
1858 );
1859 assert_eq!(
1860 out,
1861 "[harn] inner caught: inner\n[harn] outer caught: outer"
1862 );
1863 }
1864
1865 #[test]
1868 fn test_parallel_basic() {
1869 let out = run_output(
1870 "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1871 );
1872 assert_eq!(out, "[harn] [0, 10, 20]");
1873 }
1874
1875 #[test]
1876 fn test_parallel_no_variable() {
1877 let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1878 assert_eq!(out, "[harn] [42, 42, 42]");
1879 }
1880
1881 #[test]
1882 fn test_parallel_each_basic() {
1883 let out = run_output(
1884 "pipeline t(task) { let results = parallel each [1, 2, 3] { x -> x * x }\nlog(results) }",
1885 );
1886 assert_eq!(out, "[harn] [1, 4, 9]");
1887 }
1888
1889 #[test]
1890 fn test_spawn_await() {
1891 let out = run_output(
1892 r#"pipeline t(task) {
1893let handle = spawn { log("spawned") }
1894let result = await(handle)
1895log("done")
1896}"#,
1897 );
1898 assert_eq!(out, "[harn] spawned\n[harn] done");
1899 }
1900
1901 #[test]
1902 fn test_spawn_cancel() {
1903 let out = run_output(
1904 r#"pipeline t(task) {
1905let handle = spawn { log("should be cancelled") }
1906cancel(handle)
1907log("cancelled")
1908}"#,
1909 );
1910 assert_eq!(out, "[harn] cancelled");
1911 }
1912
1913 #[test]
1914 fn test_spawn_returns_value() {
1915 let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1916 assert_eq!(out, "[harn] 42");
1917 }
1918
1919 #[test]
1922 fn test_deadline_success() {
1923 let out = run_output(
1924 r#"pipeline t(task) {
1925let result = deadline 5s { log("within deadline")
192642 }
1927log(result)
1928}"#,
1929 );
1930 assert_eq!(out, "[harn] within deadline\n[harn] 42");
1931 }
1932
1933 #[test]
1934 fn test_deadline_exceeded() {
1935 let result = run_harn_result(
1936 r#"pipeline t(task) {
1937deadline 1ms {
1938 var i = 0
1939 while i < 1000000 { i = i + 1 }
1940}
1941}"#,
1942 );
1943 assert!(result.is_err());
1944 }
1945
1946 #[test]
1947 fn test_deadline_caught_by_try() {
1948 let out = run_output(
1949 r#"pipeline t(task) {
1950try {
1951 deadline 1ms {
1952 var i = 0
1953 while i < 1000000 { i = i + 1 }
1954 }
1955} catch(e) {
1956 log("caught")
1957}
1958}"#,
1959 );
1960 assert_eq!(out, "[harn] caught");
1961 }
1962
1963 fn run_harn_with_denied(
1965 source: &str,
1966 denied: HashSet<String>,
1967 ) -> Result<(String, VmValue), VmError> {
1968 let rt = tokio::runtime::Builder::new_current_thread()
1969 .enable_all()
1970 .build()
1971 .unwrap();
1972 rt.block_on(async {
1973 let local = tokio::task::LocalSet::new();
1974 local
1975 .run_until(async {
1976 let mut lexer = Lexer::new(source);
1977 let tokens = lexer.tokenize().unwrap();
1978 let mut parser = Parser::new(tokens);
1979 let program = parser.parse().unwrap();
1980 let chunk = Compiler::new().compile(&program).unwrap();
1981
1982 let mut vm = Vm::new();
1983 register_vm_stdlib(&mut vm);
1984 vm.set_denied_builtins(denied);
1985 let result = vm.execute(&chunk).await?;
1986 Ok((vm.output().to_string(), result))
1987 })
1988 .await
1989 })
1990 }
1991
1992 #[test]
1993 fn test_sandbox_deny_builtin() {
1994 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1995 let result = run_harn_with_denied(
1996 r#"pipeline t(task) {
1997let xs = [1, 2]
1998push(xs, 3)
1999}"#,
2000 denied,
2001 );
2002 let err = result.unwrap_err();
2003 let msg = format!("{err}");
2004 assert!(
2005 msg.contains("not permitted"),
2006 "expected not permitted, got: {msg}"
2007 );
2008 assert!(
2009 msg.contains("push"),
2010 "expected builtin name in error, got: {msg}"
2011 );
2012 }
2013
2014 #[test]
2015 fn test_sandbox_allowed_builtin_works() {
2016 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2018 let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
2019 let (output, _) = result.unwrap();
2020 assert_eq!(output.trim(), "[harn] hello");
2021 }
2022
2023 #[test]
2024 fn test_sandbox_empty_denied_set() {
2025 let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
2027 let (output, _) = result.unwrap();
2028 assert_eq!(output.trim(), "[harn] ok");
2029 }
2030
2031 #[test]
2032 fn test_sandbox_propagates_to_spawn() {
2033 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2035 let result = run_harn_with_denied(
2036 r#"pipeline t(task) {
2037let handle = spawn {
2038 let xs = [1, 2]
2039 push(xs, 3)
2040}
2041await(handle)
2042}"#,
2043 denied,
2044 );
2045 let err = result.unwrap_err();
2046 let msg = format!("{err}");
2047 assert!(
2048 msg.contains("not permitted"),
2049 "expected not permitted in spawned VM, got: {msg}"
2050 );
2051 }
2052
2053 #[test]
2054 fn test_sandbox_propagates_to_parallel() {
2055 let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
2057 let result = run_harn_with_denied(
2058 r#"pipeline t(task) {
2059let results = parallel(2) { i ->
2060 let xs = [1, 2]
2061 push(xs, 3)
2062}
2063}"#,
2064 denied,
2065 );
2066 let err = result.unwrap_err();
2067 let msg = format!("{err}");
2068 assert!(
2069 msg.contains("not permitted"),
2070 "expected not permitted in parallel VM, got: {msg}"
2071 );
2072 }
2073
2074 #[test]
2075 fn test_if_else_has_lexical_block_scope() {
2076 let out = run_output(
2077 r#"pipeline t(task) {
2078let x = "outer"
2079if true {
2080 let x = "inner"
2081 log(x)
2082} else {
2083 let x = "other"
2084 log(x)
2085}
2086log(x)
2087}"#,
2088 );
2089 assert_eq!(out, "[harn] inner\n[harn] outer");
2090 }
2091
2092 #[test]
2093 fn test_loop_and_catch_bindings_are_block_scoped() {
2094 let out = run_output(
2095 r#"pipeline t(task) {
2096let label = "outer"
2097for item in [1, 2] {
2098 let label = "loop ${item}"
2099 log(label)
2100}
2101try {
2102 throw("boom")
2103} catch (label) {
2104 log(label)
2105}
2106log(label)
2107}"#,
2108 );
2109 assert_eq!(
2110 out,
2111 "[harn] loop 1\n[harn] loop 2\n[harn] boom\n[harn] outer"
2112 );
2113 }
2114}