Skip to main content

endbasic_core/
image.rs

1// EndBASIC
2// Copyright 2026 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Compiled program representation.
18
19use crate::ast::ExprType;
20use crate::bytecode::{self, Opcode, opcode_of};
21use crate::compiler::SymbolKey;
22use crate::mem::ConstantDatum;
23use crate::reader::LineCol;
24use std::collections::HashMap;
25
26/// Formats an instruction for debugging.
27///
28/// Panics if the instruction is malformed.
29pub(crate) fn format_instr(instr: u32) -> String {
30    match opcode_of(instr) {
31        Opcode::AddDouble => bytecode::format_add_double(instr),
32        Opcode::AddInteger => bytecode::format_add_integer(instr),
33        Opcode::Alloc => bytecode::format_alloc(instr),
34        Opcode::AllocArray => bytecode::format_alloc_array(instr),
35        Opcode::BitwiseAnd => bytecode::format_bitwise_and(instr),
36        Opcode::BitwiseNot => bytecode::format_bitwise_not(instr),
37        Opcode::BitwiseOr => bytecode::format_bitwise_or(instr),
38        Opcode::BitwiseXor => bytecode::format_bitwise_xor(instr),
39        Opcode::Call => bytecode::format_call(instr),
40        Opcode::Concat => bytecode::format_concat(instr),
41        Opcode::DivideDouble => bytecode::format_divide_double(instr),
42        Opcode::DivideInteger => bytecode::format_divide_integer(instr),
43        Opcode::DoubleToInteger => bytecode::format_double_to_integer(instr),
44        Opcode::EqualBoolean => bytecode::format_equal_boolean(instr),
45        Opcode::EqualDouble => bytecode::format_equal_double(instr),
46        Opcode::EqualInteger => bytecode::format_equal_integer(instr),
47        Opcode::EqualText => bytecode::format_equal_text(instr),
48        Opcode::Eof => bytecode::format_eof(instr),
49        Opcode::End => bytecode::format_end(instr),
50        Opcode::Gosub => bytecode::format_gosub(instr),
51        Opcode::GreaterDouble => bytecode::format_greater_double(instr),
52        Opcode::GreaterEqualDouble => bytecode::format_greater_equal_double(instr),
53        Opcode::GreaterEqualInteger => bytecode::format_greater_equal_integer(instr),
54        Opcode::GreaterEqualText => bytecode::format_greater_equal_text(instr),
55        Opcode::GreaterInteger => bytecode::format_greater_integer(instr),
56        Opcode::GreaterText => bytecode::format_greater_text(instr),
57        Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr),
58        Opcode::Jump => bytecode::format_jump(instr),
59        Opcode::JumpIfFalse => bytecode::format_jump_if_false(instr),
60        Opcode::LessDouble => bytecode::format_less_double(instr),
61        Opcode::LessEqualDouble => bytecode::format_less_equal_double(instr),
62        Opcode::LessEqualInteger => bytecode::format_less_equal_integer(instr),
63        Opcode::LessEqualText => bytecode::format_less_equal_text(instr),
64        Opcode::LessInteger => bytecode::format_less_integer(instr),
65        Opcode::LessText => bytecode::format_less_text(instr),
66        Opcode::LoadArray => bytecode::format_load_array(instr),
67        Opcode::LoadConstant => bytecode::format_load_constant(instr),
68        Opcode::LoadInteger => bytecode::format_load_integer(instr),
69        Opcode::LoadRegisterPointer => bytecode::format_load_register_ptr(instr),
70        Opcode::ModuloDouble => bytecode::format_modulo_double(instr),
71        Opcode::ModuloInteger => bytecode::format_modulo_integer(instr),
72        Opcode::Move => bytecode::format_move(instr),
73        Opcode::MultiplyDouble => bytecode::format_multiply_double(instr),
74        Opcode::MultiplyInteger => bytecode::format_multiply_integer(instr),
75        Opcode::NegateDouble => bytecode::format_negate_double(instr),
76        Opcode::NegateInteger => bytecode::format_negate_integer(instr),
77        Opcode::NotEqualBoolean => bytecode::format_not_equal_boolean(instr),
78        Opcode::NotEqualDouble => bytecode::format_not_equal_double(instr),
79        Opcode::NotEqualInteger => bytecode::format_not_equal_integer(instr),
80        Opcode::NotEqualText => bytecode::format_not_equal_text(instr),
81        Opcode::Nop => bytecode::format_nop(instr),
82        Opcode::PowerDouble => bytecode::format_power_double(instr),
83        Opcode::PowerInteger => bytecode::format_power_integer(instr),
84        Opcode::Return => bytecode::format_return(instr),
85        Opcode::SetErrorHandler => bytecode::format_set_error_handler(instr),
86        Opcode::ShiftLeft => bytecode::format_shift_left(instr),
87        Opcode::ShiftRight => bytecode::format_shift_right(instr),
88        Opcode::StoreArray => bytecode::format_store_array(instr),
89        Opcode::SubtractDouble => bytecode::format_subtract_double(instr),
90        Opcode::SubtractInteger => bytecode::format_subtract_integer(instr),
91        Opcode::Upcall => bytecode::format_upcall(instr),
92        Opcode::UpcallAsync => bytecode::format_upcall_async(instr),
93    }
94}
95
96/// Information about a global variable tracked for post-execution querying.
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub(crate) struct GlobalVarInfo {
99    /// Global register index (0 to `Register::MAX_GLOBAL - 1`).
100    pub(crate) reg: u8,
101
102    /// Element type (for arrays, the element type; for scalars, the scalar type).
103    pub(crate) subtype: ExprType,
104
105    /// Number of dimensions: 0 for scalars, >=1 for arrays.
106    pub(crate) ndims: usize,
107}
108
109/// Per-instruction metadata stored in `DebugInfo`.
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub(crate) struct InstrMetadata {
112    /// Source location that generated this instruction.
113    pub(crate) linecol: LineCol,
114
115    /// True if this instruction is the start of a statement.
116    pub(crate) is_stmt_start: bool,
117
118    /// Source locations of the call arguments, if this is a UPCALL instruction.
119    ///
120    /// Each entry corresponds to one register slot in the argument area, in the same order
121    /// that `compile_args` allocates them.  Empty for all other instruction types.
122    pub(crate) arg_linecols: Vec<LineCol>,
123}
124
125/// Debugging information for a compiled program.
126#[derive(Default)]
127pub struct DebugInfo {
128    /// Per-instruction metadata, one entry per instruction in the image's code.
129    pub(crate) instrs: Vec<InstrMetadata>,
130
131    /// Maps instruction addresses to the names of user-defined callables that start or end
132    /// at those addresses.  If the boolean is true, the position is a callable start.
133    pub(crate) callables: HashMap<usize, (SymbolKey, bool)>,
134
135    /// Maps global variable names to their register assignments and type information.
136    ///
137    /// This includes both host-pre-defined globals (from `compile_with_globals`) and
138    /// globals declared via `DIM SHARED` in the user's program.
139    pub(crate) global_vars: HashMap<SymbolKey, GlobalVarInfo>,
140
141    /// Maps program-scope variable names to their register assignments and type information.
142    ///
143    /// Program-scope variables are the outer-most non-global variables in a script.  Their
144    /// registers are local-register indices interpreted relative to the outer frame.
145    pub(crate) program_vars: HashMap<SymbolKey, GlobalVarInfo>,
146}
147
148/// Incremental update to append into an existing `Image`.
149pub(crate) struct ImageDelta {
150    /// Suffix of bytecode instructions to append after dropping the current EOF terminator.
151    pub(crate) code: Vec<u32>,
152
153    /// Additional upcall names referenced by the updated program.
154    pub(crate) upcalls: Vec<SymbolKey>,
155
156    /// Additional constants referenced by the updated program.
157    pub(crate) constants: Vec<ConstantDatum>,
158
159    /// Additional `DATA` values captured by the updated program.
160    pub(crate) data: Vec<Option<ConstantDatum>>,
161
162    /// Per-instruction metadata matching `code`.
163    pub(crate) instrs: Vec<InstrMetadata>,
164
165    /// Full user-callable metadata for the updated program.
166    pub(crate) callables: HashMap<usize, (SymbolKey, bool)>,
167
168    /// Full global variable metadata for the updated program.
169    pub(crate) global_vars: HashMap<SymbolKey, GlobalVarInfo>,
170
171    /// Full program-scope variable metadata for the updated program.
172    pub(crate) program_vars: HashMap<SymbolKey, GlobalVarInfo>,
173}
174
175/// Representation of a compiled EndBASIC program.
176///
177/// Images always have at least one instruction so that the VM can make this assumption.
178pub struct Image {
179    /// The bytecode instructions of the compiled program.
180    pub(crate) code: Vec<u32>,
181
182    /// Names of external callables referenced by the program, indexed by upcall ID.
183    pub(crate) upcalls: Vec<SymbolKey>,
184
185    /// Pool of constant values used by the program.
186    pub(crate) constants: Vec<ConstantDatum>,
187
188    /// Values captured from all `DATA` statements in source order.
189    pub(crate) data: Vec<Option<ConstantDatum>>,
190
191    /// Debugging information for error reporting and disassembly.
192    pub(crate) debug_info: DebugInfo,
193
194    /// Marker to prevent external construction; ensures `code` is never empty.
195    _internal: (),
196}
197
198impl Default for Image {
199    fn default() -> Self {
200        Self::new(
201            vec![
202                // The minimum valid program requires an explicit terminator so that the VM knows
203                // to exit.
204                bytecode::make_eof(),
205            ],
206            vec![],
207            vec![],
208            vec![],
209            DebugInfo {
210                instrs: vec![InstrMetadata {
211                    linecol: LineCol { line: 0, col: 0 },
212                    is_stmt_start: true,
213                    arg_linecols: vec![],
214                }],
215                callables: HashMap::default(),
216                global_vars: HashMap::default(),
217                program_vars: HashMap::default(),
218            },
219        )
220    }
221}
222
223impl Image {
224    pub(crate) fn new(
225        code: Vec<u32>,
226        upcalls: Vec<SymbolKey>,
227        constants: Vec<ConstantDatum>,
228        data: Vec<Option<ConstantDatum>>,
229        debug_info: DebugInfo,
230    ) -> Self {
231        debug_assert!(!code.is_empty(), "Compiler must ensure the image is not empty");
232        debug_assert_eq!(code.len(), debug_info.instrs.len());
233        Self { code, upcalls, constants, data, debug_info, _internal: () }
234    }
235
236    /// Appends `delta` to the image, replacing the current trailing EOF terminator.
237    pub(crate) fn append(&mut self, delta: ImageDelta) {
238        debug_assert_eq!(self.code.last().copied(), Some(bytecode::make_eof()));
239        debug_assert_eq!(self.debug_info.instrs.len(), self.code.len());
240        debug_assert_eq!(delta.code.len(), delta.instrs.len());
241        debug_assert_eq!(delta.code.last().copied(), Some(bytecode::make_eof()));
242
243        self.code.pop();
244        self.debug_info.instrs.pop();
245
246        self.code.extend(delta.code);
247        self.upcalls.extend(delta.upcalls);
248        self.constants.extend(delta.constants);
249        self.data.extend(delta.data);
250        self.debug_info.instrs.extend(delta.instrs);
251        self.debug_info.callables = delta.callables;
252        self.debug_info.global_vars = delta.global_vars;
253        self.debug_info.program_vars = delta.program_vars;
254    }
255
256    /// Disassembles the image into a textual representation for debugging.
257    pub fn disasm(&self) -> Vec<String> {
258        let mut lines = Vec::with_capacity(self.code.len());
259
260        for ((i, instr), meta) in
261            self.code.iter().copied().enumerate().zip(self.debug_info.instrs.iter())
262        {
263            let pos = meta.linecol;
264            if let Some((key, is_start)) = self.debug_info.callables.get(&i) {
265                if *is_start {
266                    lines.push("".to_owned());
267                    lines.push(format!(";; {} (BEGIN)", key));
268                } else {
269                    lines.push(format!(";; {} (END)", key));
270                    lines.push("".to_owned());
271                }
272            }
273
274            let mut line = format!("{:04}:   {}", i, format_instr(instr));
275
276            while line.len() < 40 {
277                line.push(' ');
278            }
279            line.push_str(&format!("; {}", pos));
280
281            match opcode_of(instr) {
282                Opcode::Call => {
283                    let (_reg, target) = bytecode::parse_call(instr);
284                    let target = usize::from(target);
285                    let (name, _is_start) = self
286                        .debug_info
287                        .callables
288                        .get(&target)
289                        .expect("All CALL targets must be defined");
290                    line.push_str(&format!(", {}", name))
291                }
292
293                Opcode::Upcall => {
294                    let (index, _first_reg) = bytecode::parse_upcall(instr);
295                    let name = &self.upcalls[usize::from(index)];
296                    line.push_str(&format!(", {}", name))
297                }
298
299                Opcode::LoadConstant => {
300                    let (_reg, index) = bytecode::parse_load_constant(instr);
301                    let constant = &self.constants[usize::from(index)];
302                    line.push_str(&format!(", {}", constant.as_disassembly()))
303                }
304
305                Opcode::UpcallAsync => {
306                    let (index, _first_reg) = bytecode::parse_upcall_async(instr);
307                    let name = &self.upcalls[usize::from(index)];
308                    line.push_str(&format!(", {}", name))
309                }
310
311                _ => (),
312            }
313
314            lines.push(line);
315        }
316
317        lines
318    }
319}