Skip to main content

harn_vm/
vm.rs

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