Skip to main content

harn_vm/
vm.rs

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