Skip to main content

endbasic_core/vm/
mod.rs

1// EndBASIC
2// Copyright 2026 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Virtual machine for EndBASIC execution.
18
19#[cfg(test)]
20use crate::CallError;
21use crate::UpcallError;
22use crate::bytecode::{ExitCode, Register};
23use crate::callable::{Callable, Scope};
24use crate::compiler::SymbolKey;
25use crate::image::{GlobalVarInfo, Image};
26use crate::mem::{ConstantDatum, DatumPtr, Heap, HeapDatum};
27use crate::num::U24;
28use crate::reader::LineCol;
29use std::collections::HashMap;
30use std::rc::Rc;
31
32mod context;
33use context::{Context, ErrorHandler, InternalStopReason};
34
35/// Default maximum number of call stack frames.
36const DEFAULT_MAX_CALL_STACK: usize = 4096;
37
38/// Limits for VM execution resources.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct Limits {
41    /// Maximum number of frames the call stack can contain.
42    pub max_call_stack: usize,
43
44    /// Maximum number of entries the heap can contain.
45    pub max_heap_entries: U24,
46}
47
48impl Default for Limits {
49    fn default() -> Self {
50        Self { max_call_stack: DEFAULT_MAX_CALL_STACK, max_heap_entries: U24::MAX }
51    }
52}
53
54/// Error returned when a global variable access encounters a type or shape mismatch.
55///
56/// This is distinct from a missing variable, which is represented by `None` in the
57/// return value of `get_global` and `get_global_array`.
58#[derive(Debug, thiserror::Error)]
59pub enum GetGlobalError {
60    /// The variable exists but is an array; use `get_global_array` instead.
61    #[error("'{0}' is an array variable; use get_global_array to access it")]
62    IsArray(String),
63
64    /// The variable exists but is a scalar; use `get_global` instead.
65    #[error("'{0}' is a scalar variable; use get_global to access it")]
66    IsScalar(String),
67
68    /// The array subscripts are out of bounds or invalid.
69    #[error("{0}")]
70    SubscriptOutOfBounds(String),
71}
72
73/// Result type for global variable access operations.
74pub type GetGlobalResult<T> = Result<T, GetGlobalError>;
75
76/// Opaque handle to invoke a pending upcall.
77pub struct UpcallHandler<'a> {
78    vm: &'a mut Vm,
79    image: &'a Image,
80}
81
82impl<'a> UpcallHandler<'a> {
83    /// Invokes the pending upcall.
84    pub async fn invoke(self) -> Result<(), UpcallError> {
85        let vm = self.vm;
86        let image = self.image;
87        let (index, first_reg, upcall_pc) = vm
88            .pending_upcall
89            .take()
90            .expect("This is only reachable when the VM has a pending upcall");
91        let (upcall, scope) = vm.prepare_upcall(image, index, first_reg, upcall_pc);
92        let result = upcall.async_exec(scope).await;
93        match vm.handle_upcall_result(image, upcall_pc, result) {
94            Ok(()) => Ok(()),
95            Err(e) => {
96                vm.park_at_eof(image);
97                Err(e)
98            }
99        }
100    }
101}
102
103/// Representation of termination states from program execution.
104pub enum StopReason<'a> {
105    /// Execution terminated due to an `END` instruction.
106    End(ExitCode),
107
108    /// Execution terminated due to natural fallthrough.
109    Eof,
110
111    /// Execution stopped due to an instruction-level exception.
112    Exception(LineCol, String),
113
114    /// Execution stopped due to an asynchronous upcall that requires service from the caller.
115    UpcallAsync(UpcallHandler<'a>),
116
117    /// Execution stopped to yield control back to the caller.
118    Yield,
119}
120
121/// Virtual machine for EndBASIC program execution.
122pub struct Vm {
123    /// Mapping of all available upcall names to their handlers.
124    upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>>,
125
126    /// Upcall names already resolved into `upcalls`.
127    upcall_names: Vec<SymbolKey>,
128
129    /// Upcalls used by the current image in index order.
130    upcalls: Vec<Rc<dyn Callable>>,
131
132    /// Heap memory for dynamic allocations.
133    heap: Heap,
134
135    /// Processor context for execution.
136    context: Context,
137
138    /// Last error seen by the VM, if any.
139    last_error: Option<(LineCol, String)>,
140
141    /// Details about the pending upcall that has to be handled by the caller.
142    ///
143    /// The tuple contains the upcall index, the first argument register, and the PC of the
144    /// upcall instruction (for arg position lookup in `DebugInfo`).
145    pending_upcall: Option<(u16, Register, usize)>,
146}
147
148impl Vm {
149    /// Resolves upcall metadata and builds an execution scope for invocation.
150    fn prepare_upcall<'a>(
151        &'a mut self,
152        image: &'a Image,
153        index: u16,
154        first_reg: Register,
155        upcall_pc: usize,
156    ) -> (Rc<dyn Callable>, Scope<'a>) {
157        let upcall = self.upcalls[usize::from(index)].clone();
158        let is_function = upcall.metadata().return_type().is_some();
159        let scope = self.upcall_scope(image, first_reg, is_function, upcall_pc);
160        (upcall, scope)
161    }
162
163    /// Handles the result of an upcall invocation.
164    ///
165    /// Returns `Ok(())` if invocation succeeded or if an exception handler consumed the
166    /// failure.  Returns `Err` only if execution must stop with an uncaught exception.
167    fn handle_upcall_result(
168        &mut self,
169        image: &Image,
170        upcall_pc: usize,
171        result: crate::callable::CallResult<()>,
172    ) -> Result<(), UpcallError> {
173        match result {
174            Ok(()) => Ok(()),
175            Err(e) => {
176                let default_pos = image.debug_info.instrs[upcall_pc].linecol;
177                let upcall_error = e.to_upcall_error(default_pos);
178                let (pos, message) = upcall_error.parts();
179                if self.handle_exception(image, upcall_pc, pos, message) {
180                    Ok(())
181                } else {
182                    Err(upcall_error)
183                }
184            }
185        }
186    }
187
188    /// Returns the scalar value named `key` from `vars`, decoding values from `read_raw`.
189    fn get_scalar_var(
190        &self,
191        image: &Image,
192        key: &SymbolKey,
193        vars: &HashMap<SymbolKey, GlobalVarInfo>,
194        read_raw: fn(&Context, u8) -> u64,
195    ) -> GetGlobalResult<Option<ConstantDatum>> {
196        let Some(info) = vars.get(key) else {
197            return Ok(None);
198        };
199        if info.ndims != 0 {
200            return Err(GetGlobalError::IsArray(key.to_string()));
201        }
202        let raw = read_raw(&self.context, info.reg);
203        Ok(Some(ConstantDatum::from_raw(raw, info.subtype, &image.constants, &self.heap)))
204    }
205
206    /// Returns the array element named `key` from `vars`, decoding values from `read_raw`.
207    fn get_array_var(
208        &self,
209        image: &Image,
210        key: &SymbolKey,
211        vars: &HashMap<SymbolKey, GlobalVarInfo>,
212        subscripts: &[i32],
213        read_raw: fn(&Context, u8) -> u64,
214    ) -> GetGlobalResult<Option<ConstantDatum>> {
215        let Some(info) = vars.get(key) else {
216            return Ok(None);
217        };
218        if info.ndims == 0 {
219            return Err(GetGlobalError::IsScalar(key.to_string()));
220        }
221        let raw = read_raw(&self.context, info.reg);
222        let ptr = DatumPtr::from(raw);
223        let heap_idx = ptr.heap_index();
224        let HeapDatum::Array(a) = self.heap.get(heap_idx) else {
225            panic!("Array variable does not point to an array on the heap");
226        };
227        let flat_idx = a.flat_index(subscripts).map_err(GetGlobalError::SubscriptOutOfBounds)?;
228        let v = a.values[flat_idx];
229        Ok(Some(ConstantDatum::from_raw(v, info.subtype, &image.constants, &self.heap)))
230    }
231
232    /// Creates a new VM with the given `upcalls_by_name` as the available built-in callables.
233    pub fn new(upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>>) -> Self {
234        Self::new_with_limits(upcalls_by_name, Limits::default())
235    }
236
237    /// Creates a new VM with the given `upcalls_by_name` and resource `limits`.
238    pub fn new_with_limits(
239        upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>>,
240        limits: Limits,
241    ) -> Self {
242        Self {
243            upcalls_by_name,
244            upcall_names: vec![],
245            upcalls: vec![],
246            heap: Heap::new(limits.max_heap_entries),
247            context: Context::new(limits.max_call_stack),
248            last_error: None,
249            pending_upcall: None,
250        }
251    }
252
253    /// Resets any existing execution state, including upcall caches and the program counter.
254    pub fn reset(&mut self) {
255        self.upcall_names.clear();
256        self.upcalls.clear();
257        self.heap.clear();
258        self.context.clear_runtime_state();
259        self.last_error = None;
260        self.pending_upcall = None;
261    }
262
263    /// Resets runtime state (registers, heap, last error, call stack, program counter) but preserves
264    /// upcall caches so the same image can continue to be executed.
265    ///
266    /// Note: because the program counter is also reset, callers need to either re-set it to a
267    /// specific location or replace the image with one that resumes from the start.
268    pub fn clear(&mut self) {
269        self.heap.clear();
270        self.context.clear_runtime_state();
271        self.last_error = None;
272        self.pending_upcall = None;
273    }
274
275    /// Clears the current error handler without affecting execution state or captured errors.
276    pub fn clear_error_handler(&mut self) {
277        self.context.clear_error_handler();
278    }
279
280    /// Synchronizes cached upcall handlers with the externally-owned `image`.
281    fn sync_upcalls(&mut self, image: &Image) {
282        debug_assert!(
283            image.upcalls.starts_with(self.upcall_names.as_slice()),
284            "Vm::reset() is required before executing a different image",
285        );
286
287        for key in &image.upcalls[self.upcalls.len()..] {
288            self.upcalls.push(
289                self.upcalls_by_name
290                    .get(key)
291                    .expect("All upcalls exposed during compilation must be present at runtime")
292                    .clone(),
293            );
294            self.upcall_names.push(key.clone());
295        }
296    }
297
298    /// Parks execution at the current EOF instruction so later appended code can resume.
299    fn park_at_eof(&mut self, image: &Image) {
300        debug_assert!(!image.code.is_empty());
301        self.context.set_pc(image.code.len() - 1);
302    }
303
304    /// Constructs a `Scope` for an upcall with arguments starting at `reg`.
305    ///
306    /// `upcall_pc` is the address of the UPCALL instruction in the image, used to look up
307    /// per-argument source locations from `DebugInfo`.  `is_function` indicates whether the
308    /// upcall is a function (with a return value slot) so that `Scope::arg_offset` can be set
309    /// appropriately.
310    fn upcall_scope<'a>(
311        &'a mut self,
312        image: &'a Image,
313        reg: Register,
314        is_function: bool,
315        upcall_pc: usize,
316    ) -> Scope<'a> {
317        let arg_linecols = image
318            .debug_info
319            .instrs
320            .get(upcall_pc)
321            .map(|m| m.arg_linecols.as_slice())
322            .unwrap_or(&[]);
323        self.context.upcall_scope(
324            reg,
325            is_function,
326            image.constants.as_slice(),
327            &mut self.heap,
328            arg_linecols,
329            &self.last_error,
330            image.data.as_slice(),
331        )
332    }
333
334    /// Handles an exception raised at `pc`, corresponding to `pos`, with `message`.  Returns true if the error was handled.
335    fn handle_exception(
336        &mut self,
337        image: &Image,
338        pc: usize,
339        pos: LineCol,
340        message: String,
341    ) -> bool {
342        self.last_error = Some((pos, message));
343
344        match self.context.error_handler() {
345            ErrorHandler::None => false,
346            ErrorHandler::Jump { active: true, .. } => false,
347            ErrorHandler::Jump { active: false, addr } => {
348                self.context.set_error_handler_active();
349                self.context.set_pc(addr);
350                true
351            }
352            ErrorHandler::ResumeNext => {
353                let mut next_pc = image.code.len();
354                for (idx, meta) in image.debug_info.instrs.iter().enumerate().skip(pc + 1) {
355                    if meta.is_stmt_start {
356                        next_pc = idx;
357                        break;
358                    }
359                }
360                self.context.set_pc(next_pc);
361                true
362            }
363        }
364    }
365
366    /// Returns the value of the global scalar variable `key` as a `ConstantDatum`.
367    ///
368    /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the
369    /// variable was not declared).  Returns `Err` if the variable exists but is an
370    /// array; in that case, use `get_global_array` instead.
371    pub fn get_global(
372        &self,
373        image: &Image,
374        key: &SymbolKey,
375    ) -> GetGlobalResult<Option<ConstantDatum>> {
376        self.get_scalar_var(image, key, &image.debug_info.global_vars, Context::get_global_reg_raw)
377    }
378
379    /// Returns the value of an element in the global array variable `key` at the given
380    /// `subscripts` as a `ConstantDatum`.
381    ///
382    /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the
383    /// variable was not declared).  Returns `Err` if the variable exists but is a scalar
384    /// (use `get_global` instead), or if the subscripts are out of bounds.
385    pub fn get_global_array(
386        &self,
387        image: &Image,
388        key: &SymbolKey,
389        subscripts: &[i32],
390    ) -> GetGlobalResult<Option<ConstantDatum>> {
391        self.get_array_var(
392            image,
393            key,
394            &image.debug_info.global_vars,
395            subscripts,
396            Context::get_global_reg_raw,
397        )
398    }
399
400    /// Returns the value of the program-scope scalar variable `key` as a `ConstantDatum`.
401    ///
402    /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the
403    /// variable was not declared).  Returns `Err` if the variable exists but is an
404    /// array; in that case, use `get_program_array` instead.
405    pub fn get_program(
406        &self,
407        image: &Image,
408        key: &SymbolKey,
409    ) -> GetGlobalResult<Option<ConstantDatum>> {
410        self.get_scalar_var(
411            image,
412            key,
413            &image.debug_info.program_vars,
414            Context::get_program_reg_raw,
415        )
416    }
417
418    /// Returns the value of an element in the program-scope array variable `key` at the
419    /// given `subscripts` as a `ConstantDatum`.
420    ///
421    /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the
422    /// variable was not declared).  Returns `Err` if the variable exists but is a scalar
423    /// (use `get_program` instead), or if the subscripts are out of bounds.
424    pub fn get_program_array(
425        &self,
426        image: &Image,
427        key: &SymbolKey,
428        subscripts: &[i32],
429    ) -> GetGlobalResult<Option<ConstantDatum>> {
430        self.get_array_var(
431            image,
432            key,
433            &image.debug_info.program_vars,
434            subscripts,
435            Context::get_program_reg_raw,
436        )
437    }
438
439    /// Starts or resumes execution of `image`.
440    ///
441    /// Returns a `StopReason` indicating why execution stopped, which may be due to program
442    /// termination, an exception, or a pending upcall that requires caller handling.
443    pub fn exec<'a>(&'a mut self, image: &'a Image) -> StopReason<'a> {
444        self.sync_upcalls(image);
445
446        loop {
447            if self.pending_upcall.is_some() {
448                return StopReason::UpcallAsync(UpcallHandler { vm: self, image });
449            }
450
451            match self.context.exec(image, &mut self.heap) {
452                InternalStopReason::End(code) => {
453                    self.park_at_eof(image);
454                    return StopReason::End(code);
455                }
456                InternalStopReason::Eof => return StopReason::Eof,
457                InternalStopReason::Exception(pc, e) => {
458                    let pos = image.debug_info.instrs[pc].linecol;
459                    if !self.handle_exception(image, pc, pos, e.clone()) {
460                        self.park_at_eof(image);
461                        return StopReason::Exception(pos, e);
462                    }
463                }
464                InternalStopReason::Upcall(index, first_reg, upcall_pc) => {
465                    let (upcall, scope) = self.prepare_upcall(image, index, first_reg, upcall_pc);
466                    let result = upcall.exec(scope);
467                    if let Err(upcall_error) = self.handle_upcall_result(image, upcall_pc, result) {
468                        let (pos, message) = upcall_error.parts();
469                        self.park_at_eof(image);
470                        return StopReason::Exception(pos, message);
471                    }
472                }
473
474                InternalStopReason::UpcallAsync(index, first_reg, upcall_pc) => {
475                    self.pending_upcall = Some((index, first_reg, upcall_pc));
476                    return StopReason::UpcallAsync(UpcallHandler { vm: self, image });
477                }
478
479                InternalStopReason::Yield => return StopReason::Yield,
480            }
481        }
482    }
483
484    /// Stops execution of `image` so that the next call to `exec` starts at EOF.
485    ///
486    /// This is useful when external events interrupt execution and the caller wants
487    /// to avoid resuming a partially-run image by mistake.
488    pub fn interrupt(&mut self, image: &Image) {
489        self.pending_upcall = None;
490        self.park_at_eof(image);
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::Compiler;
498    use crate::ast::{ArgSep, ExprType};
499    use crate::callable::{
500        ArgSepSyntax, CallResult, CallableMetadata, CallableMetadataBuilder, RequiredValueSyntax,
501        SingularArgSyntax,
502    };
503    use crate::compiler::SymbolKey;
504    use crate::image::Image;
505    use crate::reader::LineCol;
506    use crate::testutils::OutCommand;
507    use async_trait::async_trait;
508    use futures_lite::future::yield_now;
509    use std::borrow::Cow;
510    use std::cell::RefCell;
511    use std::collections::HashMap;
512    use std::io;
513    use std::rc::Rc;
514
515    /// A test callable that captures the source positions of argument register slots.
516    ///
517    /// On each invocation, records the result of `scope.get_pos(n)` for `0..nargs` into
518    /// `positions`.
519    struct PosCapture {
520        metadata: Rc<CallableMetadata>,
521        nargs: u8,
522        positions: Rc<RefCell<Vec<LineCol>>>,
523    }
524
525    impl PosCapture {
526        /// Creates a new `PosCapture` callable named `POS_CAPTURE` that expects
527        /// `nargs` required integer arguments separated by commas.
528        fn new(nargs: u8, positions: Rc<RefCell<Vec<LineCol>>>) -> Rc<Self> {
529            let singular: Vec<SingularArgSyntax> = (0..nargs)
530                .map(|i| {
531                    let sep = if i == nargs - 1 {
532                        ArgSepSyntax::End
533                    } else {
534                        ArgSepSyntax::Exactly(ArgSep::Long)
535                    };
536                    SingularArgSyntax::RequiredValue(
537                        RequiredValueSyntax {
538                            name: Cow::Borrowed("arg"),
539                            vtype: ExprType::Integer,
540                        },
541                        sep,
542                    )
543                })
544                .collect();
545            let md = CallableMetadataBuilder::new("POS_CAPTURE")
546                .with_dynamic_syntax(vec![(singular, None)])
547                .test_build();
548            Rc::from(Self { metadata: md, nargs, positions })
549        }
550    }
551
552    impl Callable for PosCapture {
553        fn metadata(&self) -> Rc<CallableMetadata> {
554            self.metadata.clone()
555        }
556
557        fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
558            let mut positions = self.positions.borrow_mut();
559            for i in 0..self.nargs {
560                positions.push(scope.get_pos(i));
561            }
562            Ok(())
563        }
564    }
565
566    struct ReturnFortyTwoFunction {
567        metadata: Rc<CallableMetadata>,
568    }
569
570    impl ReturnFortyTwoFunction {
571        fn new() -> Rc<Self> {
572            let md = CallableMetadataBuilder::new("RET42")
573                .with_return_type(ExprType::Integer)
574                .with_syntax(&[(&[], None)])
575                .test_build();
576            Rc::from(Self { metadata: md })
577        }
578    }
579
580    impl Callable for ReturnFortyTwoFunction {
581        fn metadata(&self) -> Rc<CallableMetadata> {
582            self.metadata.clone()
583        }
584
585        fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
586            scope.return_integer(42)
587        }
588    }
589
590    struct IoErrorCommand {
591        metadata: Rc<CallableMetadata>,
592    }
593
594    impl IoErrorCommand {
595        fn new() -> Rc<Self> {
596            let md = CallableMetadataBuilder::new("IOFAIL")
597                .with_dynamic_syntax(vec![(vec![], None)])
598                .test_build();
599            Rc::from(Self { metadata: md })
600        }
601    }
602
603    struct AsyncIncrementFunction {
604        metadata: Rc<CallableMetadata>,
605    }
606
607    impl AsyncIncrementFunction {
608        fn new() -> Rc<Self> {
609            let md = CallableMetadataBuilder::new("ASYNC_INCREMENT")
610                .with_return_type(ExprType::Integer)
611                .with_async(true)
612                .with_syntax(&[(
613                    &[SingularArgSyntax::RequiredValue(
614                        RequiredValueSyntax {
615                            name: Cow::Borrowed("value"),
616                            vtype: ExprType::Integer,
617                        },
618                        ArgSepSyntax::End,
619                    )],
620                    None,
621                )])
622                .test_build();
623            Rc::from(Self { metadata: md })
624        }
625    }
626
627    #[async_trait(?Send)]
628    impl Callable for AsyncIncrementFunction {
629        fn metadata(&self) -> Rc<CallableMetadata> {
630            self.metadata.clone()
631        }
632
633        async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
634            let value = scope.get_integer(0) + 1;
635            yield_now().await;
636            scope.return_integer(value)
637        }
638    }
639
640    struct AsyncIoErrorCommand {
641        metadata: Rc<CallableMetadata>,
642    }
643
644    impl AsyncIoErrorCommand {
645        fn new() -> Rc<Self> {
646            let md = CallableMetadataBuilder::new("ASYNC_IOFAIL")
647                .with_async(true)
648                .with_dynamic_syntax(vec![(vec![], None)])
649                .test_build();
650            Rc::from(Self { metadata: md })
651        }
652    }
653
654    #[async_trait(?Send)]
655    impl Callable for AsyncIoErrorCommand {
656        fn metadata(&self) -> Rc<CallableMetadata> {
657            self.metadata.clone()
658        }
659
660        async fn async_exec(&self, _scope: Scope<'_>) -> CallResult<()> {
661            yield_now().await;
662            Err(CallError::from(io::Error::other("mock async I/O error")))
663        }
664    }
665
666    #[async_trait(?Send)]
667    impl Callable for IoErrorCommand {
668        fn metadata(&self) -> Rc<CallableMetadata> {
669            self.metadata.clone()
670        }
671
672        fn exec(&self, _scope: Scope<'_>) -> CallResult<()> {
673            Err(CallError::from(io::Error::other("mock I/O error")))
674        }
675    }
676
677    /// Runs the VM to completion, invoking every upcall as it is encountered.
678    async fn run_to_end(vm: &mut Vm, image: &Image) {
679        loop {
680            match vm.exec(image) {
681                StopReason::End(_) => break,
682                StopReason::Eof => break,
683                StopReason::Exception(_, msg) => panic!("Unexpected exception: {}", msg),
684                StopReason::UpcallAsync(handler) => handler.invoke().await.unwrap(),
685                StopReason::Yield => (),
686            }
687        }
688    }
689
690    #[test]
691    fn test_exec_without_load_is_eof() {
692        let mut vm = Vm::new(HashMap::default());
693        let image = Image::default();
694        match vm.exec(&image) {
695            StopReason::Eof => (),
696            _ => panic!("Unexpected stop reason"),
697        }
698    }
699
700    #[test]
701    fn test_exec_empty_image_is_eof() {
702        let mut vm = Vm::new(HashMap::default());
703        let image = Image::default();
704        match vm.exec(&image) {
705            StopReason::Eof => (),
706            _ => panic!("Unexpected stop reason"),
707        }
708    }
709
710    #[test]
711    fn test_exec_empty_compilation_is_eof() {
712        let mut vm = Vm::new(HashMap::default());
713        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
714        let image = compiler.compile(&mut b"".as_slice()).unwrap();
715        match vm.exec(&image) {
716            StopReason::Eof => (),
717            _ => panic!("Unexpected stop reason"),
718        }
719    }
720
721    #[tokio::test]
722    async fn test_exec_upcall_flow() {
723        let data = Rc::from(RefCell::from(vec![]));
724        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
725        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
726
727        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
728        let image = compiler.compile(&mut b"OUT 30: OUT 20".as_slice()).unwrap();
729
730        let mut vm = Vm::new(upcalls_by_name);
731
732        match vm.exec(&image) {
733            StopReason::Eof => (),
734            _ => panic!("Execution should stop at EOF"),
735        }
736        assert_eq!(["30", "20"], *data.borrow().as_slice());
737    }
738
739    #[tokio::test]
740    async fn test_exec_async_upcall_flow() {
741        let data = Rc::from(RefCell::from(vec![]));
742        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
743        upcalls_by_name.insert(SymbolKey::from("ASYNC_INCREMENT"), AsyncIncrementFunction::new());
744        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
745
746        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
747        let image = compiler.compile(&mut b"OUT ASYNC_INCREMENT(123): OUT 5".as_slice()).unwrap();
748        let mut vm = Vm::new(upcalls_by_name);
749
750        match vm.exec(&image) {
751            StopReason::UpcallAsync(handler) => handler.invoke().await.unwrap(),
752            _ => panic!("Execution should stop at ASYNC_INCREMENT upcall"),
753        }
754
755        assert!(data.borrow().is_empty());
756
757        match vm.exec(&image) {
758            StopReason::Eof => (),
759            _ => panic!("Execution should stop at EOF"),
760        }
761
762        assert_eq!(["124", "5"], *data.borrow().as_slice());
763    }
764
765    #[tokio::test]
766    async fn test_exec_async_upcall_error_can_resume_after_append() {
767        let data = Rc::from(RefCell::from(vec![]));
768        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
769        upcalls_by_name.insert(SymbolKey::from("ASYNC_IOFAIL"), AsyncIoErrorCommand::new());
770        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
771
772        let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
773        let mut image = Image::default();
774        compiler.compile_more(&mut image, &mut b"ASYNC_IOFAIL".as_slice()).unwrap();
775
776        let mut vm = Vm::new(upcalls_by_name);
777        match vm.exec(&image) {
778            StopReason::UpcallAsync(handler) => {
779                let error = handler.invoke().await.unwrap_err();
780                let (pos, message) = error.parts();
781                assert_eq!(LineCol { line: 1, col: 1 }, pos);
782                assert_eq!("mock async I/O error", message);
783            }
784            _ => panic!("Execution should stop at ASYNC_IOFAIL upcall"),
785        }
786
787        match vm.exec(&image) {
788            StopReason::Eof => (),
789            _ => panic!("Execution should park at EOF after an ASYNC_IOFAIL exception"),
790        }
791
792        compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap();
793        match vm.exec(&image) {
794            StopReason::Eof => (),
795            _ => panic!("Execution should resume at newly appended code"),
796        }
797        assert_eq!(["2"], *data.borrow().as_slice());
798
799        match vm.exec(&image) {
800            StopReason::Eof => (),
801            _ => panic!("Execution should stop at EOF after appended code"),
802        }
803    }
804
805    #[tokio::test]
806    async fn test_interrupt_cancels_pending_async_upcall() {
807        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
808        upcalls_by_name.insert(SymbolKey::from("ASYNC_INCREMENT"), AsyncIncrementFunction::new());
809
810        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
811        let image = compiler.compile(&mut b"x = ASYNC_INCREMENT(123)".as_slice()).unwrap();
812        let mut vm = Vm::new(upcalls_by_name);
813
814        match vm.exec(&image) {
815            StopReason::UpcallAsync(_) => (),
816            _ => panic!("Execution should stop at ASYNC_INCREMENT upcall"),
817        }
818
819        vm.interrupt(&image);
820        match vm.exec(&image) {
821            StopReason::Eof => (),
822            _ => panic!("Execution should stop at EOF after interrupting a pending upcall"),
823        }
824    }
825
826    #[tokio::test]
827    async fn test_interrupt_after_pending_async_upcall_can_resume_after_append() {
828        let data = Rc::from(RefCell::from(vec![]));
829        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
830        upcalls_by_name.insert(SymbolKey::from("ASYNC_INCREMENT"), AsyncIncrementFunction::new());
831        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
832
833        let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
834        let mut image = Image::default();
835        compiler.compile_more(&mut image, &mut b"x = ASYNC_INCREMENT(123)".as_slice()).unwrap();
836
837        let mut vm = Vm::new(upcalls_by_name);
838        match vm.exec(&image) {
839            StopReason::UpcallAsync(_) => (),
840            _ => panic!("Execution should stop at ASYNC_INCREMENT upcall"),
841        }
842
843        vm.interrupt(&image);
844        match vm.exec(&image) {
845            StopReason::Eof => (),
846            _ => panic!("Execution should stop at EOF after interrupting a pending upcall"),
847        }
848
849        compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap();
850        match vm.exec(&image) {
851            StopReason::Eof => (),
852            _ => panic!("Execution should resume at newly appended code"),
853        }
854        assert_eq!(["2"], *data.borrow().as_slice());
855
856        match vm.exec(&image) {
857            StopReason::Eof => (),
858            _ => panic!("Execution should stop at EOF after appended code"),
859        }
860    }
861
862    #[tokio::test]
863    async fn test_exec_end_code_default() {
864        let mut vm = Vm::new(HashMap::default());
865        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
866        let image = compiler.compile(&mut b"END".as_slice()).unwrap();
867        match vm.exec(&image) {
868            StopReason::End(code) if code.is_success() => (),
869            _ => panic!("Unexpected stop reason"),
870        }
871    }
872
873    #[tokio::test]
874    async fn test_exec_end_code_explicit() {
875        let mut vm = Vm::new(HashMap::default());
876        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
877        let image = compiler.compile(&mut b"END 3".as_slice()).unwrap();
878        match vm.exec(&image) {
879            StopReason::End(code) if code.to_i32() == 3 => (),
880            _ => panic!("Unexpected stop reason"),
881        }
882    }
883
884    #[tokio::test]
885    async fn test_exec_end_can_resume_after_append() {
886        let data = Rc::from(RefCell::from(vec![]));
887        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
888        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
889
890        let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
891        let mut image = Image::default();
892        compiler.compile_more(&mut image, &mut b"END 3".as_slice()).unwrap();
893
894        let mut vm = Vm::new(upcalls_by_name);
895        match vm.exec(&image) {
896            StopReason::End(code) if code.to_i32() == 3 => (),
897            _ => panic!("Unexpected stop reason"),
898        }
899        match vm.exec(&image) {
900            StopReason::Eof => (),
901            _ => panic!("Execution should park at EOF after END"),
902        }
903
904        compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap();
905        match vm.exec(&image) {
906            StopReason::Eof => (),
907            _ => panic!("Execution should resume at newly appended code"),
908        }
909        assert_eq!(["2"], *data.borrow().as_slice());
910
911        match vm.exec(&image) {
912            StopReason::Eof => (),
913            _ => panic!("Execution should stop at EOF after appended code"),
914        }
915    }
916
917    #[tokio::test]
918    async fn test_exec_exception_can_resume_after_append() {
919        let data = Rc::from(RefCell::from(vec![]));
920        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
921        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
922
923        let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
924        let mut image = Image::default();
925        compiler.compile_more(&mut image, &mut b"a = 1 / 0".as_slice()).unwrap();
926
927        let mut vm = Vm::new(upcalls_by_name);
928        match vm.exec(&image) {
929            StopReason::Exception(_, msg) if msg == "Division by zero" => (),
930            _ => panic!("Unexpected stop reason"),
931        }
932        match vm.exec(&image) {
933            StopReason::Eof => (),
934            _ => panic!("Execution should park at EOF after an exception"),
935        }
936
937        compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap();
938        match vm.exec(&image) {
939            StopReason::Eof => (),
940            _ => panic!("Execution should resume at newly appended code"),
941        }
942        assert_eq!(["2"], *data.borrow().as_slice());
943
944        match vm.exec(&image) {
945            StopReason::Eof => (),
946            _ => panic!("Execution should stop at EOF after appended code"),
947        }
948    }
949
950    #[tokio::test]
951    async fn test_exec_upcall_can_return_with_scope_helper() {
952        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
953        upcalls_by_name.insert(SymbolKey::from("RET42"), ReturnFortyTwoFunction::new());
954
955        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
956        let image = compiler.compile(&mut b"x = RET42".as_slice()).unwrap();
957        let mut vm = Vm::new(upcalls_by_name);
958        run_to_end(&mut vm, &image).await;
959
960        assert_eq!(
961            Some(ConstantDatum::Integer(42)),
962            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
963        );
964    }
965
966    #[tokio::test]
967    async fn test_exec_upcall_io_error_is_reported() {
968        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
969        upcalls_by_name.insert(SymbolKey::from("IOFAIL"), IoErrorCommand::new());
970
971        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
972        let image = compiler.compile(&mut b"IOFAIL".as_slice()).unwrap();
973        let mut vm = Vm::new(upcalls_by_name);
974
975        match vm.exec(&image) {
976            StopReason::Exception(_, msg) if msg == "mock I/O error" => (),
977            _ => panic!("Execution should stop at an IOFAIL exception"),
978        };
979
980        match vm.exec(&image) {
981            StopReason::Eof => (),
982            _ => panic!("Execution should stop at EOF after serving error"),
983        }
984    }
985
986    #[tokio::test]
987    async fn test_exec_upcall_io_error_can_be_caught() {
988        let data = Rc::from(RefCell::from(vec![]));
989        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
990        upcalls_by_name.insert(SymbolKey::from("IOFAIL"), IoErrorCommand::new());
991        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
992
993        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
994        let image = compiler
995            .compile(
996                &mut br#"
997                    ON ERROR GOTO @recover
998                    IOFAIL
999                    END 5
1000                    @recover
1001                    OUT "ok"
1002                "#
1003                .as_slice(),
1004            )
1005            .unwrap();
1006        let mut vm = Vm::new(upcalls_by_name);
1007
1008        match vm.exec(&image) {
1009            StopReason::Eof => (),
1010            _ => panic!("Execution should complete after handling IOFAIL"),
1011        };
1012
1013        match vm.exec(&image) {
1014            StopReason::Eof => (),
1015            _ => panic!("Execution should have reached EOF after OUT"),
1016        }
1017
1018        assert_eq!(["ok"], *data.borrow().as_slice());
1019        assert_eq!(
1020            Some((LineCol { line: 3, col: 21 }, "mock I/O error".to_owned())),
1021            vm.last_error
1022        );
1023    }
1024
1025    #[tokio::test]
1026    async fn test_exec_yields_on_backward_jump() {
1027        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1028        let image = compiler.compile(&mut b"x = 0: DO: x = x + 1: LOOP".as_slice()).unwrap();
1029        let mut vm = Vm::new(HashMap::default());
1030
1031        match vm.exec(&image) {
1032            StopReason::Yield => (),
1033            _ => panic!("Execution should yield in a loop"),
1034        }
1035        assert_eq!(
1036            Some(ConstantDatum::Integer(1)),
1037            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1038        );
1039
1040        match vm.exec(&image) {
1041            StopReason::Yield => (),
1042            _ => panic!("Execution should continue yielding in a loop"),
1043        }
1044        assert_eq!(
1045            Some(ConstantDatum::Integer(2)),
1046            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1047        );
1048
1049        vm.interrupt(&image);
1050        match vm.exec(&image) {
1051            StopReason::Eof => (),
1052            _ => panic!("Execution should stop at EOF after interrupt"),
1053        }
1054    }
1055
1056    #[tokio::test]
1057    async fn test_exec_yields_after_gosub_return() {
1058        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1059        let image =
1060            compiler.compile(&mut b"GOSUB @foo: END\n@foo: x = x + 1: RETURN".as_slice()).unwrap();
1061        let mut vm = Vm::new(HashMap::default());
1062
1063        match vm.exec(&image) {
1064            StopReason::Yield => (),
1065            _ => panic!("Execution should yield after returning from GOSUB"),
1066        }
1067        assert_eq!(
1068            Some(ConstantDatum::Integer(1)),
1069            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1070        );
1071
1072        match vm.exec(&image) {
1073            StopReason::End(code) if code.is_success() => (),
1074            _ => panic!("Execution should continue after yield"),
1075        }
1076    }
1077
1078    #[tokio::test]
1079    async fn test_interrupt_parks_execution_at_eof() {
1080        let data = Rc::from(RefCell::from(vec![]));
1081        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1082        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
1083
1084        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1085        let image = compiler.compile(&mut b"OUT 1: OUT 2".as_slice()).unwrap();
1086        let mut vm = Vm::new(upcalls_by_name);
1087
1088        match vm.exec(&image) {
1089            StopReason::Eof => (),
1090            _ => panic!("Execution should stop at EOF"),
1091        }
1092        assert_eq!(["1", "2"], *data.borrow().as_slice());
1093
1094        vm.interrupt(&image);
1095        match vm.exec(&image) {
1096            StopReason::Eof => (),
1097            _ => panic!("Execution should be parked at EOF after interruption"),
1098        }
1099        assert_eq!(["1", "2"], *data.borrow().as_slice());
1100    }
1101
1102    #[tokio::test]
1103    async fn test_clear_resets_runtime_state() {
1104        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1105        let image = compiler.compile(&mut b"x = 7".as_slice()).unwrap();
1106        let mut vm = Vm::new(HashMap::default());
1107        run_to_end(&mut vm, &image).await;
1108
1109        assert_eq!(
1110            Some(ConstantDatum::Integer(7)),
1111            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1112        );
1113
1114        vm.clear();
1115
1116        assert_eq!(
1117            Some(ConstantDatum::Integer(0)),
1118            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1119        );
1120    }
1121
1122    #[tokio::test]
1123    async fn test_clear_preserves_upcall_caches() {
1124        let data = Rc::from(RefCell::from(vec![]));
1125        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1126        upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone()));
1127
1128        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1129        let image = compiler.compile(&mut b"OUT 3".as_slice()).unwrap();
1130        let mut vm = Vm::new(upcalls_by_name);
1131
1132        match vm.exec(&image) {
1133            StopReason::Eof => (),
1134            _ => panic!("Execution should stop at EOF"),
1135        }
1136        assert_eq!(["3"], *data.borrow().as_slice());
1137
1138        vm.clear();
1139
1140        match vm.exec(&image) {
1141            StopReason::Eof => (),
1142            _ => panic!("Execution should still stop at EOF after clear"),
1143        }
1144        assert_eq!(["3", "3"], *data.borrow().as_slice());
1145    }
1146
1147    #[tokio::test]
1148    async fn test_reset_preserves_call_stack_limit() {
1149        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1150        let image = compiler
1151            .compile(
1152                &mut br#"
1153                    SUB recurse(n%)
1154                        IF n < 20 THEN
1155                            recurse n + 1
1156                        END IF
1157                    END SUB
1158
1159                    recurse 0
1160                "#
1161                .as_slice(),
1162            )
1163            .unwrap();
1164        let mut vm = Vm::new_with_limits(
1165            HashMap::default(),
1166            Limits { max_call_stack: 8, max_heap_entries: U24::MAX },
1167        );
1168
1169        match vm.exec(&image) {
1170            StopReason::Exception(_, msg) if msg == "Out of call stack space" => (),
1171            _ => panic!("Execution should stop when the call stack limit is reached"),
1172        }
1173
1174        vm.reset();
1175
1176        match vm.exec(&image) {
1177            StopReason::Exception(_, msg) if msg == "Out of call stack space" => (),
1178            _ => panic!("Execution should preserve the configured call stack limit after reset"),
1179        }
1180    }
1181
1182    #[tokio::test]
1183    async fn test_scope_get_pos_no_args() {
1184        let positions: Rc<RefCell<Vec<LineCol>>> = Rc::default();
1185        let cmd = PosCapture::new(0, positions.clone());
1186        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1187        upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd);
1188
1189        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1190        let image = compiler.compile(&mut b"POS_CAPTURE".as_slice()).unwrap();
1191        let mut vm = Vm::new(upcalls_by_name);
1192        run_to_end(&mut vm, &image).await;
1193
1194        let pos = positions.borrow();
1195        assert_eq!(&[] as &[LineCol], pos.as_slice());
1196    }
1197
1198    #[tokio::test]
1199    async fn test_scope_get_pos_single_arg() {
1200        let positions: Rc<RefCell<Vec<LineCol>>> = Rc::default();
1201        let cmd = PosCapture::new(1, positions.clone());
1202        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1203        upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd);
1204
1205        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1206        let image = compiler.compile(&mut b"POS_CAPTURE 42".as_slice()).unwrap();
1207        let mut vm = Vm::new(upcalls_by_name);
1208        run_to_end(&mut vm, &image).await;
1209
1210        let pos = positions.borrow();
1211        assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice());
1212    }
1213
1214    #[tokio::test]
1215    async fn test_scope_get_pos_multiple_args() {
1216        let positions: Rc<RefCell<Vec<LineCol>>> = Rc::default();
1217        let cmd = PosCapture::new(3, positions.clone());
1218        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1219        upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd);
1220
1221        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1222        let image = compiler.compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice()).unwrap();
1223        let mut vm = Vm::new(upcalls_by_name);
1224        run_to_end(&mut vm, &image).await;
1225
1226        let pos = positions.borrow();
1227        assert_eq!(
1228            &[
1229                LineCol { line: 1, col: 13 },
1230                LineCol { line: 1, col: 16 },
1231                LineCol { line: 1, col: 19 }
1232            ],
1233            pos.as_slice()
1234        );
1235    }
1236
1237    #[tokio::test]
1238    async fn test_scope_get_pos_expression_arg() {
1239        let positions: Rc<RefCell<Vec<LineCol>>> = Rc::default();
1240        let cmd = PosCapture::new(1, positions.clone());
1241        let mut upcalls_by_name: HashMap<SymbolKey, Rc<dyn Callable>> = HashMap::new();
1242        upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd);
1243
1244        let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap();
1245        let image = compiler.compile(&mut b"POS_CAPTURE 1 + 2".as_slice()).unwrap();
1246        let mut vm = Vm::new(upcalls_by_name);
1247        run_to_end(&mut vm, &image).await;
1248
1249        let pos = positions.borrow();
1250        assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice());
1251    }
1252
1253    #[tokio::test]
1254    async fn test_get_program_scalar() {
1255        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1256        let image = compiler.compile(&mut b"x = 123".as_slice()).unwrap();
1257        let mut vm = Vm::new(HashMap::default());
1258        run_to_end(&mut vm, &image).await;
1259
1260        assert_eq!(
1261            Some(ConstantDatum::Integer(123)),
1262            vm.get_program(&image, &SymbolKey::from("x")).unwrap()
1263        );
1264        assert_eq!(None, vm.get_program(&image, &SymbolKey::from("missing")).unwrap());
1265    }
1266
1267    #[tokio::test]
1268    async fn test_get_program_array() {
1269        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1270        let image =
1271            compiler.compile(&mut b"DIM arr(2) AS INTEGER: arr(1) = 45".as_slice()).unwrap();
1272        let mut vm = Vm::new(HashMap::default());
1273        run_to_end(&mut vm, &image).await;
1274
1275        assert_eq!(
1276            Some(ConstantDatum::Integer(45)),
1277            vm.get_program_array(&image, &SymbolKey::from("arr"), &[1]).unwrap()
1278        );
1279    }
1280
1281    #[tokio::test]
1282    async fn test_get_program_type_mismatch_errors() {
1283        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1284        let image =
1285            compiler.compile(&mut b"x = 1: DIM arr(2) AS INTEGER: arr(1) = 45".as_slice()).unwrap();
1286        let mut vm = Vm::new(HashMap::default());
1287        run_to_end(&mut vm, &image).await;
1288
1289        match vm.get_program(&image, &SymbolKey::from("arr")) {
1290            Err(GetGlobalError::IsArray(name)) => assert_eq!("ARR", name),
1291            other => panic!("Unexpected result: {:?}", other),
1292        }
1293
1294        match vm.get_program_array(&image, &SymbolKey::from("x"), &[0]) {
1295            Err(GetGlobalError::IsScalar(name)) => assert_eq!("X", name),
1296            other => panic!("Unexpected result: {:?}", other),
1297        }
1298    }
1299
1300    #[tokio::test]
1301    async fn test_get_program_array_out_of_bounds() {
1302        let compiler = Compiler::new(&HashMap::default(), &[]).unwrap();
1303        let image =
1304            compiler.compile(&mut b"DIM arr(2) AS INTEGER: arr(1) = 45".as_slice()).unwrap();
1305        let mut vm = Vm::new(HashMap::default());
1306        run_to_end(&mut vm, &image).await;
1307
1308        match vm.get_program_array(&image, &SymbolKey::from("arr"), &[3]) {
1309            Err(GetGlobalError::SubscriptOutOfBounds(_)) => (),
1310            other => panic!("Unexpected result: {:?}", other),
1311        }
1312    }
1313}