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