Skip to main content

harn_vm/
vm.rs

1mod format;
2mod imports;
3mod methods;
4mod ops;
5
6use std::cell::RefCell;
7use std::collections::{BTreeMap, HashSet};
8use std::future::Future;
9use std::pin::Pin;
10use std::rc::Rc;
11use std::time::Instant;
12
13use crate::chunk::{Chunk, CompiledFunction, Constant};
14use crate::value::{
15    ErrorCategory, ModuleFunctionRegistry, VmAsyncBuiltinFn, VmBuiltinFn, VmClosure, VmEnv,
16    VmError, VmTaskHandle, VmValue,
17};
18
19thread_local! {
20    static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> = const { RefCell::new(Vec::new()) };
21}
22
23/// RAII guard that starts a tracing span on creation and ends it on drop.
24struct ScopeSpan(u64);
25
26impl ScopeSpan {
27    fn new(kind: crate::tracing::SpanKind, name: String) -> Self {
28        Self(crate::tracing::span_start(kind, name))
29    }
30}
31
32impl Drop for ScopeSpan {
33    fn drop(&mut self) {
34        crate::tracing::span_end(self.0);
35    }
36}
37
38/// Call frame for function execution.
39pub(crate) struct CallFrame {
40    pub(crate) chunk: Chunk,
41    pub(crate) ip: usize,
42    pub(crate) stack_base: usize,
43    pub(crate) saved_env: VmEnv,
44    /// Function name for stack traces (empty for top-level pipeline).
45    pub(crate) fn_name: String,
46    /// Number of arguments actually passed by the caller (for default arg support).
47    pub(crate) argc: usize,
48    /// Saved VM_SOURCE_DIR to restore when this frame is popped.
49    /// Set when entering a closure that originated from an imported module.
50    pub(crate) saved_source_dir: Option<std::path::PathBuf>,
51    /// Module-local named functions available to symbolic calls within this frame.
52    pub(crate) module_functions: Option<ModuleFunctionRegistry>,
53}
54
55/// Exception handler for try/catch.
56pub(crate) struct ExceptionHandler {
57    pub(crate) catch_ip: usize,
58    pub(crate) stack_depth: usize,
59    pub(crate) frame_depth: usize,
60    pub(crate) env_scope_depth: usize,
61    /// If non-empty, this catch only handles errors whose enum_name matches.
62    pub(crate) error_type: String,
63}
64
65/// Debug action returned by the debug hook.
66#[derive(Debug, Clone, PartialEq)]
67pub enum DebugAction {
68    /// Continue execution normally.
69    Continue,
70    /// Stop (breakpoint hit, step complete).
71    Stop,
72}
73
74/// Information about current execution state for the debugger.
75#[derive(Debug, Clone)]
76pub struct DebugState {
77    pub line: usize,
78    pub variables: BTreeMap<String, VmValue>,
79    pub frame_name: String,
80    pub frame_depth: usize,
81}
82
83/// Iterator state for for-in loops: either a pre-collected vec, an async channel, or a generator.
84pub(crate) enum IterState {
85    Vec {
86        items: Vec<VmValue>,
87        idx: usize,
88    },
89    Channel {
90        receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
91        closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
92    },
93    Generator {
94        gen: crate::value::VmGenerator,
95    },
96}
97
98#[derive(Clone)]
99pub(crate) struct LoadedModule {
100    pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
101    pub(crate) public_names: HashSet<String>,
102}
103
104/// The Harn bytecode virtual machine.
105pub struct Vm {
106    pub(crate) stack: Vec<VmValue>,
107    pub(crate) env: VmEnv,
108    pub(crate) output: String,
109    pub(crate) builtins: BTreeMap<String, VmBuiltinFn>,
110    pub(crate) async_builtins: BTreeMap<String, VmAsyncBuiltinFn>,
111    /// Iterator state for for-in loops.
112    pub(crate) iterators: Vec<IterState>,
113    /// Call frame stack.
114    pub(crate) frames: Vec<CallFrame>,
115    /// Exception handler stack.
116    pub(crate) exception_handlers: Vec<ExceptionHandler>,
117    /// Spawned async task handles.
118    pub(crate) spawned_tasks: BTreeMap<String, VmTaskHandle>,
119    /// Counter for generating unique task IDs.
120    pub(crate) task_counter: u64,
121    /// Active deadline stack: (deadline_instant, frame_depth).
122    pub(crate) deadlines: Vec<(Instant, usize)>,
123    /// Breakpoints (source line numbers).
124    pub(crate) breakpoints: Vec<usize>,
125    /// Whether the VM is in step mode.
126    pub(crate) step_mode: bool,
127    /// The frame depth at which stepping started (for step-over).
128    pub(crate) step_frame_depth: usize,
129    /// Whether the VM is currently stopped at a debug point.
130    pub(crate) stopped: bool,
131    /// Last source line executed (to detect line changes).
132    pub(crate) last_line: usize,
133    /// Source directory for resolving imports.
134    pub(crate) source_dir: Option<std::path::PathBuf>,
135    /// Modules currently being imported (cycle prevention).
136    pub(crate) imported_paths: Vec<std::path::PathBuf>,
137    /// Loaded module cache keyed by canonical or synthetic module path.
138    pub(crate) module_cache: BTreeMap<std::path::PathBuf, LoadedModule>,
139    /// Source file path for error reporting.
140    pub(crate) source_file: Option<String>,
141    /// Source text for error reporting.
142    pub(crate) source_text: Option<String>,
143    /// Optional bridge for delegating unknown builtins in bridge mode.
144    pub(crate) bridge: Option<Rc<crate::bridge::HostBridge>>,
145    /// Builtins denied by sandbox mode (`--deny` / `--allow` flags).
146    pub(crate) denied_builtins: HashSet<String>,
147    /// Cancellation token for cooperative graceful shutdown (set by parent).
148    pub(crate) cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
149    /// Captured stack trace from the most recent error (fn_name, line, col).
150    pub(crate) error_stack_trace: Vec<(String, usize, usize, Option<String>)>,
151    /// Yield channel sender for generator execution. When set, `Op::Yield`
152    /// sends values through this channel instead of being a no-op.
153    pub(crate) yield_sender: Option<tokio::sync::mpsc::Sender<VmValue>>,
154    /// Project root directory (detected via harn.toml).
155    /// Used as base directory for metadata, store, and checkpoint operations.
156    pub(crate) project_root: Option<std::path::PathBuf>,
157    /// Global constants (e.g. `pi`, `e`). Checked as a fallback in `GetVar`
158    /// after the environment, so user-defined variables can shadow them.
159    pub(crate) globals: BTreeMap<String, VmValue>,
160}
161
162impl Vm {
163    pub fn new() -> Self {
164        Self {
165            stack: Vec::with_capacity(256),
166            env: VmEnv::new(),
167            output: String::new(),
168            builtins: BTreeMap::new(),
169            async_builtins: BTreeMap::new(),
170            iterators: Vec::new(),
171            frames: Vec::new(),
172            exception_handlers: Vec::new(),
173            spawned_tasks: BTreeMap::new(),
174            task_counter: 0,
175            deadlines: Vec::new(),
176            breakpoints: Vec::new(),
177            step_mode: false,
178            step_frame_depth: 0,
179            stopped: false,
180            last_line: 0,
181            source_dir: None,
182            imported_paths: Vec::new(),
183            module_cache: BTreeMap::new(),
184            source_file: None,
185            source_text: None,
186            bridge: None,
187            denied_builtins: HashSet::new(),
188            cancel_token: None,
189            error_stack_trace: Vec::new(),
190            yield_sender: None,
191            project_root: None,
192            globals: BTreeMap::new(),
193        }
194    }
195
196    /// Set the bridge for delegating unknown builtins in bridge mode.
197    pub fn set_bridge(&mut self, bridge: Rc<crate::bridge::HostBridge>) {
198        self.bridge = Some(bridge);
199    }
200
201    /// Set builtins that are denied in sandbox mode.
202    /// When called, the given builtin names will produce a permission error.
203    pub fn set_denied_builtins(&mut self, denied: HashSet<String>) {
204        self.denied_builtins = denied;
205    }
206
207    /// Set source info for error reporting (file path and source text).
208    pub fn set_source_info(&mut self, file: &str, text: &str) {
209        self.source_file = Some(file.to_string());
210        self.source_text = Some(text.to_string());
211    }
212
213    /// Set breakpoints by source line number.
214    pub fn set_breakpoints(&mut self, lines: Vec<usize>) {
215        self.breakpoints = lines;
216    }
217
218    /// Enable step mode (stop at next line).
219    pub fn set_step_mode(&mut self, step: bool) {
220        self.step_mode = step;
221        self.step_frame_depth = self.frames.len();
222    }
223
224    /// Enable step-over mode (stop at next line at same or lower frame depth).
225    pub fn set_step_over(&mut self) {
226        self.step_mode = true;
227        self.step_frame_depth = self.frames.len();
228    }
229
230    /// Enable step-out mode (stop when returning from current frame).
231    pub fn set_step_out(&mut self) {
232        self.step_mode = true;
233        self.step_frame_depth = self.frames.len().saturating_sub(1);
234    }
235
236    /// Check if the VM is stopped at a debug point.
237    pub fn is_stopped(&self) -> bool {
238        self.stopped
239    }
240
241    /// Get the current debug state (variables, line, etc.).
242    pub fn debug_state(&self) -> DebugState {
243        let line = self.current_line();
244        let variables = self.env.all_variables();
245        let frame_name = if self.frames.len() > 1 {
246            format!("frame_{}", self.frames.len() - 1)
247        } else {
248            "pipeline".to_string()
249        };
250        DebugState {
251            line,
252            variables,
253            frame_name,
254            frame_depth: self.frames.len(),
255        }
256    }
257
258    /// Get all stack frames for the debugger.
259    pub fn debug_stack_frames(&self) -> Vec<(String, usize)> {
260        let mut frames = Vec::new();
261        for (i, frame) in self.frames.iter().enumerate() {
262            let line = if frame.ip > 0 && frame.ip - 1 < frame.chunk.lines.len() {
263                frame.chunk.lines[frame.ip - 1] as usize
264            } else {
265                0
266            };
267            let name = if frame.fn_name.is_empty() {
268                if i == 0 {
269                    "pipeline".to_string()
270                } else {
271                    format!("fn_{}", i)
272                }
273            } else {
274                frame.fn_name.clone()
275            };
276            frames.push((name, line));
277        }
278        frames
279    }
280
281    /// Get the current source line.
282    fn current_line(&self) -> usize {
283        if let Some(frame) = self.frames.last() {
284            let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
285            if ip < frame.chunk.lines.len() {
286                return frame.chunk.lines[ip] as usize;
287            }
288        }
289        0
290    }
291
292    /// Execute one instruction, returning whether to stop (breakpoint/step).
293    /// Returns Ok(None) to continue, Ok(Some(val)) on program end, Err on error.
294    pub async fn step_execute(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
295        // Check if we need to stop at this line
296        let current_line = self.current_line();
297        let line_changed = current_line != self.last_line && current_line > 0;
298
299        if line_changed {
300            self.last_line = current_line;
301
302            // Check breakpoints
303            if self.breakpoints.contains(&current_line) {
304                self.stopped = true;
305                return Ok(Some((VmValue::Nil, true))); // true = stopped
306            }
307
308            // Check step mode
309            if self.step_mode && self.frames.len() <= self.step_frame_depth + 1 {
310                self.step_mode = false;
311                self.stopped = true;
312                return Ok(Some((VmValue::Nil, true))); // true = stopped
313            }
314        }
315
316        // Execute one instruction cycle
317        self.stopped = false;
318        self.execute_one_cycle().await
319    }
320
321    /// Execute a single instruction cycle.
322    async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
323        // Check deadline
324        if let Some(&(deadline, _)) = self.deadlines.last() {
325            if Instant::now() > deadline {
326                self.deadlines.pop();
327                let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
328                match self.handle_error(err) {
329                    Ok(None) => return Ok(None),
330                    Ok(Some(val)) => return Ok(Some((val, false))),
331                    Err(e) => return Err(e),
332                }
333            }
334        }
335
336        // Get current frame
337        let frame = match self.frames.last_mut() {
338            Some(f) => f,
339            None => {
340                let val = self.stack.pop().unwrap_or(VmValue::Nil);
341                return Ok(Some((val, false)));
342            }
343        };
344
345        // Check if we've reached end of chunk
346        if frame.ip >= frame.chunk.code.len() {
347            let val = self.stack.pop().unwrap_or(VmValue::Nil);
348            let popped_frame = self.frames.pop().unwrap();
349            if self.frames.is_empty() {
350                return Ok(Some((val, false)));
351            } else {
352                self.env = popped_frame.saved_env;
353                self.stack.truncate(popped_frame.stack_base);
354                self.stack.push(val);
355                return Ok(None);
356            }
357        }
358
359        let op = frame.chunk.code[frame.ip];
360        frame.ip += 1;
361
362        match self.execute_op(op).await {
363            Ok(Some(val)) => Ok(Some((val, false))),
364            Ok(None) => Ok(None),
365            Err(VmError::Return(val)) => {
366                if let Some(popped_frame) = self.frames.pop() {
367                    if let Some(ref dir) = popped_frame.saved_source_dir {
368                        crate::stdlib::set_thread_source_dir(dir);
369                    }
370                    let current_depth = self.frames.len();
371                    self.exception_handlers
372                        .retain(|h| h.frame_depth <= current_depth);
373                    if self.frames.is_empty() {
374                        return Ok(Some((val, false)));
375                    }
376                    self.env = popped_frame.saved_env;
377                    self.stack.truncate(popped_frame.stack_base);
378                    self.stack.push(val);
379                    Ok(None)
380                } else {
381                    Ok(Some((val, false)))
382                }
383            }
384            Err(e) => {
385                if self.error_stack_trace.is_empty() {
386                    self.error_stack_trace = self.capture_stack_trace();
387                }
388                match self.handle_error(e) {
389                    Ok(None) => {
390                        self.error_stack_trace.clear();
391                        Ok(None)
392                    }
393                    Ok(Some(val)) => Ok(Some((val, false))),
394                    Err(e) => Err(self.enrich_error_with_line(e)),
395                }
396            }
397        }
398    }
399
400    /// Initialize execution (push the initial frame).
401    pub fn start(&mut self, chunk: &Chunk) {
402        self.frames.push(CallFrame {
403            chunk: chunk.clone(),
404            ip: 0,
405            stack_base: self.stack.len(),
406            saved_env: self.env.clone(),
407            fn_name: String::new(),
408            argc: 0,
409            saved_source_dir: None,
410            module_functions: None,
411        });
412    }
413
414    /// Register a sync builtin function.
415    pub fn register_builtin<F>(&mut self, name: &str, f: F)
416    where
417        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
418    {
419        self.builtins.insert(name.to_string(), Rc::new(f));
420    }
421
422    /// Remove a sync builtin (so an async version can take precedence).
423    pub fn unregister_builtin(&mut self, name: &str) {
424        self.builtins.remove(name);
425    }
426
427    /// Register an async builtin function.
428    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
429    where
430        F: Fn(Vec<VmValue>) -> Fut + 'static,
431        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
432    {
433        self.async_builtins
434            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
435    }
436
437    /// Create a child VM that shares builtins and env but has fresh execution state.
438    /// Used for parallel/spawn to fork the VM for concurrent tasks.
439    fn child_vm(&self) -> Vm {
440        Vm {
441            stack: Vec::with_capacity(64),
442            env: self.env.clone(),
443            output: String::new(),
444            builtins: self.builtins.clone(),
445            async_builtins: self.async_builtins.clone(),
446            iterators: Vec::new(),
447            frames: Vec::new(),
448            exception_handlers: Vec::new(),
449            spawned_tasks: BTreeMap::new(),
450            task_counter: 0,
451            deadlines: self.deadlines.clone(),
452            breakpoints: Vec::new(),
453            step_mode: false,
454            step_frame_depth: 0,
455            stopped: false,
456            last_line: 0,
457            source_dir: self.source_dir.clone(),
458            imported_paths: Vec::new(),
459            module_cache: self.module_cache.clone(),
460            source_file: self.source_file.clone(),
461            source_text: self.source_text.clone(),
462            bridge: self.bridge.clone(),
463            denied_builtins: self.denied_builtins.clone(),
464            cancel_token: None,
465            error_stack_trace: Vec::new(),
466            yield_sender: None,
467            project_root: self.project_root.clone(),
468            globals: self.globals.clone(),
469        }
470    }
471
472    /// Set the source directory for import resolution and introspection.
473    /// Also auto-detects the project root if not already set.
474    pub fn set_source_dir(&mut self, dir: &std::path::Path) {
475        self.source_dir = Some(dir.to_path_buf());
476        crate::stdlib::set_thread_source_dir(dir);
477        // Auto-detect project root if not explicitly set.
478        if self.project_root.is_none() {
479            self.project_root = crate::stdlib::process::find_project_root(dir);
480        }
481    }
482
483    /// Explicitly set the project root directory.
484    /// Used by ACP/CLI to override auto-detection.
485    pub fn set_project_root(&mut self, root: &std::path::Path) {
486        self.project_root = Some(root.to_path_buf());
487    }
488
489    /// Get the project root directory, falling back to source_dir.
490    pub fn project_root(&self) -> Option<&std::path::Path> {
491        self.project_root.as_deref().or(self.source_dir.as_deref())
492    }
493
494    /// Return all registered builtin names (sync + async).
495    pub fn builtin_names(&self) -> Vec<String> {
496        let mut names: Vec<String> = self.builtins.keys().cloned().collect();
497        names.extend(self.async_builtins.keys().cloned());
498        names
499    }
500
501    /// Set a global constant (e.g. `pi`, `e`).
502    /// Stored separately from the environment so user-defined variables can shadow them.
503    pub fn set_global(&mut self, name: &str, value: VmValue) {
504        self.globals.insert(name.to_string(), value);
505    }
506
507    /// Get the captured output.
508    pub fn output(&self) -> &str {
509        &self.output
510    }
511
512    /// Execute a compiled chunk.
513    pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
514        let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
515        let result = self.run_chunk(chunk).await;
516        crate::tracing::span_end(span_id);
517        result
518    }
519
520    /// Convert a VmError into either a handled exception (returning Ok) or a propagated error.
521    fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
522        // Extract the thrown value from the error
523        let thrown_value = match &error {
524            VmError::Thrown(v) => v.clone(),
525            other => VmValue::String(Rc::from(other.to_string())),
526        };
527
528        if let Some(handler) = self.exception_handlers.pop() {
529            // Check if this is a typed catch that doesn't match the thrown value
530            if !handler.error_type.is_empty() {
531                let matches = match &thrown_value {
532                    VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
533                    _ => false,
534                };
535                if !matches {
536                    // This handler doesn't match — try the next one
537                    return self.handle_error(error);
538                }
539            }
540
541            // Unwind call frames back to the handler's frame depth
542            while self.frames.len() > handler.frame_depth {
543                if let Some(frame) = self.frames.pop() {
544                    if let Some(ref dir) = frame.saved_source_dir {
545                        crate::stdlib::set_thread_source_dir(dir);
546                    }
547                    self.env = frame.saved_env;
548                }
549            }
550
551            // Clean up deadlines from unwound frames
552            while self
553                .deadlines
554                .last()
555                .is_some_and(|d| d.1 > handler.frame_depth)
556            {
557                self.deadlines.pop();
558            }
559
560            self.env.truncate_scopes(handler.env_scope_depth);
561
562            // Restore stack to handler's depth
563            self.stack.truncate(handler.stack_depth);
564
565            // Push the error value onto the stack (catch body can access it)
566            self.stack.push(thrown_value);
567
568            // Set the IP in the current frame to the catch handler
569            if let Some(frame) = self.frames.last_mut() {
570                frame.ip = handler.catch_ip;
571            }
572
573            Ok(None) // Continue execution
574        } else {
575            Err(error) // No handler, propagate
576        }
577    }
578
579    async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
580        self.run_chunk_entry(chunk, 0, None, None).await
581    }
582
583    async fn run_chunk_entry(
584        &mut self,
585        chunk: &Chunk,
586        argc: usize,
587        saved_source_dir: Option<std::path::PathBuf>,
588        module_functions: Option<ModuleFunctionRegistry>,
589    ) -> Result<VmValue, VmError> {
590        self.frames.push(CallFrame {
591            chunk: chunk.clone(),
592            ip: 0,
593            stack_base: self.stack.len(),
594            saved_env: self.env.clone(),
595            fn_name: String::new(),
596            argc,
597            saved_source_dir,
598            module_functions,
599        });
600
601        loop {
602            // Check deadline before each instruction
603            if let Some(&(deadline, _)) = self.deadlines.last() {
604                if Instant::now() > deadline {
605                    self.deadlines.pop();
606                    let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
607                    match self.handle_error(err) {
608                        Ok(None) => continue,
609                        Ok(Some(val)) => return Ok(val),
610                        Err(e) => return Err(e),
611                    }
612                }
613            }
614
615            // Get current frame
616            let frame = match self.frames.last_mut() {
617                Some(f) => f,
618                None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
619            };
620
621            // Check if we've reached end of chunk
622            if frame.ip >= frame.chunk.code.len() {
623                let val = self.stack.pop().unwrap_or(VmValue::Nil);
624                let popped_frame = self.frames.pop().unwrap();
625                if let Some(ref dir) = popped_frame.saved_source_dir {
626                    crate::stdlib::set_thread_source_dir(dir);
627                }
628
629                if self.frames.is_empty() {
630                    // We're done with the top-level chunk
631                    return Ok(val);
632                } else {
633                    // Returning from a function call
634                    self.env = popped_frame.saved_env;
635                    self.stack.truncate(popped_frame.stack_base);
636                    self.stack.push(val);
637                    continue;
638                }
639            }
640
641            let op = frame.chunk.code[frame.ip];
642            frame.ip += 1;
643
644            match self.execute_op(op).await {
645                Ok(Some(val)) => return Ok(val),
646                Ok(None) => continue,
647                Err(VmError::Return(val)) => {
648                    // Pop the current frame
649                    if let Some(popped_frame) = self.frames.pop() {
650                        if let Some(ref dir) = popped_frame.saved_source_dir {
651                            crate::stdlib::set_thread_source_dir(dir);
652                        }
653                        // Clean up exception handlers from the returned frame
654                        let current_depth = self.frames.len();
655                        self.exception_handlers
656                            .retain(|h| h.frame_depth <= current_depth);
657
658                        if self.frames.is_empty() {
659                            return Ok(val);
660                        }
661                        self.env = popped_frame.saved_env;
662                        self.stack.truncate(popped_frame.stack_base);
663                        self.stack.push(val);
664                    } else {
665                        return Ok(val);
666                    }
667                }
668                Err(e) => {
669                    // Capture stack trace before error handling unwinds frames
670                    if self.error_stack_trace.is_empty() {
671                        self.error_stack_trace = self.capture_stack_trace();
672                    }
673                    match self.handle_error(e) {
674                        Ok(None) => {
675                            self.error_stack_trace.clear();
676                            continue; // Handler found, continue
677                        }
678                        Ok(Some(val)) => return Ok(val),
679                        Err(e) => return Err(self.enrich_error_with_line(e)),
680                    }
681                }
682            }
683        }
684    }
685
686    /// Capture the current call stack as (fn_name, line, col, source_file) tuples.
687    fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
688        self.frames
689            .iter()
690            .map(|f| {
691                let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
692                let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
693                let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
694                (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
695            })
696            .collect()
697    }
698
699    /// Enrich a VmError with source line information from the captured stack
700    /// trace. Appends ` (line N)` to error variants whose messages don't
701    /// already carry location context.
702    fn enrich_error_with_line(&self, error: VmError) -> VmError {
703        // Determine the line from the captured stack trace (innermost frame).
704        let line = self
705            .error_stack_trace
706            .last()
707            .map(|(_, l, _, _)| *l)
708            .unwrap_or_else(|| self.current_line());
709        if line == 0 {
710            return error;
711        }
712        let suffix = format!(" (line {line})");
713        match error {
714            VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
715            VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
716            VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
717            VmError::UndefinedVariable(name) => {
718                VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
719            }
720            VmError::UndefinedBuiltin(name) => {
721                VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
722            }
723            VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
724                "Cannot assign to immutable binding: {name}{suffix}"
725            )),
726            VmError::StackOverflow => {
727                VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
728            }
729            // Leave these untouched:
730            // - Thrown: user-thrown errors should not be silently modified
731            // - CategorizedError: structured errors for agent orchestration
732            // - Return: control flow, not a real error
733            // - StackUnderflow / InvalidInstruction: internal VM bugs
734            other => other,
735        }
736    }
737
738    const MAX_FRAMES: usize = 512;
739
740    /// Merge the caller's env into a closure's captured env for function calls.
741    fn merge_env_into_closure(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
742        let mut call_env = closure.env.clone();
743        for scope in &caller_env.scopes {
744            for (name, (val, mutable)) in &scope.vars {
745                if call_env.get(name).is_none() {
746                    let _ = call_env.define(name, val.clone(), *mutable);
747                }
748            }
749        }
750        call_env
751    }
752
753    fn resolve_named_closure(&self, name: &str) -> Option<Rc<VmClosure>> {
754        if let Some(VmValue::Closure(closure)) = self.env.get(name) {
755            return Some(closure);
756        }
757        self.frames
758            .last()
759            .and_then(|frame| frame.module_functions.as_ref())
760            .and_then(|registry| registry.borrow().get(name).cloned())
761    }
762
763    /// Push a new call frame for a closure invocation.
764    fn push_closure_frame(
765        &mut self,
766        closure: &VmClosure,
767        args: &[VmValue],
768        _parent_functions: &[CompiledFunction],
769    ) -> Result<(), VmError> {
770        if self.frames.len() >= Self::MAX_FRAMES {
771            return Err(VmError::StackOverflow);
772        }
773        let saved_env = self.env.clone();
774
775        // If this closure originated from an imported module, switch
776        // the thread-local source dir so that render() and other
777        // source-relative builtins resolve relative to the module.
778        let saved_source_dir = if let Some(ref dir) = closure.source_dir {
779            let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
780            crate::stdlib::set_thread_source_dir(dir);
781            prev
782        } else {
783            None
784        };
785
786        let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
787        call_env.push_scope();
788
789        let default_start = closure
790            .func
791            .default_start
792            .unwrap_or(closure.func.params.len());
793        for (i, param) in closure.func.params.iter().enumerate() {
794            if i < args.len() {
795                let _ = call_env.define(param, args[i].clone(), false);
796            } else if i < default_start {
797                let _ = call_env.define(param, VmValue::Nil, false);
798            }
799        }
800
801        self.env = call_env;
802
803        self.frames.push(CallFrame {
804            chunk: closure.func.chunk.clone(),
805            ip: 0,
806            stack_base: self.stack.len(),
807            saved_env,
808            fn_name: closure.func.name.clone(),
809            argc: args.len(),
810            saved_source_dir,
811            module_functions: closure.module_functions.clone(),
812        });
813
814        Ok(())
815    }
816
817    /// Create a generator value by spawning the closure body as an async task.
818    /// The generator body communicates yielded values through an mpsc channel.
819    pub(crate) fn create_generator(&self, closure: &VmClosure, args: &[VmValue]) -> VmValue {
820        use crate::value::VmGenerator;
821
822        // Buffer size of 1: the generator produces one value at a time.
823        let (tx, rx) = tokio::sync::mpsc::channel::<VmValue>(1);
824
825        let mut child = self.child_vm();
826        child.yield_sender = Some(tx);
827
828        // Set up the environment for the generator body
829        let saved_env = child.env.clone();
830        let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
831        call_env.push_scope();
832
833        let default_start = closure
834            .func
835            .default_start
836            .unwrap_or(closure.func.params.len());
837        for (i, param) in closure.func.params.iter().enumerate() {
838            if i < args.len() {
839                let _ = call_env.define(param, args[i].clone(), false);
840            } else if i < default_start {
841                let _ = call_env.define(param, VmValue::Nil, false);
842            }
843        }
844        child.env = call_env;
845
846        let chunk = closure.func.chunk.clone();
847        let saved_source_dir = if let Some(ref dir) = closure.source_dir {
848            let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
849            crate::stdlib::set_thread_source_dir(dir);
850            prev
851        } else {
852            None
853        };
854        let module_functions = closure.module_functions.clone();
855        let argc = args.len();
856        // Spawn the generator body as an async task.
857        // The task will execute until return, sending yielded values through the channel.
858        tokio::task::spawn_local(async move {
859            let _ = child
860                .run_chunk_entry(&chunk, argc, saved_source_dir, module_functions)
861                .await;
862            // When the generator body finishes (return or fall-through),
863            // the sender is dropped, signaling completion to the receiver.
864        });
865
866        VmValue::Generator(VmGenerator {
867            done: Rc::new(std::cell::Cell::new(false)),
868            receiver: Rc::new(tokio::sync::Mutex::new(rx)),
869        })
870    }
871
872    fn pop(&mut self) -> Result<VmValue, VmError> {
873        self.stack.pop().ok_or(VmError::StackUnderflow)
874    }
875
876    fn peek(&self) -> Result<&VmValue, VmError> {
877        self.stack.last().ok_or(VmError::StackUnderflow)
878    }
879
880    fn const_string(c: &Constant) -> Result<String, VmError> {
881        match c {
882            Constant::String(s) => Ok(s.clone()),
883            _ => Err(VmError::TypeError("expected string constant".into())),
884        }
885    }
886
887    /// Call a closure (used by method calls like .map/.filter etc.)
888    /// Uses recursive execution for simplicity in method dispatch.
889    fn call_closure<'a>(
890        &'a mut self,
891        closure: &'a VmClosure,
892        args: &'a [VmValue],
893        _parent_functions: &'a [CompiledFunction],
894    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
895        Box::pin(async move {
896            let saved_env = self.env.clone();
897            let saved_frames = std::mem::take(&mut self.frames);
898            let saved_handlers = std::mem::take(&mut self.exception_handlers);
899            let saved_iterators = std::mem::take(&mut self.iterators);
900            let saved_deadlines = std::mem::take(&mut self.deadlines);
901
902            let mut call_env = Self::merge_env_into_closure(&saved_env, closure);
903            call_env.push_scope();
904
905            let default_start = closure
906                .func
907                .default_start
908                .unwrap_or(closure.func.params.len());
909            for (i, param) in closure.func.params.iter().enumerate() {
910                if i < args.len() {
911                    let _ = call_env.define(param, args[i].clone(), false);
912                } else if i < default_start {
913                    let _ = call_env.define(param, VmValue::Nil, false);
914                }
915            }
916
917            self.env = call_env;
918            let argc = args.len();
919            let saved_source_dir = if let Some(ref dir) = closure.source_dir {
920                let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
921                crate::stdlib::set_thread_source_dir(dir);
922                prev
923            } else {
924                None
925            };
926            let result = self
927                .run_chunk_entry(
928                    &closure.func.chunk,
929                    argc,
930                    saved_source_dir,
931                    closure.module_functions.clone(),
932                )
933                .await;
934
935            self.env = saved_env;
936            self.frames = saved_frames;
937            self.exception_handlers = saved_handlers;
938            self.iterators = saved_iterators;
939            self.deadlines = saved_deadlines;
940
941            result
942        })
943    }
944
945    /// Public wrapper for `call_closure`, used by the MCP server to invoke
946    /// tool handler closures from outside the VM execution loop.
947    pub async fn call_closure_pub(
948        &mut self,
949        closure: &VmClosure,
950        args: &[VmValue],
951        functions: &[CompiledFunction],
952    ) -> Result<VmValue, VmError> {
953        self.call_closure(closure, args, functions).await
954    }
955
956    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
957    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
958    async fn call_named_builtin(
959        &mut self,
960        name: &str,
961        args: Vec<VmValue>,
962    ) -> Result<VmValue, VmError> {
963        // Auto-trace LLM calls and tool calls
964        let span_kind = match name {
965            "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
966            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
967            _ => None,
968        };
969        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
970
971        // Sandbox check: deny builtins blocked by --deny/--allow flags.
972        if self.denied_builtins.contains(name) {
973            return Err(VmError::CategorizedError {
974                message: format!("Tool '{}' is not permitted.", name),
975                category: ErrorCategory::ToolRejected,
976            });
977        }
978        crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
979        if let Some(builtin) = self.builtins.get(name).cloned() {
980            builtin(&args, &mut self.output)
981        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
982            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
983                slot.borrow_mut().push(self.child_vm());
984            });
985            let result = async_builtin(args).await;
986            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
987                slot.borrow_mut().pop();
988            });
989            result
990        } else if let Some(bridge) = &self.bridge {
991            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
992            let args_json: Vec<serde_json::Value> =
993                args.iter().map(crate::llm::vm_value_to_json).collect();
994            let result = bridge
995                .call(
996                    "builtin_call",
997                    serde_json::json!({"name": name, "args": args_json}),
998                )
999                .await?;
1000            Ok(crate::bridge::json_result_to_vm_value(&result))
1001        } else {
1002            let all_builtins = self
1003                .builtins
1004                .keys()
1005                .chain(self.async_builtins.keys())
1006                .map(|s| s.as_str());
1007            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
1008                return Err(VmError::Runtime(format!(
1009                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
1010                )));
1011            }
1012            Err(VmError::UndefinedBuiltin(name.to_string()))
1013        }
1014    }
1015}
1016
1017/// Clone the VM at the top of the async-builtin child VM stack, returning a
1018/// fresh `Vm` instance that callers own and can use without coordinating
1019/// with other concurrent users of the stack. This replaces the legacy
1020/// `take/restore` pattern: that pattern serialized access because only one
1021/// consumer could hold the single stack entry at a time, which prevented
1022/// any form of concurrent tool-handler execution within a single
1023/// agent_loop iteration. Cloning is cheap — the VM struct shares its
1024/// heavy state (env, builtins, bridge, module_cache) via `Arc`/`Rc` — so
1025/// multiple concurrent handlers can each have their own execution context.
1026///
1027/// Returns `None` if no parent VM is currently pushed on the stack.
1028pub fn clone_async_builtin_child_vm() -> Option<Vm> {
1029    CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
1030}
1031
1032/// Legacy API preserved for backward compatibility with any out-of-tree
1033/// callers. New code should use `clone_async_builtin_child_vm()` instead
1034/// — `take` serializes concurrent callers because only one can hold the
1035/// popped value at a time. Internally this now delegates to a clone so
1036/// even legacy callers don't deadlock each other, but the name is kept
1037/// until external callers migrate.
1038#[deprecated(
1039    note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
1040)]
1041pub fn take_async_builtin_child_vm() -> Option<Vm> {
1042    clone_async_builtin_child_vm()
1043}
1044
1045/// Legacy API — now a no-op because `take_async_builtin_child_vm` returns
1046/// a clone rather than popping the stack, so there is nothing to restore.
1047/// Kept for backward compatibility.
1048#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
1049pub fn restore_async_builtin_child_vm(_vm: Vm) {
1050    // No-op: the new clone-based API doesn't require restoration since
1051    // the caller owns a fresh clone and the stack is never mutated.
1052    CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1053        // Intentionally ignore — kept as a syntactic no-op block so the
1054        // function signature remains stable.
1055        let _ = slot;
1056    });
1057}
1058
1059impl Default for Vm {
1060    fn default() -> Self {
1061        Self::new()
1062    }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067    use super::*;
1068    use crate::compiler::Compiler;
1069    use crate::stdlib::register_vm_stdlib;
1070    use harn_lexer::Lexer;
1071    use harn_parser::Parser;
1072
1073    fn run_harn(source: &str) -> (String, VmValue) {
1074        let rt = tokio::runtime::Builder::new_current_thread()
1075            .enable_all()
1076            .build()
1077            .unwrap();
1078        rt.block_on(async {
1079            let local = tokio::task::LocalSet::new();
1080            local
1081                .run_until(async {
1082                    let mut lexer = Lexer::new(source);
1083                    let tokens = lexer.tokenize().unwrap();
1084                    let mut parser = Parser::new(tokens);
1085                    let program = parser.parse().unwrap();
1086                    let chunk = Compiler::new().compile(&program).unwrap();
1087
1088                    let mut vm = Vm::new();
1089                    register_vm_stdlib(&mut vm);
1090                    let result = vm.execute(&chunk).await.unwrap();
1091                    (vm.output().to_string(), result)
1092                })
1093                .await
1094        })
1095    }
1096
1097    fn run_output(source: &str) -> String {
1098        run_harn(source).0.trim_end().to_string()
1099    }
1100
1101    fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
1102        let rt = tokio::runtime::Builder::new_current_thread()
1103            .enable_all()
1104            .build()
1105            .unwrap();
1106        rt.block_on(async {
1107            let local = tokio::task::LocalSet::new();
1108            local
1109                .run_until(async {
1110                    let mut lexer = Lexer::new(source);
1111                    let tokens = lexer.tokenize().unwrap();
1112                    let mut parser = Parser::new(tokens);
1113                    let program = parser.parse().unwrap();
1114                    let chunk = Compiler::new().compile(&program).unwrap();
1115
1116                    let mut vm = Vm::new();
1117                    register_vm_stdlib(&mut vm);
1118                    let result = vm.execute(&chunk).await?;
1119                    Ok((vm.output().to_string(), result))
1120                })
1121                .await
1122        })
1123    }
1124
1125    #[test]
1126    fn test_arithmetic() {
1127        let out =
1128            run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
1129        assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
1130    }
1131
1132    #[test]
1133    fn test_mixed_arithmetic() {
1134        let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
1135        assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
1136    }
1137
1138    #[test]
1139    fn test_comparisons() {
1140        let out =
1141            run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
1142        assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
1143    }
1144
1145    #[test]
1146    fn test_let_var() {
1147        let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
1148        assert_eq!(out, "[harn] 42\n[harn] 2");
1149    }
1150
1151    #[test]
1152    fn test_if_else() {
1153        let out = run_output(
1154            r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
1155        );
1156        assert_eq!(out, "[harn] yes\n[harn] no");
1157    }
1158
1159    #[test]
1160    fn test_while_loop() {
1161        let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
1162        assert_eq!(out, "[harn] 5");
1163    }
1164
1165    #[test]
1166    fn test_for_in() {
1167        let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
1168        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
1169    }
1170
1171    #[test]
1172    fn test_fn_decl_and_call() {
1173        let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
1174        assert_eq!(out, "[harn] 7");
1175    }
1176
1177    #[test]
1178    fn test_closure() {
1179        let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
1180        assert_eq!(out, "[harn] 10");
1181    }
1182
1183    #[test]
1184    fn test_closure_capture() {
1185        let out = run_output(
1186            "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
1187        );
1188        assert_eq!(out, "[harn] 15");
1189    }
1190
1191    #[test]
1192    fn test_string_concat() {
1193        let out = run_output(
1194            r#"pipeline t(task) { let a = "hello" + " " + "world"
1195log(a) }"#,
1196        );
1197        assert_eq!(out, "[harn] hello world");
1198    }
1199
1200    #[test]
1201    fn test_list_map() {
1202        let out = run_output(
1203            "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1204        );
1205        assert_eq!(out, "[harn] [2, 4, 6]");
1206    }
1207
1208    #[test]
1209    fn test_list_filter() {
1210        let out = run_output(
1211            "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1212        );
1213        assert_eq!(out, "[harn] [4, 5]");
1214    }
1215
1216    #[test]
1217    fn test_list_reduce() {
1218        let out = run_output(
1219            "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1220        );
1221        assert_eq!(out, "[harn] 10");
1222    }
1223
1224    #[test]
1225    fn test_dict_access() {
1226        let out = run_output(
1227            r#"pipeline t(task) { let d = {name: "test", value: 42}
1228log(d.name)
1229log(d.value) }"#,
1230        );
1231        assert_eq!(out, "[harn] test\n[harn] 42");
1232    }
1233
1234    #[test]
1235    fn test_dict_methods() {
1236        let out = run_output(
1237            r#"pipeline t(task) { let d = {a: 1, b: 2}
1238log(d.keys())
1239log(d.values())
1240log(d.has("a"))
1241log(d.has("z")) }"#,
1242        );
1243        assert_eq!(
1244            out,
1245            "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1246        );
1247    }
1248
1249    #[test]
1250    fn test_pipe_operator() {
1251        let out = run_output(
1252            "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1253        );
1254        assert_eq!(out, "[harn] 10");
1255    }
1256
1257    #[test]
1258    fn test_pipe_with_closure() {
1259        let out = run_output(
1260            r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1261log(r) }"#,
1262        );
1263        assert_eq!(out, "[harn] [hello, world]");
1264    }
1265
1266    #[test]
1267    fn test_nil_coalescing() {
1268        let out = run_output(
1269            r#"pipeline t(task) { let a = nil ?? "fallback"
1270log(a)
1271let b = "present" ?? "fallback"
1272log(b) }"#,
1273        );
1274        assert_eq!(out, "[harn] fallback\n[harn] present");
1275    }
1276
1277    #[test]
1278    fn test_logical_operators() {
1279        let out =
1280            run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1281        assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1282    }
1283
1284    #[test]
1285    fn test_match() {
1286        let out = run_output(
1287            r#"pipeline t(task) { let x = "b"
1288match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1289        );
1290        assert_eq!(out, "[harn] second");
1291    }
1292
1293    #[test]
1294    fn test_subscript() {
1295        let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1296        assert_eq!(out, "[harn] 20");
1297    }
1298
1299    #[test]
1300    fn test_string_methods() {
1301        let out = run_output(
1302            r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1303log("a,b,c".split(","))
1304log("  hello  ".trim())
1305log("hello".starts_with("hel"))
1306log("hello".ends_with("lo"))
1307log("hello".substring(1, 3)) }"#,
1308        );
1309        assert_eq!(
1310            out,
1311            "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1312        );
1313    }
1314
1315    #[test]
1316    fn test_list_properties() {
1317        let out = run_output(
1318            "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1319        );
1320        assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1321    }
1322
1323    #[test]
1324    fn test_recursive_function() {
1325        let out = run_output(
1326            "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1327        );
1328        assert_eq!(out, "[harn] 55");
1329    }
1330
1331    #[test]
1332    fn test_ternary() {
1333        let out = run_output(
1334            r#"pipeline t(task) { let x = 5
1335let r = x > 0 ? "positive" : "non-positive"
1336log(r) }"#,
1337        );
1338        assert_eq!(out, "[harn] positive");
1339    }
1340
1341    #[test]
1342    fn test_for_in_dict() {
1343        let out = run_output(
1344            "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1345        );
1346        assert_eq!(out, "[harn] a\n[harn] b");
1347    }
1348
1349    #[test]
1350    fn test_list_any_all() {
1351        let out = run_output(
1352            "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 })) }",
1353        );
1354        assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1355    }
1356
1357    #[test]
1358    fn test_disassembly() {
1359        let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1360        let tokens = lexer.tokenize().unwrap();
1361        let mut parser = Parser::new(tokens);
1362        let program = parser.parse().unwrap();
1363        let chunk = Compiler::new().compile(&program).unwrap();
1364        let disasm = chunk.disassemble("test");
1365        assert!(disasm.contains("CONSTANT"));
1366        assert!(disasm.contains("ADD"));
1367        assert!(disasm.contains("CALL"));
1368    }
1369
1370    // --- Error handling tests ---
1371
1372    #[test]
1373    fn test_try_catch_basic() {
1374        let out = run_output(
1375            r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1376        );
1377        assert_eq!(out, "[harn] caught: oops");
1378    }
1379
1380    #[test]
1381    fn test_try_no_error() {
1382        let out = run_output(
1383            r#"pipeline t(task) {
1384var result = 0
1385try { result = 42 } catch(e) { result = 0 }
1386log(result)
1387}"#,
1388        );
1389        assert_eq!(out, "[harn] 42");
1390    }
1391
1392    #[test]
1393    fn test_throw_uncaught() {
1394        let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1395        assert!(result.is_err());
1396    }
1397
1398    // --- Additional test coverage ---
1399
1400    fn run_vm(source: &str) -> String {
1401        let rt = tokio::runtime::Builder::new_current_thread()
1402            .enable_all()
1403            .build()
1404            .unwrap();
1405        rt.block_on(async {
1406            let local = tokio::task::LocalSet::new();
1407            local
1408                .run_until(async {
1409                    let mut lexer = Lexer::new(source);
1410                    let tokens = lexer.tokenize().unwrap();
1411                    let mut parser = Parser::new(tokens);
1412                    let program = parser.parse().unwrap();
1413                    let chunk = Compiler::new().compile(&program).unwrap();
1414                    let mut vm = Vm::new();
1415                    register_vm_stdlib(&mut vm);
1416                    vm.execute(&chunk).await.unwrap();
1417                    vm.output().to_string()
1418                })
1419                .await
1420        })
1421    }
1422
1423    fn run_vm_err(source: &str) -> String {
1424        let rt = tokio::runtime::Builder::new_current_thread()
1425            .enable_all()
1426            .build()
1427            .unwrap();
1428        rt.block_on(async {
1429            let local = tokio::task::LocalSet::new();
1430            local
1431                .run_until(async {
1432                    let mut lexer = Lexer::new(source);
1433                    let tokens = lexer.tokenize().unwrap();
1434                    let mut parser = Parser::new(tokens);
1435                    let program = parser.parse().unwrap();
1436                    let chunk = Compiler::new().compile(&program).unwrap();
1437                    let mut vm = Vm::new();
1438                    register_vm_stdlib(&mut vm);
1439                    match vm.execute(&chunk).await {
1440                        Err(e) => format!("{}", e),
1441                        Ok(_) => panic!("Expected error"),
1442                    }
1443                })
1444                .await
1445        })
1446    }
1447
1448    #[test]
1449    fn test_hello_world() {
1450        let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1451        assert_eq!(out, "[harn] hello\n");
1452    }
1453
1454    #[test]
1455    fn test_arithmetic_new() {
1456        let out = run_vm("pipeline default(task) { log(2 + 3) }");
1457        assert_eq!(out, "[harn] 5\n");
1458    }
1459
1460    #[test]
1461    fn test_string_concat_new() {
1462        let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1463        assert_eq!(out, "[harn] ab\n");
1464    }
1465
1466    #[test]
1467    fn test_if_else_new() {
1468        let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1469        assert_eq!(out, "[harn] 1\n");
1470    }
1471
1472    #[test]
1473    fn test_for_loop_new() {
1474        let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1475        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1476    }
1477
1478    #[test]
1479    fn test_while_loop_new() {
1480        let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1481        assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1482    }
1483
1484    #[test]
1485    fn test_function_call_new() {
1486        let out =
1487            run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1488        assert_eq!(out, "[harn] 5\n");
1489    }
1490
1491    #[test]
1492    fn test_closure_new() {
1493        let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1494        assert_eq!(out, "[harn] 10\n");
1495    }
1496
1497    #[test]
1498    fn test_recursion() {
1499        let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1500        assert_eq!(out, "[harn] 120\n");
1501    }
1502
1503    #[test]
1504    fn test_try_catch_new() {
1505        let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1506        assert_eq!(out, "[harn] err\n");
1507    }
1508
1509    #[test]
1510    fn test_try_no_error_new() {
1511        let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1512        assert_eq!(out, "[harn] 1\n");
1513    }
1514
1515    #[test]
1516    fn test_list_map_new() {
1517        let out =
1518            run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1519        assert_eq!(out, "[harn] [2, 4, 6]\n");
1520    }
1521
1522    #[test]
1523    fn test_list_filter_new() {
1524        let out = run_vm(
1525            "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1526        );
1527        assert_eq!(out, "[harn] [3, 4]\n");
1528    }
1529
1530    #[test]
1531    fn test_dict_access_new() {
1532        let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1533        assert_eq!(out, "[harn] Alice\n");
1534    }
1535
1536    #[test]
1537    fn test_string_interpolation() {
1538        let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1539        assert_eq!(out, "[harn] val=42\n");
1540    }
1541
1542    #[test]
1543    fn test_match_new() {
1544        let out = run_vm(
1545            "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1546        );
1547        assert_eq!(out, "[harn] 2\n");
1548    }
1549
1550    #[test]
1551    fn test_json_roundtrip() {
1552        let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1553        assert!(out.contains("\"a\""));
1554        assert!(out.contains("1"));
1555    }
1556
1557    #[test]
1558    fn test_type_of() {
1559        let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1560        assert_eq!(out, "[harn] int\n[harn] string\n");
1561    }
1562
1563    #[test]
1564    fn test_stack_overflow() {
1565        let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1566        assert!(
1567            err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1568            "Expected stack overflow error, got: {}",
1569            err
1570        );
1571    }
1572
1573    #[test]
1574    fn test_division_by_zero() {
1575        let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1576        assert!(
1577            err.contains("Division by zero") || err.contains("division"),
1578            "Expected division by zero error, got: {}",
1579            err
1580        );
1581    }
1582
1583    #[test]
1584    fn test_float_division_by_zero_uses_ieee_values() {
1585        let out = run_vm(
1586            "pipeline default(task) { log(is_nan(0.0 / 0.0))\nlog(is_infinite(1.0 / 0.0))\nlog(is_infinite(-1.0 / 0.0)) }",
1587        );
1588        assert_eq!(out, "[harn] true\n[harn] true\n[harn] true\n");
1589    }
1590
1591    #[test]
1592    fn test_reusing_catch_binding_name_in_same_block() {
1593        let out = run_vm(
1594            r#"pipeline default(task) {
1595try {
1596    throw "a"
1597} catch e {
1598    log(e)
1599}
1600try {
1601    throw "b"
1602} catch e {
1603    log(e)
1604}
1605}"#,
1606        );
1607        assert_eq!(out, "[harn] a\n[harn] b\n");
1608    }
1609
1610    #[test]
1611    fn test_try_catch_nested() {
1612        let out = run_output(
1613            r#"pipeline t(task) {
1614try {
1615    try {
1616        throw "inner"
1617    } catch(e) {
1618        log("inner caught: " + e)
1619        throw "outer"
1620    }
1621} catch(e2) {
1622    log("outer caught: " + e2)
1623}
1624}"#,
1625        );
1626        assert_eq!(
1627            out,
1628            "[harn] inner caught: inner\n[harn] outer caught: outer"
1629        );
1630    }
1631
1632    // --- Concurrency tests ---
1633
1634    #[test]
1635    fn test_parallel_basic() {
1636        let out = run_output(
1637            "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1638        );
1639        assert_eq!(out, "[harn] [0, 10, 20]");
1640    }
1641
1642    #[test]
1643    fn test_parallel_no_variable() {
1644        let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1645        assert_eq!(out, "[harn] [42, 42, 42]");
1646    }
1647
1648    #[test]
1649    fn test_parallel_map_basic() {
1650        let out = run_output(
1651            "pipeline t(task) { let results = parallel_map([1, 2, 3]) { x -> x * x }\nlog(results) }",
1652        );
1653        assert_eq!(out, "[harn] [1, 4, 9]");
1654    }
1655
1656    #[test]
1657    fn test_spawn_await() {
1658        let out = run_output(
1659            r#"pipeline t(task) {
1660let handle = spawn { log("spawned") }
1661let result = await(handle)
1662log("done")
1663}"#,
1664        );
1665        assert_eq!(out, "[harn] spawned\n[harn] done");
1666    }
1667
1668    #[test]
1669    fn test_spawn_cancel() {
1670        let out = run_output(
1671            r#"pipeline t(task) {
1672let handle = spawn { log("should be cancelled") }
1673cancel(handle)
1674log("cancelled")
1675}"#,
1676        );
1677        assert_eq!(out, "[harn] cancelled");
1678    }
1679
1680    #[test]
1681    fn test_spawn_returns_value() {
1682        let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1683        assert_eq!(out, "[harn] 42");
1684    }
1685
1686    // --- Deadline tests ---
1687
1688    #[test]
1689    fn test_deadline_success() {
1690        let out = run_output(
1691            r#"pipeline t(task) {
1692let result = deadline 5s { log("within deadline")
169342 }
1694log(result)
1695}"#,
1696        );
1697        assert_eq!(out, "[harn] within deadline\n[harn] 42");
1698    }
1699
1700    #[test]
1701    fn test_deadline_exceeded() {
1702        let result = run_harn_result(
1703            r#"pipeline t(task) {
1704deadline 1ms {
1705  var i = 0
1706  while i < 1000000 { i = i + 1 }
1707}
1708}"#,
1709        );
1710        assert!(result.is_err());
1711    }
1712
1713    #[test]
1714    fn test_deadline_caught_by_try() {
1715        let out = run_output(
1716            r#"pipeline t(task) {
1717try {
1718  deadline 1ms {
1719    var i = 0
1720    while i < 1000000 { i = i + 1 }
1721  }
1722} catch(e) {
1723  log("caught")
1724}
1725}"#,
1726        );
1727        assert_eq!(out, "[harn] caught");
1728    }
1729
1730    /// Helper that runs Harn source with a set of denied builtins.
1731    fn run_harn_with_denied(
1732        source: &str,
1733        denied: HashSet<String>,
1734    ) -> Result<(String, VmValue), VmError> {
1735        let rt = tokio::runtime::Builder::new_current_thread()
1736            .enable_all()
1737            .build()
1738            .unwrap();
1739        rt.block_on(async {
1740            let local = tokio::task::LocalSet::new();
1741            local
1742                .run_until(async {
1743                    let mut lexer = Lexer::new(source);
1744                    let tokens = lexer.tokenize().unwrap();
1745                    let mut parser = Parser::new(tokens);
1746                    let program = parser.parse().unwrap();
1747                    let chunk = Compiler::new().compile(&program).unwrap();
1748
1749                    let mut vm = Vm::new();
1750                    register_vm_stdlib(&mut vm);
1751                    vm.set_denied_builtins(denied);
1752                    let result = vm.execute(&chunk).await?;
1753                    Ok((vm.output().to_string(), result))
1754                })
1755                .await
1756        })
1757    }
1758
1759    #[test]
1760    fn test_sandbox_deny_builtin() {
1761        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1762        let result = run_harn_with_denied(
1763            r#"pipeline t(task) {
1764let xs = [1, 2]
1765push(xs, 3)
1766}"#,
1767            denied,
1768        );
1769        let err = result.unwrap_err();
1770        let msg = format!("{err}");
1771        assert!(
1772            msg.contains("not permitted"),
1773            "expected not permitted, got: {msg}"
1774        );
1775        assert!(
1776            msg.contains("push"),
1777            "expected builtin name in error, got: {msg}"
1778        );
1779    }
1780
1781    #[test]
1782    fn test_sandbox_allowed_builtin_works() {
1783        // Denying "push" should not block "log"
1784        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1785        let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
1786        let (output, _) = result.unwrap();
1787        assert_eq!(output.trim(), "[harn] hello");
1788    }
1789
1790    #[test]
1791    fn test_sandbox_empty_denied_set() {
1792        // With an empty denied set, everything should work.
1793        let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
1794        let (output, _) = result.unwrap();
1795        assert_eq!(output.trim(), "[harn] ok");
1796    }
1797
1798    #[test]
1799    fn test_sandbox_propagates_to_spawn() {
1800        // Denied builtins should propagate to spawned VMs.
1801        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1802        let result = run_harn_with_denied(
1803            r#"pipeline t(task) {
1804let handle = spawn {
1805  let xs = [1, 2]
1806  push(xs, 3)
1807}
1808await(handle)
1809}"#,
1810            denied,
1811        );
1812        let err = result.unwrap_err();
1813        let msg = format!("{err}");
1814        assert!(
1815            msg.contains("not permitted"),
1816            "expected not permitted in spawned VM, got: {msg}"
1817        );
1818    }
1819
1820    #[test]
1821    fn test_sandbox_propagates_to_parallel() {
1822        // Denied builtins should propagate to parallel VMs.
1823        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1824        let result = run_harn_with_denied(
1825            r#"pipeline t(task) {
1826let results = parallel(2) { i ->
1827  let xs = [1, 2]
1828  push(xs, 3)
1829}
1830}"#,
1831            denied,
1832        );
1833        let err = result.unwrap_err();
1834        let msg = format!("{err}");
1835        assert!(
1836            msg.contains("not permitted"),
1837            "expected not permitted in parallel VM, got: {msg}"
1838        );
1839    }
1840
1841    #[test]
1842    fn test_if_else_has_lexical_block_scope() {
1843        let out = run_output(
1844            r#"pipeline t(task) {
1845let x = "outer"
1846if true {
1847  let x = "inner"
1848  log(x)
1849} else {
1850  let x = "other"
1851  log(x)
1852}
1853log(x)
1854}"#,
1855        );
1856        assert_eq!(out, "[harn] inner\n[harn] outer");
1857    }
1858
1859    #[test]
1860    fn test_loop_and_catch_bindings_are_block_scoped() {
1861        let out = run_output(
1862            r#"pipeline t(task) {
1863let label = "outer"
1864for item in [1, 2] {
1865  let label = "loop " + item
1866  log(label)
1867}
1868try {
1869  throw("boom")
1870} catch (label) {
1871  log(label)
1872}
1873log(label)
1874}"#,
1875        );
1876        assert_eq!(
1877            out,
1878            "[harn] loop 1\n[harn] loop 2\n[harn] boom\n[harn] outer"
1879        );
1880    }
1881}