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