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