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            Err(VmError::UndefinedBuiltin(name.to_string()))
867        }
868    }
869}
870
871impl Default for Vm {
872    fn default() -> Self {
873        Self::new()
874    }
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880    use crate::compiler::Compiler;
881    use crate::stdlib::register_vm_stdlib;
882    use harn_lexer::Lexer;
883    use harn_parser::Parser;
884
885    fn run_harn(source: &str) -> (String, VmValue) {
886        let rt = tokio::runtime::Builder::new_current_thread()
887            .enable_all()
888            .build()
889            .unwrap();
890        rt.block_on(async {
891            let local = tokio::task::LocalSet::new();
892            local
893                .run_until(async {
894                    let mut lexer = Lexer::new(source);
895                    let tokens = lexer.tokenize().unwrap();
896                    let mut parser = Parser::new(tokens);
897                    let program = parser.parse().unwrap();
898                    let chunk = Compiler::new().compile(&program).unwrap();
899
900                    let mut vm = Vm::new();
901                    register_vm_stdlib(&mut vm);
902                    let result = vm.execute(&chunk).await.unwrap();
903                    (vm.output().to_string(), result)
904                })
905                .await
906        })
907    }
908
909    fn run_output(source: &str) -> String {
910        run_harn(source).0.trim_end().to_string()
911    }
912
913    fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
914        let rt = tokio::runtime::Builder::new_current_thread()
915            .enable_all()
916            .build()
917            .unwrap();
918        rt.block_on(async {
919            let local = tokio::task::LocalSet::new();
920            local
921                .run_until(async {
922                    let mut lexer = Lexer::new(source);
923                    let tokens = lexer.tokenize().unwrap();
924                    let mut parser = Parser::new(tokens);
925                    let program = parser.parse().unwrap();
926                    let chunk = Compiler::new().compile(&program).unwrap();
927
928                    let mut vm = Vm::new();
929                    register_vm_stdlib(&mut vm);
930                    let result = vm.execute(&chunk).await?;
931                    Ok((vm.output().to_string(), result))
932                })
933                .await
934        })
935    }
936
937    #[test]
938    fn test_arithmetic() {
939        let out =
940            run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
941        assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
942    }
943
944    #[test]
945    fn test_mixed_arithmetic() {
946        let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
947        assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
948    }
949
950    #[test]
951    fn test_comparisons() {
952        let out =
953            run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
954        assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
955    }
956
957    #[test]
958    fn test_let_var() {
959        let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
960        assert_eq!(out, "[harn] 42\n[harn] 2");
961    }
962
963    #[test]
964    fn test_if_else() {
965        let out = run_output(
966            r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
967        );
968        assert_eq!(out, "[harn] yes\n[harn] no");
969    }
970
971    #[test]
972    fn test_while_loop() {
973        let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
974        assert_eq!(out, "[harn] 5");
975    }
976
977    #[test]
978    fn test_for_in() {
979        let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
980        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
981    }
982
983    #[test]
984    fn test_fn_decl_and_call() {
985        let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
986        assert_eq!(out, "[harn] 7");
987    }
988
989    #[test]
990    fn test_closure() {
991        let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
992        assert_eq!(out, "[harn] 10");
993    }
994
995    #[test]
996    fn test_closure_capture() {
997        let out = run_output(
998            "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
999        );
1000        assert_eq!(out, "[harn] 15");
1001    }
1002
1003    #[test]
1004    fn test_string_concat() {
1005        let out = run_output(
1006            r#"pipeline t(task) { let a = "hello" + " " + "world"
1007log(a) }"#,
1008        );
1009        assert_eq!(out, "[harn] hello world");
1010    }
1011
1012    #[test]
1013    fn test_list_map() {
1014        let out = run_output(
1015            "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1016        );
1017        assert_eq!(out, "[harn] [2, 4, 6]");
1018    }
1019
1020    #[test]
1021    fn test_list_filter() {
1022        let out = run_output(
1023            "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1024        );
1025        assert_eq!(out, "[harn] [4, 5]");
1026    }
1027
1028    #[test]
1029    fn test_list_reduce() {
1030        let out = run_output(
1031            "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1032        );
1033        assert_eq!(out, "[harn] 10");
1034    }
1035
1036    #[test]
1037    fn test_dict_access() {
1038        let out = run_output(
1039            r#"pipeline t(task) { let d = {name: "test", value: 42}
1040log(d.name)
1041log(d.value) }"#,
1042        );
1043        assert_eq!(out, "[harn] test\n[harn] 42");
1044    }
1045
1046    #[test]
1047    fn test_dict_methods() {
1048        let out = run_output(
1049            r#"pipeline t(task) { let d = {a: 1, b: 2}
1050log(d.keys())
1051log(d.values())
1052log(d.has("a"))
1053log(d.has("z")) }"#,
1054        );
1055        assert_eq!(
1056            out,
1057            "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1058        );
1059    }
1060
1061    #[test]
1062    fn test_pipe_operator() {
1063        let out = run_output(
1064            "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1065        );
1066        assert_eq!(out, "[harn] 10");
1067    }
1068
1069    #[test]
1070    fn test_pipe_with_closure() {
1071        let out = run_output(
1072            r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1073log(r) }"#,
1074        );
1075        assert_eq!(out, "[harn] [hello, world]");
1076    }
1077
1078    #[test]
1079    fn test_nil_coalescing() {
1080        let out = run_output(
1081            r#"pipeline t(task) { let a = nil ?? "fallback"
1082log(a)
1083let b = "present" ?? "fallback"
1084log(b) }"#,
1085        );
1086        assert_eq!(out, "[harn] fallback\n[harn] present");
1087    }
1088
1089    #[test]
1090    fn test_logical_operators() {
1091        let out =
1092            run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1093        assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1094    }
1095
1096    #[test]
1097    fn test_match() {
1098        let out = run_output(
1099            r#"pipeline t(task) { let x = "b"
1100match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1101        );
1102        assert_eq!(out, "[harn] second");
1103    }
1104
1105    #[test]
1106    fn test_subscript() {
1107        let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1108        assert_eq!(out, "[harn] 20");
1109    }
1110
1111    #[test]
1112    fn test_string_methods() {
1113        let out = run_output(
1114            r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1115log("a,b,c".split(","))
1116log("  hello  ".trim())
1117log("hello".starts_with("hel"))
1118log("hello".ends_with("lo"))
1119log("hello".substring(1, 3)) }"#,
1120        );
1121        assert_eq!(
1122            out,
1123            "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1124        );
1125    }
1126
1127    #[test]
1128    fn test_list_properties() {
1129        let out = run_output(
1130            "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1131        );
1132        assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1133    }
1134
1135    #[test]
1136    fn test_recursive_function() {
1137        let out = run_output(
1138            "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1139        );
1140        assert_eq!(out, "[harn] 55");
1141    }
1142
1143    #[test]
1144    fn test_ternary() {
1145        let out = run_output(
1146            r#"pipeline t(task) { let x = 5
1147let r = x > 0 ? "positive" : "non-positive"
1148log(r) }"#,
1149        );
1150        assert_eq!(out, "[harn] positive");
1151    }
1152
1153    #[test]
1154    fn test_for_in_dict() {
1155        let out = run_output(
1156            "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1157        );
1158        assert_eq!(out, "[harn] a\n[harn] b");
1159    }
1160
1161    #[test]
1162    fn test_list_any_all() {
1163        let out = run_output(
1164            "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 })) }",
1165        );
1166        assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1167    }
1168
1169    #[test]
1170    fn test_disassembly() {
1171        let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1172        let tokens = lexer.tokenize().unwrap();
1173        let mut parser = Parser::new(tokens);
1174        let program = parser.parse().unwrap();
1175        let chunk = Compiler::new().compile(&program).unwrap();
1176        let disasm = chunk.disassemble("test");
1177        assert!(disasm.contains("CONSTANT"));
1178        assert!(disasm.contains("ADD"));
1179        assert!(disasm.contains("CALL"));
1180    }
1181
1182    // --- Error handling tests ---
1183
1184    #[test]
1185    fn test_try_catch_basic() {
1186        let out = run_output(
1187            r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1188        );
1189        assert_eq!(out, "[harn] caught: oops");
1190    }
1191
1192    #[test]
1193    fn test_try_no_error() {
1194        let out = run_output(
1195            r#"pipeline t(task) {
1196var result = 0
1197try { result = 42 } catch(e) { result = 0 }
1198log(result)
1199}"#,
1200        );
1201        assert_eq!(out, "[harn] 42");
1202    }
1203
1204    #[test]
1205    fn test_throw_uncaught() {
1206        let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1207        assert!(result.is_err());
1208    }
1209
1210    // --- Additional test coverage ---
1211
1212    fn run_vm(source: &str) -> String {
1213        let rt = tokio::runtime::Builder::new_current_thread()
1214            .enable_all()
1215            .build()
1216            .unwrap();
1217        rt.block_on(async {
1218            let local = tokio::task::LocalSet::new();
1219            local
1220                .run_until(async {
1221                    let mut lexer = Lexer::new(source);
1222                    let tokens = lexer.tokenize().unwrap();
1223                    let mut parser = Parser::new(tokens);
1224                    let program = parser.parse().unwrap();
1225                    let chunk = Compiler::new().compile(&program).unwrap();
1226                    let mut vm = Vm::new();
1227                    register_vm_stdlib(&mut vm);
1228                    vm.execute(&chunk).await.unwrap();
1229                    vm.output().to_string()
1230                })
1231                .await
1232        })
1233    }
1234
1235    fn run_vm_err(source: &str) -> String {
1236        let rt = tokio::runtime::Builder::new_current_thread()
1237            .enable_all()
1238            .build()
1239            .unwrap();
1240        rt.block_on(async {
1241            let local = tokio::task::LocalSet::new();
1242            local
1243                .run_until(async {
1244                    let mut lexer = Lexer::new(source);
1245                    let tokens = lexer.tokenize().unwrap();
1246                    let mut parser = Parser::new(tokens);
1247                    let program = parser.parse().unwrap();
1248                    let chunk = Compiler::new().compile(&program).unwrap();
1249                    let mut vm = Vm::new();
1250                    register_vm_stdlib(&mut vm);
1251                    match vm.execute(&chunk).await {
1252                        Err(e) => format!("{}", e),
1253                        Ok(_) => panic!("Expected error"),
1254                    }
1255                })
1256                .await
1257        })
1258    }
1259
1260    #[test]
1261    fn test_hello_world() {
1262        let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1263        assert_eq!(out, "[harn] hello\n");
1264    }
1265
1266    #[test]
1267    fn test_arithmetic_new() {
1268        let out = run_vm("pipeline default(task) { log(2 + 3) }");
1269        assert_eq!(out, "[harn] 5\n");
1270    }
1271
1272    #[test]
1273    fn test_string_concat_new() {
1274        let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1275        assert_eq!(out, "[harn] ab\n");
1276    }
1277
1278    #[test]
1279    fn test_if_else_new() {
1280        let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1281        assert_eq!(out, "[harn] 1\n");
1282    }
1283
1284    #[test]
1285    fn test_for_loop_new() {
1286        let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1287        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1288    }
1289
1290    #[test]
1291    fn test_while_loop_new() {
1292        let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1293        assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1294    }
1295
1296    #[test]
1297    fn test_function_call_new() {
1298        let out =
1299            run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1300        assert_eq!(out, "[harn] 5\n");
1301    }
1302
1303    #[test]
1304    fn test_closure_new() {
1305        let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1306        assert_eq!(out, "[harn] 10\n");
1307    }
1308
1309    #[test]
1310    fn test_recursion() {
1311        let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1312        assert_eq!(out, "[harn] 120\n");
1313    }
1314
1315    #[test]
1316    fn test_try_catch_new() {
1317        let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1318        assert_eq!(out, "[harn] err\n");
1319    }
1320
1321    #[test]
1322    fn test_try_no_error_new() {
1323        let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1324        assert_eq!(out, "[harn] 1\n");
1325    }
1326
1327    #[test]
1328    fn test_list_map_new() {
1329        let out =
1330            run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1331        assert_eq!(out, "[harn] [2, 4, 6]\n");
1332    }
1333
1334    #[test]
1335    fn test_list_filter_new() {
1336        let out = run_vm(
1337            "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1338        );
1339        assert_eq!(out, "[harn] [3, 4]\n");
1340    }
1341
1342    #[test]
1343    fn test_dict_access_new() {
1344        let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1345        assert_eq!(out, "[harn] Alice\n");
1346    }
1347
1348    #[test]
1349    fn test_string_interpolation() {
1350        let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1351        assert_eq!(out, "[harn] val=42\n");
1352    }
1353
1354    #[test]
1355    fn test_match_new() {
1356        let out = run_vm(
1357            "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1358        );
1359        assert_eq!(out, "[harn] 2\n");
1360    }
1361
1362    #[test]
1363    fn test_json_roundtrip() {
1364        let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1365        assert!(out.contains("\"a\""));
1366        assert!(out.contains("1"));
1367    }
1368
1369    #[test]
1370    fn test_type_of() {
1371        let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1372        assert_eq!(out, "[harn] int\n[harn] string\n");
1373    }
1374
1375    #[test]
1376    fn test_stack_overflow() {
1377        let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1378        assert!(
1379            err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1380            "Expected stack overflow error, got: {}",
1381            err
1382        );
1383    }
1384
1385    #[test]
1386    fn test_division_by_zero() {
1387        let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1388        assert!(
1389            err.contains("Division by zero") || err.contains("division"),
1390            "Expected division by zero error, got: {}",
1391            err
1392        );
1393    }
1394
1395    #[test]
1396    fn test_try_catch_nested() {
1397        let out = run_output(
1398            r#"pipeline t(task) {
1399try {
1400    try {
1401        throw "inner"
1402    } catch(e) {
1403        log("inner caught: " + e)
1404        throw "outer"
1405    }
1406} catch(e) {
1407    log("outer caught: " + e)
1408}
1409}"#,
1410        );
1411        assert_eq!(
1412            out,
1413            "[harn] inner caught: inner\n[harn] outer caught: outer"
1414        );
1415    }
1416
1417    // --- Concurrency tests ---
1418
1419    #[test]
1420    fn test_parallel_basic() {
1421        let out = run_output(
1422            "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1423        );
1424        assert_eq!(out, "[harn] [0, 10, 20]");
1425    }
1426
1427    #[test]
1428    fn test_parallel_no_variable() {
1429        let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1430        assert_eq!(out, "[harn] [42, 42, 42]");
1431    }
1432
1433    #[test]
1434    fn test_parallel_map_basic() {
1435        let out = run_output(
1436            "pipeline t(task) { let results = parallel_map([1, 2, 3]) { x -> x * x }\nlog(results) }",
1437        );
1438        assert_eq!(out, "[harn] [1, 4, 9]");
1439    }
1440
1441    #[test]
1442    fn test_spawn_await() {
1443        let out = run_output(
1444            r#"pipeline t(task) {
1445let handle = spawn { log("spawned") }
1446let result = await(handle)
1447log("done")
1448}"#,
1449        );
1450        assert_eq!(out, "[harn] spawned\n[harn] done");
1451    }
1452
1453    #[test]
1454    fn test_spawn_cancel() {
1455        let out = run_output(
1456            r#"pipeline t(task) {
1457let handle = spawn { log("should be cancelled") }
1458cancel(handle)
1459log("cancelled")
1460}"#,
1461        );
1462        assert_eq!(out, "[harn] cancelled");
1463    }
1464
1465    #[test]
1466    fn test_spawn_returns_value() {
1467        let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1468        assert_eq!(out, "[harn] 42");
1469    }
1470
1471    // --- Deadline tests ---
1472
1473    #[test]
1474    fn test_deadline_success() {
1475        let out = run_output(
1476            r#"pipeline t(task) {
1477let result = deadline 5s { log("within deadline")
147842 }
1479log(result)
1480}"#,
1481        );
1482        assert_eq!(out, "[harn] within deadline\n[harn] 42");
1483    }
1484
1485    #[test]
1486    fn test_deadline_exceeded() {
1487        let result = run_harn_result(
1488            r#"pipeline t(task) {
1489deadline 1ms {
1490  var i = 0
1491  while i < 1000000 { i = i + 1 }
1492}
1493}"#,
1494        );
1495        assert!(result.is_err());
1496    }
1497
1498    #[test]
1499    fn test_deadline_caught_by_try() {
1500        let out = run_output(
1501            r#"pipeline t(task) {
1502try {
1503  deadline 1ms {
1504    var i = 0
1505    while i < 1000000 { i = i + 1 }
1506  }
1507} catch(e) {
1508  log("caught")
1509}
1510}"#,
1511        );
1512        assert_eq!(out, "[harn] caught");
1513    }
1514
1515    /// Helper that runs Harn source with a set of denied builtins.
1516    fn run_harn_with_denied(
1517        source: &str,
1518        denied: HashSet<String>,
1519    ) -> Result<(String, VmValue), VmError> {
1520        let rt = tokio::runtime::Builder::new_current_thread()
1521            .enable_all()
1522            .build()
1523            .unwrap();
1524        rt.block_on(async {
1525            let local = tokio::task::LocalSet::new();
1526            local
1527                .run_until(async {
1528                    let mut lexer = Lexer::new(source);
1529                    let tokens = lexer.tokenize().unwrap();
1530                    let mut parser = Parser::new(tokens);
1531                    let program = parser.parse().unwrap();
1532                    let chunk = Compiler::new().compile(&program).unwrap();
1533
1534                    let mut vm = Vm::new();
1535                    register_vm_stdlib(&mut vm);
1536                    vm.set_denied_builtins(denied);
1537                    let result = vm.execute(&chunk).await?;
1538                    Ok((vm.output().to_string(), result))
1539                })
1540                .await
1541        })
1542    }
1543
1544    #[test]
1545    fn test_sandbox_deny_builtin() {
1546        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1547        let result = run_harn_with_denied(
1548            r#"pipeline t(task) {
1549let xs = [1, 2]
1550push(xs, 3)
1551}"#,
1552            denied,
1553        );
1554        let err = result.unwrap_err();
1555        let msg = format!("{err}");
1556        assert!(
1557            msg.contains("Permission denied"),
1558            "expected permission denied, got: {msg}"
1559        );
1560        assert!(
1561            msg.contains("push"),
1562            "expected builtin name in error, got: {msg}"
1563        );
1564    }
1565
1566    #[test]
1567    fn test_sandbox_allowed_builtin_works() {
1568        // Denying "push" should not block "log"
1569        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1570        let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
1571        let (output, _) = result.unwrap();
1572        assert_eq!(output.trim(), "[harn] hello");
1573    }
1574
1575    #[test]
1576    fn test_sandbox_empty_denied_set() {
1577        // With an empty denied set, everything should work.
1578        let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
1579        let (output, _) = result.unwrap();
1580        assert_eq!(output.trim(), "[harn] ok");
1581    }
1582
1583    #[test]
1584    fn test_sandbox_propagates_to_spawn() {
1585        // Denied builtins should propagate to spawned VMs.
1586        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1587        let result = run_harn_with_denied(
1588            r#"pipeline t(task) {
1589let handle = spawn {
1590  let xs = [1, 2]
1591  push(xs, 3)
1592}
1593await(handle)
1594}"#,
1595            denied,
1596        );
1597        let err = result.unwrap_err();
1598        let msg = format!("{err}");
1599        assert!(
1600            msg.contains("Permission denied"),
1601            "expected permission denied in spawned VM, got: {msg}"
1602        );
1603    }
1604
1605    #[test]
1606    fn test_sandbox_propagates_to_parallel() {
1607        // Denied builtins should propagate to parallel VMs.
1608        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1609        let result = run_harn_with_denied(
1610            r#"pipeline t(task) {
1611let results = parallel(2) { i ->
1612  let xs = [1, 2]
1613  push(xs, 3)
1614}
1615}"#,
1616            denied,
1617        );
1618        let err = result.unwrap_err();
1619        let msg = format!("{err}");
1620        assert!(
1621            msg.contains("Permission denied"),
1622            "expected permission denied in parallel VM, got: {msg}"
1623        );
1624    }
1625}