Skip to main content

harn_vm/
vm.rs

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