Skip to main content

nwnrs_nwscript/
vm.rs

1use std::{collections::HashMap, error::Error, fmt, str};
2
3use crate::{
4    CompilerErrorCode, NcsAuxCode, NcsInstruction, NcsOpcode, NcsReadError, Ndb, NdbFunction,
5    NdbType, decode_ncs_instructions,
6};
7
8/// One opaque object id visible to the VM runtime.
9pub type VmObjectId = u32;
10
11/// One engine-structure payload carried by a stack value.
12#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmEngineStructureValue {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            VmEngineStructureValue::Word(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Word",
                    &__self_0),
            VmEngineStructureValue::Text(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Text",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmEngineStructureValue {
    #[inline]
    fn clone(&self) -> VmEngineStructureValue {
        match self {
            VmEngineStructureValue::Word(__self_0) =>
                VmEngineStructureValue::Word(::core::clone::Clone::clone(__self_0)),
            VmEngineStructureValue::Text(__self_0) =>
                VmEngineStructureValue::Text(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for VmEngineStructureValue {
    #[inline]
    fn eq(&self, other: &VmEngineStructureValue) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (VmEngineStructureValue::Word(__self_0),
                    VmEngineStructureValue::Word(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (VmEngineStructureValue::Text(__self_0),
                    VmEngineStructureValue::Text(__arg1_0)) =>
                    __self_0 == __arg1_0,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}PartialEq)]
13pub enum VmEngineStructureValue {
14    /// One 32-bit opaque payload.
15    Word(u32),
16    /// One textual payload used by some engine structures.
17    Text(String),
18}
19
20/// One runtime stack value.
21#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmValue {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            VmValue::Int(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Int",
                    &__self_0),
            VmValue::Float(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Float",
                    &__self_0),
            VmValue::String(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "String",
                    &__self_0),
            VmValue::Object(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Object",
                    &__self_0),
            VmValue::EngineStructure { index: __self_0, value: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "EngineStructure", "index", __self_0, "value", &__self_1),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmValue {
    #[inline]
    fn clone(&self) -> VmValue {
        match self {
            VmValue::Int(__self_0) =>
                VmValue::Int(::core::clone::Clone::clone(__self_0)),
            VmValue::Float(__self_0) =>
                VmValue::Float(::core::clone::Clone::clone(__self_0)),
            VmValue::String(__self_0) =>
                VmValue::String(::core::clone::Clone::clone(__self_0)),
            VmValue::Object(__self_0) =>
                VmValue::Object(::core::clone::Clone::clone(__self_0)),
            VmValue::EngineStructure { index: __self_0, value: __self_1 } =>
                VmValue::EngineStructure {
                    index: ::core::clone::Clone::clone(__self_0),
                    value: ::core::clone::Clone::clone(__self_1),
                },
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for VmValue {
    #[inline]
    fn eq(&self, other: &VmValue) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (VmValue::Int(__self_0), VmValue::Int(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (VmValue::Float(__self_0), VmValue::Float(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (VmValue::String(__self_0), VmValue::String(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (VmValue::Object(__self_0), VmValue::Object(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (VmValue::EngineStructure { index: __self_0, value: __self_1
                    }, VmValue::EngineStructure {
                    index: __arg1_0, value: __arg1_1 }) =>
                    __self_0 == __arg1_0 && __self_1 == __arg1_1,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}PartialEq)]
22pub enum VmValue {
23    /// Integer value.
24    Int(i32),
25    /// Floating-point value.
26    Float(f32),
27    /// UTF-8 string value.
28    String(String),
29    /// Object id value.
30    Object(VmObjectId),
31    /// One opaque engine-structure value.
32    EngineStructure {
33        /// Upstream engine-structure index.
34        index: u8,
35        /// Runtime payload.
36        value: VmEngineStructureValue,
37    },
38}
39
40impl VmValue {
41    /// Returns a short display name for diagnostics.
42    #[must_use]
43    pub fn kind_name(&self) -> &'static str {
44        match self {
45            Self::Int(_) => "int",
46            Self::Float(_) => "float",
47            Self::String(_) => "string",
48            Self::Object(_) => "object",
49            Self::EngineStructure {
50                ..
51            } => "engine structure",
52        }
53    }
54}
55
56impl VmEngineStructureValue {
57    /// Returns the contained 32-bit payload when this value is `Word`.
58    #[must_use]
59    pub fn as_word(&self) -> Option<u32> {
60        match self {
61            Self::Word(value) => Some(*value),
62            Self::Text(_) => None,
63        }
64    }
65
66    /// Returns the contained string slice when this value is `Text`.
67    #[must_use]
68    pub fn as_text(&self) -> Option<&str> {
69        match self {
70            Self::Word(_) => None,
71            Self::Text(value) => Some(value),
72        }
73    }
74}
75
76/// Errors returned while executing `NCS` bytecode.
77#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            VmError::Read(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Read",
                    &__self_0),
            VmError::Unsupported {
                offset: __self_0,
                opcode: __self_1,
                auxcode: __self_2,
                message: __self_3 } =>
                ::core::fmt::Formatter::debug_struct_field4_finish(f,
                    "Unsupported", "offset", __self_0, "opcode", __self_1,
                    "auxcode", __self_2, "message", &__self_3),
            VmError::StackUnderflow { message: __self_0 } =>
                ::core::fmt::Formatter::debug_struct_field1_finish(f,
                    "StackUnderflow", "message", &__self_0),
            VmError::TypeMismatch {
                offset: __self_0,
                message: __self_1,
                expected: __self_2,
                actual: __self_3 } =>
                ::core::fmt::Formatter::debug_struct_field4_finish(f,
                    "TypeMismatch", "offset", __self_0, "message", __self_1,
                    "expected", __self_2, "actual", &__self_3),
            VmError::InvalidInstructionPointer { offset: __self_0 } =>
                ::core::fmt::Formatter::debug_struct_field1_finish(f,
                    "InvalidInstructionPointer", "offset", &__self_0),
            VmError::InvalidExtra {
                offset: __self_0,
                opcode: __self_1,
                auxcode: __self_2,
                message: __self_3 } =>
                ::core::fmt::Formatter::debug_struct_field4_finish(f,
                    "InvalidExtra", "offset", __self_0, "opcode", __self_1,
                    "auxcode", __self_2, "message", &__self_3),
            VmError::InvalidCommand { offset: __self_0, command: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "InvalidCommand", "offset", __self_0, "command", &__self_1),
            VmError::Setup { message: __self_0 } =>
                ::core::fmt::Formatter::debug_struct_field1_finish(f, "Setup",
                    "message", &__self_0),
            VmError::InstructionLimitExceeded {
                offset: __self_0, limit: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "InstructionLimitExceeded", "offset", __self_0, "limit",
                    &__self_1),
        }
    }
}Debug)]
78pub enum VmError {
79    /// Decoding the bytecode stream failed before execution began.
80    Read(NcsReadError),
81    /// One instruction requested a feature this VM does not yet implement.
82    Unsupported {
83        /// Byte offset of the instruction within the code section.
84        offset:  usize,
85        /// Opcode that failed.
86        opcode:  NcsOpcode,
87        /// Auxcode that failed.
88        auxcode: NcsAuxCode,
89        /// Human-readable explanation.
90        message: String,
91    },
92    /// One stack access ran past the available values.
93    StackUnderflow {
94        /// Human-readable explanation.
95        message: String,
96    },
97    /// One instruction expected a value of a different runtime type.
98    TypeMismatch {
99        /// Byte offset of the instruction within the code section.
100        offset:   usize,
101        /// Human-readable explanation.
102        message:  String,
103        /// Optional expected type description.
104        expected: Option<&'static str>,
105        /// Actual runtime value kind.
106        actual:   &'static str,
107    },
108    /// One jump or return target did not point at a valid instruction.
109    InvalidInstructionPointer {
110        /// Byte offset that could not be resolved.
111        offset: usize,
112    },
113    /// One opcode payload was malformed.
114    InvalidExtra {
115        /// Byte offset of the instruction within the code section.
116        offset:  usize,
117        /// Opcode whose payload failed.
118        opcode:  NcsOpcode,
119        /// Auxcode whose payload failed.
120        auxcode: NcsAuxCode,
121        /// Human-readable explanation.
122        message: String,
123    },
124    /// One command id was invoked without a registered handler.
125    InvalidCommand {
126        /// Byte offset of the `ACTION` instruction.
127        offset:  usize,
128        /// Unhandled command id.
129        command: u16,
130    },
131    /// One host-side setup or invocation request was invalid before execution.
132    Setup {
133        /// Human-readable explanation.
134        message: String,
135    },
136    /// One VM run exceeded the configured instruction budget.
137    InstructionLimitExceeded {
138        /// Byte offset of the instruction that would execute next.
139        offset: usize,
140        /// Maximum instruction count allowed for the run.
141        limit:  usize,
142    },
143}
144
145impl VmError {
146    /// Returns the closest upstream-aligned VM/compiler error code when one
147    /// exists.
148    #[must_use]
149    pub fn code(&self) -> Option<CompilerErrorCode> {
150        match self {
151            Self::Read(NcsReadError::Opcode(_)) => Some(CompilerErrorCode::VmInvalidOpCode),
152            Self::Read(NcsReadError::AuxCode(_)) => Some(CompilerErrorCode::VmInvalidAuxCode),
153            Self::Read(
154                NcsReadError::Header(_)
155                | NcsReadError::TruncatedInstruction {
156                    ..
157                },
158            )
159            | Self::InvalidExtra {
160                ..
161            } => Some(CompilerErrorCode::VmInvalidExtraDataOnOpCode),
162            Self::Unsupported {
163                ..
164            } => None,
165            Self::StackUnderflow {
166                ..
167            } => Some(CompilerErrorCode::VmStackUnderflow),
168            Self::TypeMismatch {
169                ..
170            } => Some(CompilerErrorCode::VmUnknownTypeOnRunTimeStack),
171            Self::InvalidInstructionPointer {
172                ..
173            } => Some(CompilerErrorCode::VmIpOutOfCodeSegment),
174            Self::InvalidCommand {
175                ..
176            } => Some(CompilerErrorCode::VmInvalidCommand),
177            Self::Setup {
178                ..
179            } => None,
180            Self::InstructionLimitExceeded {
181                ..
182            } => None,
183        }
184    }
185}
186
187impl fmt::Display for VmError {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            Self::Read(error) => error.fmt(f),
191            Self::Unsupported {
192                offset,
193                opcode,
194                auxcode,
195                message,
196            } => f.write_fmt(format_args!("unsupported VM instruction {0}.{1} at byte {2}: {3}",
        opcode, auxcode, offset, message))write!(
197                f,
198                "unsupported VM instruction {}.{} at byte {}: {}",
199                opcode, auxcode, offset, message
200            ),
201            Self::StackUnderflow {
202                message,
203            } => f.write_fmt(format_args!("VM stack underflow: {0}", message))write!(f, "VM stack underflow: {message}"),
204            Self::TypeMismatch {
205                offset,
206                message,
207                expected,
208                actual,
209            } => match expected {
210                Some(expected) => f.write_fmt(format_args!("VM type mismatch at byte {0}: {1} (expected {2}, got {3})",
        offset, message, expected, actual))write!(
211                    f,
212                    "VM type mismatch at byte {}: {} (expected {}, got {})",
213                    offset, message, expected, actual
214                ),
215                None => f.write_fmt(format_args!("VM type mismatch at byte {0}: {1} ({2})", offset,
        message, actual))write!(
216                    f,
217                    "VM type mismatch at byte {}: {} ({})",
218                    offset, message, actual
219                ),
220            },
221            Self::InvalidInstructionPointer {
222                offset,
223            } => f.write_fmt(format_args!("VM instruction pointer left the code segment at byte {0}",
        offset))write!(
224                f,
225                "VM instruction pointer left the code segment at byte {offset}"
226            ),
227            Self::InvalidExtra {
228                offset,
229                opcode,
230                auxcode,
231                message,
232            } => f.write_fmt(format_args!("invalid {0}.{1} payload at byte {2}: {3}", opcode,
        auxcode, offset, message))write!(
233                f,
234                "invalid {}.{} payload at byte {}: {}",
235                opcode, auxcode, offset, message
236            ),
237            Self::InvalidCommand {
238                offset,
239                command,
240            } => {
241                f.write_fmt(format_args!("invalid VM command {0} at byte {1}", command,
        offset))write!(f, "invalid VM command {} at byte {}", command, offset)
242            }
243            Self::Setup {
244                message,
245            } => f.write_str(message),
246            Self::InstructionLimitExceeded {
247                offset,
248                limit,
249            } => f.write_fmt(format_args!("VM instruction limit of {0} exceeded before byte {1}",
        limit, offset))write!(
250                f,
251                "VM instruction limit of {} exceeded before byte {}",
252                limit, offset
253            ),
254        }
255    }
256}
257
258impl Error for VmError {}
259
260impl From<NcsReadError> for VmError {
261    fn from(value: NcsReadError) -> Self {
262        Self::Read(value)
263    }
264}
265
266/// One instruction-dispatch trace event emitted by the VM.
267#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmTraceEvent {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "VmTraceEvent",
            "offset", &self.offset, "ip", &self.ip, "sp", &self.sp, "bp",
            &self.bp, "instruction", &&self.instruction)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmTraceEvent {
    #[inline]
    fn clone(&self) -> VmTraceEvent {
        VmTraceEvent {
            offset: ::core::clone::Clone::clone(&self.offset),
            ip: ::core::clone::Clone::clone(&self.ip),
            sp: ::core::clone::Clone::clone(&self.sp),
            bp: ::core::clone::Clone::clone(&self.bp),
            instruction: ::core::clone::Clone::clone(&self.instruction),
        }
    }
}Clone)]
268pub struct VmTraceEvent {
269    /// Script byte offset of the instruction about to execute.
270    pub offset:      usize,
271    /// Current instruction pointer in code-section bytes.
272    pub ip:          usize,
273    /// Current stack pointer in stack cells.
274    pub sp:          usize,
275    /// Current base pointer in stack cells.
276    pub bp:          usize,
277    /// One cloned instruction payload.
278    pub instruction: NcsInstruction,
279}
280
281#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmProgramInstruction {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "VmProgramInstruction", "offset", &self.offset, "instruction",
            &&self.instruction)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmProgramInstruction {
    #[inline]
    fn clone(&self) -> VmProgramInstruction {
        VmProgramInstruction {
            offset: ::core::clone::Clone::clone(&self.offset),
            instruction: ::core::clone::Clone::clone(&self.instruction),
        }
    }
}Clone)]
282struct VmProgramInstruction {
283    offset:      usize,
284    instruction: NcsInstruction,
285}
286
287#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmFunctionShape {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "VmFunctionShape", "arg_cells", &self.arg_cells, "return_cells",
            &&self.return_cells)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmFunctionShape {
    #[inline]
    fn clone(&self) -> VmFunctionShape {
        let _: ::core::clone::AssertParamIsClone<usize>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for VmFunctionShape { }Copy)]
288struct VmFunctionShape {
289    arg_cells:    usize,
290    return_cells: usize,
291}
292
293#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmFunctionDebug {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "VmFunctionDebug", "label", &self.label, "start", &self.start,
            "end", &&self.end)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmFunctionDebug {
    #[inline]
    fn clone(&self) -> VmFunctionDebug {
        VmFunctionDebug {
            label: ::core::clone::Clone::clone(&self.label),
            start: ::core::clone::Clone::clone(&self.start),
            end: ::core::clone::Clone::clone(&self.end),
        }
    }
}Clone)]
294struct VmFunctionDebug {
295    label: String,
296    start: usize,
297    end:   usize,
298}
299
300#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmSourceLineDebug {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f,
            "VmSourceLineDebug", "file_name", &self.file_name, "is_root",
            &self.is_root, "line_number", &self.line_number, "start",
            &self.start, "end", &&self.end)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmSourceLineDebug {
    #[inline]
    fn clone(&self) -> VmSourceLineDebug {
        VmSourceLineDebug {
            file_name: ::core::clone::Clone::clone(&self.file_name),
            is_root: ::core::clone::Clone::clone(&self.is_root),
            line_number: ::core::clone::Clone::clone(&self.line_number),
            start: ::core::clone::Clone::clone(&self.start),
            end: ::core::clone::Clone::clone(&self.end),
        }
    }
}Clone)]
301struct VmSourceLineDebug {
302    file_name:   String,
303    is_root:     bool,
304    line_number: usize,
305    start:       usize,
306    end:         usize,
307}
308
309#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmCallCleanup {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "VmCallCleanup",
            "arg_cells", &self.arg_cells, "return_cells", &&self.return_cells)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmCallCleanup {
    #[inline]
    fn clone(&self) -> VmCallCleanup {
        let _: ::core::clone::AssertParamIsClone<usize>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for VmCallCleanup { }Copy)]
310struct VmCallCleanup {
311    arg_cells:    usize,
312    return_cells: usize,
313}
314
315#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmReturnFrame {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "VmReturnFrame",
            "target", &self.target, "cleanup", &&self.cleanup)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmReturnFrame {
    #[inline]
    fn clone(&self) -> VmReturnFrame {
        let _: ::core::clone::AssertParamIsClone<usize>;
        let _: ::core::clone::AssertParamIsClone<Option<VmCallCleanup>>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for VmReturnFrame { }Copy)]
