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