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