316struct VmReturnFrame {
317    target:  usize,
318    cleanup: Option<VmCallCleanup>,
319}
320
321#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmProgram {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "VmProgram",
            "instructions", &self.instructions, "offsets_to_index",
            &self.offsets_to_index, "function_shapes", &self.function_shapes,
            "functions", &self.functions, "source_lines", &&self.source_lines)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmProgram {
    #[inline]
    fn clone(&self) -> VmProgram {
        VmProgram {
            instructions: ::core::clone::Clone::clone(&self.instructions),
            offsets_to_index: ::core::clone::Clone::clone(&self.offsets_to_index),
            function_shapes: ::core::clone::Clone::clone(&self.function_shapes),
            functions: ::core::clone::Clone::clone(&self.functions),
            source_lines: ::core::clone::Clone::clone(&self.source_lines),
        }
    }
}Clone)]
322struct VmProgram {
323    instructions:     Vec<VmProgramInstruction>,
324    offsets_to_index: HashMap<usize, usize>,
325    function_shapes:  HashMap<usize, VmFunctionShape>,
326    functions:        Vec<VmFunctionDebug>,
327    source_lines:     Vec<VmSourceLineDebug>,
328}
329
330impl VmProgram {
331    fn decode(bytes: &[u8]) -> Result<Self, VmError> {
332        let instructions = decode_ncs_instructions(bytes)?;
333        Ok(Self::from_instructions(instructions))
334    }
335
336    fn from_instructions(instructions: Vec<NcsInstruction>) -> Self {
337        let mut decoded = Vec::with_capacity(instructions.len());
338        let mut offsets_to_index = HashMap::with_capacity(instructions.len());
339        let mut offset = 0usize;
340        for (index, instruction) in instructions.into_iter().enumerate() {
341            let encoded_len = instruction.encoded_len();
342            offsets_to_index.insert(offset, index);
343            decoded.push(VmProgramInstruction {
344                offset,
345                instruction,
346            });
347            offset += encoded_len;
348        }
349        Self {
350            instructions: decoded,
351            offsets_to_index,
352            function_shapes: HashMap::new(),
353            functions: Vec::new(),
354            source_lines: Vec::new(),
355        }
356    }
357
358    fn instruction_at(&self, offset: usize) -> Option<&VmProgramInstruction> {
359        self.offsets_to_index
360            .get(&offset)
361            .and_then(|index| self.instructions.get(*index))
362    }
363
364    fn attach_ndb(&mut self, ndb: &Ndb) -> Result<(), VmError> {
365        self.function_shapes.clear();
366        self.functions.clear();
367        self.source_lines.clear();
368        for function in &ndb.functions {
369            let start =
370                usize::try_from(function.binary_start).map_err(|_error| VmError::Setup {
371                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("function {0:?} start offset exceeds usize range",
                function.label))
    })format!(
372                        "function {:?} start offset exceeds usize range",
373                        function.label
374                    ),
375                })?;
376            let end = usize::try_from(function.binary_end).map_err(|_error| VmError::Setup {
377                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("function {0:?} end offset exceeds usize range",
                function.label))
    })format!(
378                    "function {:?} end offset exceeds usize range",
379                    function.label
380                ),
381            })?;
382            self.function_shapes.insert(
383                start,
384                VmFunctionShape {
385                    arg_cells:    function
386                        .args
387                        .iter()
388                        .map(cells_for_ndb_type)
389                        .try_fold(0usize, |total, cells| cells.map(|cells| total + cells))?,
390                    return_cells: if function.return_type == NdbType::Void {
391                        0
392                    } else {
393                        cells_for_ndb_type(&function.return_type)?
394                    },
395                },
396            );
397            self.functions.push(VmFunctionDebug {
398                label: function.label.to_string(),
399                start,
400                end,
401            });
402        }
403        for line in &ndb.lines {
404            let start = usize::try_from(line.binary_start).map_err(|_error| VmError::Setup {
405                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("line mapping {0}:{1} start offset exceeds usize range",
                line.file_num, line.line_num))
    })format!(
406                    "line mapping {}:{} start offset exceeds usize range",
407                    line.file_num, line.line_num
408                ),
409            })?;
410            let end = usize::try_from(line.binary_end).map_err(|_error| VmError::Setup {
411                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("line mapping {0}:{1} end offset exceeds usize range",
                line.file_num, line.line_num))
    })format!(
412                    "line mapping {}:{} end offset exceeds usize range",
413                    line.file_num, line.line_num
414                ),
415            })?;
416            let file = ndb.files.get(line.file_num).ok_or_else(|| VmError::Setup {
417                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("line mapping {0}:{1} references missing file index {2}",
                line.file_num, line.line_num, line.file_num))
    })format!(
418                    "line mapping {}:{} references missing file index {}",
419                    line.file_num, line.line_num, line.file_num
420                ),
421            })?;
422            self.source_lines.push(VmSourceLineDebug {
423                file_name: file.name.clone(),
424                is_root: file.is_root,
425                line_number: line.line_num,
426                start,
427                end,
428            });
429        }
430        Ok(())
431    }
432
433    fn function_at(&self, offset: usize) -> Option<&VmFunctionDebug> {
434        self.functions
435            .iter()
436            .find(|function| contains_debug_offset(offset, function.start, function.end))
437    }
438
439    fn source_line_at(&self, offset: usize) -> Option<&VmSourceLineDebug> {
440        self.source_lines
441            .iter()
442            .find(|line| contains_debug_offset(offset, line.start, line.end))
443    }
444}
445
446fn contains_debug_offset(offset: usize, start: usize, end: usize) -> bool {
447    if end <= start {
448        offset == start
449    } else {
450        (start..end).contains(&offset)
451    }
452}
453
454/// One debugger-visible source location derived from attached `NDB` metadata.
455#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmSourceLocation {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "VmSourceLocation", "file_name", &self.file_name, "is_root",
            &self.is_root, "line_number", &&self.line_number)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmSourceLocation {
    #[inline]
    fn clone(&self) -> VmSourceLocation {
        VmSourceLocation {
            file_name: ::core::clone::Clone::clone(&self.file_name),
            is_root: ::core::clone::Clone::clone(&self.is_root),
            line_number: ::core::clone::Clone::clone(&self.line_number),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for VmSourceLocation {
    #[inline]
    fn eq(&self, other: &VmSourceLocation) -> bool {
        self.is_root == other.is_root && self.file_name == other.file_name &&
            self.line_number == other.line_number
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for VmSourceLocation {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
        let _: ::core::cmp::AssertParamIsEq<usize>;
    }
}Eq)]
456pub struct VmSourceLocation {
457    /// File name as recorded in the attached `NDB` table.
458    pub file_name:   String,
459    /// Whether this file is the root script file.
460    pub is_root:     bool,
461    /// One-based source line number.
462    pub line_number: usize,
463}
464
465/// One debugger-visible function range derived from attached `NDB` metadata.
466#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmFunctionInfo {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "VmFunctionInfo", "name", &self.name, "start", &self.start, "end",
            &&self.end)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmFunctionInfo {
    #[inline]
    fn clone(&self) -> VmFunctionInfo {
        VmFunctionInfo {
            name: ::core::clone::Clone::clone(&self.name),
            start: ::core::clone::Clone::clone(&self.start),
            end: ::core::clone::Clone::clone(&self.end),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for VmFunctionInfo {
    #[inline]
    fn eq(&self, other: &VmFunctionInfo) -> bool {
        self.name == other.name && self.start == other.start &&
            self.end == other.end
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for VmFunctionInfo {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<usize>;
    }
}Eq)]
467pub struct VmFunctionInfo {
468    /// Function name as recorded in the attached `NDB` table.
469    pub name:  String,
470    /// Start byte offset in the code section.
471    pub start: usize,
472    /// End byte offset in the code section.
473    pub end:   usize,
474}
475
476/// One deferred NWScript action captured by `STORESTATE`.
477#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmSituation {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["label", "program", "ip", "sp", "bp", "stack"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.label, &self.program, &self.ip, &self.sp, &self.bp,
                        &&self.stack];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "VmSituation",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmSituation {
    #[inline]
    fn clone(&self) -> VmSituation {
        VmSituation {
            label: ::core::clone::Clone::clone(&self.label),
            program: ::core::clone::Clone::clone(&self.program),
            ip: ::core::clone::Clone::clone(&self.ip),
            sp: ::core::clone::Clone::clone(&self.sp),
            bp: ::core::clone::Clone::clone(&self.bp),
            stack: ::core::clone::Clone::clone(&self.stack),
        }
    }
}Clone)]
478pub struct VmSituation {
479    label:   String,
480    program: VmProgram,
481    ip:      usize,
482    sp:      usize,
483    bp:      usize,
484    stack:   Vec<VmValue>,
485}
486
487impl VmSituation {
488    /// Returns the user-facing label associated with this saved situation.
489    #[must_use]
490    pub fn label(&self) -> &str {
491        &self.label
492    }
493
494    /// Returns the saved instruction pointer in code-section bytes.
495    #[must_use]
496    pub fn ip(&self) -> usize {
497        self.ip
498    }
499
500    /// Returns the saved stack pointer in stack cells.
501    #[must_use]
502    pub fn sp(&self) -> usize {
503        self.sp
504    }
505
506    /// Returns the saved base pointer in stack cells.
507    #[must_use]
508    pub fn bp(&self) -> usize {
509        self.bp
510    }
511
512    /// Returns the saved stack snapshot.
513    #[must_use]
514    pub fn stack(&self) -> &[VmValue] {
515        &self.stack
516    }
517
518    /// Rehydrates this situation into a runnable script snapshot.
519    #[must_use]
520    pub fn to_script(&self) -> VmScript {
521        VmScript {
522            label:           self.label.clone(),
523            program:         self.program.clone(),
524            ip:              self.ip,
525            sp:              self.sp,
526            bp:              self.bp,
527            ret:             Vec::new(),
528            stack:           self.stack.clone(),
529            save_ip:         0,
530            save_sp:         0,
531            save_bp:         0,
532            saved_situation: None,
533            abort_requested: false,
534            aborted:         false,
535        }
536    }
537}
538
539/// One executable `NCS` script plus its VM runtime state.
540#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmScript {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["label", "program", "ip", "sp", "bp", "ret", "stack", "save_ip",
                        "save_sp", "save_bp", "saved_situation", "abort_requested",
                        "aborted"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.label, &self.program, &self.ip, &self.sp, &self.bp,
                        &self.ret, &self.stack, &self.save_ip, &self.save_sp,
                        &self.save_bp, &self.saved_situation, &self.abort_requested,
                        &&self.aborted];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "VmScript",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmScript {
    #[inline]
    fn clone(&self) -> VmScript {
        VmScript {
            label: ::core::clone::Clone::clone(&self.label),
            program: ::core::clone::Clone::clone(&self.program),
            ip: ::core::clone::Clone::clone(&self.ip),
            sp: ::core::clone::Clone::clone(&self.sp),
            bp: ::core::clone::Clone::clone(&self.bp),
            ret: ::core::clone::Clone::clone(&self.ret),
            stack: ::core::clone::Clone::clone(&self.stack),
            save_ip: ::core::clone::Clone::clone(&self.save_ip),
            save_sp: ::core::clone::Clone::clone(&self.save_sp),
            save_bp: ::core::clone::Clone::clone(&self.save_bp),
            saved_situation: ::core::clone::Clone::clone(&self.saved_situation),
            abort_requested: ::core::clone::Clone::clone(&self.abort_requested),
            aborted: ::core::clone::Clone::clone(&self.aborted),
        }
    }
}Clone)]
541pub struct VmScript {
542    label:           String,
543    program:         VmProgram,
544    ip:              usize,
545    sp:              usize,
546    bp:              usize,
547    ret:             Vec<VmReturnFrame>,
548    stack:           Vec<VmValue>,
549    save_ip:         usize,
550    save_sp:         usize,
551    save_bp:         usize,
552    saved_situation: Option<VmSituation>,
553    abort_requested: bool,
554    aborted:         bool,
555}
556
557impl VmScript {
558    /// Decodes one `NCS V1.0` bytecode stream into a runnable script.
559    ///
560    /// # Errors
561    ///
562    /// Returns [`VmError`] if the bytecode is malformed.
563    pub fn from_bytes(bytes: &[u8], label: impl Into<String>) -> Result<Self, VmError> {
564        Ok(Self {
565            label:           label.into(),
566            program:         VmProgram::decode(bytes)?,
567            ip:              0,
568            sp:              0,
569            bp:              0,
570            ret:             Vec::new(),
571            stack:           Vec::new(),
572            save_ip:         0,
573            save_sp:         0,
574            save_bp:         0,
575            saved_situation: None,
576            abort_requested: false,
577            aborted:         false,
578        })
579    }
580
581    /// Builds a runnable script from decoded instructions.
582    #[must_use]
583    pub fn from_instructions(instructions: Vec<NcsInstruction>, label: impl Into<String>) -> Self {
584        Self {
585            label:           label.into(),
586            program:         VmProgram::from_instructions(instructions),
587            ip:              0,
588            sp:              0,
589            bp:              0,
590            ret:             Vec::new(),
591            stack:           Vec::new(),
592            save_ip:         0,
593            save_sp:         0,
594            save_bp:         0,
595            saved_situation: None,
596            abort_requested: false,
597            aborted:         false,
598        }
599    }
600
601    /// Attaches one `NDB` debug table so the VM can recover user-function frame
602    /// shapes.
603    ///
604    /// # Errors
605    ///
606    /// Returns [`VmError`] if one function record uses unsupported stack
607    /// metadata.
608    pub fn attach_ndb(&mut self, ndb: &Ndb) -> Result<(), VmError> {
609        self.program.attach_ndb(ndb)
610    }
611
612    /// Decodes one `NCS V1.0` bytecode stream and attaches one `NDB` debug
613    /// table.
614    ///
615    /// # Errors
616    ///
617    /// Returns [`VmError`] if the bytecode or attached debug metadata is
618    /// malformed.
619    pub fn from_bytes_with_ndb(
620        bytes: &[u8],
621        label: impl Into<String>,
622        ndb: &Ndb,
623    ) -> Result<Self, VmError> {
624        let mut script = Self::from_bytes(bytes, label)?;
625        script.attach_ndb(ndb)?;
626        Ok(script)
627    }
628
629    /// Executes this script using the supplied VM command table.
630    ///
631    /// # Errors
632    ///
633    /// Returns [`VmError`] if execution fails.
634    pub fn run(&mut self, vm: &Vm) -> Result<(), VmError> {
635        vm.run(self)
636    }
637
638    /// Returns the user-facing label associated with this script.
639    #[must_use]
640    pub fn label(&self) -> &str {
641        &self.label
642    }
643
644    /// Returns the current instruction pointer in code-section bytes.
645    #[must_use]
646    pub fn ip(&self) -> usize {
647        self.ip
648    }
649
650    /// Returns the current stack pointer in stack cells.
651    #[must_use]
652    pub fn sp(&self) -> usize {
653        self.sp
654    }
655
656    /// Returns the current base pointer in stack cells.
657    #[must_use]
658    pub fn bp(&self) -> usize {
659        self.bp
660    }
661
662    /// Returns the decoded instruction at the current instruction pointer.
663    #[must_use]
664    pub fn current_instruction(&self) -> Option<&NcsInstruction> {
665        self.program
666            .instruction_at(self.ip)
667            .map(|decoded| &decoded.instruction)
668    }
669
670    /// Returns the decoded instruction at one byte offset, if present.
671    #[must_use]
672    pub fn instruction_at(&self, offset: usize) -> Option<&NcsInstruction> {
673        self.program
674            .instruction_at(offset)
675            .map(|decoded| &decoded.instruction)
676    }
677
678    /// Returns the attached debug function that contains the current
679    /// instruction pointer.
680    #[must_use]
681    pub fn current_function(&self) -> Option<VmFunctionInfo> {
682        self.function_at(self.ip)
683    }
684
685    /// Returns the attached debug function that contains one byte offset.
686    #[must_use]
687    pub fn function_at(&self, offset: usize) -> Option<VmFunctionInfo> {
688        self.program
689            .function_at(offset)
690            .map(|function| VmFunctionInfo {
691                name:  function.label.clone(),
692                start: function.start,
693                end:   function.end,
694            })
695    }
696
697    /// Returns the attached source location for the current instruction
698    /// pointer.
699    #[must_use]
700    pub fn current_source_location(&self) -> Option<VmSourceLocation> {
701        self.source_location_at(self.ip)
702    }
703
704    /// Returns the attached source location for one byte offset.
705    #[must_use]
706    pub fn source_location_at(&self, offset: usize) -> Option<VmSourceLocation> {
707        self.program
708            .source_line_at(offset)
709            .map(|line| VmSourceLocation {
710                file_name:   line.file_name.clone(),
711                is_root:     line.is_root,
712                line_number: line.line_number,
713            })
714    }
715
716    /// Returns the instruction pointer saved by `STOREIP` or `STORESTATE`.
717    #[must_use]
718    pub fn save_ip(&self) -> usize {
719        self.save_ip
720    }
721
722    /// Returns the stack pointer saved by `STORESTATE`.
723    #[must_use]
724    pub fn save_sp(&self) -> usize {
725        self.save_sp
726    }
727
728    /// Returns the base pointer saved by `STORESTATE`.
729    #[must_use]
730    pub fn save_bp(&self) -> usize {
731        self.save_bp
732    }
733
734    /// Returns the current stack values.
735    #[must_use]
736    pub fn stack(&self) -> &[VmValue] {
737        &self.stack
738    }
739
740    /// Returns a compact debugger-oriented stack rendering with `^` at the base
741    /// pointer and `*` at the top stack cell.
742    #[must_use]
743    pub fn stack_string(&self) -> String {
744        let mut rendered = String::new();
745        for (index, value) in self.stack.iter().enumerate() {
746            if index > 0 {
747                rendered.push(' ');
748            }
749            if index == self.bp {
750                rendered.push('^');
751            }
752            if index + 1 == self.sp {
753                rendered.push('*');
754            }
755            rendered.push_str(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0:?}", value))
    })format!("{value:?}"));
756        }
757        rendered
758    }
759
760    /// Returns the current return-frame depth.
761    #[must_use]
762    pub fn return_depth(&self) -> usize {
763        self.ret.len()
764    }
765
766    /// Returns the last deferred action snapshot captured by `STORESTATE`.
767    #[must_use]
768    pub fn saved_situation(&self) -> Option<&VmSituation> {
769        self.saved_situation.as_ref()
770    }
771
772    /// Removes and returns the last deferred action snapshot captured by
773    /// `STORESTATE`.
774    pub fn take_saved_situation(&mut self) -> Option<VmSituation> {
775        self.saved_situation.take()
776    }
777
778    /// Configures this script to call one named user function directly.
779    ///
780    /// This bypasses the compiler-emitted loader.
781    ///
782    /// If the script uses globals, the caller must initialize the loader/global
783    /// frame first before invoking a named function directly.
784    ///
785    /// # Errors
786    ///
787    /// Returns [`VmError`] if the function cannot be found or the argument
788    /// types are unsupported.
789    pub fn prepare_function_call(
790        &mut self,
791        ndb: &Ndb,
792        name: &str,
793        args: &[VmValue],
794    ) -> Result<(), VmError> {
795        self.attach_ndb(ndb)?;
796        let function = ndb
797            .functions
798            .iter()
799            .find(|function| function.label == name)
800            .ok_or_else(|| VmError::Setup {
801                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown NDB function {0:?}", name))
    })format!("unknown NDB function {name:?}"),
802            })?;
803        expect_argument_count(function, args.len())?;
804
805        self.ip = usize::try_from(function.binary_start).map_err(|_error| VmError::Setup {
806            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("function {0:?} start offset exceeds usize range",
                name))
    })format!("function {name:?} start offset exceeds usize range"),
