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