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