807        })?;
808        let preserved_sp = self.sp;
809        let preserved_bp = self.bp;
810        self.sp = preserved_sp;
811        self.bp = preserved_bp;
812        self.ret.clear();
813        self.ret.push(VmReturnFrame {
814            target:  usize::MAX,
815            cleanup: Some(VmCallCleanup {
816                arg_cells:    function
817                    .args
818                    .iter()
819                    .map(cells_for_ndb_type)
820                    .try_fold(0usize, |total, cells| cells.map(|cells| total + cells))?,
821                return_cells: if function.return_type == NdbType::Void {
822                    0
823                } else {
824                    cells_for_ndb_type(&function.return_type)?
825                },
826            }),
827        });
828        self.save_ip = 0;
829        self.save_sp = 0;
830        self.save_bp = 0;
831        self.saved_situation = None;
832        self.abort_requested = false;
833        self.aborted = false;
834
835        if preserved_sp == 0 {
836            self.stack.clear();
837            self.bp = 0;
838        }
839        if function.return_type != NdbType::Void {
840            self.push(default_value_for_ndb_type(&function.return_type)?);
841        }
842        for (expected, actual) in function.args.iter().zip(args) {
843            validate_entry_argument(expected, actual)?;
844            self.push(actual.clone());
845        }
846        Ok(())
847    }
848
849    /// Reads one scalar/object/string/engine-structure return value after a
850    /// direct function call.
851    ///
852    /// # Errors
853    ///
854    /// Returns [`VmError`] if the function cannot be found or uses an
855    /// unsupported return type.
856    pub fn function_return_value(&self, ndb: &Ndb, name: &str) -> Result<Option<VmValue>, VmError> {
857        let function = ndb
858            .functions
859            .iter()
860            .find(|function| function.label == name)
861            .ok_or_else(|| VmError::Setup {
862                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown NDB function {0:?}", name))
    })format!("unknown NDB function {name:?}"),
863            })?;
864        if function.return_type == NdbType::Void {
865            return Ok(None);
866        }
867        validate_supported_ndb_value_type(&function.return_type, "return type", None)?;
868        self.stack
869            .last()
870            .cloned()
871            .ok_or_else(|| VmError::StackUnderflow {
872                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("function {0:?} return slot is missing",
                name))
    })format!("function {name:?} return slot is missing"),
873            })
874            .map(Some)
875    }
876
877    /// Requests that this script abort once control returns to the VM
878    /// dispatcher.
879    pub fn abort(&mut self) {
880        self.abort_requested = true;
881    }
882
883    /// Returns whether the last VM run terminated via `abort()`.
884    #[must_use]
885    pub fn aborted(&self) -> bool {
886        self.aborted
887    }
888
889    /// Pushes one raw stack value.
890    pub fn push(&mut self, value: VmValue) {
891        self.stack.push(value);
892        self.sp += 1;
893    }
894
895    /// Pushes one integer value.
896    pub fn push_int(&mut self, value: i32) {
897        self.push(VmValue::Int(value));
898    }
899
900    /// Pushes one floating-point value.
901    pub fn push_float(&mut self, value: f32) {
902        self.push(VmValue::Float(value));
903    }
904
905    /// Pushes one string value.
906    pub fn push_string(&mut self, value: impl Into<String>) {
907        self.push(VmValue::String(value.into()));
908    }
909
910    /// Pushes one object id.
911    pub fn push_object(&mut self, value: VmObjectId) {
912        self.push(VmValue::Object(value));
913    }
914
915    /// Pushes one engine-structure value.
916    pub fn push_engine_structure(&mut self, index: u8, value: VmEngineStructureValue) {
917        self.push(VmValue::EngineStructure {
918            index,
919            value,
920        });
921    }
922
923    /// Pushes one vector value as three stack cells in `x, y, z` order.
924    pub fn push_vector(&mut self, value: [f32; 3]) {
925        for component in value {
926            self.push_float(component);
927        }
928    }
929
930    /// Pops one raw stack value.
931    ///
932    /// # Errors
933    ///
934    /// Returns [`VmError`] if the stack is empty.
935    pub fn pop(&mut self) -> Result<VmValue, VmError> {
936        let value = self.stack.pop().ok_or_else(|| VmError::StackUnderflow {
937            message: "attempted to pop from an empty stack".to_string(),
938        })?;
939        self.sp -= 1;
940        Ok(value)
941    }
942
943    /// Pops one integer value.
944    ///
945    /// # Errors
946    ///
947    /// Returns [`VmError`] if the stack top is not an integer.
948    pub fn pop_int(&mut self) -> Result<i32, VmError> {
949        match self.pop()? {
950            VmValue::Int(value) => Ok(value),
951            other => Err(VmError::TypeMismatch {
952                offset:   self.ip,
953                message:  "expected integer on stack top".to_string(),
954                expected: Some("int"),
955                actual:   other.kind_name(),
956            }),
957        }
958    }
959
960    /// Pops one floating-point value.
961    ///
962    /// # Errors
963    ///
964    /// Returns [`VmError`] if the stack top is not a float.
965    pub fn pop_float(&mut self) -> Result<f32, VmError> {
966        match self.pop()? {
967            VmValue::Float(value) => Ok(value),
968            other => Err(VmError::TypeMismatch {
969                offset:   self.ip,
970                message:  "expected float on stack top".to_string(),
971                expected: Some("float"),
972                actual:   other.kind_name(),
973            }),
974        }
975    }
976
977    /// Pops one string value.
978    ///
979    /// # Errors
980    ///
981    /// Returns [`VmError`] if the stack top is not a string.
982    pub fn pop_string(&mut self) -> Result<String, VmError> {
983        match self.pop()? {
984            VmValue::String(value) => Ok(value),
985            other => Err(VmError::TypeMismatch {
986                offset:   self.ip,
987                message:  "expected string on stack top".to_string(),
988                expected: Some("string"),
989                actual:   other.kind_name(),
990            }),
991        }
992    }
993
994    /// Pops one object id.
995    ///
996    /// # Errors
997    ///
998    /// Returns [`VmError`] if the stack top is not an object.
999    pub fn pop_object(&mut self) -> Result<VmObjectId, VmError> {
1000        match self.pop()? {
1001            VmValue::Object(value) => Ok(value),
1002            other => Err(VmError::TypeMismatch {
1003                offset:   self.ip,
1004                message:  "expected object on stack top".to_string(),
1005                expected: Some("object"),
1006                actual:   other.kind_name(),
1007            }),
1008        }
1009    }
1010
1011    /// Pops one engine-structure value.
1012    ///
1013    /// # Errors
1014    ///
1015    /// Returns [`VmError`] if the stack top is not an engine structure.
1016    pub fn pop_engine_structure(&mut self) -> Result<(u8, VmEngineStructureValue), VmError> {
1017        match self.pop()? {
1018            VmValue::EngineStructure {
1019                index,
1020                value,
1021            } => Ok((index, value)),
1022            other => Err(VmError::TypeMismatch {
1023                offset:   self.ip,
1024                message:  "expected engine structure on stack top".to_string(),
1025                expected: Some("engine structure"),
1026                actual:   other.kind_name(),
1027            }),
1028        }
1029    }
1030
1031    /// Pops one engine-structure value and checks its engine-structure index.
1032    ///
1033    /// # Errors
1034    ///
1035    /// Returns [`VmError`] if the stack top is not the requested engine
1036    /// structure.
1037    pub fn pop_engine_structure_index(
1038        &mut self,
1039        expected_index: u8,
1040    ) -> Result<VmEngineStructureValue, VmError> {
1041        let (index, value) = self.pop_engine_structure()?;
1042        if index != expected_index {
1043            return Err(VmError::TypeMismatch {
1044                offset:   self.ip,
1045                message:  ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected engine structure {0} on stack top, found {1}",
                expected_index, index))
    })format!(
1046                    "expected engine structure {} on stack top, found {}",
1047                    expected_index, index
1048                ),
1049                expected: Some("engine structure"),
1050                actual:   "engine structure",
1051            });
1052        }
1053        Ok(value)
1054    }
1055
1056    /// Pops one vector value from three float stack cells.
1057    ///
1058    /// # Errors
1059    ///
1060    /// Returns [`VmError`] if the top three cells are not floats.
1061    pub fn pop_vector(&mut self) -> Result<[f32; 3], VmError> {
1062        let z = self.pop_float()?;
1063        let y = self.pop_float()?;
1064        let x = self.pop_float()?;
1065        Ok([x, y, z])
1066    }
1067
1068    fn set_stack_pointer(&mut self, pointer: usize) -> Result<(), VmError> {
1069        if pointer > self.stack.len() {
1070            return Err(VmError::StackUnderflow {
1071                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attempted to move stack pointer to {0}, but stack has {1} values",
                pointer, self.stack.len()))
    })format!(
1072                    "attempted to move stack pointer to {}, but stack has {} values",
1073                    pointer,
1074                    self.stack.len()
1075                ),
1076            });
1077        }
1078        self.stack.truncate(pointer);
1079        self.sp = pointer;
1080        Ok(())
1081    }
1082
1083    fn assign_cell(&mut self, src: usize, dst: usize) -> Result<(), VmError> {
1084        let Some(value) = self.stack.get(src).cloned() else {
1085            return Err(VmError::StackUnderflow {
1086                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attempted to copy from missing stack cell {0}",
                src))
    })format!("attempted to copy from missing stack cell {src}"),
1087            });
1088        };
1089        if dst >= self.stack.len() {
1090            self.stack.push(value);
1091            self.sp += 1;
1092        } else {
1093            let Some(target) = self.stack.get_mut(dst) else {
1094                return Err(VmError::StackUnderflow {
1095                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attempted to write missing stack cell {0}",
                dst))
    })format!("attempted to write missing stack cell {dst}"),
1096                });
1097            };
1098            *target = value;
1099        }
1100        Ok(())
1101    }
1102}
1103
1104/// One immutable `ACTION` handler.
1105pub type VmCommandHandler = dyn Fn(&mut VmScript, u16, u8) -> Result<(), VmError> + 'static;
1106
1107/// One engine-structure default factory used by `RSADD`.
1108pub type VmEngineStructureFactory = dyn Fn(u8) -> VmEngineStructureValue + 'static;
1109
1110/// One engine-structure equality hook used by `EQ` and `NEQ`.
1111pub type VmEngineStructureComparer =
1112    dyn Fn(u8, &VmEngineStructureValue, &VmEngineStructureValue) -> bool + 'static;
1113
1114/// One instruction trace hook invoked before the VM executes each opcode.
1115pub type VmTraceHook = dyn Fn(&VmScript, &VmTraceEvent) + 'static;
1116
1117/// One set of optional execution controls for a VM run.
1118#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmRunOptions {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(f, "VmRunOptions",
            "max_instructions", &&self.max_instructions)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmRunOptions {
    #[inline]
    fn clone(&self) -> VmRunOptions {
        let _: ::core::clone::AssertParamIsClone<Option<usize>>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for VmRunOptions { }Copy, #[automatically_derived]
impl ::core::default::Default for VmRunOptions {
    #[inline]
    fn default() -> VmRunOptions {
        VmRunOptions { max_instructions: ::core::default::Default::default() }
    }
}Default)]
1119pub struct VmRunOptions {
1120    /// Maximum number of instructions that may execute before the VM aborts
1121    /// with an error.
1122    pub max_instructions: Option<usize>,
1123}
1124
1125/// One result returned after executing exactly one instruction.
1126#[derive(#[automatically_derived]
impl ::core::fmt::Debug for VmStepOutcome {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                VmStepOutcome::Running => "Running",
                VmStepOutcome::Halted => "Halted",
                VmStepOutcome::Aborted => "Aborted",
            })
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for VmStepOutcome {
    #[inline]
    fn clone(&self) -> VmStepOutcome { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for VmStepOutcome { }Copy, #[automatically_derived]
impl ::core::cmp::PartialEq for VmStepOutcome {
    #[inline]
    fn eq(&self, other: &VmStepOutcome) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for VmStepOutcome {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {}
}Eq)]
1127pub enum VmStepOutcome {
1128    /// One instruction executed and the script can continue.
1129    Running,
1130    /// The script returned from the outermost frame.
1131    Halted,
1132    /// A host action handler requested abort and the VM stopped cleanly.
1133    Aborted,
1134}
1135
1136/// One command-dispatch table used to execute `ACTION` opcodes.
1137#[derive(#[automatically_derived]
impl ::core::default::Default for Vm {
    #[inline]
    fn default() -> Vm {
        Vm {
            commands: ::core::default::Default::default(),
            engine_structures: ::core::default::Default::default(),
            engine_structure_comparers: ::core::default::Default::default(),
            trace_hook: ::core::default::Default::default(),
        }
    }
}Default)]
1138pub struct Vm {
1139    commands:                   Vec<Option<Box<VmCommandHandler>>>,
1140    engine_structures:          Vec<Option<Box<VmEngineStructureFactory>>>,
1141    engine_structure_comparers: Vec<Option<Box<VmEngineStructureComparer>>>,
1142    trace_hook:                 Option<Box<VmTraceHook>>,
1143}
1144
1145impl fmt::Debug for Vm {
1146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147        f.debug_struct("Vm")
1148            .field("registered_commands", &self.commands.len())
1149            .field(
1150                "registered_engine_structures",
1151                &self.engine_structures.len(),
1152            )
1153            .field(
1154                "registered_engine_structure_comparers",
1155                &self.engine_structure_comparers.len(),
1156            )
1157            .field("has_trace_hook", &self.trace_hook.is_some())
1158            .finish()
1159    }
1160}
1161
1162impl Vm {
1163    /// Creates an empty VM command table.
1164    #[must_use]
1165    pub fn new() -> Self {
1166        Self::default()
1167    }
1168
1169    /// Registers one command handler by its numeric action id.
1170    pub fn define_command<F>(&mut self, command: u16, handler: F)
1171    where
1172        F: Fn(&mut VmScript, u16, u8) -> Result<(), VmError> + 'static,
1173    {
1174        let index = usize::from(command);
1175        if self.commands.len() <= index {
1176            self.commands.resize_with(index + 1, || None);
1177        }
1178        if let Some(slot) = self.commands.get_mut(index) {
1179            *slot = Some(Box::new(handler));
1180        }
1181    }
1182
1183    /// Registers one zero-metadata command handler.
1184    pub fn define_simple_command<F>(&mut self, command: u16, handler: F)
1185    where
1186        F: Fn(&mut VmScript) -> Result<(), VmError> + 'static,
1187    {
1188        self.define_command(command, move |script, _command, _argc| handler(script));
1189    }
1190
1191    /// Registers one engine-structure default factory by its numeric type
1192    /// index.
1193    pub fn define_engine_structure<F>(&mut self, index: u8, factory: F)
1194    where
1195        F: Fn(u8) -> VmEngineStructureValue + 'static,
1196    {
1197        let index = usize::from(index);
1198        if self.engine_structures.len() <= index {
1199            self.engine_structures.resize_with(index + 1, || None);
1200        }
1201        if let Some(slot) = self.engine_structures.get_mut(index) {
1202            *slot = Some(Box::new(factory));
1203        }
1204    }
1205
1206    /// Registers one fixed default engine-structure value by its numeric type
1207    /// index.
1208    pub fn define_engine_structure_default(&mut self, index: u8, value: VmEngineStructureValue) {
1209        self.define_engine_structure(index, move |_index| value.clone());
1210    }
1211
1212    /// Registers one engine-structure equality hook by its numeric type index.
1213    pub fn define_engine_structure_comparer<F>(&mut self, index: u8, comparer: F)
1214    where
1215        F: Fn(u8, &VmEngineStructureValue, &VmEngineStructureValue) -> bool + 'static,
1216    {
1217        let index = usize::from(index);
1218        if self.engine_structure_comparers.len() <= index {
1219            self.engine_structure_comparers
1220                .resize_with(index + 1, || None);
1221        }
1222        if let Some(slot) = self.engine_structure_comparers.get_mut(index) {
1223            *slot = Some(Box::new(comparer));
1224        }
1225    }
1226
1227    /// Registers one instruction trace hook invoked before each opcode
1228    /// executes.
1229    pub fn define_trace_hook<F>(&mut self, hook: F)
1230    where
1231        F: Fn(&VmScript, &VmTraceEvent) + 'static,
1232    {
1233        self.trace_hook = Some(Box::new(hook));
1234    }
1235
1236    /// Removes the currently registered instruction trace hook.
1237    pub fn clear_trace_hook(&mut self) {
1238        self.trace_hook = None;
1239    }
1240
1241    /// Executes one script until it returns from the outermost frame.
1242    ///
1243    /// # Errors
1244    ///
1245    /// Returns [`VmError`] if execution fails.
1246    pub fn run(&self, script: &mut VmScript) -> Result<(), VmError> {
1247        self.run_with_options(script, VmRunOptions::default())
1248    }
1249
1250    /// Executes exactly one instruction.
1251    ///
1252    /// # Errors
1253    ///
1254    /// Returns [`VmError`] if decoding or execution fails.
1255    pub fn step(&self, script: &mut VmScript) -> Result<VmStepOutcome, VmError> {
1256        const HALT_IP: usize = usize::MAX;
1257
1258        script.aborted = false;
1259
1260        if script.ret.is_empty() {
1261            script.ret.push(VmReturnFrame {
1262                target:  HALT_IP,
1263                cleanup: None,
1264            });
1265        }
1266
1267        if consume_abort_request(script) {
1268            return Ok(VmStepOutcome::Aborted);
1269        }
1270
1271        let decoded = script
1272            .program
1273            .instruction_at(script.ip)
1274            .ok_or(VmError::InvalidInstructionPointer {
1275                offset: script.ip
1276            })?
1277            .clone();
1278        self.emit_trace(script, &decoded);
1279        let next_ip = decoded.offset + decoded.instruction.encoded_len();
1280
1281        match decoded.instruction.opcode {
1282            NcsOpcode::NoOperation => {
1283                script.ip = next_ip;
1284            }
1285            NcsOpcode::Jmp => {
1286                script.ip = jump_target(decoded.offset, read_i32(&decoded, 0)?)?;
1287            }
1288            NcsOpcode::Jsr => {
1289                let target = jump_target(decoded.offset, read_i32(&decoded, 0)?)?;
1290                script.ret.push(VmReturnFrame {
1291                    target:  next_ip,
1292                    cleanup: script
1293                        .program
1294                        .function_shapes
1295                        .get(&target)
1296                        .copied()
1297                        .map(|shape| VmCallCleanup {
1298                            arg_cells:    shape.arg_cells,
1299                            return_cells: shape.return_cells,
1300                        }),
1301                });
1302                script.ip = target;
1303            }
1304            NcsOpcode::Jz => {
1305                if script.pop_int()? == 0 {
1306                    script.ip = jump_target(decoded.offset, read_i32(&decoded, 0)?)?;
1307                } else {
1308                    script.ip = next_ip;
1309                }
1310            }
1311            NcsOpcode::Jnz => {
1312                if script.pop_int()? != 0 {
1313                    script.ip = jump_target(decoded.offset, read_i32(&decoded, 0)?)?;
1314                } else {
1315                    script.ip = next_ip;
1316                }
1317            }
1318            NcsOpcode::Ret => {
1319                let frame = script.ret.pop().ok_or_else(|| VmError::StackUnderflow {
1320                    message: "attempted to return without a return frame".to_string(),
1321                })?;
1322                if let Some(cleanup) = frame.cleanup {
1323                    cleanup_call_frame(script, cleanup)?;
1324                }
1325                if frame.target == HALT_IP {
1326                    return Ok(VmStepOutcome::Halted);
1327                }
1328                script.ip = frame.target;
1329            }
1330            NcsOpcode::SaveBasePointer => {
1331                script.push_int(
1332                    i32::try_from(script.bp).map_err(|_error| {
1333                        invalid_extra(&decoded, "base pointer exceeds i32 range")
1334                    })?,
1335                );
1336                script.bp = script.sp.saturating_sub(1);
1337                script.ip = next_ip;
1338            }
1339            NcsOpcode::RestoreBasePointer => {
1340                script.bp = usize::try_from(script.pop_int()?)
1341                    .map_err(|_error| invalid_extra(&decoded, "negative base pointer restore"))?;
1342                script.ip = next_ip;
1343            }
1344            NcsOpcode::RunstackAdd => {
1345                push_default_value(script, &decoded, self)?;
1346                script.ip = next_ip;
1347            }
1348            NcsOpcode::RunstackCopy | NcsOpcode::RunstackCopyBase => {
1349                let base = if decoded.instruction.opcode == NcsOpcode::RunstackCopyBase {
1350                    script.bp
1351                } else {
1352                    script.sp
1353                };
1354                let src = relative_stack_cell(&decoded, base, read_i32(&decoded, 0)?)?;
1355                let cells = usize::from(read_u16(&decoded, 4)?) / 4;
1356                for index in 0..cells {
1357                    script.assign_cell(src + index, script.sp + index)?;
1358                }
1359                script.ip = next_ip;
1360            }
1361            NcsOpcode::Assignment | NcsOpcode::AssignmentBase => {
1362                let cells = usize::from(read_u16(&decoded, 4)?) / 4;
1363                let dst = if decoded.instruction.opcode == NcsOpcode::AssignmentBase {
1364                    relative_stack_cell(&decoded, script.bp, read_i32(&decoded, 0)?)?
1365                } else {
1366                    let encoded_offset = read_i32(&decoded, 0)?;
1367                    match relative_stack_cell(&decoded, script.sp, encoded_offset) {
1368                        Ok(dst) => dst,
1369                        Err(VmError::StackUnderflow {
1370                            ..
1371                        }) => relative_stack_cell(&decoded, script.sp + cells, encoded_offset)?,
1372                        Err(error) => return Err(error),
1373                    }
1374                };
1375                for index in 0..cells {
1376                    script.assign_cell(script.sp.saturating_sub(cells) + index, dst + index)?;
1377                }
1378                script.ip = next_ip;
1379            }
1380            NcsOpcode::Constant => {
1381                push_constant_value(script, &decoded)?;
1382                script.ip = next_ip;
1383            }
1384            NcsOpcode::ModifyStackPointer => {
1385                let byte_delta = read_i32(&decoded, 0)?;
1386                if byte_delta > 0 {
1387                    let cells = usize::try_from(byte_delta / 4)
1388                        .map_err(|_error| invalid_extra(&decoded, "invalid MOVSP payload"))?;
1389                    for _ in 0..cells {
1390                        script.push_int(0);
1391                    }
1392                    script.ip = next_ip;
1393                    return Ok(VmStepOutcome::Running);
1394                }
1395                let cells = usize::try_from((-byte_delta) / 4)
1396                    .map_err(|_error| invalid_extra(&decoded, "invalid MOVSP payload"))?;
1397                script.set_stack_pointer(script.sp.saturating_sub(cells))?;
1398                script.ip = next_ip;
1399            }
1400            NcsOpcode::Increment
1401            | NcsOpcode::Decrement
1402            | NcsOpcode::IncrementBase
1403            | NcsOpcode::DecrementBase => {
1404                let base = if #[allow(non_exhaustive_omitted_patterns)] match decoded.instruction.opcode {
    NcsOpcode::IncrementBase | NcsOpcode::DecrementBase => true,
    _ => false,
}matches!(
1405                    decoded.instruction.opcode,
1406                    NcsOpcode::IncrementBase | NcsOpcode::DecrementBase
1407                ) {
1408                    script.bp
1409                } else {
1410                    script.sp
1411                };
1412                let dst = relative_stack_cell(&decoded, base, read_i32(&decoded, 0)?)?;
1413                let delta = if #[allow(non_exhaustive_omitted_patterns)] match decoded.instruction.opcode {
    NcsOpcode::Increment | NcsOpcode::IncrementBase => true,
    _ => false,
}matches!(
1414                    decoded.instruction.opcode,
1415                    NcsOpcode::Increment | NcsOpcode::IncrementBase
1416                ) {
1417                    1
1418                } else {
1419                    -1
1420                };
1421                let value = script
1422                    .stack
1423                    .get_mut(dst)
1424                    .ok_or_else(|| VmError::StackUnderflow {
1425                        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attempted to update missing stack cell {0}",
                dst))
    })format!("attempted to update missing stack cell {dst}"),
