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