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