1426                    })?;
1427                match value {
1428                    VmValue::Int(int_value) => *int_value += delta,
1429                    other => {
1430                        return Err(VmError::TypeMismatch {
1431                            offset:   decoded.offset,
1432                            message:  "increment/decrement requires integer target".to_string(),
1433                            expected: Some("int"),
1434                            actual:   other.kind_name(),
1435                        });
1436                    }
1437                }
1438                script.ip = next_ip;
1439            }
1440            NcsOpcode::Negation => {
1441                match decoded.instruction.auxcode {
1442                    NcsAuxCode::TypeInteger => {
1443                        let value = script.pop_int()?;
1444                        script.push_int(-value);
1445                    }
1446                    NcsAuxCode::TypeFloat => {
1447                        let value = script.pop_float()?;
1448                        script.push_float(-value);
1449                    }
1450                    NcsAuxCode::TypeTypeVectorVector => {
1451                        let [x, y, z] = script.pop_vector()?;
1452                        script.push_vector([-x, -y, -z]);
1453                    }
1454                    _ => {
1455                        return unsupported(
1456                            &decoded,
1457                            "NEG only supports integer, float, and vector",
1458                        );
1459                    }
1460                }
1461                script.ip = next_ip;
1462            }
1463            NcsOpcode::Equal
1464            | NcsOpcode::NotEqual
1465            | NcsOpcode::Lt
1466            | NcsOpcode::Gt
1467            | NcsOpcode::Leq
1468            | NcsOpcode::Geq => {
1469                apply_comparison(script, &decoded, self)?;
1470                script.ip = next_ip;
1471            }
1472            NcsOpcode::LogicalOr => {
1473                let rhs = script.pop_int()? != 0;
1474                let lhs = script.pop_int()? != 0;
1475                script.push_int(bool_to_int(lhs || rhs));
1476                script.ip = next_ip;
1477            }
1478            NcsOpcode::LogicalAnd => {
1479                let rhs = script.pop_int()? != 0;
1480                let lhs = script.pop_int()? != 0;
1481                script.push_int(bool_to_int(lhs && rhs));
1482                script.ip = next_ip;
1483            }
1484            NcsOpcode::InclusiveOr => {
1485                let rhs = script.pop_int()?;
1486                let lhs = script.pop_int()?;
1487                script.push_int(lhs | rhs);
1488                script.ip = next_ip;
1489            }
1490            NcsOpcode::ExclusiveOr => {
1491                let rhs = script.pop_int()?;
1492                let lhs = script.pop_int()?;
1493                script.push_int(lhs ^ rhs);
1494                script.ip = next_ip;
1495            }
1496            NcsOpcode::BooleanAnd => {
1497                let rhs = script.pop_int()?;
1498                let lhs = script.pop_int()?;
1499                script.push_int(lhs & rhs);
1500                script.ip = next_ip;
1501            }
1502            NcsOpcode::BooleanNot => {
1503                let value = script.pop_int()? == 0;
1504                script.push_int(bool_to_int(value));
1505                script.ip = next_ip;
1506            }
1507            NcsOpcode::OnesComplement => {
1508                let value = script.pop_int()?;
1509                script.push_int(!value);
1510                script.ip = next_ip;
1511            }
1512            NcsOpcode::ShiftLeft => {
1513                let rhs = script.pop_int()?;
1514                let lhs = script.pop_int()?;
1515                script.push_int(lhs.wrapping_shl(rhs as u32));
1516                script.ip = next_ip;
1517            }
1518            NcsOpcode::ShiftRight => {
1519                let rhs = script.pop_int()?;
1520                let lhs = script.pop_int()?;
1521                script.push_int(lhs.wrapping_shr(rhs as u32));
1522                script.ip = next_ip;
1523            }
1524            NcsOpcode::UShiftRight => {
1525                let rhs = script.pop_int()?;
1526                let lhs = script.pop_int()?;
1527                script.push_int(((lhs as u32).wrapping_shr(rhs as u32)) as i32);
1528                script.ip = next_ip;
1529            }
1530            NcsOpcode::Add => {
1531                apply_add(script, &decoded)?;
1532                script.ip = next_ip;
1533            }
1534            NcsOpcode::Sub => {
1535                apply_sub(script, &decoded)?;
1536                script.ip = next_ip;
1537            }
1538            NcsOpcode::Mul => {
1539                apply_mul(script, &decoded)?;
1540                script.ip = next_ip;
1541            }
1542            NcsOpcode::Div => {
1543                apply_div(script, &decoded)?;
1544                script.ip = next_ip;
1545            }
1546            NcsOpcode::Modulus => {
1547                let rhs = script.pop_int()?;
1548                let lhs = script.pop_int()?;
1549                if rhs == 0 {
1550                    return unsupported(&decoded, "modulus by zero");
1551                }
1552                script.push_int(lhs % rhs);
1553                script.ip = next_ip;
1554            }
1555            NcsOpcode::DeStruct => {
1556                let size_orig = usize::from(read_u16(&decoded, 0)?) / 4;
1557                let start = usize::from(read_u16(&decoded, 2)?) / 4;
1558                let size = usize::from(read_u16(&decoded, 4)?) / 4;
1559
1560                if size + start < size_orig {
1561                    let new_sp = script.sp.saturating_sub(size_orig) + size + start;
1562                    script.set_stack_pointer(new_sp)?;
1563                }
1564
1565                if start > 0 {
1566                    let from = script.sp.saturating_sub(size + start);
1567                    let to = script.sp.saturating_sub(start);
1568                    for index in from..to {
1569                        script.assign_cell(index + start, index)?;
1570                    }
1571                    script.set_stack_pointer(script.sp.saturating_sub(start))?;
1572                }
1573                script.ip = next_ip;
1574            }
1575            NcsOpcode::StoreIp | NcsOpcode::StoreState => {
1576                script.save_ip =
1577                    jump_target(decoded.offset, i32::from(decoded.instruction.auxcode as u8))?;
1578                if decoded.instruction.opcode == NcsOpcode::StoreState {
1579                    let situation = capture_saved_situation(script, &decoded, script.save_ip)?;
1580                    script.save_bp = situation.bp;
1581                    script.save_sp = situation.sp;
1582                    script.saved_situation = Some(situation);
1583                }
1584                script.ip = next_ip;
1585            }
1586            NcsOpcode::ExecuteCommand => {
1587                let command = read_u16(&decoded, 0)?;
1588                let argc = read_u8(&decoded, 2)?;
1589                let Some(handler) = self
1590                    .commands
1591                    .get(usize::from(command))
1592                    .and_then(Option::as_ref)
1593                else {
1594                    return Err(VmError::InvalidCommand {
1595                        offset: decoded.offset,
1596                        command,
1597                    });
1598                };
1599                handler(script, command, argc)?;
1600                if consume_abort_request(script) {
1601                    return Ok(VmStepOutcome::Aborted);
1602                }
1603                script.ip = next_ip;
1604            }
1605        }
1606
1607        Ok(VmStepOutcome::Running)
1608    }
1609
1610    /// Continues execution until the script is about to execute the instruction
1611    /// at `offset`.
1612    ///
1613    /// If the script reaches the requested offset, this returns
1614    /// [`VmStepOutcome::Running`] without executing that instruction.
1615    ///
1616    /// # Errors
1617    ///
1618    /// Returns [`VmError`] if execution fails or exceeds the configured
1619    /// instruction budget.
1620    pub fn run_until_offset(
1621        &self,
1622        script: &mut VmScript,
1623        offset: usize,
1624        options: VmRunOptions,
1625    ) -> Result<VmStepOutcome, VmError> {
1626        let mut instructions_executed = 0usize;
1627        loop {
1628            if script.ip == offset {
1629                return Ok(VmStepOutcome::Running);
1630            }
1631            if let Some(limit) = options.max_instructions
1632                && instructions_executed >= limit
1633            {
1634                return Err(VmError::InstructionLimitExceeded {
1635                    offset: script.ip,
1636                    limit,
1637                });
1638            }
1639            instructions_executed += 1;
1640            match self.step(script)? {
1641                VmStepOutcome::Running => {}
1642                outcome => return Ok(outcome),
1643            }
1644        }
1645    }
1646
1647    /// Executes the current instruction, stepping over user-function calls.
1648    ///
1649    /// For non-`JSR` instructions this behaves like [`Vm::step`]. For `JSR`, it
1650    /// runs until control returns to the caller, preserving a debugger-friendly
1651    /// "step over" behavior.
1652    ///
1653    /// # Errors
1654    ///
1655    /// Returns [`VmError`] if execution fails or exceeds the configured
1656    /// instruction budget.
1657    pub fn step_over(
1658        &self,
1659        script: &mut VmScript,
1660        options: VmRunOptions,
1661    ) -> Result<VmStepOutcome, VmError> {
1662        let Some(instruction) = script.current_instruction().cloned() else {
1663            return Err(VmError::InvalidInstructionPointer {
1664                offset: script.ip
1665            });
1666        };
1667        if instruction.opcode != NcsOpcode::Jsr {
1668            return self.step(script);
1669        }
1670
1671        let return_offset = script.ip + instruction.encoded_len();
1672        let depth = script.ret.len();
1673        match self.step(script)? {
1674            VmStepOutcome::Running => {}
1675            outcome => return Ok(outcome),
1676        }
1677
1678        let mut instructions_executed = 0usize;
1679        loop {
1680            if script.ip == return_offset && script.ret.len() == depth {
1681                return Ok(VmStepOutcome::Running);
1682            }
1683            if let Some(limit) = options.max_instructions
1684                && instructions_executed >= limit
1685            {
1686                return Err(VmError::InstructionLimitExceeded {
1687                    offset: script.ip,
1688                    limit,
1689                });
1690            }
1691            instructions_executed += 1;
1692            match self.step(script)? {
1693                VmStepOutcome::Running => {}
1694                outcome => return Ok(outcome),
1695            }
1696        }
1697    }
1698
1699    /// Continues execution until the current function returns to its caller.
1700    ///
1701    /// If the script is currently at top level, this runs until the script
1702    /// halts or aborts.
1703    ///
1704    /// # Errors
1705    ///
1706    /// Returns [`VmError`] if execution fails or exceeds the configured
1707    /// instruction budget.
1708    pub fn step_out(
1709        &self,
1710        script: &mut VmScript,
1711        options: VmRunOptions,
1712    ) -> Result<VmStepOutcome, VmError> {
1713        let depth = script.ret.len();
1714        let mut instructions_executed = 0usize;
1715        loop {
1716            if let Some(limit) = options.max_instructions
1717                && instructions_executed >= limit
1718            {
1719                return Err(VmError::InstructionLimitExceeded {
1720                    offset: script.ip,
1721                    limit,
1722                });
1723            }
1724            instructions_executed += 1;
1725            match self.step(script)? {
1726                VmStepOutcome::Running => {
1727                    if script.ret.len() < depth {
1728                        return Ok(VmStepOutcome::Running);
1729                    }
1730                }
1731                outcome => return Ok(outcome),
1732            }
1733        }
1734    }
1735
1736    /// Continues execution until the script is about to execute one instruction
1737    /// mapped to `file_name:line_number` in attached `NDB` metadata.
1738    ///
1739    /// If the script reaches the requested line, this returns
1740    /// [`VmStepOutcome::Running`] without executing that instruction.
1741    ///
1742    /// # Errors
1743    ///
1744    /// Returns [`VmError`] if the script has no matching debug mapping,
1745    /// execution fails, or the instruction budget is exceeded.
1746    pub fn run_until_line(
1747        &self,
1748        script: &mut VmScript,
1749        file_name: &str,
1750        line_number: usize,
1751        options: VmRunOptions,
1752    ) -> Result<VmStepOutcome, VmError> {
1753        let target = script
1754            .program
1755            .source_lines
1756            .iter()
1757            .find(|line| {
1758                line.file_name.eq_ignore_ascii_case(file_name) && line.line_number == line_number
1759            })
1760            .map(|line| line.start)
1761            .ok_or_else(|| VmError::Setup {
1762                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("no attached source line mapping for {0}:{1}",
                file_name, line_number))
    })format!("no attached source line mapping for {file_name}:{line_number}"),
1763            })?;
1764        self.run_until_offset(script, target, options)
1765    }
1766
1767    /// Continues execution until the script is about to execute one instruction
1768    /// inside the named function from attached `NDB` metadata.
1769    ///
1770    /// If the script reaches the requested function, this returns
1771    /// [`VmStepOutcome::Running`] without executing that instruction.
1772    ///
1773    /// # Errors
1774    ///
1775    /// Returns [`VmError`] if the script has no matching debug mapping,
1776    /// execution fails, or the instruction budget is exceeded.
1777    pub fn run_until_function(
1778        &self,
1779        script: &mut VmScript,
1780        name: &str,
1781        options: VmRunOptions,
1782    ) -> Result<VmStepOutcome, VmError> {
1783        let target = script
1784            .program
1785            .functions
1786            .iter()
1787            .find(|function| function.label == name)
1788            .map(|function| function.start)
1789            .ok_or_else(|| VmError::Setup {
1790                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("no attached function mapping for {0:?}",
                name))
    })format!("no attached function mapping for {name:?}"),
