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