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