1791            })?;
1792        self.run_until_offset(script, target, options)
1793    }
1794
1795    /// Executes one script with explicit runtime controls until it returns from
1796    /// the outermost frame.
1797    ///
1798    /// # Errors
1799    ///
1800    /// Returns [`VmError`] if execution fails or exceeds the configured
1801    /// instruction budget.
1802    pub fn run_with_options(
1803        &self,
1804        script: &mut VmScript,
1805        options: VmRunOptions,
1806    ) -> Result<(), VmError> {
1807        let mut instructions_executed = 0usize;
1808
1809        loop {
1810            if let Some(limit) = options.max_instructions
1811                && instructions_executed >= limit
1812            {
1813                return Err(VmError::InstructionLimitExceeded {
1814                    offset: script.ip,
1815                    limit,
1816                });
1817            }
1818            instructions_executed += 1;
1819            match self.step(script)? {
1820                VmStepOutcome::Running => {}
1821                VmStepOutcome::Halted | VmStepOutcome::Aborted => break,
1822            }
1823        }
1824
1825        Ok(())
1826    }
1827
1828    /// Executes one previously captured NWScript action situation.
1829    ///
1830    /// # Errors
1831    ///
1832    /// Returns [`VmError`] if execution fails.
1833    pub fn run_situation(&self, situation: &VmSituation) -> Result<VmScript, VmError> {
1834        self.run_situation_with_options(situation, VmRunOptions::default())
1835    }
1836
1837    /// Executes one previously captured NWScript action situation with explicit
1838    /// runtime controls.
1839    ///
1840    /// # Errors
1841    ///
1842    /// Returns [`VmError`] if execution fails.
1843    pub fn run_situation_with_options(
1844        &self,
1845        situation: &VmSituation,
1846        options: VmRunOptions,
1847    ) -> Result<VmScript, VmError> {
1848        let mut script = situation.to_script();
1849        self.run_with_options(&mut script, options)?;
1850        Ok(script)
1851    }
1852
1853    /// Decodes one script, runs it, and returns the finished runtime state.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns [`VmError`] if decoding or execution fails.
1858    pub fn run_bytes(&self, bytes: &[u8], label: impl Into<String>) -> Result<VmScript, VmError> {
1859        self.run_bytes_with_options(bytes, label, VmRunOptions::default())
1860    }
1861
1862    /// Decodes one script, runs it with explicit runtime controls, and returns
1863    /// the finished runtime state.
1864    ///
1865    /// # Errors
1866    ///
1867    /// Returns [`VmError`] if decoding or execution fails.
1868    pub fn run_bytes_with_options(
1869        &self,
1870        bytes: &[u8],
1871        label: impl Into<String>,
1872        options: VmRunOptions,
1873    ) -> Result<VmScript, VmError> {
1874        let mut script = VmScript::from_bytes(bytes, label)?;
1875        self.run_with_options(&mut script, options)?;
1876        Ok(script)
1877    }
1878
1879    /// Decodes one script, attaches one `NDB` table, runs it, and returns the
1880    /// finished runtime state.
1881    ///
1882    /// This is the recommended path for compiled scripts that contain
1883    /// user-function calls, because the VM uses `NDB` metadata to recover
1884    /// callee stack shapes.
1885    ///
1886    /// # Errors
1887    ///
1888    /// Returns [`VmError`] if decoding, metadata attachment, or execution
1889    /// fails.
1890    pub fn run_bytes_with_ndb(
1891        &self,
1892        bytes: &[u8],
1893        label: impl Into<String>,
1894        ndb: &Ndb,
1895    ) -> Result<VmScript, VmError> {
1896        self.run_bytes_with_ndb_and_options(bytes, label, ndb, VmRunOptions::default())
1897    }
1898
1899    /// Decodes one script, attaches one `NDB` table, and runs it with explicit
1900    /// runtime controls.
1901    ///
1902    /// # Errors
1903    ///
1904    /// Returns [`VmError`] if decoding, metadata attachment, or execution
1905    /// fails.
1906    pub fn run_bytes_with_ndb_and_options(
1907        &self,
1908        bytes: &[u8],
1909        label: impl Into<String>,
1910        ndb: &Ndb,
1911        options: VmRunOptions,
1912    ) -> Result<VmScript, VmError> {
1913        let mut script = VmScript::from_bytes_with_ndb(bytes, label, ndb)?;
1914        self.run_with_options(&mut script, options)?;
1915        Ok(script)
1916    }
1917
1918    /// Decodes one script, prepares a named direct function call, and runs it.
1919    ///
1920    /// This bypasses the compiler-emitted loader and initializes globals
1921    /// automatically for `main()`-style entry loaders when needed.
1922    ///
1923    /// # Errors
1924    ///
1925    /// Returns [`VmError`] if decoding fails, the function cannot be invoked
1926    /// directly, or execution fails.
1927    pub fn run_function_bytes(
1928        &self,
1929        bytes: &[u8],
1930        label: impl Into<String>,
1931        ndb: &Ndb,
1932        name: &str,
1933        args: &[VmValue],
1934    ) -> Result<VmScript, VmError> {
1935        self.run_function_bytes_with_options(bytes, label, ndb, name, args, VmRunOptions::default())
1936    }
1937
1938    /// Decodes one script, prepares a named direct function call, and runs it
1939    /// with explicit runtime controls.
1940    ///
1941    /// # Errors
1942    ///
1943    /// Returns [`VmError`] if decoding fails, the function cannot be invoked
1944    /// directly, execution fails, or the instruction budget is exceeded.
1945    pub fn run_function_bytes_with_options(
1946        &self,
1947        bytes: &[u8],
1948        label: impl Into<String>,
1949        ndb: &Ndb,
1950        name: &str,
1951        args: &[VmValue],
1952        options: VmRunOptions,
1953    ) -> Result<VmScript, VmError> {
1954        let mut script = VmScript::from_bytes_with_ndb(bytes, label, ndb)?;
1955        if script_has_globals(ndb) {
1956            self.bootstrap_globals_for_direct_call(&mut script, ndb, options)?;
1957        }
1958        script.prepare_function_call(ndb, name, args)?;
1959        self.run_with_options(&mut script, options)?;
1960        Ok(script)
1961    }
1962
1963    fn default_engine_structure_value(&self, index: u8) -> VmEngineStructureValue {
1964        self.engine_structures
1965            .get(usize::from(index))
1966            .and_then(Option::as_ref)
1967            .map_or_else(
1968                || default_engine_structure_value(index),
1969                |factory| factory(index),
1970            )
1971    }
1972
1973    fn compare_engine_structure_values(
1974        &self,
1975        index: u8,
1976        lhs: &VmEngineStructureValue,
1977        rhs: &VmEngineStructureValue,
1978    ) -> bool {
1979        self.engine_structure_comparers
1980            .get(usize::from(index))
1981            .and_then(Option::as_ref)
1982            .map_or_else(|| lhs == rhs, |comparer| comparer(index, lhs, rhs))
1983    }
1984
1985    fn emit_trace(&self, script: &VmScript, decoded: &VmProgramInstruction) {
1986        let Some(hook) = self.trace_hook.as_ref() else {
1987            return;
1988        };
1989        hook(
1990            script,
1991            &VmTraceEvent {
1992                offset:      decoded.offset,
1993                ip:          script.ip,
1994                sp:          script.sp,
1995                bp:          script.bp,
1996                instruction: decoded.instruction.clone(),
1997            },
1998        );
1999    }
2000
2001    fn bootstrap_globals_for_direct_call(
2002        &self,
2003        script: &mut VmScript,
2004        ndb: &Ndb,
2005        options: VmRunOptions,
2006    ) -> Result<(), VmError> {
2007        if !script_has_globals(ndb) {
2008            return Ok(());
2009        }
2010
2011        let entry_name = entry_function_name(ndb).ok_or_else(|| VmError::Setup {
2012            message: "script entry function is missing".to_string(),
2013        })?;
2014        let entry = ndb
2015            .functions
2016            .iter()
2017            .find(|function| function.label == entry_name)
2018            .ok_or_else(|| VmError::Setup {
2019                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing NDB entry for {0:?}",
                entry_name))
    })format!("missing NDB entry for {entry_name:?}"),
2020            })?;
2021        let entry_start = usize::try_from(entry.binary_start).map_err(|_error| VmError::Setup {
2022            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("entry function {0:?} start offset exceeds usize range",
                entry_name))
    })format!("entry function {entry_name:?} start offset exceeds usize range"),
2023        })?;
2024
2025        let mut bootstrap =
2026            VmScript::from_instructions(Vec::new(), ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}#bootstrap", script.label))
    })format!("{}#bootstrap", script.label));
2027        bootstrap.program = script.program.clone();
2028        let entry_index = *bootstrap.program.offsets_to_index.get(&entry_start).ok_or(
2029            VmError::InvalidInstructionPointer {
2030                offset: entry_start,
2031            },
2032        )?;
2033        let Some(instruction) = bootstrap.program.instructions.get_mut(entry_index) else {
2034            return Err(VmError::InvalidInstructionPointer {
2035                offset: entry_start,
2036            });
2037        };
2038        instruction.instruction = NcsInstruction {
2039            opcode:  NcsOpcode::Ret,
2040            auxcode: NcsAuxCode::None,
2041            extra:   Vec::new(),
2042        };
2043        self.run_with_options(&mut bootstrap, options)?;
2044
2045        let globals_cells = global_stack_cells(ndb)?;
2046        let prefix_cells = loader_prefix_cells(ndb)?;
2047        let globals_end = prefix_cells + globals_cells;
2048        let globals = bootstrap
2049            .stack
2050            .get(prefix_cells..globals_end)
2051            .ok_or_else(|| VmError::StackUnderflow {
2052                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("bootstrapped globals frame expected cells {0}..{1}, but stack only has {2}",
                prefix_cells, globals_end, bootstrap.stack.len()))
    })format!(
2053                    "bootstrapped globals frame expected cells {}..{}, but stack only has {}",
2054                    prefix_cells,
2055                    globals_end,
2056                    bootstrap.stack.len()
2057                ),
2058            })?
2059            .to_vec();
2060        script.ip = 0;
2061        script.sp = globals.len();
2062        script.bp = 0;
2063        script.ret.clear();
2064        script.stack = globals;
2065        if globals_cells > 0 {
2066            script.push_int(0);
2067            script.bp = globals_cells;
2068        }
2069        script.save_ip = 0;
2070        script.save_sp = 0;
2071        script.save_bp = 0;
2072        script.saved_situation = None;
2073        script.abort_requested = false;
2074        script.aborted = false;
2075        Ok(())
2076    }
2077}
2078
2079fn script_has_globals(ndb: &Ndb) -> bool {
2080    ndb.variables
2081        .iter()
2082        .any(|variable| variable.binary_end == u32::MAX && variable.label != "#retval")
2083}
2084
2085fn entry_function_name(ndb: &Ndb) -> Option<&'static str> {
2086    if ndb
2087        .functions
2088        .iter()
2089        .any(|function| function.label == "main")
2090    {
2091        Some("main")
2092    } else if ndb
2093        .functions
2094        .iter()
2095        .any(|function| function.label == "StartingConditional")
2096    {
2097        Some("StartingConditional")
2098    } else {
2099        None
2100    }
2101}
2102
2103fn global_stack_cells(ndb: &Ndb) -> Result<usize, VmError> {
2104    ndb.variables
2105        .iter()
2106        .filter(|variable| variable.binary_end == u32::MAX && variable.label != "#retval")
2107        .try_fold(0usize, |cells, variable| {
2108            let start =
2109                usize::try_from(variable.stack_loc / 4).map_err(|_error| VmError::Setup {
2110                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("global {0:?} stack location exceeds usize range",
                variable.label))
    })format!(
2111                        "global {:?} stack location exceeds usize range",
2112                        variable.label
2113                    ),
2114                })?;
2115            let width = cells_for_ndb_type(&variable.ty)?;
2116            Ok(cells.max(start + width))
2117        })
2118}
2119
2120fn loader_prefix_cells(ndb: &Ndb) -> Result<usize, VmError> {
2121    let Some(retval) = ndb
2122        .variables
2123        .iter()
2124        .find(|variable| variable.binary_end == u32::MAX && variable.label == "#retval")
2125    else {
2126        return Ok(0);
2127    };
2128    cells_for_ndb_type(&retval.ty)
2129}
2130
2131fn expect_argument_count(function: &NdbFunction, actual: usize) -> Result<(), VmError> {
2132    if function.args.len() != actual {
2133        return Err(VmError::Setup {
2134            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("function {0:?} expects {1} arguments, got {2}",
                function.label, function.args.len(), actual))
    })format!(
2135                "function {:?} expects {} arguments, got {}",
2136                function.label,
2137                function.args.len(),
2138                actual
2139            ),
2140        });
2141    }
2142    Ok(())
2143}
2144
2145fn validate_supported_ndb_value_type(
2146    ty: &NdbType,
2147    role: &str,
2148    index: Option<usize>,
2149) -> Result<(), VmError> {
2150    match ty {
2151        NdbType::Float
2152        | NdbType::Int
2153        | NdbType::Void
2154        | NdbType::Object
2155        | NdbType::String
2156        | NdbType::EngineStructure(_) => Ok(()),
2157        NdbType::Struct(struct_index) => Err(VmError::Setup {
2158            message: match index {
2159                Some(index) => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported direct-call {0} at position {1}: struct t{2:04}",
                role, index, struct_index))
    })format!(
2160                    "unsupported direct-call {role} at position {index}: struct t{struct_index:04}"
2161                ),
2162                None => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported direct-call {0}: struct t{1:04}",
                role, struct_index))
    })format!("unsupported direct-call {role}: struct t{struct_index:04}"),
2163            },
2164        }),
2165        NdbType::Unknown | NdbType::Raw(_) => Err(VmError::Setup {
2166            message: match index {
2167                Some(index) => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported direct-call {0} at position {1}: {2}",
                role, index, ty))
    })format!("unsupported direct-call {role} at position {index}: {ty}"),
2168                None => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported direct-call {0}: {1}",
                role, ty))
    })format!("unsupported direct-call {role}: {ty}"),
2169            },
2170        }),
2171    }
2172}
2173
2174fn validate_entry_argument(expected: &NdbType, actual: &VmValue) -> Result<(), VmError> {
2175    validate_supported_ndb_value_type(expected, "argument type", None)?;
2176    match (expected, actual) {
2177        (NdbType::Int, VmValue::Int(_))
2178        | (NdbType::Float, VmValue::Float(_))
2179        | (NdbType::String, VmValue::String(_))
2180        | (NdbType::Object, VmValue::Object(_)) => Ok(()),
2181        (
2182            NdbType::EngineStructure(expected),
2183            VmValue::EngineStructure {
2184                index, ..
2185            },
2186        ) if expected == index => Ok(()),
2187        _ => Err(VmError::Setup {
2188            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("argument type mismatch: expected {0}, got {1}",
                expected, actual.kind_name()))
    })format!(
2189                "argument type mismatch: expected {}, got {}",
2190                expected,
2191                actual.kind_name()
2192            ),
2193        }),
2194    }
2195}
2196
2197fn default_value_for_ndb_type(ty: &NdbType) -> Result<VmValue, VmError> {
2198    validate_supported_ndb_value_type(ty, "return type", None)?;
2199    Ok(match ty {
2200        NdbType::Float => VmValue::Float(0.0),
2201        NdbType::Int => VmValue::Int(0),
2202        NdbType::Void => {
2203            return Err(VmError::Setup {
2204                message: "void return slots are not materialized".to_string(),
2205            });
2206        }
2207        NdbType::Object => VmValue::Object(0),
2208        NdbType::String => VmValue::String(String::new()),
2209        NdbType::EngineStructure(index) => VmValue::EngineStructure {
2210            index: *index,
2211            value: default_engine_structure_value(*index),
2212        },
2213        NdbType::Struct(_) | NdbType::Unknown | NdbType::Raw(_) => ::core::panicking::panic("internal error: entered unreachable code")unreachable!(),
2214    })
2215}
2216
2217fn read_u8(decoded: &VmProgramInstruction, start: usize) -> Result<u8, VmError> {
2218    decoded
2219        .instruction
2220        .extra
2221        .get(start)
2222        .copied()
2223        .ok_or_else(|| invalid_extra(decoded, "payload ended early"))
2224}
2225
2226fn read_u16(decoded: &VmProgramInstruction, start: usize) -> Result<u16, VmError> {
2227    let window = decoded
2228        .instruction
2229        .extra
2230        .get(start..start + 2)
2231        .ok_or_else(|| invalid_extra(decoded, "payload ended early"))?;
2232    let bytes =
2233        <[u8; 2]>::try_from(window).map_err(|_error| invalid_extra(decoded, "bad u16 payload"))?;
2234    Ok(u16::from_be_bytes(bytes))
2235}
2236
2237fn read_i32(decoded: &VmProgramInstruction, start: usize) -> Result<i32, VmError> {
2238    let window = decoded
2239        .instruction
2240        .extra
2241        .get(start..start + 4)
2242        .ok_or_else(|| invalid_extra(decoded, "payload ended early"))?;
2243    let bytes =
2244        <[u8; 4]>::try_from(window).map_err(|_error| invalid_extra(decoded, "bad i32 payload"))?;
2245    Ok(i32::from_be_bytes(bytes))
2246}
2247
2248fn read_u32(decoded: &VmProgramInstruction, start: usize) -> Result<u32, VmError> {
2249    let window = decoded
2250        .instruction
2251        .extra
2252        .get(start..start + 4)
2253        .ok_or_else(|| invalid_extra(decoded, "payload ended early"))?;
2254    let bytes =
2255        <[u8; 4]>::try_from(window).map_err(|_error| invalid_extra(decoded, "bad u32 payload"))?;
2256    Ok(u32::from_be_bytes(bytes))
2257}
2258
2259fn read_f32(decoded: &VmProgramInstruction, start: usize) -> Result<f32, VmError> {
2260    Ok(f32::from_bits(read_u32(decoded, start)?))
2261}
2262
2263fn read_ncs_string(decoded: &VmProgramInstruction) -> Result<String, VmError> {
2264    let length = usize::from(read_u16(decoded, 0)?);
2265    let window = decoded
2266        .instruction
2267        .extra
2268        .get(2..2 + length)
2269        .ok_or_else(|| invalid_extra(decoded, "string payload shorter than declared length"))?;
2270    let value = str::from_utf8(window).map_err(|error| {
2271        invalid_extra(decoded, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid UTF-8 string payload: {0}",
                error))
    })format!("invalid UTF-8 string payload: {error}"))
2272    })?;
2273    Ok(value.to_string())
2274}
2275
2276fn relative_stack_cell(
2277    decoded: &VmProgramInstruction,
2278    base: usize,
2279    encoded_offset: i32,
2280) -> Result<usize, VmError> {
2281    if encoded_offset > 0 || encoded_offset % 4 != 0 {
2282        return Err(invalid_extra(
2283            decoded,
2284            &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected negative 4-byte-aligned stack offset, got {0}",
                encoded_offset))
    })format!("expected negative 4-byte-aligned stack offset, got {encoded_offset}"),
2285        ));
2286    }
2287    let cells = usize::try_from((-encoded_offset) / 4)
2288        .map_err(|_error| invalid_extra(decoded, "invalid negative stack offset"))?;
2289    base.checked_sub(cells)
2290        .ok_or_else(|| VmError::StackUnderflow {
2291            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("stack offset {0} underflowed base {1}",
                encoded_offset, base))
    })format!("stack offset {encoded_offset} underflowed base {base}"),
