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