Skip to main content

harn_vm/
vm.rs

1mod format;
2mod imports;
3pub mod iter;
4mod methods;
5mod ops;
6
7use std::cell::RefCell;
8use std::collections::{BTreeMap, HashSet};
9use std::future::Future;
10use std::pin::Pin;
11use std::rc::Rc;
12use std::time::Instant;
13
14use crate::chunk::{Chunk, CompiledFunction, Constant};
15use crate::value::{
16    ErrorCategory, ModuleFunctionRegistry, VmAsyncBuiltinFn, VmBuiltinFn, VmClosure, VmEnv,
17    VmError, VmTaskHandle, VmValue,
18};
19
20thread_local! {
21    static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> = const { RefCell::new(Vec::new()) };
22}
23
24/// RAII guard that starts a tracing span on creation and ends it on drop.
25struct ScopeSpan(u64);
26
27impl ScopeSpan {
28    fn new(kind: crate::tracing::SpanKind, name: String) -> Self {
29        Self(crate::tracing::span_start(kind, name))
30    }
31}
32
33impl Drop for ScopeSpan {
34    fn drop(&mut self) {
35        crate::tracing::span_end(self.0);
36    }
37}
38
39/// Call frame for function execution.
40pub(crate) struct CallFrame {
41    pub(crate) chunk: Chunk,
42    pub(crate) ip: usize,
43    pub(crate) stack_base: usize,
44    pub(crate) saved_env: VmEnv,
45    /// Iterator stack depth to restore when this frame unwinds.
46    pub(crate) saved_iterator_depth: usize,
47    /// Function name for stack traces (empty for top-level pipeline).
48    pub(crate) fn_name: String,
49    /// Number of arguments actually passed by the caller (for default arg support).
50    pub(crate) argc: usize,
51    /// Saved VM_SOURCE_DIR to restore when this frame is popped.
52    /// Set when entering a closure that originated from an imported module.
53    pub(crate) saved_source_dir: Option<std::path::PathBuf>,
54    /// Module-local named functions available to symbolic calls within this frame.
55    pub(crate) module_functions: Option<ModuleFunctionRegistry>,
56    /// Shared module-level env for top-level `var` / `let` bindings of
57    /// this frame's originating module. Looked up after `self.env` and
58    /// before `self.globals` by `GetVar` / `SetVar`, giving each module
59    /// its own live static state that persists across calls. See the
60    /// `module_state` field on `VmClosure` for the full rationale.
61    pub(crate) module_state: Option<crate::value::ModuleState>,
62}
63
64/// Exception handler for try/catch.
65pub(crate) struct ExceptionHandler {
66    pub(crate) catch_ip: usize,
67    pub(crate) stack_depth: usize,
68    pub(crate) frame_depth: usize,
69    pub(crate) env_scope_depth: usize,
70    /// If non-empty, this catch only handles errors whose enum_name matches.
71    pub(crate) error_type: String,
72}
73
74/// Debug action returned by the debug hook.
75#[derive(Debug, Clone, PartialEq)]
76pub enum DebugAction {
77    /// Continue execution normally.
78    Continue,
79    /// Stop (breakpoint hit, step complete).
80    Stop,
81}
82
83/// Information about current execution state for the debugger.
84#[derive(Debug, Clone)]
85pub struct DebugState {
86    pub line: usize,
87    pub variables: BTreeMap<String, VmValue>,
88    pub frame_name: String,
89    pub frame_depth: usize,
90}
91
92type DebugHook = dyn FnMut(&DebugState) -> DebugAction;
93
94/// Iterator state for for-in loops: either a pre-collected vec, an async channel, or a generator.
95pub(crate) enum IterState {
96    Vec {
97        items: Vec<VmValue>,
98        idx: usize,
99    },
100    Channel {
101        receiver: std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
102        closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
103    },
104    Generator {
105        gen: crate::value::VmGenerator,
106    },
107    /// Step through a lazy range without materializing a Vec.
108    /// `next` holds the value to emit on the next IterNext; `stop` is
109    /// the first value that terminates the iteration (one past the end).
110    Range {
111        next: i64,
112        stop: i64,
113    },
114    VmIter {
115        handle: std::rc::Rc<std::cell::RefCell<crate::vm::iter::VmIter>>,
116    },
117}
118
119#[derive(Clone)]
120pub(crate) struct LoadedModule {
121    pub(crate) functions: BTreeMap<String, Rc<VmClosure>>,
122    pub(crate) public_names: HashSet<String>,
123}
124
125/// The Harn bytecode virtual machine.
126pub struct Vm {
127    pub(crate) stack: Vec<VmValue>,
128    pub(crate) env: VmEnv,
129    pub(crate) output: String,
130    pub(crate) builtins: BTreeMap<String, VmBuiltinFn>,
131    pub(crate) async_builtins: BTreeMap<String, VmAsyncBuiltinFn>,
132    /// Iterator state for for-in loops.
133    pub(crate) iterators: Vec<IterState>,
134    /// Call frame stack.
135    pub(crate) frames: Vec<CallFrame>,
136    /// Exception handler stack.
137    pub(crate) exception_handlers: Vec<ExceptionHandler>,
138    /// Spawned async task handles.
139    pub(crate) spawned_tasks: BTreeMap<String, VmTaskHandle>,
140    /// Counter for generating unique task IDs.
141    pub(crate) task_counter: u64,
142    /// Active deadline stack: (deadline_instant, frame_depth).
143    pub(crate) deadlines: Vec<(Instant, usize)>,
144    /// Breakpoints (source line numbers).
145    pub(crate) breakpoints: Vec<usize>,
146    /// Whether the VM is in step mode.
147    pub(crate) step_mode: bool,
148    /// The frame depth at which stepping started (for step-over).
149    pub(crate) step_frame_depth: usize,
150    /// Whether the VM is currently stopped at a debug point.
151    pub(crate) stopped: bool,
152    /// Last source line executed (to detect line changes).
153    pub(crate) last_line: usize,
154    /// Source directory for resolving imports.
155    pub(crate) source_dir: Option<std::path::PathBuf>,
156    /// Modules currently being imported (cycle prevention).
157    pub(crate) imported_paths: Vec<std::path::PathBuf>,
158    /// Loaded module cache keyed by canonical or synthetic module path.
159    pub(crate) module_cache: BTreeMap<std::path::PathBuf, LoadedModule>,
160    /// Source file path for error reporting.
161    pub(crate) source_file: Option<String>,
162    /// Source text for error reporting.
163    pub(crate) source_text: Option<String>,
164    /// Optional bridge for delegating unknown builtins in bridge mode.
165    pub(crate) bridge: Option<Rc<crate::bridge::HostBridge>>,
166    /// Builtins denied by sandbox mode (`--deny` / `--allow` flags).
167    pub(crate) denied_builtins: HashSet<String>,
168    /// Cancellation token for cooperative graceful shutdown (set by parent).
169    pub(crate) cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
170    /// Captured stack trace from the most recent error (fn_name, line, col).
171    pub(crate) error_stack_trace: Vec<(String, usize, usize, Option<String>)>,
172    /// Yield channel sender for generator execution. When set, `Op::Yield`
173    /// sends values through this channel instead of being a no-op.
174    pub(crate) yield_sender: Option<tokio::sync::mpsc::Sender<VmValue>>,
175    /// Project root directory (detected via harn.toml).
176    /// Used as base directory for metadata, store, and checkpoint operations.
177    pub(crate) project_root: Option<std::path::PathBuf>,
178    /// Global constants (e.g. `pi`, `e`). Checked as a fallback in `GetVar`
179    /// after the environment, so user-defined variables can shadow them.
180    pub(crate) globals: BTreeMap<String, VmValue>,
181    /// Optional debugger hook invoked when execution advances to a new source line.
182    pub(crate) debug_hook: Option<Box<DebugHook>>,
183}
184
185impl Vm {
186    pub fn new() -> Self {
187        Self {
188            stack: Vec::with_capacity(256),
189            env: VmEnv::new(),
190            output: String::new(),
191            builtins: BTreeMap::new(),
192            async_builtins: BTreeMap::new(),
193            iterators: Vec::new(),
194            frames: Vec::new(),
195            exception_handlers: Vec::new(),
196            spawned_tasks: BTreeMap::new(),
197            task_counter: 0,
198            deadlines: Vec::new(),
199            breakpoints: Vec::new(),
200            step_mode: false,
201            step_frame_depth: 0,
202            stopped: false,
203            last_line: 0,
204            source_dir: None,
205            imported_paths: Vec::new(),
206            module_cache: BTreeMap::new(),
207            source_file: None,
208            source_text: None,
209            bridge: None,
210            denied_builtins: HashSet::new(),
211            cancel_token: None,
212            error_stack_trace: Vec::new(),
213            yield_sender: None,
214            project_root: None,
215            globals: BTreeMap::new(),
216            debug_hook: None,
217        }
218    }
219
220    /// Set the bridge for delegating unknown builtins in bridge mode.
221    pub fn set_bridge(&mut self, bridge: Rc<crate::bridge::HostBridge>) {
222        self.bridge = Some(bridge);
223    }
224
225    /// Set builtins that are denied in sandbox mode.
226    /// When called, the given builtin names will produce a permission error.
227    pub fn set_denied_builtins(&mut self, denied: HashSet<String>) {
228        self.denied_builtins = denied;
229    }
230
231    /// Set source info for error reporting (file path and source text).
232    pub fn set_source_info(&mut self, file: &str, text: &str) {
233        self.source_file = Some(file.to_string());
234        self.source_text = Some(text.to_string());
235    }
236
237    /// Set breakpoints by source line number.
238    pub fn set_breakpoints(&mut self, lines: Vec<usize>) {
239        self.breakpoints = lines;
240    }
241
242    /// Enable step mode (stop at next line).
243    pub fn set_step_mode(&mut self, step: bool) {
244        self.step_mode = step;
245        self.step_frame_depth = self.frames.len();
246    }
247
248    /// Enable step-over mode (stop at next line at same or lower frame depth).
249    pub fn set_step_over(&mut self) {
250        self.step_mode = true;
251        self.step_frame_depth = self.frames.len();
252    }
253
254    /// Register a debug hook invoked whenever execution advances to a new source line.
255    pub fn set_debug_hook<F>(&mut self, hook: F)
256    where
257        F: FnMut(&DebugState) -> DebugAction + 'static,
258    {
259        self.debug_hook = Some(Box::new(hook));
260    }
261
262    /// Clear the current debug hook.
263    pub fn clear_debug_hook(&mut self) {
264        self.debug_hook = None;
265    }
266
267    /// Enable step-out mode (stop when returning from current frame).
268    pub fn set_step_out(&mut self) {
269        self.step_mode = true;
270        self.step_frame_depth = self.frames.len().saturating_sub(1);
271    }
272
273    /// Check if the VM is stopped at a debug point.
274    pub fn is_stopped(&self) -> bool {
275        self.stopped
276    }
277
278    /// Get the current debug state (variables, line, etc.).
279    pub fn debug_state(&self) -> DebugState {
280        let line = self.current_line();
281        let variables = self.env.all_variables();
282        let frame_name = if self.frames.len() > 1 {
283            format!("frame_{}", self.frames.len() - 1)
284        } else {
285            "pipeline".to_string()
286        };
287        DebugState {
288            line,
289            variables,
290            frame_name,
291            frame_depth: self.frames.len(),
292        }
293    }
294
295    /// Get all stack frames for the debugger.
296    pub fn debug_stack_frames(&self) -> Vec<(String, usize)> {
297        let mut frames = Vec::new();
298        for (i, frame) in self.frames.iter().enumerate() {
299            let line = if frame.ip > 0 && frame.ip - 1 < frame.chunk.lines.len() {
300                frame.chunk.lines[frame.ip - 1] as usize
301            } else {
302                0
303            };
304            let name = if frame.fn_name.is_empty() {
305                if i == 0 {
306                    "pipeline".to_string()
307                } else {
308                    format!("fn_{}", i)
309                }
310            } else {
311                frame.fn_name.clone()
312            };
313            frames.push((name, line));
314        }
315        frames
316    }
317
318    /// Get the current source line.
319    fn current_line(&self) -> usize {
320        if let Some(frame) = self.frames.last() {
321            let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
322            if ip < frame.chunk.lines.len() {
323                return frame.chunk.lines[ip] as usize;
324            }
325        }
326        0
327    }
328
329    /// Execute one instruction, returning whether to stop (breakpoint/step).
330    /// Returns Ok(None) to continue, Ok(Some(val)) on program end, Err on error.
331    pub async fn step_execute(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
332        let current_line = self.current_line();
333        let line_changed = current_line != self.last_line && current_line > 0;
334
335        if line_changed {
336            self.last_line = current_line;
337
338            let state = self.debug_state();
339            if let Some(hook) = self.debug_hook.as_mut() {
340                if matches!(hook(&state), DebugAction::Stop) {
341                    self.stopped = true;
342                    return Ok(Some((VmValue::Nil, true)));
343                }
344            }
345
346            if self.breakpoints.contains(&current_line) {
347                self.stopped = true;
348                return Ok(Some((VmValue::Nil, true)));
349            }
350
351            if self.step_mode && self.frames.len() <= self.step_frame_depth + 1 {
352                self.step_mode = false;
353                self.stopped = true;
354                return Ok(Some((VmValue::Nil, true)));
355            }
356        }
357
358        self.stopped = false;
359        self.execute_one_cycle().await
360    }
361
362    async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
363        if let Some(&(deadline, _)) = self.deadlines.last() {
364            if Instant::now() > deadline {
365                self.deadlines.pop();
366                let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
367                match self.handle_error(err) {
368                    Ok(None) => return Ok(None),
369                    Ok(Some(val)) => return Ok(Some((val, false))),
370                    Err(e) => return Err(e),
371                }
372            }
373        }
374
375        let frame = match self.frames.last_mut() {
376            Some(f) => f,
377            None => {
378                let val = self.stack.pop().unwrap_or(VmValue::Nil);
379                return Ok(Some((val, false)));
380            }
381        };
382
383        if frame.ip >= frame.chunk.code.len() {
384            let val = self.stack.pop().unwrap_or(VmValue::Nil);
385            let popped_frame = self.frames.pop().unwrap();
386            if self.frames.is_empty() {
387                return Ok(Some((val, false)));
388            } else {
389                self.iterators.truncate(popped_frame.saved_iterator_depth);
390                self.env = popped_frame.saved_env;
391                self.stack.truncate(popped_frame.stack_base);
392                self.stack.push(val);
393                return Ok(None);
394            }
395        }
396
397        let op = frame.chunk.code[frame.ip];
398        frame.ip += 1;
399
400        match self.execute_op(op).await {
401            Ok(Some(val)) => Ok(Some((val, false))),
402            Ok(None) => Ok(None),
403            Err(VmError::Return(val)) => {
404                if let Some(popped_frame) = self.frames.pop() {
405                    if let Some(ref dir) = popped_frame.saved_source_dir {
406                        crate::stdlib::set_thread_source_dir(dir);
407                    }
408                    let current_depth = self.frames.len();
409                    self.exception_handlers
410                        .retain(|h| h.frame_depth <= current_depth);
411                    if self.frames.is_empty() {
412                        return Ok(Some((val, false)));
413                    }
414                    self.iterators.truncate(popped_frame.saved_iterator_depth);
415                    self.env = popped_frame.saved_env;
416                    self.stack.truncate(popped_frame.stack_base);
417                    self.stack.push(val);
418                    Ok(None)
419                } else {
420                    Ok(Some((val, false)))
421                }
422            }
423            Err(e) => {
424                if self.error_stack_trace.is_empty() {
425                    self.error_stack_trace = self.capture_stack_trace();
426                }
427                match self.handle_error(e) {
428                    Ok(None) => {
429                        self.error_stack_trace.clear();
430                        Ok(None)
431                    }
432                    Ok(Some(val)) => Ok(Some((val, false))),
433                    Err(e) => Err(self.enrich_error_with_line(e)),
434                }
435            }
436        }
437    }
438
439    /// Initialize execution (push the initial frame).
440    pub fn start(&mut self, chunk: &Chunk) {
441        self.frames.push(CallFrame {
442            chunk: chunk.clone(),
443            ip: 0,
444            stack_base: self.stack.len(),
445            saved_env: self.env.clone(),
446            saved_iterator_depth: self.iterators.len(),
447            fn_name: String::new(),
448            argc: 0,
449            saved_source_dir: None,
450            module_functions: None,
451            module_state: None,
452        });
453    }
454
455    /// Register a sync builtin function.
456    pub fn register_builtin<F>(&mut self, name: &str, f: F)
457    where
458        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
459    {
460        self.builtins.insert(name.to_string(), Rc::new(f));
461    }
462
463    /// Remove a sync builtin (so an async version can take precedence).
464    pub fn unregister_builtin(&mut self, name: &str) {
465        self.builtins.remove(name);
466    }
467
468    /// Register an async builtin function.
469    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
470    where
471        F: Fn(Vec<VmValue>) -> Fut + 'static,
472        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
473    {
474        self.async_builtins
475            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
476    }
477
478    /// Create a child VM that shares builtins and env but has fresh execution state.
479    /// Used for parallel/spawn to fork the VM for concurrent tasks.
480    fn child_vm(&self) -> Vm {
481        Vm {
482            stack: Vec::with_capacity(64),
483            env: self.env.clone(),
484            output: String::new(),
485            builtins: self.builtins.clone(),
486            async_builtins: self.async_builtins.clone(),
487            iterators: Vec::new(),
488            frames: Vec::new(),
489            exception_handlers: Vec::new(),
490            spawned_tasks: BTreeMap::new(),
491            task_counter: 0,
492            deadlines: self.deadlines.clone(),
493            breakpoints: Vec::new(),
494            step_mode: false,
495            step_frame_depth: 0,
496            stopped: false,
497            last_line: 0,
498            source_dir: self.source_dir.clone(),
499            imported_paths: Vec::new(),
500            module_cache: self.module_cache.clone(),
501            source_file: self.source_file.clone(),
502            source_text: self.source_text.clone(),
503            bridge: self.bridge.clone(),
504            denied_builtins: self.denied_builtins.clone(),
505            cancel_token: None,
506            error_stack_trace: Vec::new(),
507            yield_sender: None,
508            project_root: self.project_root.clone(),
509            globals: self.globals.clone(),
510            debug_hook: None,
511        }
512    }
513
514    /// Set the source directory for import resolution and introspection.
515    /// Also auto-detects the project root if not already set.
516    pub fn set_source_dir(&mut self, dir: &std::path::Path) {
517        self.source_dir = Some(dir.to_path_buf());
518        crate::stdlib::set_thread_source_dir(dir);
519        // Auto-detect project root if not explicitly set.
520        if self.project_root.is_none() {
521            self.project_root = crate::stdlib::process::find_project_root(dir);
522        }
523    }
524
525    /// Explicitly set the project root directory.
526    /// Used by ACP/CLI to override auto-detection.
527    pub fn set_project_root(&mut self, root: &std::path::Path) {
528        self.project_root = Some(root.to_path_buf());
529    }
530
531    /// Get the project root directory, falling back to source_dir.
532    pub fn project_root(&self) -> Option<&std::path::Path> {
533        self.project_root.as_deref().or(self.source_dir.as_deref())
534    }
535
536    /// Return all registered builtin names (sync + async).
537    pub fn builtin_names(&self) -> Vec<String> {
538        let mut names: Vec<String> = self.builtins.keys().cloned().collect();
539        names.extend(self.async_builtins.keys().cloned());
540        names
541    }
542
543    /// Set a global constant (e.g. `pi`, `e`).
544    /// Stored separately from the environment so user-defined variables can shadow them.
545    pub fn set_global(&mut self, name: &str, value: VmValue) {
546        self.globals.insert(name.to_string(), value);
547    }
548
549    /// Get the captured output.
550    pub fn output(&self) -> &str {
551        &self.output
552    }
553
554    /// Execute a compiled chunk.
555    pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
556        let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
557        let result = self.run_chunk(chunk).await;
558        crate::tracing::span_end(span_id);
559        result
560    }
561
562    /// Convert a VmError into either a handled exception (returning Ok) or a propagated error.
563    fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
564        let thrown_value = match &error {
565            VmError::Thrown(v) => v.clone(),
566            other => VmValue::String(Rc::from(other.to_string())),
567        };
568
569        if let Some(handler) = self.exception_handlers.pop() {
570            if !handler.error_type.is_empty() {
571                // Typed catch: only match when the thrown enum's type equals the declared type.
572                let matches = match &thrown_value {
573                    VmValue::EnumVariant { enum_name, .. } => *enum_name == handler.error_type,
574                    _ => false,
575                };
576                if !matches {
577                    return self.handle_error(error);
578                }
579            }
580
581            while self.frames.len() > handler.frame_depth {
582                if let Some(frame) = self.frames.pop() {
583                    if let Some(ref dir) = frame.saved_source_dir {
584                        crate::stdlib::set_thread_source_dir(dir);
585                    }
586                    self.iterators.truncate(frame.saved_iterator_depth);
587                    self.env = frame.saved_env;
588                }
589            }
590
591            // Drop deadlines that belonged to unwound frames.
592            while self
593                .deadlines
594                .last()
595                .is_some_and(|d| d.1 > handler.frame_depth)
596            {
597                self.deadlines.pop();
598            }
599
600            self.env.truncate_scopes(handler.env_scope_depth);
601
602            self.stack.truncate(handler.stack_depth);
603            self.stack.push(thrown_value);
604
605            if let Some(frame) = self.frames.last_mut() {
606                frame.ip = handler.catch_ip;
607            }
608
609            Ok(None)
610        } else {
611            Err(error)
612        }
613    }
614
615    async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
616        self.run_chunk_entry(chunk, 0, None, None, None).await
617    }
618
619    async fn run_chunk_entry(
620        &mut self,
621        chunk: &Chunk,
622        argc: usize,
623        saved_source_dir: Option<std::path::PathBuf>,
624        module_functions: Option<ModuleFunctionRegistry>,
625        module_state: Option<crate::value::ModuleState>,
626    ) -> Result<VmValue, VmError> {
627        self.frames.push(CallFrame {
628            chunk: chunk.clone(),
629            ip: 0,
630            stack_base: self.stack.len(),
631            saved_env: self.env.clone(),
632            saved_iterator_depth: self.iterators.len(),
633            fn_name: String::new(),
634            argc,
635            saved_source_dir,
636            module_functions,
637            module_state,
638        });
639
640        loop {
641            if let Some(&(deadline, _)) = self.deadlines.last() {
642                if Instant::now() > deadline {
643                    self.deadlines.pop();
644                    let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
645                    match self.handle_error(err) {
646                        Ok(None) => continue,
647                        Ok(Some(val)) => return Ok(val),
648                        Err(e) => return Err(e),
649                    }
650                }
651            }
652
653            let frame = match self.frames.last_mut() {
654                Some(f) => f,
655                None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
656            };
657
658            if frame.ip >= frame.chunk.code.len() {
659                let val = self.stack.pop().unwrap_or(VmValue::Nil);
660                let popped_frame = self.frames.pop().unwrap();
661                if let Some(ref dir) = popped_frame.saved_source_dir {
662                    crate::stdlib::set_thread_source_dir(dir);
663                }
664
665                if self.frames.is_empty() {
666                    return Ok(val);
667                } else {
668                    self.iterators.truncate(popped_frame.saved_iterator_depth);
669                    self.env = popped_frame.saved_env;
670                    self.stack.truncate(popped_frame.stack_base);
671                    self.stack.push(val);
672                    continue;
673                }
674            }
675
676            let op = frame.chunk.code[frame.ip];
677            frame.ip += 1;
678
679            match self.execute_op(op).await {
680                Ok(Some(val)) => return Ok(val),
681                Ok(None) => continue,
682                Err(VmError::Return(val)) => {
683                    if let Some(popped_frame) = self.frames.pop() {
684                        if let Some(ref dir) = popped_frame.saved_source_dir {
685                            crate::stdlib::set_thread_source_dir(dir);
686                        }
687                        let current_depth = self.frames.len();
688                        self.exception_handlers
689                            .retain(|h| h.frame_depth <= current_depth);
690
691                        if self.frames.is_empty() {
692                            return Ok(val);
693                        }
694                        self.iterators.truncate(popped_frame.saved_iterator_depth);
695                        self.env = popped_frame.saved_env;
696                        self.stack.truncate(popped_frame.stack_base);
697                        self.stack.push(val);
698                    } else {
699                        return Ok(val);
700                    }
701                }
702                Err(e) => {
703                    // Capture stack trace before error handling unwinds frames.
704                    if self.error_stack_trace.is_empty() {
705                        self.error_stack_trace = self.capture_stack_trace();
706                    }
707                    match self.handle_error(e) {
708                        Ok(None) => {
709                            self.error_stack_trace.clear();
710                            continue;
711                        }
712                        Ok(Some(val)) => return Ok(val),
713                        Err(e) => return Err(self.enrich_error_with_line(e)),
714                    }
715                }
716            }
717        }
718    }
719
720    /// Capture the current call stack as (fn_name, line, col, source_file) tuples.
721    fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
722        self.frames
723            .iter()
724            .map(|f| {
725                let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
726                let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
727                let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
728                (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
729            })
730            .collect()
731    }
732
733    /// Enrich a VmError with source line information from the captured stack
734    /// trace. Appends ` (line N)` to error variants whose messages don't
735    /// already carry location context.
736    fn enrich_error_with_line(&self, error: VmError) -> VmError {
737        // Determine the line from the captured stack trace (innermost frame).
738        let line = self
739            .error_stack_trace
740            .last()
741            .map(|(_, l, _, _)| *l)
742            .unwrap_or_else(|| self.current_line());
743        if line == 0 {
744            return error;
745        }
746        let suffix = format!(" (line {line})");
747        match error {
748            VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
749            VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
750            VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
751            VmError::UndefinedVariable(name) => {
752                VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
753            }
754            VmError::UndefinedBuiltin(name) => {
755                VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
756            }
757            VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
758                "Cannot assign to immutable binding: {name}{suffix}"
759            )),
760            VmError::StackOverflow => {
761                VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
762            }
763            // Leave these untouched:
764            // - Thrown: user-thrown errors should not be silently modified
765            // - CategorizedError: structured errors for agent orchestration
766            // - Return: control flow, not a real error
767            // - StackUnderflow / InvalidInstruction: internal VM bugs
768            other => other,
769        }
770    }
771
772    const MAX_FRAMES: usize = 512;
773
774    /// Build the call-time env for a closure invocation.
775    ///
776    /// Harn is **lexically scoped for data**: a closure sees exactly the
777    /// data names it captured at creation time, plus its parameters,
778    /// plus names from its originating module's `module_state`, plus
779    /// the module-function registry. The caller's *data* locals are
780    /// intentionally not visible — that would be dynamic scoping, which
781    /// is neither what Harn's TS-flavored surface suggests to users nor
782    /// something real stdlib code relies on.
783    ///
784    /// **Exception: closure-typed bindings.** Function *names* are
785    /// late-bound, Python-`LOAD_GLOBAL`-style. When a local recursive
786    /// fn is declared in a pipeline body (or inside another function),
787    /// the closure is created BEFORE its own name is defined in the
788    /// enclosing scope, so `closure.env` captures a snapshot that is
789    /// missing the self-reference. To make `fn fact(n) { fact(n-1) }`
790    /// work without a letrec trick, we merge closure-typed entries
791    /// from the caller's scope stack — but only closure-typed ones.
792    /// Data locals are never leaked across call boundaries, so the
793    /// surprising "caller's variable magically visible in callee"
794    /// semantic is ruled out.
795    ///
796    /// Imported module closures have `module_state` set, at which
797    /// point the full lexical environment is already available via
798    /// `closure.env` + `module_state`, and we skip the closure merge
799    /// entirely as a fast path. This is the hot path for context-
800    /// builder workloads (~65% of VM CPU before this optimization).
801    fn closure_call_env(caller_env: &VmEnv, closure: &VmClosure) -> VmEnv {
802        if closure.module_state.is_some() {
803            return closure.env.clone();
804        }
805        let mut call_env = closure.env.clone();
806        // Late-bind only closure-typed names from the caller — enough
807        // for local recursive / mutually-recursive fns to self-reference
808        // without leaking caller-local data into the callee.
809        for scope in &caller_env.scopes {
810            for (name, (val, mutable)) in &scope.vars {
811                if matches!(val, VmValue::Closure(_)) && call_env.get(name).is_none() {
812                    let _ = call_env.define(name, val.clone(), *mutable);
813                }
814            }
815        }
816        call_env
817    }
818
819    fn resolve_named_closure(&self, name: &str) -> Option<Rc<VmClosure>> {
820        if let Some(VmValue::Closure(closure)) = self.env.get(name) {
821            return Some(closure);
822        }
823        self.frames
824            .last()
825            .and_then(|frame| frame.module_functions.as_ref())
826            .and_then(|registry| registry.borrow().get(name).cloned())
827    }
828
829    /// Push a new call frame for a closure invocation.
830    fn push_closure_frame(
831        &mut self,
832        closure: &VmClosure,
833        args: &[VmValue],
834        _parent_functions: &[CompiledFunction],
835    ) -> Result<(), VmError> {
836        if self.frames.len() >= Self::MAX_FRAMES {
837            return Err(VmError::StackOverflow);
838        }
839        let saved_env = self.env.clone();
840
841        // If this closure originated from an imported module, switch
842        // the thread-local source dir so that render() and other
843        // source-relative builtins resolve relative to the module.
844        let saved_source_dir = if let Some(ref dir) = closure.source_dir {
845            let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
846            crate::stdlib::set_thread_source_dir(dir);
847            prev
848        } else {
849            None
850        };
851
852        let mut call_env = Self::closure_call_env(&saved_env, closure);
853        call_env.push_scope();
854
855        let default_start = closure
856            .func
857            .default_start
858            .unwrap_or(closure.func.params.len());
859        let param_count = closure.func.params.len();
860        for (i, param) in closure.func.params.iter().enumerate() {
861            if closure.func.has_rest_param && i == param_count - 1 {
862                // Rest parameter: collect remaining args into a list
863                let rest_args = if i < args.len() {
864                    args[i..].to_vec()
865                } else {
866                    Vec::new()
867                };
868                let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
869            } else if i < args.len() {
870                let _ = call_env.define(param, args[i].clone(), false);
871            } else if i < default_start {
872                let _ = call_env.define(param, VmValue::Nil, false);
873            }
874        }
875
876        self.env = call_env;
877
878        self.frames.push(CallFrame {
879            chunk: closure.func.chunk.clone(),
880            ip: 0,
881            stack_base: self.stack.len(),
882            saved_env,
883            saved_iterator_depth: self.iterators.len(),
884            fn_name: closure.func.name.clone(),
885            argc: args.len(),
886            saved_source_dir,
887            module_functions: closure.module_functions.clone(),
888            module_state: closure.module_state.clone(),
889        });
890
891        Ok(())
892    }
893
894    /// Create a generator value by spawning the closure body as an async task.
895    /// The generator body communicates yielded values through an mpsc channel.
896    pub(crate) fn create_generator(&self, closure: &VmClosure, args: &[VmValue]) -> VmValue {
897        use crate::value::VmGenerator;
898
899        // Buffer size of 1: the generator produces one value at a time.
900        let (tx, rx) = tokio::sync::mpsc::channel::<VmValue>(1);
901
902        let mut child = self.child_vm();
903        child.yield_sender = Some(tx);
904
905        // Set up the environment for the generator body. The generator
906        // body runs in its own child VM; closure_call_env walks the
907        // current (parent) env so locally-defined generator closures
908        // can self-reference via the narrow closure-only merge. See
909        // `Vm::closure_call_env`.
910        let parent_env = self.env.clone();
911        let mut call_env = Self::closure_call_env(&parent_env, closure);
912        call_env.push_scope();
913
914        let default_start = closure
915            .func
916            .default_start
917            .unwrap_or(closure.func.params.len());
918        let param_count = closure.func.params.len();
919        for (i, param) in closure.func.params.iter().enumerate() {
920            if closure.func.has_rest_param && i == param_count - 1 {
921                let rest_args = if i < args.len() {
922                    args[i..].to_vec()
923                } else {
924                    Vec::new()
925                };
926                let _ = call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
927            } else if i < args.len() {
928                let _ = call_env.define(param, args[i].clone(), false);
929            } else if i < default_start {
930                let _ = call_env.define(param, VmValue::Nil, false);
931            }
932        }
933        child.env = call_env;
934
935        let chunk = closure.func.chunk.clone();
936        let saved_source_dir = if let Some(ref dir) = closure.source_dir {
937            let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
938            crate::stdlib::set_thread_source_dir(dir);
939            prev
940        } else {
941            None
942        };
943        let module_functions = closure.module_functions.clone();
944        let module_state = closure.module_state.clone();
945        let argc = args.len();
946        // Spawn the generator body as an async task.
947        // The task will execute until return, sending yielded values through the channel.
948        tokio::task::spawn_local(async move {
949            let _ = child
950                .run_chunk_entry(
951                    &chunk,
952                    argc,
953                    saved_source_dir,
954                    module_functions,
955                    module_state,
956                )
957                .await;
958            // When the generator body finishes (return or fall-through),
959            // the sender is dropped, signaling completion to the receiver.
960        });
961
962        VmValue::Generator(VmGenerator {
963            done: Rc::new(std::cell::Cell::new(false)),
964            receiver: Rc::new(tokio::sync::Mutex::new(rx)),
965        })
966    }
967
968    fn pop(&mut self) -> Result<VmValue, VmError> {
969        self.stack.pop().ok_or(VmError::StackUnderflow)
970    }
971
972    fn peek(&self) -> Result<&VmValue, VmError> {
973        self.stack.last().ok_or(VmError::StackUnderflow)
974    }
975
976    fn const_string(c: &Constant) -> Result<String, VmError> {
977        match c {
978            Constant::String(s) => Ok(s.clone()),
979            _ => Err(VmError::TypeError("expected string constant".into())),
980        }
981    }
982
983    /// Call a closure (used by method calls like .map/.filter etc.)
984    /// Uses recursive execution for simplicity in method dispatch.
985    fn call_closure<'a>(
986        &'a mut self,
987        closure: &'a VmClosure,
988        args: &'a [VmValue],
989        _parent_functions: &'a [CompiledFunction],
990    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
991        Box::pin(async move {
992            let saved_env = self.env.clone();
993            let saved_frames = std::mem::take(&mut self.frames);
994            let saved_handlers = std::mem::take(&mut self.exception_handlers);
995            let saved_iterators = std::mem::take(&mut self.iterators);
996            let saved_deadlines = std::mem::take(&mut self.deadlines);
997
998            let mut call_env = Self::closure_call_env(&saved_env, closure);
999            call_env.push_scope();
1000
1001            let default_start = closure
1002                .func
1003                .default_start
1004                .unwrap_or(closure.func.params.len());
1005            let param_count = closure.func.params.len();
1006            for (i, param) in closure.func.params.iter().enumerate() {
1007                if closure.func.has_rest_param && i == param_count - 1 {
1008                    let rest_args = if i < args.len() {
1009                        args[i..].to_vec()
1010                    } else {
1011                        Vec::new()
1012                    };
1013                    let _ =
1014                        call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
1015                } else if i < args.len() {
1016                    let _ = call_env.define(param, args[i].clone(), false);
1017                } else if i < default_start {
1018                    let _ = call_env.define(param, VmValue::Nil, false);
1019                }
1020            }
1021
1022            self.env = call_env;
1023            let argc = args.len();
1024            let saved_source_dir = if let Some(ref dir) = closure.source_dir {
1025                let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
1026                crate::stdlib::set_thread_source_dir(dir);
1027                prev
1028            } else {
1029                None
1030            };
1031            let result = self
1032                .run_chunk_entry(
1033                    &closure.func.chunk,
1034                    argc,
1035                    saved_source_dir,
1036                    closure.module_functions.clone(),
1037                    closure.module_state.clone(),
1038                )
1039                .await;
1040
1041            self.env = saved_env;
1042            self.frames = saved_frames;
1043            self.exception_handlers = saved_handlers;
1044            self.iterators = saved_iterators;
1045            self.deadlines = saved_deadlines;
1046
1047            result
1048        })
1049    }
1050
1051    /// Invoke a value as a callable. Supports `VmValue::Closure` and
1052    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
1053    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
1054    /// user-defined closures.
1055    #[allow(clippy::manual_async_fn)]
1056    fn call_callable_value<'a>(
1057        &'a mut self,
1058        callable: &'a VmValue,
1059        args: &'a [VmValue],
1060        functions: &'a [CompiledFunction],
1061    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
1062        Box::pin(async move {
1063            match callable {
1064                VmValue::Closure(closure) => self.call_closure(closure, args, functions).await,
1065                VmValue::BuiltinRef(name) => {
1066                    let name_owned = name.to_string();
1067                    self.call_named_builtin(&name_owned, args.to_vec()).await
1068                }
1069                other => Err(VmError::TypeError(format!(
1070                    "expected callable, got {}",
1071                    other.type_name()
1072                ))),
1073            }
1074        })
1075    }
1076
1077    /// Returns true if `v` is callable via `call_callable_value`.
1078    fn is_callable_value(v: &VmValue) -> bool {
1079        matches!(v, VmValue::Closure(_) | VmValue::BuiltinRef(_))
1080    }
1081
1082    /// Public wrapper for `call_closure`, used by the MCP server to invoke
1083    /// tool handler closures from outside the VM execution loop.
1084    pub async fn call_closure_pub(
1085        &mut self,
1086        closure: &VmClosure,
1087        args: &[VmValue],
1088        functions: &[CompiledFunction],
1089    ) -> Result<VmValue, VmError> {
1090        self.call_closure(closure, args, functions).await
1091    }
1092
1093    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
1094    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
1095    async fn call_named_builtin(
1096        &mut self,
1097        name: &str,
1098        args: Vec<VmValue>,
1099    ) -> Result<VmValue, VmError> {
1100        // Auto-trace LLM calls and tool calls.
1101        let span_kind = match name {
1102            "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
1103            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
1104            _ => None,
1105        };
1106        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
1107
1108        // Sandbox check: deny builtins blocked by --deny/--allow flags.
1109        if self.denied_builtins.contains(name) {
1110            return Err(VmError::CategorizedError {
1111                message: format!("Tool '{}' is not permitted.", name),
1112                category: ErrorCategory::ToolRejected,
1113            });
1114        }
1115        crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
1116        if let Some(builtin) = self.builtins.get(name).cloned() {
1117            builtin(&args, &mut self.output)
1118        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
1119            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1120                slot.borrow_mut().push(self.child_vm());
1121            });
1122            let result = async_builtin(args).await;
1123            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1124                slot.borrow_mut().pop();
1125            });
1126            result
1127        } else if let Some(bridge) = &self.bridge {
1128            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
1129            let args_json: Vec<serde_json::Value> =
1130                args.iter().map(crate::llm::vm_value_to_json).collect();
1131            let result = bridge
1132                .call(
1133                    "builtin_call",
1134                    serde_json::json!({"name": name, "args": args_json}),
1135                )
1136                .await?;
1137            Ok(crate::bridge::json_result_to_vm_value(&result))
1138        } else {
1139            let all_builtins = self
1140                .builtins
1141                .keys()
1142                .chain(self.async_builtins.keys())
1143                .map(|s| s.as_str());
1144            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
1145                return Err(VmError::Runtime(format!(
1146                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
1147                )));
1148            }
1149            Err(VmError::UndefinedBuiltin(name.to_string()))
1150        }
1151    }
1152}
1153
1154/// Clone the VM at the top of the async-builtin child VM stack, returning a
1155/// fresh `Vm` instance the caller owns. Enables concurrent tool-handler
1156/// execution within a single agent_loop iteration — the VM shares its heavy
1157/// state (env, builtins, bridge, module_cache) via `Arc`/`Rc`, so cloning is
1158/// cheap and each handler gets its own execution context.
1159///
1160/// Returns `None` if no parent VM is currently pushed on the stack.
1161pub fn clone_async_builtin_child_vm() -> Option<Vm> {
1162    CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
1163}
1164
1165/// Legacy API preserved for out-of-tree callers; new code should use
1166/// `clone_async_builtin_child_vm()`. `take/restore` serialized concurrent
1167/// callers because only one could hold the popped value at a time.
1168#[deprecated(
1169    note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
1170)]
1171pub fn take_async_builtin_child_vm() -> Option<Vm> {
1172    clone_async_builtin_child_vm()
1173}
1174
1175/// Legacy no-op retained for backward compatibility.
1176#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
1177pub fn restore_async_builtin_child_vm(_vm: Vm) {
1178    CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
1179        let _ = slot;
1180    });
1181}
1182
1183impl Default for Vm {
1184    fn default() -> Self {
1185        Self::new()
1186    }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use crate::compiler::Compiler;
1193    use crate::stdlib::register_vm_stdlib;
1194    use harn_lexer::Lexer;
1195    use harn_parser::Parser;
1196
1197    fn run_harn(source: &str) -> (String, VmValue) {
1198        let rt = tokio::runtime::Builder::new_current_thread()
1199            .enable_all()
1200            .build()
1201            .unwrap();
1202        rt.block_on(async {
1203            let local = tokio::task::LocalSet::new();
1204            local
1205                .run_until(async {
1206                    let mut lexer = Lexer::new(source);
1207                    let tokens = lexer.tokenize().unwrap();
1208                    let mut parser = Parser::new(tokens);
1209                    let program = parser.parse().unwrap();
1210                    let chunk = Compiler::new().compile(&program).unwrap();
1211
1212                    let mut vm = Vm::new();
1213                    register_vm_stdlib(&mut vm);
1214                    let result = vm.execute(&chunk).await.unwrap();
1215                    (vm.output().to_string(), result)
1216                })
1217                .await
1218        })
1219    }
1220
1221    fn run_output(source: &str) -> String {
1222        run_harn(source).0.trim_end().to_string()
1223    }
1224
1225    fn run_harn_result(source: &str) -> Result<(String, VmValue), VmError> {
1226        let rt = tokio::runtime::Builder::new_current_thread()
1227            .enable_all()
1228            .build()
1229            .unwrap();
1230        rt.block_on(async {
1231            let local = tokio::task::LocalSet::new();
1232            local
1233                .run_until(async {
1234                    let mut lexer = Lexer::new(source);
1235                    let tokens = lexer.tokenize().unwrap();
1236                    let mut parser = Parser::new(tokens);
1237                    let program = parser.parse().unwrap();
1238                    let chunk = Compiler::new().compile(&program).unwrap();
1239
1240                    let mut vm = Vm::new();
1241                    register_vm_stdlib(&mut vm);
1242                    let result = vm.execute(&chunk).await?;
1243                    Ok((vm.output().to_string(), result))
1244                })
1245                .await
1246        })
1247    }
1248
1249    #[test]
1250    fn test_arithmetic() {
1251        let out =
1252            run_output("pipeline t(task) { log(2 + 3)\nlog(10 - 4)\nlog(3 * 5)\nlog(10 / 3) }");
1253        assert_eq!(out, "[harn] 5\n[harn] 6\n[harn] 15\n[harn] 3");
1254    }
1255
1256    #[test]
1257    fn test_mixed_arithmetic() {
1258        let out = run_output("pipeline t(task) { log(3 + 1.5)\nlog(10 - 2.5) }");
1259        assert_eq!(out, "[harn] 4.5\n[harn] 7.5");
1260    }
1261
1262    #[test]
1263    fn test_exponentiation() {
1264        let out = run_output(
1265            "pipeline t(task) { log(2 ** 8)\nlog(2 * 3 ** 2)\nlog(2 ** 3 ** 2)\nlog(2 ** -1) }",
1266        );
1267        assert_eq!(out, "[harn] 256\n[harn] 18\n[harn] 512\n[harn] 0.5");
1268    }
1269
1270    #[test]
1271    fn test_comparisons() {
1272        let out =
1273            run_output("pipeline t(task) { log(1 < 2)\nlog(2 > 3)\nlog(1 == 1)\nlog(1 != 2) }");
1274        assert_eq!(out, "[harn] true\n[harn] false\n[harn] true\n[harn] true");
1275    }
1276
1277    #[test]
1278    fn test_let_var() {
1279        let out = run_output("pipeline t(task) { let x = 42\nlog(x)\nvar y = 1\ny = 2\nlog(y) }");
1280        assert_eq!(out, "[harn] 42\n[harn] 2");
1281    }
1282
1283    #[test]
1284    fn test_if_else() {
1285        let out = run_output(
1286            r#"pipeline t(task) { if true { log("yes") } if false { log("wrong") } else { log("no") } }"#,
1287        );
1288        assert_eq!(out, "[harn] yes\n[harn] no");
1289    }
1290
1291    #[test]
1292    fn test_while_loop() {
1293        let out = run_output("pipeline t(task) { var i = 0\n while i < 5 { i = i + 1 }\n log(i) }");
1294        assert_eq!(out, "[harn] 5");
1295    }
1296
1297    #[test]
1298    fn test_for_in() {
1299        let out = run_output("pipeline t(task) { for item in [1, 2, 3] { log(item) } }");
1300        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3");
1301    }
1302
1303    #[test]
1304    fn test_inner_for_return_does_not_leak_iterator_into_caller() {
1305        let out = run_output(
1306            r#"pipeline t(task) {
1307  fn first_match() {
1308    for pattern in ["a", "b"] {
1309      return pattern
1310    }
1311    return ""
1312  }
1313
1314  var seen = []
1315  for path in ["outer"] {
1316    seen = seen + [path + ":" + first_match()]
1317  }
1318  log(join(seen, ","))
1319}"#,
1320        );
1321        assert_eq!(out, "[harn] outer:a");
1322    }
1323
1324    #[test]
1325    fn test_fn_decl_and_call() {
1326        let out = run_output("pipeline t(task) { fn add(a, b) { return a + b }\nlog(add(3, 4)) }");
1327        assert_eq!(out, "[harn] 7");
1328    }
1329
1330    #[test]
1331    fn test_closure() {
1332        let out = run_output("pipeline t(task) { let double = { x -> x * 2 }\nlog(double(5)) }");
1333        assert_eq!(out, "[harn] 10");
1334    }
1335
1336    #[test]
1337    fn test_closure_capture() {
1338        let out = run_output(
1339            "pipeline t(task) { let base = 10\nfn offset(x) { return x + base }\nlog(offset(5)) }",
1340        );
1341        assert_eq!(out, "[harn] 15");
1342    }
1343
1344    #[test]
1345    fn test_string_concat() {
1346        let out = run_output(
1347            r#"pipeline t(task) { let a = "hello" + " " + "world"
1348log(a) }"#,
1349        );
1350        assert_eq!(out, "[harn] hello world");
1351    }
1352
1353    #[test]
1354    fn test_list_map() {
1355        let out = run_output(
1356            "pipeline t(task) { let doubled = [1, 2, 3].map({ x -> x * 2 })\nlog(doubled) }",
1357        );
1358        assert_eq!(out, "[harn] [2, 4, 6]");
1359    }
1360
1361    #[test]
1362    fn test_list_filter() {
1363        let out = run_output(
1364            "pipeline t(task) { let big = [1, 2, 3, 4, 5].filter({ x -> x > 3 })\nlog(big) }",
1365        );
1366        assert_eq!(out, "[harn] [4, 5]");
1367    }
1368
1369    #[test]
1370    fn test_list_reduce() {
1371        let out = run_output(
1372            "pipeline t(task) { let sum = [1, 2, 3, 4].reduce(0, { acc, x -> acc + x })\nlog(sum) }",
1373        );
1374        assert_eq!(out, "[harn] 10");
1375    }
1376
1377    #[test]
1378    fn test_dict_access() {
1379        let out = run_output(
1380            r#"pipeline t(task) { let d = {name: "test", value: 42}
1381log(d.name)
1382log(d.value) }"#,
1383        );
1384        assert_eq!(out, "[harn] test\n[harn] 42");
1385    }
1386
1387    #[test]
1388    fn test_dict_methods() {
1389        let out = run_output(
1390            r#"pipeline t(task) { let d = {a: 1, b: 2}
1391log(d.keys())
1392log(d.values())
1393log(d.has("a"))
1394log(d.has("z")) }"#,
1395        );
1396        assert_eq!(
1397            out,
1398            "[harn] [a, b]\n[harn] [1, 2]\n[harn] true\n[harn] false"
1399        );
1400    }
1401
1402    #[test]
1403    fn test_pipe_operator() {
1404        let out = run_output(
1405            "pipeline t(task) { fn double(x) { return x * 2 }\nlet r = 5 |> double\nlog(r) }",
1406        );
1407        assert_eq!(out, "[harn] 10");
1408    }
1409
1410    #[test]
1411    fn test_pipe_with_closure() {
1412        let out = run_output(
1413            r#"pipeline t(task) { let r = "hello world" |> { s -> s.split(" ") }
1414log(r) }"#,
1415        );
1416        assert_eq!(out, "[harn] [hello, world]");
1417    }
1418
1419    #[test]
1420    fn test_nil_coalescing() {
1421        let out = run_output(
1422            r#"pipeline t(task) { let a = nil ?? "fallback"
1423log(a)
1424let b = "present" ?? "fallback"
1425log(b) }"#,
1426        );
1427        assert_eq!(out, "[harn] fallback\n[harn] present");
1428    }
1429
1430    #[test]
1431    fn test_logical_operators() {
1432        let out =
1433            run_output("pipeline t(task) { log(true && false)\nlog(true || false)\nlog(!true) }");
1434        assert_eq!(out, "[harn] false\n[harn] true\n[harn] false");
1435    }
1436
1437    #[test]
1438    fn test_match() {
1439        let out = run_output(
1440            r#"pipeline t(task) { let x = "b"
1441match x { "a" -> { log("first") } "b" -> { log("second") } "c" -> { log("third") } } }"#,
1442        );
1443        assert_eq!(out, "[harn] second");
1444    }
1445
1446    #[test]
1447    fn test_subscript() {
1448        let out = run_output("pipeline t(task) { let arr = [10, 20, 30]\nlog(arr[1]) }");
1449        assert_eq!(out, "[harn] 20");
1450    }
1451
1452    #[test]
1453    fn test_string_methods() {
1454        let out = run_output(
1455            r#"pipeline t(task) { log("hello world".replace("world", "harn"))
1456log("a,b,c".split(","))
1457log("  hello  ".trim())
1458log("hello".starts_with("hel"))
1459log("hello".ends_with("lo"))
1460log("hello".substring(1, 3)) }"#,
1461        );
1462        assert_eq!(
1463            out,
1464            "[harn] hello harn\n[harn] [a, b, c]\n[harn] hello\n[harn] true\n[harn] true\n[harn] el"
1465        );
1466    }
1467
1468    #[test]
1469    fn test_list_properties() {
1470        let out = run_output(
1471            "pipeline t(task) { let list = [1, 2, 3]\nlog(list.count)\nlog(list.empty)\nlog(list.first)\nlog(list.last) }",
1472        );
1473        assert_eq!(out, "[harn] 3\n[harn] false\n[harn] 1\n[harn] 3");
1474    }
1475
1476    #[test]
1477    fn test_recursive_function() {
1478        let out = run_output(
1479            "pipeline t(task) { fn fib(n) { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }\nlog(fib(10)) }",
1480        );
1481        assert_eq!(out, "[harn] 55");
1482    }
1483
1484    #[test]
1485    fn test_ternary() {
1486        let out = run_output(
1487            r#"pipeline t(task) { let x = 5
1488let r = x > 0 ? "positive" : "non-positive"
1489log(r) }"#,
1490        );
1491        assert_eq!(out, "[harn] positive");
1492    }
1493
1494    #[test]
1495    fn test_for_in_dict() {
1496        let out = run_output(
1497            "pipeline t(task) { let d = {a: 1, b: 2}\nfor entry in d { log(entry.key) } }",
1498        );
1499        assert_eq!(out, "[harn] a\n[harn] b");
1500    }
1501
1502    #[test]
1503    fn test_list_any_all() {
1504        let out = run_output(
1505            "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 })) }",
1506        );
1507        assert_eq!(out, "[harn] true\n[harn] true\n[harn] false");
1508    }
1509
1510    #[test]
1511    fn test_disassembly() {
1512        let mut lexer = Lexer::new("pipeline t(task) { log(2 + 3) }");
1513        let tokens = lexer.tokenize().unwrap();
1514        let mut parser = Parser::new(tokens);
1515        let program = parser.parse().unwrap();
1516        let chunk = Compiler::new().compile(&program).unwrap();
1517        let disasm = chunk.disassemble("test");
1518        assert!(disasm.contains("CONSTANT"));
1519        assert!(disasm.contains("ADD"));
1520        assert!(disasm.contains("CALL"));
1521    }
1522
1523    // --- Error handling tests ---
1524
1525    #[test]
1526    fn test_try_catch_basic() {
1527        let out = run_output(
1528            r#"pipeline t(task) { try { throw "oops" } catch(e) { log("caught: " + e) } }"#,
1529        );
1530        assert_eq!(out, "[harn] caught: oops");
1531    }
1532
1533    #[test]
1534    fn test_try_no_error() {
1535        let out = run_output(
1536            r#"pipeline t(task) {
1537var result = 0
1538try { result = 42 } catch(e) { result = 0 }
1539log(result)
1540}"#,
1541        );
1542        assert_eq!(out, "[harn] 42");
1543    }
1544
1545    #[test]
1546    fn test_throw_uncaught() {
1547        let result = run_harn_result(r#"pipeline t(task) { throw "boom" }"#);
1548        assert!(result.is_err());
1549    }
1550
1551    // --- Additional test coverage ---
1552
1553    fn run_vm(source: &str) -> String {
1554        let rt = tokio::runtime::Builder::new_current_thread()
1555            .enable_all()
1556            .build()
1557            .unwrap();
1558        rt.block_on(async {
1559            let local = tokio::task::LocalSet::new();
1560            local
1561                .run_until(async {
1562                    let mut lexer = Lexer::new(source);
1563                    let tokens = lexer.tokenize().unwrap();
1564                    let mut parser = Parser::new(tokens);
1565                    let program = parser.parse().unwrap();
1566                    let chunk = Compiler::new().compile(&program).unwrap();
1567                    let mut vm = Vm::new();
1568                    register_vm_stdlib(&mut vm);
1569                    vm.execute(&chunk).await.unwrap();
1570                    vm.output().to_string()
1571                })
1572                .await
1573        })
1574    }
1575
1576    fn run_vm_err(source: &str) -> String {
1577        let rt = tokio::runtime::Builder::new_current_thread()
1578            .enable_all()
1579            .build()
1580            .unwrap();
1581        rt.block_on(async {
1582            let local = tokio::task::LocalSet::new();
1583            local
1584                .run_until(async {
1585                    let mut lexer = Lexer::new(source);
1586                    let tokens = lexer.tokenize().unwrap();
1587                    let mut parser = Parser::new(tokens);
1588                    let program = parser.parse().unwrap();
1589                    let chunk = Compiler::new().compile(&program).unwrap();
1590                    let mut vm = Vm::new();
1591                    register_vm_stdlib(&mut vm);
1592                    match vm.execute(&chunk).await {
1593                        Err(e) => format!("{}", e),
1594                        Ok(_) => panic!("Expected error"),
1595                    }
1596                })
1597                .await
1598        })
1599    }
1600
1601    #[test]
1602    fn test_hello_world() {
1603        let out = run_vm(r#"pipeline default(task) { log("hello") }"#);
1604        assert_eq!(out, "[harn] hello\n");
1605    }
1606
1607    #[test]
1608    fn test_arithmetic_new() {
1609        let out = run_vm("pipeline default(task) { log(2 + 3) }");
1610        assert_eq!(out, "[harn] 5\n");
1611    }
1612
1613    #[test]
1614    fn test_string_concat_new() {
1615        let out = run_vm(r#"pipeline default(task) { log("a" + "b") }"#);
1616        assert_eq!(out, "[harn] ab\n");
1617    }
1618
1619    #[test]
1620    fn test_if_else_new() {
1621        let out = run_vm("pipeline default(task) { if true { log(1) } else { log(2) } }");
1622        assert_eq!(out, "[harn] 1\n");
1623    }
1624
1625    #[test]
1626    fn test_for_loop_new() {
1627        let out = run_vm("pipeline default(task) { for i in [1, 2, 3] { log(i) } }");
1628        assert_eq!(out, "[harn] 1\n[harn] 2\n[harn] 3\n");
1629    }
1630
1631    #[test]
1632    fn test_while_loop_new() {
1633        let out = run_vm("pipeline default(task) { var i = 0\nwhile i < 3 { log(i)\ni = i + 1 } }");
1634        assert_eq!(out, "[harn] 0\n[harn] 1\n[harn] 2\n");
1635    }
1636
1637    #[test]
1638    fn test_function_call_new() {
1639        let out =
1640            run_vm("pipeline default(task) { fn add(a, b) { return a + b }\nlog(add(2, 3)) }");
1641        assert_eq!(out, "[harn] 5\n");
1642    }
1643
1644    #[test]
1645    fn test_closure_new() {
1646        let out = run_vm("pipeline default(task) { let f = { x -> x * 2 }\nlog(f(5)) }");
1647        assert_eq!(out, "[harn] 10\n");
1648    }
1649
1650    #[test]
1651    fn test_recursion() {
1652        let out = run_vm("pipeline default(task) { fn fact(n) { if n <= 1 { return 1 }\nreturn n * fact(n - 1) }\nlog(fact(5)) }");
1653        assert_eq!(out, "[harn] 120\n");
1654    }
1655
1656    #[test]
1657    fn test_try_catch_new() {
1658        let out = run_vm(r#"pipeline default(task) { try { throw "err" } catch (e) { log(e) } }"#);
1659        assert_eq!(out, "[harn] err\n");
1660    }
1661
1662    #[test]
1663    fn test_try_no_error_new() {
1664        let out = run_vm("pipeline default(task) { try { log(1) } catch (e) { log(2) } }");
1665        assert_eq!(out, "[harn] 1\n");
1666    }
1667
1668    #[test]
1669    fn test_list_map_new() {
1670        let out =
1671            run_vm("pipeline default(task) { let r = [1, 2, 3].map({ x -> x * 2 })\nlog(r) }");
1672        assert_eq!(out, "[harn] [2, 4, 6]\n");
1673    }
1674
1675    #[test]
1676    fn test_list_filter_new() {
1677        let out = run_vm(
1678            "pipeline default(task) { let r = [1, 2, 3, 4].filter({ x -> x > 2 })\nlog(r) }",
1679        );
1680        assert_eq!(out, "[harn] [3, 4]\n");
1681    }
1682
1683    #[test]
1684    fn test_dict_access_new() {
1685        let out = run_vm("pipeline default(task) { let d = {name: \"Alice\"}\nlog(d.name) }");
1686        assert_eq!(out, "[harn] Alice\n");
1687    }
1688
1689    #[test]
1690    fn test_string_interpolation() {
1691        let out = run_vm("pipeline default(task) { let x = 42\nlog(\"val=${x}\") }");
1692        assert_eq!(out, "[harn] val=42\n");
1693    }
1694
1695    #[test]
1696    fn test_match_new() {
1697        let out = run_vm(
1698            "pipeline default(task) { let x = \"b\"\nmatch x { \"a\" -> { log(1) } \"b\" -> { log(2) } } }",
1699        );
1700        assert_eq!(out, "[harn] 2\n");
1701    }
1702
1703    #[test]
1704    fn test_json_roundtrip() {
1705        let out = run_vm("pipeline default(task) { let s = json_stringify({a: 1})\nlog(s) }");
1706        assert!(out.contains("\"a\""));
1707        assert!(out.contains("1"));
1708    }
1709
1710    #[test]
1711    fn test_type_of() {
1712        let out = run_vm("pipeline default(task) { log(type_of(42))\nlog(type_of(\"hi\")) }");
1713        assert_eq!(out, "[harn] int\n[harn] string\n");
1714    }
1715
1716    #[test]
1717    fn test_stack_overflow() {
1718        let err = run_vm_err("pipeline default(task) { fn f() { f() }\nf() }");
1719        assert!(
1720            err.contains("stack") || err.contains("overflow") || err.contains("recursion"),
1721            "Expected stack overflow error, got: {}",
1722            err
1723        );
1724    }
1725
1726    #[test]
1727    fn test_division_by_zero() {
1728        let err = run_vm_err("pipeline default(task) { log(1 / 0) }");
1729        assert!(
1730            err.contains("Division by zero") || err.contains("division"),
1731            "Expected division by zero error, got: {}",
1732            err
1733        );
1734    }
1735
1736    #[test]
1737    fn test_float_division_by_zero_uses_ieee_values() {
1738        let out = run_vm(
1739            "pipeline default(task) { log(is_nan(0.0 / 0.0))\nlog(is_infinite(1.0 / 0.0))\nlog(is_infinite(-1.0 / 0.0)) }",
1740        );
1741        assert_eq!(out, "[harn] true\n[harn] true\n[harn] true\n");
1742    }
1743
1744    #[test]
1745    fn test_reusing_catch_binding_name_in_same_block() {
1746        let out = run_vm(
1747            r#"pipeline default(task) {
1748try {
1749    throw "a"
1750} catch e {
1751    log(e)
1752}
1753try {
1754    throw "b"
1755} catch e {
1756    log(e)
1757}
1758}"#,
1759        );
1760        assert_eq!(out, "[harn] a\n[harn] b\n");
1761    }
1762
1763    #[test]
1764    fn test_try_catch_nested() {
1765        let out = run_output(
1766            r#"pipeline t(task) {
1767try {
1768    try {
1769        throw "inner"
1770    } catch(e) {
1771        log("inner caught: " + e)
1772        throw "outer"
1773    }
1774} catch(e2) {
1775    log("outer caught: " + e2)
1776}
1777}"#,
1778        );
1779        assert_eq!(
1780            out,
1781            "[harn] inner caught: inner\n[harn] outer caught: outer"
1782        );
1783    }
1784
1785    // --- Concurrency tests ---
1786
1787    #[test]
1788    fn test_parallel_basic() {
1789        let out = run_output(
1790            "pipeline t(task) { let results = parallel(3) { i -> i * 10 }\nlog(results) }",
1791        );
1792        assert_eq!(out, "[harn] [0, 10, 20]");
1793    }
1794
1795    #[test]
1796    fn test_parallel_no_variable() {
1797        let out = run_output("pipeline t(task) { let results = parallel(3) { 42 }\nlog(results) }");
1798        assert_eq!(out, "[harn] [42, 42, 42]");
1799    }
1800
1801    #[test]
1802    fn test_parallel_each_basic() {
1803        let out = run_output(
1804            "pipeline t(task) { let results = parallel each [1, 2, 3] { x -> x * x }\nlog(results) }",
1805        );
1806        assert_eq!(out, "[harn] [1, 4, 9]");
1807    }
1808
1809    #[test]
1810    fn test_spawn_await() {
1811        let out = run_output(
1812            r#"pipeline t(task) {
1813let handle = spawn { log("spawned") }
1814let result = await(handle)
1815log("done")
1816}"#,
1817        );
1818        assert_eq!(out, "[harn] spawned\n[harn] done");
1819    }
1820
1821    #[test]
1822    fn test_spawn_cancel() {
1823        let out = run_output(
1824            r#"pipeline t(task) {
1825let handle = spawn { log("should be cancelled") }
1826cancel(handle)
1827log("cancelled")
1828}"#,
1829        );
1830        assert_eq!(out, "[harn] cancelled");
1831    }
1832
1833    #[test]
1834    fn test_spawn_returns_value() {
1835        let out = run_output("pipeline t(task) { let h = spawn { 42 }\nlet r = await(h)\nlog(r) }");
1836        assert_eq!(out, "[harn] 42");
1837    }
1838
1839    // --- Deadline tests ---
1840
1841    #[test]
1842    fn test_deadline_success() {
1843        let out = run_output(
1844            r#"pipeline t(task) {
1845let result = deadline 5s { log("within deadline")
184642 }
1847log(result)
1848}"#,
1849        );
1850        assert_eq!(out, "[harn] within deadline\n[harn] 42");
1851    }
1852
1853    #[test]
1854    fn test_deadline_exceeded() {
1855        let result = run_harn_result(
1856            r#"pipeline t(task) {
1857deadline 1ms {
1858  var i = 0
1859  while i < 1000000 { i = i + 1 }
1860}
1861}"#,
1862        );
1863        assert!(result.is_err());
1864    }
1865
1866    #[test]
1867    fn test_deadline_caught_by_try() {
1868        let out = run_output(
1869            r#"pipeline t(task) {
1870try {
1871  deadline 1ms {
1872    var i = 0
1873    while i < 1000000 { i = i + 1 }
1874  }
1875} catch(e) {
1876  log("caught")
1877}
1878}"#,
1879        );
1880        assert_eq!(out, "[harn] caught");
1881    }
1882
1883    /// Helper that runs Harn source with a set of denied builtins.
1884    fn run_harn_with_denied(
1885        source: &str,
1886        denied: HashSet<String>,
1887    ) -> Result<(String, VmValue), VmError> {
1888        let rt = tokio::runtime::Builder::new_current_thread()
1889            .enable_all()
1890            .build()
1891            .unwrap();
1892        rt.block_on(async {
1893            let local = tokio::task::LocalSet::new();
1894            local
1895                .run_until(async {
1896                    let mut lexer = Lexer::new(source);
1897                    let tokens = lexer.tokenize().unwrap();
1898                    let mut parser = Parser::new(tokens);
1899                    let program = parser.parse().unwrap();
1900                    let chunk = Compiler::new().compile(&program).unwrap();
1901
1902                    let mut vm = Vm::new();
1903                    register_vm_stdlib(&mut vm);
1904                    vm.set_denied_builtins(denied);
1905                    let result = vm.execute(&chunk).await?;
1906                    Ok((vm.output().to_string(), result))
1907                })
1908                .await
1909        })
1910    }
1911
1912    #[test]
1913    fn test_sandbox_deny_builtin() {
1914        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1915        let result = run_harn_with_denied(
1916            r#"pipeline t(task) {
1917let xs = [1, 2]
1918push(xs, 3)
1919}"#,
1920            denied,
1921        );
1922        let err = result.unwrap_err();
1923        let msg = format!("{err}");
1924        assert!(
1925            msg.contains("not permitted"),
1926            "expected not permitted, got: {msg}"
1927        );
1928        assert!(
1929            msg.contains("push"),
1930            "expected builtin name in error, got: {msg}"
1931        );
1932    }
1933
1934    #[test]
1935    fn test_sandbox_allowed_builtin_works() {
1936        // Denying "push" should not block "log"
1937        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1938        let result = run_harn_with_denied(r#"pipeline t(task) { log("hello") }"#, denied);
1939        let (output, _) = result.unwrap();
1940        assert_eq!(output.trim(), "[harn] hello");
1941    }
1942
1943    #[test]
1944    fn test_sandbox_empty_denied_set() {
1945        // With an empty denied set, everything should work.
1946        let result = run_harn_with_denied(r#"pipeline t(task) { log("ok") }"#, HashSet::new());
1947        let (output, _) = result.unwrap();
1948        assert_eq!(output.trim(), "[harn] ok");
1949    }
1950
1951    #[test]
1952    fn test_sandbox_propagates_to_spawn() {
1953        // Denied builtins should propagate to spawned VMs.
1954        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1955        let result = run_harn_with_denied(
1956            r#"pipeline t(task) {
1957let handle = spawn {
1958  let xs = [1, 2]
1959  push(xs, 3)
1960}
1961await(handle)
1962}"#,
1963            denied,
1964        );
1965        let err = result.unwrap_err();
1966        let msg = format!("{err}");
1967        assert!(
1968            msg.contains("not permitted"),
1969            "expected not permitted in spawned VM, got: {msg}"
1970        );
1971    }
1972
1973    #[test]
1974    fn test_sandbox_propagates_to_parallel() {
1975        // Denied builtins should propagate to parallel VMs.
1976        let denied: HashSet<String> = ["push".to_string()].into_iter().collect();
1977        let result = run_harn_with_denied(
1978            r#"pipeline t(task) {
1979let results = parallel(2) { i ->
1980  let xs = [1, 2]
1981  push(xs, 3)
1982}
1983}"#,
1984            denied,
1985        );
1986        let err = result.unwrap_err();
1987        let msg = format!("{err}");
1988        assert!(
1989            msg.contains("not permitted"),
1990            "expected not permitted in parallel VM, got: {msg}"
1991        );
1992    }
1993
1994    #[test]
1995    fn test_if_else_has_lexical_block_scope() {
1996        let out = run_output(
1997            r#"pipeline t(task) {
1998let x = "outer"
1999if true {
2000  let x = "inner"
2001  log(x)
2002} else {
2003  let x = "other"
2004  log(x)
2005}
2006log(x)
2007}"#,
2008        );
2009        assert_eq!(out, "[harn] inner\n[harn] outer");
2010    }
2011
2012    #[test]
2013    fn test_loop_and_catch_bindings_are_block_scoped() {
2014        let out = run_output(
2015            r#"pipeline t(task) {
2016let label = "outer"
2017for item in [1, 2] {
2018  let label = "loop ${item}"
2019  log(label)
2020}
2021try {
2022  throw("boom")
2023} catch (label) {
2024  log(label)
2025}
2026log(label)
2027}"#,
2028        );
2029        assert_eq!(
2030            out,
2031            "[harn] loop 1\n[harn] loop 2\n[harn] boom\n[harn] outer"
2032        );
2033    }
2034}