2292        })
2293}
2294
2295fn jump_target(current: usize, delta: i32) -> Result<usize, VmError> {
2296    if delta >= 0 {
2297        current
2298            .checked_add(usize::try_from(delta).ok().unwrap_or(usize::MAX))
2299            .ok_or(VmError::InvalidInstructionPointer {
2300                offset: current
2301            })
2302    } else {
2303        current
2304            .checked_sub(usize::try_from(-delta).ok().unwrap_or(usize::MAX))
2305            .ok_or(VmError::InvalidInstructionPointer {
2306                offset: current
2307            })
2308    }
2309}
2310
2311fn capture_saved_situation(
2312    script: &VmScript,
2313    decoded: &VmProgramInstruction,
2314    target_ip: usize,
2315) -> Result<VmSituation, VmError> {
2316    let global_bytes = usize::try_from(read_u32(decoded, 0)?)
2317        .map_err(|_error| invalid_extra(decoded, "global size exceeds usize range"))?;
2318    let stack_bytes = usize::try_from(read_u32(decoded, 4)?)
2319        .map_err(|_error| invalid_extra(decoded, "stack size exceeds usize range"))?;
2320    if global_bytes % 4 != 0 || stack_bytes % 4 != 0 {
2321        return Err(invalid_extra(
2322            decoded,
2323            "saved state sizes must be 4-byte aligned",
2324        ));
2325    }
2326
2327    let saved_sp = (global_bytes + stack_bytes) / 4;
2328    if saved_sp > script.sp {
2329        return Err(VmError::StackUnderflow {
2330            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("saved situation requested {0} cells, but stack only has {1}",
                saved_sp, script.sp))
    })format!(
2331                "saved situation requested {} cells, but stack only has {}",
2332                saved_sp, script.sp
2333            ),
2334        });
2335    }
2336
2337    Ok(VmSituation {
2338        label:   script.label.clone(),
2339        program: script.program.clone(),
2340        ip:      target_ip,
2341        sp:      saved_sp,
2342        bp:      script.bp,
2343        stack:   script
2344            .stack
2345            .get(..saved_sp)
2346            .ok_or_else(|| VmError::StackUnderflow {
2347                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("saved situation requested {0} cells, but stack only has {1}",
                saved_sp, script.stack.len()))
    })format!(
2348                    "saved situation requested {} cells, but stack only has {}",
2349                    saved_sp,
2350                    script.stack.len()
2351                ),
2352            })?
2353            .to_vec(),
2354    })
2355}
2356
2357fn default_engine_structure_value(index: u8) -> VmEngineStructureValue {
2358    if index == 7 {
2359        VmEngineStructureValue::Text(String::new())
2360    } else {
2361        VmEngineStructureValue::Word(0)
2362    }
2363}
2364
2365fn consume_abort_request(script: &mut VmScript) -> bool {
2366    if script.abort_requested {
2367        script.abort_requested = false;
2368        script.aborted = true;
2369        script.ret.clear();
2370        return true;
2371    }
2372    false
2373}
2374
2375fn cells_for_ndb_type(ty: &NdbType) -> Result<usize, VmError> {
2376    Ok(match ty {
2377        NdbType::Float
2378        | NdbType::Int
2379        | NdbType::Object
2380        | NdbType::String
2381        | NdbType::EngineStructure(_) => 1,
2382        NdbType::Void => 0,
2383        NdbType::Struct(struct_index) => {
2384            return Err(VmError::Setup {
2385                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported VM function metadata for struct t{0:04}",
                struct_index))
    })format!("unsupported VM function metadata for struct t{struct_index:04}"),
2386            });
2387        }
2388        NdbType::Unknown | NdbType::Raw(_) => {
2389            return Err(VmError::Setup {
2390                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported VM function metadata type {0}",
                ty))
    })format!("unsupported VM function metadata type {ty}"),
2391            });
2392        }
2393    })
2394}
2395
2396fn cleanup_call_frame(script: &mut VmScript, cleanup: VmCallCleanup) -> Result<(), VmError> {
2397    let frame_cells = cleanup.arg_cells + cleanup.return_cells;
2398    let frame_start =
2399        script
2400            .sp
2401            .checked_sub(frame_cells)
2402            .ok_or_else(|| VmError::StackUnderflow {
2403                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attempted to clean up {0} call-frame cells from stack with only {1} values",
                frame_cells, script.sp))
    })format!(
2404                    "attempted to clean up {} call-frame cells from stack with only {} values",
2405                    frame_cells, script.sp
2406                ),
2407            })?;
2408    if cleanup.return_cells == 0 {
2409        return script.set_stack_pointer(frame_start);
2410    }
2411
2412    let return_end = frame_start + cleanup.return_cells;
2413    let return_values = script
2414        .stack
2415        .get(frame_start..return_end)
2416        .ok_or_else(|| VmError::StackUnderflow {
2417            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing {0} return cells in call frame starting at {1}",
                cleanup.return_cells, frame_start))
    })format!(
2418                "missing {} return cells in call frame starting at {}",
2419                cleanup.return_cells, frame_start
2420            ),
2421        })?
2422        .to_vec();
2423    script.set_stack_pointer(frame_start)?;
2424    for value in return_values {
2425        script.push(value);
2426    }
2427    Ok(())
2428}
2429
2430fn push_default_value(
2431    script: &mut VmScript,
2432    decoded: &VmProgramInstruction,
2433    vm: &Vm,
2434) -> Result<(), VmError> {
2435    match decoded.instruction.auxcode {
2436        NcsAuxCode::TypeInteger => script.push_int(0),
2437        NcsAuxCode::TypeFloat => script.push_float(0.0),
2438        NcsAuxCode::TypeString => script.push_string(String::new()),
2439        NcsAuxCode::TypeObject => script.push_object(0),
2440        aux => {
2441            let Some(index) = engine_structure_index(aux) else {
2442                return unsupported(decoded, "RSADD does not support this auxcode");
2443            };
2444            script.push_engine_structure(index, vm.default_engine_structure_value(index));
2445        }
2446    }
2447    Ok(())
2448}
2449
2450fn push_constant_value(
2451    script: &mut VmScript,
2452    decoded: &VmProgramInstruction,
2453) -> Result<(), VmError> {
2454    match decoded.instruction.auxcode {
2455        NcsAuxCode::TypeInteger => script.push_int(read_i32(decoded, 0)?),
2456        NcsAuxCode::TypeFloat => script.push_float(read_f32(decoded, 0)?),
2457        NcsAuxCode::TypeString => script.push_string(read_ncs_string(decoded)?),
2458        NcsAuxCode::TypeObject => script.push_object(read_u32(decoded, 0)?),
2459        aux => {
2460            let Some(index) = engine_structure_index(aux) else {
2461                return unsupported(decoded, "CONST does not support this auxcode");
2462            };
2463            let value = if index == 7 {
2464                VmEngineStructureValue::Text(read_ncs_string(decoded)?)
2465            } else {
2466                VmEngineStructureValue::Word(read_u32(decoded, 0)?)
2467            };
2468            script.push(VmValue::EngineStructure {
2469                index,
2470                value,
2471            });
2472        }
2473    }
2474    Ok(())
2475}
2476
2477fn engine_structure_index(auxcode: NcsAuxCode) -> Option<u8> {
2478    match auxcode {
2479        NcsAuxCode::TypeEngst0 => Some(0),
2480        NcsAuxCode::TypeEngst1 => Some(1),
2481        NcsAuxCode::TypeEngst2 => Some(2),
2482        NcsAuxCode::TypeEngst3 => Some(3),
2483        NcsAuxCode::TypeEngst4 => Some(4),
2484        NcsAuxCode::TypeEngst5 => Some(5),
2485        NcsAuxCode::TypeEngst6 => Some(6),
2486        NcsAuxCode::TypeEngst7 => Some(7),
2487        NcsAuxCode::TypeEngst8 => Some(8),
2488        NcsAuxCode::TypeEngst9 => Some(9),
2489        _ => None,
2490    }
2491}
2492
2493fn apply_comparison(
2494    script: &mut VmScript,
2495    decoded: &VmProgramInstruction,
2496    vm: &Vm,
2497) -> Result<(), VmError> {
2498    if decoded.instruction.auxcode == NcsAuxCode::TypeTypeVectorVector {
2499        let rhs = script.pop_vector()?;
2500        let lhs = script.pop_vector()?;
2501        let result = match decoded.instruction.opcode {
2502            NcsOpcode::Equal => lhs == rhs,
2503            NcsOpcode::NotEqual => lhs != rhs,
2504            _ => return unsupported(decoded, "ordered comparisons are not valid for vectors"),
2505        };
2506        script.push_int(bool_to_int(result));
2507        return Ok(());
2508    }
2509
2510    if decoded.instruction.auxcode == NcsAuxCode::TypeTypeStructStruct {
2511        let size_bytes = usize::from(read_u16(decoded, 0)?);
2512        let cell_count = size_bytes / 4;
2513        let rhs_start =
2514            script
2515                .sp
2516                .checked_sub(cell_count)
2517                .ok_or_else(|| VmError::StackUnderflow {
2518                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing {0} cells for rhs struct comparison",
                cell_count))
    })format!("missing {} cells for rhs struct comparison", cell_count),
2519                })?;
2520        let lhs_start =
2521            rhs_start
2522                .checked_sub(cell_count)
2523                .ok_or_else(|| VmError::StackUnderflow {
2524                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing {0} cells for lhs struct comparison",
                cell_count))
    })format!("missing {} cells for lhs struct comparison", cell_count),
2525                })?;
2526        let equal =
2527            script.stack.get(lhs_start..rhs_start) == script.stack.get(rhs_start..script.sp);
2528        script.set_stack_pointer(lhs_start)?;
2529        let result = match decoded.instruction.opcode {
2530            NcsOpcode::Equal => equal,
2531            NcsOpcode::NotEqual => !equal,
2532            _ => return unsupported(decoded, "ordered comparisons are not valid for structs"),
2533        };
2534        script.push_int(bool_to_int(result));
2535        return Ok(());
2536    }
2537
2538    let rhs = script.pop()?;
2539    let lhs = script.pop()?;
2540    let result = match (&lhs, &rhs) {
2541        (VmValue::Int(lhs), VmValue::Int(rhs)) => {
2542            compare_ordered(decoded.instruction.opcode, lhs, rhs)
2543        }
2544        (VmValue::Int(lhs), VmValue::Float(rhs)) => {
2545            compare_ordered(decoded.instruction.opcode, &(*lhs as f32), rhs)
2546        }
2547        (VmValue::Float(lhs), VmValue::Int(rhs)) => {
2548            compare_ordered(decoded.instruction.opcode, lhs, &(*rhs as f32))
2549        }
2550        (VmValue::Float(lhs), VmValue::Float(rhs)) => {
2551            compare_ordered(decoded.instruction.opcode, lhs, rhs)
2552        }
2553        (VmValue::String(lhs), VmValue::String(rhs)) => {
2554            compare_ordered(decoded.instruction.opcode, lhs, rhs)
2555        }
2556        (VmValue::Object(lhs), VmValue::Object(rhs)) => compare_equality(decoded, lhs, rhs)?,
2557        (
2558            VmValue::EngineStructure {
2559                index: lhs_index,
2560                value: lhs_value,
2561            },
2562            VmValue::EngineStructure {
2563                index: rhs_index,
2564                value: rhs_value,
2565            },
2566        ) if lhs_index == rhs_index => {
2567            compare_engine_structure_equality(decoded, vm, *lhs_index, lhs_value, rhs_value)?
2568        }
2569        _ => {
2570            return unsupported(
2571                decoded,
2572                &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("comparison between {0} and {1} is not implemented",
                lhs.kind_name(), rhs.kind_name()))
    })format!(
2573                    "comparison between {} and {} is not implemented",
2574                    lhs.kind_name(),
2575                    rhs.kind_name()
2576                ),
2577            );
2578        }
2579    };
2580    script.push_int(bool_to_int(result));
2581    Ok(())
2582}
2583
2584fn apply_add(script: &mut VmScript, decoded: &VmProgramInstruction) -> Result<(), VmError> {
2585    if decoded.instruction.auxcode == NcsAuxCode::TypeTypeVectorVector {
2586        let rhs = script.pop_vector()?;
2587        let lhs = script.pop_vector()?;
2588        script.push_vector([lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2]]);
2589        return Ok(());
2590    }
2591    let rhs = script.pop()?;
2592    let lhs = script.pop()?;
2593    match (lhs, rhs) {
2594        (VmValue::Int(lhs), VmValue::Int(rhs)) => script.push_int(lhs.wrapping_add(rhs)),
2595        (VmValue::Int(lhs), VmValue::Float(rhs)) => script.push_float(lhs as f32 + rhs),
2596        (VmValue::Float(lhs), VmValue::Int(rhs)) => script.push_float(lhs + rhs as f32),
2597        (VmValue::Float(lhs), VmValue::Float(rhs)) => script.push_float(lhs + rhs),
2598        (VmValue::String(lhs), VmValue::String(rhs)) => {
2599            script.push_string(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", lhs, rhs))
    })format!("{lhs}{rhs}"));
2600        }
2601        _ => {
2602            return unsupported(
2603                decoded,
2604                "ADD currently supports int, float, string, and vector",
2605            );
2606        }
2607    }
2608    Ok(())
2609}
2610
2611fn apply_sub(script: &mut VmScript, decoded: &VmProgramInstruction) -> Result<(), VmError> {
2612    if decoded.instruction.auxcode == NcsAuxCode::TypeTypeVectorVector {
2613        let rhs = script.pop_vector()?;
2614        let lhs = script.pop_vector()?;
2615        script.push_vector([lhs[0] - rhs[0], lhs[1] - rhs[1], lhs[2] - rhs[2]]);
2616        return Ok(());
2617    }
2618    let rhs = script.pop()?;
2619    let lhs = script.pop()?;
2620    match (lhs, rhs) {
2621        (VmValue::Int(lhs), VmValue::Int(rhs)) => script.push_int(lhs.wrapping_sub(rhs)),
2622        (VmValue::Int(lhs), VmValue::Float(rhs)) => script.push_float(lhs as f32 - rhs),
2623        (VmValue::Float(lhs), VmValue::Int(rhs)) => script.push_float(lhs - rhs as f32),
2624        (VmValue::Float(lhs), VmValue::Float(rhs)) => script.push_float(lhs - rhs),
2625        _ => return unsupported(decoded, "SUB currently supports int, float, and vector"),
2626    }
2627    Ok(())
2628}
2629
2630fn apply_mul(script: &mut VmScript, decoded: &VmProgramInstruction) -> Result<(), VmError> {
2631    match decoded.instruction.auxcode {
2632        NcsAuxCode::TypeTypeVectorFloat => {
2633            let rhs = script.pop_float()?;
2634            let lhs = script.pop_vector()?;
2635            script.push_vector([lhs[0] * rhs, lhs[1] * rhs, lhs[2] * rhs]);
2636            return Ok(());
2637        }
2638        NcsAuxCode::TypeTypeFloatVector => {
2639            let rhs = script.pop_vector()?;
2640            let lhs = script.pop_float()?;
2641            script.push_vector([lhs * rhs[0], lhs * rhs[1], lhs * rhs[2]]);
2642            return Ok(());
2643        }
2644        _ => {}
2645    }
2646    let rhs = script.pop()?;
2647    let lhs = script.pop()?;
2648    match (lhs, rhs) {
2649        (VmValue::Int(lhs), VmValue::Int(rhs)) => script.push_int(lhs.wrapping_mul(rhs)),
2650        (VmValue::Int(lhs), VmValue::Float(rhs)) => script.push_float(lhs as f32 * rhs),
2651        (VmValue::Float(lhs), VmValue::Int(rhs)) => script.push_float(lhs * rhs as f32),
2652        (VmValue::Float(lhs), VmValue::Float(rhs)) => script.push_float(lhs * rhs),
2653        _ => return unsupported(decoded, "MUL currently supports int, float, and vector"),
2654    }
2655    Ok(())
2656}
2657
2658fn apply_div(script: &mut VmScript, decoded: &VmProgramInstruction) -> Result<(), VmError> {
2659    if decoded.instruction.auxcode == NcsAuxCode::TypeTypeVectorFloat {
2660        let rhs = script.pop_float()?;
2661        if rhs == 0.0 {
2662            return unsupported(decoded, "division by zero");
2663        }
2664        let lhs = script.pop_vector()?;
2665        script.push_vector([lhs[0] / rhs, lhs[1] / rhs, lhs[2] / rhs]);
2666        return Ok(());
2667    }
2668    let rhs = script.pop()?;
2669    let lhs = script.pop()?;
2670    match (lhs, rhs) {
2671        (VmValue::Int(lhs), VmValue::Int(rhs)) => {
2672            if rhs == 0 {
2673                return unsupported(decoded, "division by zero");
2674            }
2675            script.push_int(lhs / rhs);
2676        }
2677        (VmValue::Int(lhs), VmValue::Float(rhs)) => {
2678            if rhs == 0.0 {
2679                return unsupported(decoded, "division by zero");
2680            }
2681            script.push_float(lhs as f32 / rhs);
2682        }
2683        (VmValue::Float(lhs), VmValue::Int(rhs)) => {
2684            if rhs == 0 {
2685                return unsupported(decoded, "division by zero");
2686            }
2687            script.push_float(lhs / rhs as f32);
2688        }
2689        (VmValue::Float(lhs), VmValue::Float(rhs)) => {
2690            if rhs == 0.0 {
2691                return unsupported(decoded, "division by zero");
2692            }
2693            script.push_float(lhs / rhs);
2694        }
2695        _ => return unsupported(decoded, "DIV currently supports int, float, and vector"),
2696    }
2697    Ok(())
2698}
2699
2700fn compare_engine_structure_equality(
2701    decoded: &VmProgramInstruction,
2702    vm: &Vm,
2703    index: u8,
2704    lhs: &VmEngineStructureValue,
2705    rhs: &VmEngineStructureValue,
2706) -> Result<bool, VmError> {
2707    let equal = vm.compare_engine_structure_values(index, lhs, rhs);
2708    match decoded.instruction.opcode {
2709        NcsOpcode::Equal => Ok(equal),
2710        NcsOpcode::NotEqual => Ok(!equal),
2711        _ => unsupported(
2712            decoded,
2713            "ordered comparison is not valid for engine structures",
2714        ),
2715    }
2716}
2717
2718fn compare_equality<T: PartialEq>(
2719    decoded: &VmProgramInstruction,
2720    lhs: &T,
2721    rhs: &T,
2722) -> Result<bool, VmError> {
2723    match decoded.instruction.opcode {
2724        NcsOpcode::Equal => Ok(lhs == rhs),
2725        NcsOpcode::NotEqual => Ok(lhs != rhs),
2726        _ => unsupported(
2727            decoded,
2728            "ordered comparison is not valid for this runtime type",
2729        ),
2730    }
2731}
2732
2733fn compare_ordered<T: PartialEq + PartialOrd>(opcode: NcsOpcode, lhs: &T, rhs: &T) -> bool {
2734    match opcode {
2735        NcsOpcode::Equal => lhs == rhs,
2736        NcsOpcode::NotEqual => lhs != rhs,
2737        NcsOpcode::Lt => lhs < rhs,
2738        NcsOpcode::Gt => lhs > rhs,
2739        NcsOpcode::Leq => lhs <= rhs,
2740        NcsOpcode::Geq => lhs >= rhs,
2741        _ => false,
2742    }
2743}
2744
2745fn bool_to_int(value: bool) -> i32 {
2746    if value { 1 } else { 0 }
2747}
2748
2749fn invalid_extra(decoded: &VmProgramInstruction, message: &str) -> VmError {
2750    VmError::InvalidExtra {
2751        offset:  decoded.offset,
2752        opcode:  decoded.instruction.opcode,
2753        auxcode: decoded.instruction.auxcode,
2754        message: message.to_string(),
2755    }
2756}
2757
2758fn unsupported<T>(decoded: &VmProgramInstruction, message: &str) -> Result<T, VmError> {
2759    Err(VmError::Unsupported {
2760        offset:  decoded.offset,
2761        opcode:  decoded.instruction.opcode,
2762        auxcode: decoded.instruction.auxcode,
2763        message: message.to_string(),
2764    })
2765}
2766
2767#[cfg(test)]
2768mod tests {
2769    use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc};
2770
2771    use super::{Vm, VmEngineStructureValue, VmRunOptions, VmScript, VmStepOutcome, VmValue};
2772    use crate::{
2773        CompileOptions, InMemoryScriptResolver, NcsAuxCode, NcsInstruction, NcsOpcode, SourceId,
2774        SourceMap, compile_script, compile_script_with_source_map, load_source_bundle,
2775        parse_langspec, parse_text, read_ndb,
2776    };
2777
2778    fn compile_debug_script(
2779        file_name: &str,
2780        source: &[u8],
2781    ) -> Result<(Vec<u8>, crate::Ndb), Box<dyn std::error::Error>> {
2782        let mut source_map = SourceMap::new();
2783        let root_id = source_map.add_file(file_name.to_string(), source.to_vec());
2784        let script = parse_text(root_id, std::str::from_utf8(source)?, None)?;
2785        let artifacts = compile_script_with_source_map(
2786            &script,
2787            &source_map,
2788            root_id,
2789            None,
2790            CompileOptions::default(),
2791        )?;
2792        let ndb_bytes = artifacts
2793            .ndb
2794            .ok_or("compile_script_with_source_map did not emit NDB bytes")?;
2795        let mut reader = Cursor::new(ndb_bytes);
2796        let ndb = read_ndb(&mut reader)?;
2797        Ok((artifacts.ncs, ndb))
2798    }
2799
2800    #[test]
2801    fn runs_manual_integer_program() -> Result<(), Box<dyn std::error::Error>> {
2802        let instructions = vec![
2803            NcsInstruction {
2804                opcode:  NcsOpcode::Constant,
2805                auxcode: NcsAuxCode::TypeInteger,
2806                extra:   7_i32.to_be_bytes().to_vec(),
2807            },
2808            NcsInstruction {
2809                opcode:  NcsOpcode::Constant,
2810                auxcode: NcsAuxCode::TypeInteger,
2811                extra:   35_i32.to_be_bytes().to_vec(),
2812            },
2813            NcsInstruction {
2814                opcode:  NcsOpcode::Add,
2815                auxcode: NcsAuxCode::TypeTypeIntegerInteger,
2816                extra:   Vec::new(),
2817            },
2818            NcsInstruction {
2819                opcode:  NcsOpcode::Ret,
2820                auxcode: NcsAuxCode::None,
2821                extra:   Vec::new(),
2822            },
2823        ];
2824        let mut script = VmScript::from_instructions(instructions, "arith");
2825
2826        script.run(&Vm::new())?;
2827
2828        assert_eq!(script.stack(), &[VmValue::Int(42)]);
2829        Ok(())
2830    }
2831
2832    #[test]
2833    fn invokes_registered_action_command() -> Result<(), Box<dyn std::error::Error>> {
2834        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
2835        let script = parse_text(
2836            SourceId::new(0),
2837            "void main() { PrintInteger(42); }",
2838            Some(&langspec),
2839        )?;
2840        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
2841
2842        let called = Rc::new(RefCell::new(None));
2843        let mut vm = Vm::new();
2844        {
2845            let called = Rc::clone(&called);
2846            vm.define_command(0, move |script, _command, argc| {
2847                assert_eq!(argc, 1);
2848                *called.borrow_mut() = Some(script.pop_int()?);
2849                Ok(())
2850            });
2851        }
2852
2853        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "compiled-main")?;
2854        runtime.run(&vm)?;
2855
2856        assert_eq!(*called.borrow(), Some(42));
2857        Ok(())
2858    }
2859
2860    #[test]
2861    fn loads_and_runs_script_through_source_bundle_pipeline()
2862    -> Result<(), Box<dyn std::error::Error>> {
2863        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
2864        let mut resolver = InMemoryScriptResolver::new();
2865        resolver.insert_source("main", "void main() { PrintInteger(7); }");
2866        let bundle = load_source_bundle(&resolver, "main", crate::SourceLoadOptions::default())?;
2867        let artifacts =
2868            crate::compile_source_bundle(&bundle, Some(&langspec), CompileOptions::default())?;
2869
2870        let seen = Rc::new(RefCell::new(Vec::new()));
2871        let mut vm = Vm::new();
2872        {
2873            let seen = Rc::clone(&seen);
2874            vm.define_simple_command(0, move |script| {
2875                seen.borrow_mut().push(script.pop_int()?);
2876                Ok(())
2877            });
2878        }
2879
2880        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "bundle-main")?;
2881        runtime.run(&vm)?;
2882
2883        assert_eq!(&*seen.borrow(), &[7]);
2884        Ok(())
2885    }
2886
2887    #[test]
2888    fn executes_compiled_vector_arithmetic() -> Result<(), Box<dyn std::error::Error>> {
2889        let langspec = parse_langspec("nwscript", "void PrintVector(vector v);")?;
2890        let script = parse_text(
2891            SourceId::new(0),
2892            "void main() { PrintVector(([1.0, 2.0, 3.0] + [4.0, 5.0, 6.0]) * 2.0); }",
2893            Some(&langspec),
2894        )?;
2895        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
2896
2897        let seen = Rc::new(RefCell::new(Vec::new()));
2898        let mut vm = Vm::new();
2899        {
2900            let seen = Rc::clone(&seen);
2901            vm.define_simple_command(0, move |script| {
2902                seen.borrow_mut().push(script.pop_vector()?);
2903                Ok(())
2904            });
2905        }
2906
2907        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "vector-main")?;
2908        runtime.run(&vm)?;
2909
2910        assert_eq!(&*seen.borrow(), &[[10.0, 14.0, 18.0]]);
2911        Ok(())
2912    }
2913
2914    #[test]
2915    fn executes_compiled_mixed_numeric_arithmetic() -> Result<(), Box<dyn std::error::Error>> {
2916        let langspec = parse_langspec("nwscript", "void PrintFloat(float f);")?;
2917        let script = parse_text(
2918            SourceId::new(0),
2919            "void main() { PrintFloat(1 + 2.5); PrintFloat(5.5 - 2); PrintFloat(3 * 1.5); \
2920             PrintFloat(9.0 / 2); }",
2921            Some(&langspec),
2922        )?;
2923        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
2924
2925        let seen = Rc::new(RefCell::new(Vec::new()));
2926        let mut vm = Vm::new();
2927        {
2928            let seen = Rc::clone(&seen);
2929            vm.define_simple_command(0, move |script| {
2930                seen.borrow_mut().push(script.pop_float()?);
2931                Ok(())
2932            });
2933        }
2934
2935        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "mixed-numeric-main")?;
2936        runtime.run(&vm)?;
2937
2938        assert_eq!(&*seen.borrow(), &[3.5, 3.5, 4.5, 4.5]);
2939        Ok(())
2940    }
2941
2942    #[test]
2943    fn executes_compiled_mixed_numeric_comparisons() -> Result<(), Box<dyn std::error::Error>> {
2944        let instructions = vec![
2945            NcsInstruction {
2946                opcode:  NcsOpcode::Constant,
2947                auxcode: NcsAuxCode::TypeInteger,
2948                extra:   1_i32.to_be_bytes().to_vec(),
2949            },
2950            NcsInstruction {
2951                opcode:  NcsOpcode::Constant,
2952                auxcode: NcsAuxCode::TypeFloat,
2953                extra:   2.5_f32.to_bits().to_be_bytes().to_vec(),
2954            },
2955            NcsInstruction {
2956                opcode:  NcsOpcode::Lt,
2957                auxcode: NcsAuxCode::TypeTypeIntegerFloat,
2958                extra:   Vec::new(),
2959            },
2960            NcsInstruction {
2961                opcode:  NcsOpcode::Constant,
2962                auxcode: NcsAuxCode::TypeFloat,
2963                extra:   2.5_f32.to_bits().to_be_bytes().to_vec(),
2964            },
2965            NcsInstruction {
2966                opcode:  NcsOpcode::Constant,
2967                auxcode: NcsAuxCode::TypeInteger,
2968                extra:   1_i32.to_be_bytes().to_vec(),
2969            },
2970            NcsInstruction {
2971                opcode:  NcsOpcode::Gt,
2972                auxcode: NcsAuxCode::TypeTypeFloatInteger,
2973                extra:   Vec::new(),
2974            },
2975            NcsInstruction {
2976                opcode:  NcsOpcode::Constant,
2977                auxcode: NcsAuxCode::TypeInteger,
2978                extra:   3_i32.to_be_bytes().to_vec(),
2979            },
2980            NcsInstruction {
2981                opcode:  NcsOpcode::Constant,
2982                auxcode: NcsAuxCode::TypeFloat,
2983                extra:   3.0_f32.to_bits().to_be_bytes().to_vec(),
2984            },
2985            NcsInstruction {
2986                opcode:  NcsOpcode::Equal,
2987                auxcode: NcsAuxCode::TypeTypeIntegerFloat,
2988                extra:   Vec::new(),
2989            },
2990            NcsInstruction {
2991                opcode:  NcsOpcode::Constant,
2992                auxcode: NcsAuxCode::TypeFloat,
2993                extra:   4.5_f32.to_bits().to_be_bytes().to_vec(),
2994            },
2995            NcsInstruction {
2996                opcode:  NcsOpcode::Constant,
2997                auxcode: NcsAuxCode::TypeInteger,
2998                extra:   4_i32.to_be_bytes().to_vec(),
2999            },
3000            NcsInstruction {
3001                opcode:  NcsOpcode::NotEqual,
3002                auxcode: NcsAuxCode::TypeTypeFloatInteger,
3003                extra:   Vec::new(),
3004            },
3005            NcsInstruction {
3006                opcode:  NcsOpcode::Ret,
3007                auxcode: NcsAuxCode::None,
3008                extra:   Vec::new(),
3009            },
3010        ];
3011        let mut runtime = VmScript::from_instructions(instructions, "mixed-compare-manual");
3012        runtime.run(&Vm::new())?;
3013
3014        assert_eq!(
3015            runtime.stack(),
3016            &[
3017                VmValue::Int(1),
3018                VmValue::Int(1),
3019                VmValue::Int(1),
3020                VmValue::Int(1),
3021            ]
3022        );
3023        Ok(())
3024    }
3025
3026    #[test]
3027    fn grows_stack_with_positive_movsp() -> Result<(), Box<dyn std::error::Error>> {
3028        let instructions = vec![
3029            NcsInstruction {
3030                opcode:  NcsOpcode::ModifyStackPointer,
3031                auxcode: NcsAuxCode::None,
3032                extra:   4_i32.to_be_bytes().to_vec(),
3033            },
3034            NcsInstruction {
3035                opcode:  NcsOpcode::Ret,
3036                auxcode: NcsAuxCode::None,
3037                extra:   Vec::new(),
3038            },
3039        ];
3040        let mut script = VmScript::from_instructions(instructions, "movsp-grow");
3041
3042        script.run(&Vm::new())?;
3043
3044        assert_eq!(script.stack(), &[VmValue::Int(0)]);
3045        Ok(())
3046    }
3047
3048    #[test]
3049    fn steps_manual_program_instruction_by_instruction() -> Result<(), Box<dyn std::error::Error>> {
3050        let instructions = vec![
3051            NcsInstruction {
3052                opcode:  NcsOpcode::Constant,
3053                auxcode: NcsAuxCode::TypeInteger,
3054                extra:   7_i32.to_be_bytes().to_vec(),
3055            },
3056            NcsInstruction {
3057                opcode:  NcsOpcode::Constant,
3058                auxcode: NcsAuxCode::TypeInteger,
3059                extra:   35_i32.to_be_bytes().to_vec(),
3060            },
3061            NcsInstruction {
3062                opcode:  NcsOpcode::Add,
3063                auxcode: NcsAuxCode::TypeTypeIntegerInteger,
3064                extra:   Vec::new(),
3065            },
3066            NcsInstruction {
3067                opcode:  NcsOpcode::Ret,
3068                auxcode: NcsAuxCode::None,
3069                extra:   Vec::new(),
3070            },
3071        ];
3072        let mut script = VmScript::from_instructions(instructions, "step-arith");
3073        let vm = Vm::new();
3074
3075        assert_eq!(
3076            script
3077                .current_instruction()
3078                .map(|instruction| instruction.opcode),
3079            Some(NcsOpcode::Constant)
3080        );
3081        assert_eq!(vm.step(&mut script)?, VmStepOutcome::Running);
3082        assert_eq!(script.stack(), &[VmValue::Int(7)]);
3083
3084        assert_eq!(vm.step(&mut script)?, VmStepOutcome::Running);
3085        assert_eq!(script.stack(), &[VmValue::Int(7), VmValue::Int(35)]);
3086
3087        assert_eq!(vm.step(&mut script)?, VmStepOutcome::Running);
3088        assert_eq!(script.stack(), &[VmValue::Int(42)]);
3089
3090        assert_eq!(vm.step(&mut script)?, VmStepOutcome::Halted);
3091        assert_eq!(script.stack(), &[VmValue::Int(42)]);
3092        Ok(())
3093    }
3094
3095    #[test]
3096    fn steps_over_and_out_of_compiled_user_calls() -> Result<(), Box<dyn std::error::Error>> {
3097        let source = br#"int AddOne(int x) {
3098    return x + 1;
3099}
3100
3101int Twice(int x) {
3102    int first = AddOne(x);
3103    int second = AddOne(x);
3104    return first + second;
3105}
3106"#;
3107        let (ncs, ndb) = compile_debug_script("debug_calls.nss", source)?;
3108        let vm = Vm::new();
3109        let mut script = VmScript::from_bytes_with_ndb(&ncs, "debug-calls", &ndb)?;
3110        script.prepare_function_call(&ndb, "Twice", &[VmValue::Int(2)])?;
3111
3112        for _ in 0..32 {
3113            if script
3114                .current_instruction()
3115                .map(|instruction| instruction.opcode)
3116                == Some(NcsOpcode::Jsr)
3117            {
3118                break;
3119            }
3120            assert_eq!(vm.step(&mut script)?, VmStepOutcome::Running);
3121        }
3122
3123        let before = script.ip();
3124        assert_eq!(
3125            script.current_function().map(|function| function.name),
3126            Some("Twice".to_string())
3127        );
3128        assert_eq!(
3129            vm.step_over(&mut script, VmRunOptions::default())?,
3130            VmStepOutcome::Running
3131        );
3132        assert!(script.ip() > before);
3133        assert_eq!(
3134            script.current_function().map(|function| function.name),
3135            Some("Twice".to_string())
3136        );
3137
3138        assert_eq!(
3139            vm.run_until_function(&mut script, "AddOne", VmRunOptions::default())?,
3140            VmStepOutcome::Running
3141        );
3142        assert_eq!(
3143            script.current_function().map(|function| function.name),
3144            Some("AddOne".to_string())
3145        );
3146
3147        assert_eq!(
3148            vm.step_out(&mut script, VmRunOptions::default())?,
3149            VmStepOutcome::Running
3150        );
3151        assert_eq!(
3152            script.current_function().map(|function| function.name),
3153            Some("Twice".to_string())
3154        );
3155        Ok(())
3156    }
3157
3158    #[test]
3159    fn exposes_source_locations_and_runs_until_lines() -> Result<(), Box<dyn std::error::Error>> {
3160        let source = br#"int AddOne(int x) {
3161    return x + 1;
3162}
3163
3164int Twice(int x) {
3165    int first = AddOne(x);
3166    int second = AddOne(x);
3167    return first + second;
3168}
3169"#;
3170        let (ncs, ndb) = compile_debug_script("debug_lines.nss", source)?;
3171        let vm = Vm::new();
3172        let mut script = VmScript::from_bytes_with_ndb(&ncs, "debug-lines", &ndb)?;
3173        script.prepare_function_call(&ndb, "Twice", &[VmValue::Int(2)])?;
3174        let root_file_index = ndb
3175            .files
3176            .iter()
3177            .position(|file| file.name == "debug_lines.nss")
3178            .ok_or("missing root file entry")?;
3179        let mut root_lines = ndb
3180            .lines
3181            .iter()
3182            .filter(|line| line.file_num == root_file_index)
3183            .map(|line| line.line_num)
3184            .collect::<Vec<_>>();
3185        root_lines.sort_unstable();
3186        root_lines.dedup();
3187        let first_line = *root_lines.first().ok_or("missing root source lines")?;
3188        let second_line = *root_lines
3189            .iter()
3190            .find(|line| **line > first_line)
3191            .ok_or("missing second root source line")?;
3192
3193        assert_eq!(
3194            vm.run_until_line(
3195                &mut script,
3196                "debug_lines.nss",
3197                first_line,
3198                VmRunOptions::default()
3199            )?,
3200            VmStepOutcome::Running
3201        );
3202        let location = script
3203            .current_source_location()
3204            .ok_or("missing first source location")?;
3205        assert_eq!(location.file_name, "debug_lines.nss");
3206        assert!(location.is_root);
3207        assert_eq!(location.line_number, first_line);
3208
3209        assert_eq!(
3210            vm.run_until_line(
3211                &mut script,
3212                "debug_lines.nss",
3213                second_line,
3214                VmRunOptions::default()
3215            )?,
3216            VmStepOutcome::Running
3217        );
3218        let location = script
3219            .current_source_location()
3220            .ok_or("missing second source location")?;
3221        assert_eq!(location.line_number, second_line);
3222        assert_eq!(location.file_name, "debug_lines.nss");
3223        Ok(())
3224    }
3225
3226    #[test]
3227    fn executes_compiled_struct_equality() -> Result<(), Box<dyn std::error::Error>> {
3228        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
3229        let source = r#"
3230            struct Pair {
3231                int a;
3232                int b;
3233            };
3234
3235            void main() {
3236                struct Pair left;
3237                struct Pair right;
3238                left.a = 1;
3239                left.b = 2;
3240                right.a = 1;
3241                right.b = 2;
3242                PrintInteger(left == right);
3243            }
3244        "#;
3245        let script = parse_text(SourceId::new(0), source, Some(&langspec))?;
3246        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3247
3248        let seen = Rc::new(RefCell::new(Vec::new()));
3249        let mut vm = Vm::new();
3250        {
3251            let seen = Rc::clone(&seen);
3252            vm.define_simple_command(0, move |script| {
3253                seen.borrow_mut().push(script.pop_int()?);
3254                Ok(())
3255            });
3256        }
3257
3258        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "struct-main")?;
3259        runtime.run(&vm)?;
3260
3261        assert_eq!(&*seen.borrow(), &[1]);
3262        Ok(())
3263    }
3264
3265    #[test]
3266    fn executes_compiled_user_function_calls() -> Result<(), Box<dyn std::error::Error>> {
3267        let source = br#"
3268            int AddOne(int nValue) {
3269                return nValue + 1;
3270            }
3271
3272            void main() {
3273                PrintInteger(AddOne(41));
3274            }
3275        "#;
3276        let mut source_map = SourceMap::new();
3277        let root_id = source_map.add_file("user_call_main.nss".to_string(), source.to_vec());
3278        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
3279        let script = parse_text(root_id, std::str::from_utf8(source)?, Some(&langspec))?;
3280        let artifacts = compile_script_with_source_map(
3281            &script,
3282            &source_map,
3283            root_id,
3284            Some(&langspec),
3285            CompileOptions::default(),
3286        )?;
3287        let ndb = read_ndb(&mut std::io::Cursor::new(
3288            artifacts.ndb.clone().ok_or("missing ndb")?,
3289        ))?;
3290
3291        let seen = Rc::new(RefCell::new(Vec::new()));
3292        let mut vm = Vm::new();
3293        {
3294            let seen = Rc::clone(&seen);
3295            vm.define_simple_command(0, move |script| {
3296                seen.borrow_mut().push(script.pop_int()?);
3297                Ok(())
3298            });
3299        }
3300
3301        vm.run_bytes_with_ndb(&artifacts.ncs, "user-call-main", &ndb)?;
3302
3303        assert_eq!(&*seen.borrow(), &[42]);
3304        Ok(())
3305    }
3306
3307    #[test]
3308    fn captures_and_executes_saved_action_situations() -> Result<(), Box<dyn std::error::Error>> {
3309        let langspec = parse_langspec(
3310            "nwscript",
3311            "void DelayCommand(float seconds, action aAction);\nvoid PrintInteger(int n);",
3312        )?;
3313        let script = parse_text(
3314            SourceId::new(0),
3315            "void main() { int value = 42; DelayCommand(1.0, PrintInteger(value + 1)); }",
3316            Some(&langspec),
3317        )?;
3318        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3319
3320        let delayed = Rc::new(RefCell::new(None));
3321        let printed = Rc::new(RefCell::new(None));
3322        let mut vm = Vm::new();
3323        {
3324            let delayed = Rc::clone(&delayed);
3325            vm.define_command(0, move |script, _command, argc| {
3326                assert_eq!(argc, 2);
3327                let seconds = script.pop_float()?;
3328                assert_eq!(seconds, 1.0);
3329                *delayed.borrow_mut() = script.saved_situation().cloned();
3330                Ok(())
3331            });
3332        }
3333        {
3334            let printed = Rc::clone(&printed);
3335            vm.define_command(1, move |script, _command, argc| {
3336                assert_eq!(argc, 1);
3337                *printed.borrow_mut() = Some(script.pop_int()?);
3338                Ok(())
3339            });
3340        }
3341
3342        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "compiled-delay")?;
3343        runtime.run(&vm)?;
3344
3345        let saved = delayed
3346            .borrow()
3347            .clone()
3348            .ok_or("missing saved action situation")?;
3349        vm.run_situation(&saved)?;
3350
3351        assert_eq!(*printed.borrow(), Some(43));
3352        Ok(())
3353    }
3354
3355    #[test]
3356    fn roundtrips_engine_structures_through_action_handlers()
3357    -> Result<(), Box<dyn std::error::Error>> {
3358        let langspec = parse_langspec(
3359            "nwscript",
3360            "effect EffectDamage(int nAmount);\nvoid PrintEffect(effect eValue);",
3361        )?;
3362        let script = parse_text(
3363            SourceId::new(0),
3364            "void main() { PrintEffect(EffectDamage(42)); }",
3365            Some(&langspec),
3366        )?;
3367        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3368
3369        let seen = Rc::new(RefCell::new(Vec::new()));
3370        let mut vm = Vm::new();
3371        vm.define_command(0, move |script, _command, argc| {
3372            assert_eq!(argc, 1);
3373            let amount = script.pop_int()?;
3374            script.push_engine_structure(0, VmEngineStructureValue::Word(amount as u32));
3375            Ok(())
3376        });
3377        {
3378            let seen = Rc::clone(&seen);
3379            vm.define_command(1, move |script, _command, argc| {
3380                assert_eq!(argc, 1);
3381                let value = script.pop_engine_structure_index(0)?;
3382                let Some(value) = value.as_word() else {
3383                    return Err(super::VmError::TypeMismatch {
3384                        offset:   script.ip(),
3385                        message:  "expected word-backed effect value".to_string(),
3386                        expected: Some("engine structure"),
3387                        actual:   "engine structure",
3388                    });
3389                };
3390                seen.borrow_mut().push(value);
3391                Ok(())
3392            });
3393        }
3394
3395        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "effect-main")?;
3396        runtime.run(&vm)?;
3397
3398        assert_eq!(&*seen.borrow(), &[42]);
3399        Ok(())
3400    }
3401
3402    #[test]
3403    fn uses_host_defined_engine_structure_defaults_for_rsadd()
3404    -> Result<(), Box<dyn std::error::Error>> {
3405        let langspec = parse_langspec(
3406            "nwscript",
3407            "#define ENGINE_NUM_STRUCTURES 8\n#define ENGINE_STRUCTURE_7 json\nvoid \
3408             PrintJson(json jValue);",
3409        )?;
3410        let script = parse_text(
3411            SourceId::new(0),
3412            "void main() { json jValue; PrintJson(jValue); }",
3413            Some(&langspec),
3414        )?;
3415        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3416
3417        let seen = Rc::new(RefCell::new(Vec::new()));
3418        let mut vm = Vm::new();
3419        vm.define_engine_structure_default(7, VmEngineStructureValue::Text("{\"ok\":true}".into()));
3420        {
3421            let seen = Rc::clone(&seen);
3422            vm.define_command(0, move |script, _command, argc| {
3423                assert_eq!(argc, 1);
3424                let value = script.pop_engine_structure_index(7)?;
3425                let Some(value) = value.as_text() else {
3426                    return Err(super::VmError::TypeMismatch {
3427                        offset:   script.ip(),
3428                        message:  "expected text-backed json value".to_string(),
3429                        expected: Some("engine structure"),
3430                        actual:   "engine structure",
3431                    });
3432                };
3433                seen.borrow_mut().push(value.to_string());
3434                Ok(())
3435            });
3436        }
3437
3438        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "json-main")?;
3439        runtime.run(&vm)?;
3440
3441        assert_eq!(&*seen.borrow(), &["{\"ok\":true}".to_string()]);
3442        Ok(())
3443    }
3444
3445    #[test]
3446    fn uses_host_defined_engine_structure_comparison() -> Result<(), Box<dyn std::error::Error>> {
3447        let langspec = parse_langspec(
3448            "nwscript",
3449            "effect EffectDamage(int nAmount);\nvoid PrintInteger(int n);",
3450        )?;
3451        let script = parse_text(
3452            SourceId::new(0),
3453            "void main() { PrintInteger(EffectDamage(5) == EffectDamage(5)); }",
3454            Some(&langspec),
3455        )?;
3456        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3457
3458        let next_handle = Rc::new(RefCell::new(100_u32));
3459        let effect_values = Rc::new(RefCell::new(HashMap::<u32, i32>::new()));
3460        let seen = Rc::new(RefCell::new(Vec::new()));
3461        let mut vm = Vm::new();
3462        {
3463            let next_handle = Rc::clone(&next_handle);
3464            let effect_values = Rc::clone(&effect_values);
3465            vm.define_command(0, move |script, _command, argc| {
3466                assert_eq!(argc, 1);
3467                let amount = script.pop_int()?;
3468                let handle = *next_handle.borrow();
3469                *next_handle.borrow_mut() = handle + 1;
3470                effect_values.borrow_mut().insert(handle, amount);
3471                script.push_engine_structure(0, VmEngineStructureValue::Word(handle));
3472                Ok(())
3473            });
3474        }
3475        {
3476            let effect_values = Rc::clone(&effect_values);
3477            vm.define_engine_structure_comparer(0, move |_index, lhs, rhs| {
3478                let Some(lhs) = lhs.as_word() else {
3479                    return false;
3480                };
3481                let Some(rhs) = rhs.as_word() else {
3482                    return false;
3483                };
3484                effect_values.borrow().get(&lhs) == effect_values.borrow().get(&rhs)
3485            });
3486        }
3487        {
3488            let seen = Rc::clone(&seen);
3489            vm.define_command(1, move |script, _command, argc| {
3490                assert_eq!(argc, 1);
3491                seen.borrow_mut().push(script.pop_int()?);
3492                Ok(())
3493            });
3494        }
3495
3496        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "effect-compare-main")?;
3497        runtime.run(&vm)?;
3498
3499        assert_eq!(&*seen.borrow(), &[1]);
3500        Ok(())
3501    }
3502
3503    #[test]
3504    fn aborts_script_after_action_handler_requests_abort() -> Result<(), Box<dyn std::error::Error>>
3505    {
3506        let langspec = parse_langspec("nwscript", "void StopNow();\nvoid PrintInteger(int n);")?;
3507        let script = parse_text(
3508            SourceId::new(0),
3509            "void main() { StopNow(); PrintInteger(42); }",
3510            Some(&langspec),
3511        )?;
3512        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3513
3514        let seen = Rc::new(RefCell::new(Vec::new()));
3515        let mut vm = Vm::new();
3516        vm.define_command(0, move |script, _command, argc| {
3517            assert_eq!(argc, 0);
3518            script.abort();
3519            Ok(())
3520        });
3521        {
3522            let seen = Rc::clone(&seen);
3523            vm.define_command(1, move |script, _command, argc| {
3524                assert_eq!(argc, 1);
3525                seen.borrow_mut().push(script.pop_int()?);
3526                Ok(())
3527            });
3528        }
3529
3530        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "abort-main")?;
3531        runtime.run(&vm)?;
3532
3533        assert!(runtime.aborted());
3534        assert!(seen.borrow().is_empty());
3535        Ok(())
3536    }
3537
3538    #[test]
3539    fn emits_instruction_trace_events_during_execution() -> Result<(), Box<dyn std::error::Error>> {
3540        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
3541        let script = parse_text(
3542            SourceId::new(0),
3543            "void main() { PrintInteger(7); }",
3544            Some(&langspec),
3545        )?;
3546        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3547
3548        let seen = Rc::new(RefCell::new(Vec::new()));
3549        let traces = Rc::new(RefCell::new(Vec::new()));
3550        let mut vm = Vm::new();
3551        {
3552            let traces = Rc::clone(&traces);
3553            vm.define_trace_hook(move |_script, event| {
3554                traces.borrow_mut().push((
3555                    event.offset,
3556                    event.instruction.opcode,
3557                    event.instruction.auxcode,
3558                    event.sp,
3559                    event.bp,
3560                ));
3561            });
3562        }
3563        {
3564            let seen = Rc::clone(&seen);
3565            vm.define_command(0, move |script, _command, argc| {
3566                assert_eq!(argc, 1);
3567                seen.borrow_mut().push(script.pop_int()?);
3568                Ok(())
3569            });
3570        }
3571
3572        let mut runtime = VmScript::from_bytes(&artifacts.ncs, "trace-main")?;
3573        runtime.run(&vm)?;
3574
3575        assert_eq!(&*seen.borrow(), &[7]);
3576        let trace = traces.borrow();
3577        assert!(!trace.is_empty());
3578        assert_eq!(trace.first().map(|entry| entry.1), Some(NcsOpcode::Jsr));
3579        assert!(
3580            trace
3581                .iter()
3582                .any(|(_, opcode, auxcode, _, _)| *opcode == NcsOpcode::Constant
3583                    && *auxcode == NcsAuxCode::TypeInteger)
3584        );
3585        assert!(
3586            trace
3587                .iter()
3588                .any(|(_, opcode, _, _, _)| *opcode == NcsOpcode::ExecuteCommand)
3589        );
3590        Ok(())
3591    }
3592
3593    #[test]
3594    fn rejects_runs_that_exceed_instruction_budget() -> Result<(), Box<dyn std::error::Error>> {
3595        let langspec = parse_langspec("nwscript", "void PrintInteger(int n);")?;
3596        let script = parse_text(
3597            SourceId::new(0),
3598            "void main() { while (1) {} }",
3599            Some(&langspec),
3600        )?;
3601        let artifacts = compile_script(&script, Some(&langspec), CompileOptions::default())?;
3602
3603        let vm = Vm::new();
3604        let error = vm
3605            .run_bytes_with_options(
3606                &artifacts.ncs,
3607                "budget-main",
3608                VmRunOptions {
3609                    max_instructions: Some(16),
3610                },
3611            )
3612            .expect_err("infinite loop should exceed instruction budget");
3613
3614        assert!(matches!(
3615            error,
3616            super::VmError::InstructionLimitExceeded {
3617                limit: 16,
3618                ..
3619            }
3620        ));
3621        Ok(())
3622    }
3623
3624    #[test]
3625    fn runs_named_function_calls_from_ndb_without_globals() -> Result<(), Box<dyn std::error::Error>>
3626    {
3627        let source = br#"
3628            int AddOne(int nValue) {
3629                return nValue + 1;
3630            }
3631
3632            void main() {
3633                return;
3634            }
3635        "#;
3636        let mut source_map = SourceMap::new();
3637        let root_id = source_map.add_file("direct_call.nss".to_string(), source.to_vec());
3638        let langspec = parse_langspec("nwscript", "void Dummy();")?;
3639        let script = parse_text(root_id, std::str::from_utf8(source)?, Some(&langspec))?;
3640        let artifacts = compile_script_with_source_map(
3641            &script,
3642            &source_map,
3643            root_id,
3644            Some(&langspec),
3645            CompileOptions::default(),
3646        )?;
3647        let ndb = read_ndb(&mut std::io::Cursor::new(
3648            artifacts.ndb.clone().ok_or("missing ndb")?,
3649        ))?;
3650
3651        let vm = Vm::new();
3652        let runtime = vm.run_function_bytes(
3653            &artifacts.ncs,
3654            "direct-call",
3655            &ndb,
3656            "AddOne",
3657            &[VmValue::Int(41)],
3658        )?;
3659
3660        assert_eq!(
3661            runtime.function_return_value(&ndb, "AddOne")?,
3662            Some(VmValue::Int(42))
3663        );
3664        Ok(())
3665    }
3666
3667    #[test]
3668    fn rejects_direct_function_calls_for_scripts_with_globals()
3669    -> Result<(), Box<dyn std::error::Error>> {
3670        let source = br#"
3671            int GLOBAL = 1;
3672            int SECOND = GLOBAL + 2;
3673
3674            int ReadGlobal() {
3675                return GLOBAL + SECOND;
3676            }
3677
3678            void main() {
3679                return;
3680            }
3681        "#;
3682        let mut source_map = SourceMap::new();
3683        let root_id = source_map.add_file("globals_direct_call.nss".to_string(), source.to_vec());
3684        let langspec = parse_langspec("nwscript", "void Dummy();")?;
3685        let script = parse_text(root_id, std::str::from_utf8(source)?, Some(&langspec))?;
3686        let artifacts = compile_script_with_source_map(
3687            &script,
3688            &source_map,
3689            root_id,
3690            Some(&langspec),
3691            CompileOptions::default(),
3692        )?;
3693        let ndb = read_ndb(&mut std::io::Cursor::new(
3694            artifacts.ndb.clone().ok_or("missing ndb")?,
3695        ))?;
3696
3697        let vm = Vm::new();
3698        let runtime = vm.run_function_bytes(
3699            &artifacts.ncs,
3700            "globals-direct-call",
3701            &ndb,
3702            "ReadGlobal",
3703            &[],
3704        )?;
3705
3706        assert_eq!(
3707            runtime.function_return_value(&ndb, "ReadGlobal")?,
3708            Some(VmValue::Int(4))
3709        );
3710        Ok(())
3711    }
3712
3713    #[test]
3714    fn runs_direct_function_calls_for_globals_with_non_void_entry_loader()
3715    -> Result<(), Box<dyn std::error::Error>> {
3716        let source = br#"
3717            int GLOBAL = 1;
3718
3719            int ReadGlobal() {
3720                return GLOBAL;
3721            }
3722
3723            int StartingConditional() {
3724                return 0;
3725            }
3726        "#;
3727        let mut source_map = SourceMap::new();
3728        let root_id = source_map.add_file(
3729            "globals_loader_conditional.nss".to_string(),
3730            source.to_vec(),
3731        );
3732        let langspec = parse_langspec("nwscript", "void Dummy();")?;
3733        let script = parse_text(root_id, std::str::from_utf8(source)?, Some(&langspec))?;
3734        let artifacts = compile_script_with_source_map(
3735            &script,
3736            &source_map,
3737            root_id,
3738            Some(&langspec),
3739            CompileOptions::default(),
3740        )?;
3741        let ndb = read_ndb(&mut std::io::Cursor::new(
3742            artifacts.ndb.clone().ok_or("missing ndb")?,
3743        ))?;
3744        let vm = Vm::new();
3745        let runtime = vm.run_function_bytes(
3746            &artifacts.ncs,
3747            "globals-conditional",
3748            &ndb,
3749            "ReadGlobal",
3750            &[],
3751        )?;
3752        assert_eq!(
3753            runtime.function_return_value(&ndb, "ReadGlobal")?,
3754            Some(VmValue::Int(1))
3755        );
3756        Ok(())
3757    }
3758}