Skip to main content

endbasic_core/compiler/
top.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//! Entry point to the compilation, handling top-level definitions.
18
19use crate::ast::{
20    ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, Expr,
21    ExprType, ForSpan, IfSpan, OnErrorSpan, SelectSpan, Statement, VarRef, WhileSpan,
22};
23use crate::bytecode::{self, ErrorHandlerMode, PackedArrayType, Register};
24use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax};
25use crate::compiler::args::{compile_args, define_new_args};
26use crate::compiler::codegen::{Codegen, Fixup};
27use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer_exprs};
28use crate::compiler::syms::{
29    self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype, TempSymtable,
30};
31use crate::compiler::{Error, Result};
32use crate::image::{GlobalVarInfo, Image, ImageDelta};
33use crate::mem::ConstantDatum;
34use crate::reader::LineCol;
35use crate::{Callable, CallableMetadataBuilder, parser};
36use std::borrow::Cow;
37use std::cmp::max;
38use std::collections::{HashMap, HashSet};
39use std::io;
40use std::iter::Iterator;
41use std::rc::Rc;
42
43use super::syms::LocalSymtableSnapshot;
44
45/// Kind of a user-defined callable.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47enum CallableKind {
48    /// A function definition.
49    Function,
50
51    /// A subroutine definition.
52    Sub,
53}
54
55/// Bag of state required by various top-level compilation functions.
56///
57/// This type exists to minimize the number of complex arguments passed across functions.
58/// If possible, avoid passing it and instead pass the minimum set of required fields.
59#[derive(Clone, Default)]
60pub(super) struct Context {
61    /// The code generator accumulating bytecode instructions.
62    codegen: Codegen,
63
64    /// Collection of `DATA` values captured while compiling all statements.
65    data: Vec<Option<ConstantDatum>>,
66
67    /// Stack of pending `EXIT DO` jumps for each nested `DO` loop.
68    do_exit_stack: Vec<Vec<(usize, LineCol)>>,
69
70    /// Stack of pending `EXIT FOR` jumps for each nested `FOR` loop.
71    for_exit_stack: Vec<Vec<(usize, LineCol)>>,
72
73    /// Kind of the callable currently being compiled, if any.
74    current_callable: Option<CallableKind>,
75
76    /// Callables defined (not just declared) so far.
77    defined_callables: HashSet<SymbolKey>,
78
79    /// List of pending `EXIT FUNCTION` or `EXIT SUB` jumps in the current callable.
80    callable_exit_jumps: Vec<(usize, LineCol)>,
81}
82
83/// Converts parser-validated `DATA` expressions into image data constants.
84fn data_expr_to_constant(expr: Expr) -> ConstantDatum {
85    match expr {
86        Expr::Boolean(span) => ConstantDatum::Boolean(span.value),
87        Expr::Double(span) => ConstantDatum::Double(span.value),
88        Expr::Integer(span) => ConstantDatum::Integer(span.value),
89        Expr::Text(span) => ConstantDatum::Text(span.value),
90        _ => unreachable!("Parser guarantees DATA only contains literal values"),
91    }
92}
93
94/// Returns a statically-known `END` exit code and its source position, if any.
95fn static_end_code(expr: &Expr) -> Option<(i32, LineCol)> {
96    match expr {
97        Expr::Integer(span) => Some((span.value, span.pos)),
98        Expr::Negate(span) => {
99            let Expr::Integer(inner) = &span.expr else {
100                return None;
101            };
102            inner.value.checked_neg().map(|value| (value, span.pos))
103        }
104        _ => None,
105    }
106}
107
108/// Returns true if `expr` references `target_key`.
109fn expr_references_symbol(expr: &Expr, target_key: &SymbolKey) -> bool {
110    let mut stack = vec![expr];
111    while let Some(expr) = stack.pop() {
112        match expr {
113            Expr::Boolean(..) | Expr::Double(..) | Expr::Integer(..) | Expr::Text(..) => {}
114
115            Expr::Symbol(span) => {
116                if SymbolKey::from(&span.vref.name) == *target_key {
117                    return true;
118                }
119            }
120
121            Expr::Negate(span) | Expr::Not(span) => {
122                stack.push(&span.expr);
123            }
124
125            Expr::Call(span) => {
126                for arg in &span.args {
127                    if let Some(arg_expr) = &arg.expr {
128                        stack.push(arg_expr);
129                    }
130                }
131            }
132
133            Expr::Add(span)
134            | Expr::And(span)
135            | Expr::Divide(span)
136            | Expr::Equal(span)
137            | Expr::Greater(span)
138            | Expr::GreaterEqual(span)
139            | Expr::Less(span)
140            | Expr::LessEqual(span)
141            | Expr::Modulo(span)
142            | Expr::Multiply(span)
143            | Expr::NotEqual(span)
144            | Expr::Or(span)
145            | Expr::Power(span)
146            | Expr::ShiftLeft(span)
147            | Expr::ShiftRight(span)
148            | Expr::Subtract(span)
149            | Expr::Xor(span) => {
150                stack.push(&span.lhs);
151                stack.push(&span.rhs);
152            }
153        }
154    }
155    false
156}
157
158/// Compiles an assignment statement `span` into the `codegen` block.
159fn compile_assignment(
160    codegen: &mut Codegen,
161    symtable: &mut LocalSymtable<'_>,
162    span: AssignmentSpan,
163) -> Result<()> {
164    let AssignmentSpan { vref, vref_pos, expr } = span;
165    let key = SymbolKey::from(&vref.name);
166
167    let (reg, etype, was_defined) = match symtable.get_local_or_global(&vref) {
168        Ok((_, SymbolPrototype::Array(_))) => {
169            return Err(Error::ArrayUsedAsScalar(vref_pos, vref));
170        }
171
172        Ok((reg, SymbolPrototype::Scalar(etype))) => (reg, Some(etype), true),
173
174        Err(syms::Error::UndefinedSymbol(..)) => {
175            if symtable.get_callable(&key).is_some() {
176                return Err(Error::from_syms(syms::Error::AlreadyDefined(vref.clone()), vref_pos));
177            }
178            let reg = symtable
179                .put_local(
180                    key.clone(),
181                    SymbolPrototype::Scalar(vref.ref_type.unwrap_or(ExprType::Integer)),
182                )
183                .map_err(|e| Error::from_syms(e, vref_pos))?;
184            match vref.ref_type {
185                Some(etype) => (reg, Some(etype), false),
186                None => (reg, None, false),
187            }
188        }
189
190        Err(e) => return Err(Error::from_syms(e, vref_pos)),
191    };
192
193    let has_self_reference = was_defined && expr_references_symbol(&expr, &key);
194
195    if let Some(etype) = etype {
196        // The destination variable already exists.  Try to compile the expression into its target
197        // type and fail otherwise with a better error message.
198        let result = if has_self_reference {
199            let mut frozen = symtable.frozen();
200            let mut scope = frozen.temp_scope();
201            let temp = scope.alloc().map_err(|e| Error::from_syms(e, vref_pos))?;
202            let r = compile_expr_as_type(codegen, &mut frozen, temp, expr, etype);
203            if r.is_ok() {
204                codegen.emit(bytecode::make_move(reg, temp), vref_pos);
205            }
206            r
207        } else {
208            compile_expr_as_type(codegen, &mut symtable.frozen(), reg, expr, etype)
209        };
210
211        match result {
212            Err(Error::TypeMismatch(pos, actual, expected)) => {
213                return Err(Error::IncompatibleTypesInAssignment(pos, actual, expected));
214            }
215            r => return r,
216        }
217    }
218
219    // The destination variable doesn't exist yet but `symtable.put_local` already inserted it
220    // with the default type we gave above as part of assigning it a register.  Use the
221    // expression's type to fix up the type in the symbols table.
222    let etype = compile_expr(codegen, &mut symtable.frozen(), reg, expr)?;
223    symtable.fixup_local_type(&vref, etype).map_err(|e| Error::from_syms(e, vref_pos))
224}
225
226/// Returns the textual name of `relop` for diagnostics.
227fn case_relop_name(relop: &CaseRelOp) -> &'static str {
228    match relop {
229        CaseRelOp::Equal => "=",
230        CaseRelOp::NotEqual => "<>",
231        CaseRelOp::Less => "<",
232        CaseRelOp::LessEqual => "<=",
233        CaseRelOp::Greater => ">",
234        CaseRelOp::GreaterEqual => ">=",
235    }
236}
237
238/// Returns the bytecode opcode constructor for `relop` and operands of type `etype`.
239fn case_relop_instr(
240    relop: &CaseRelOp,
241    etype: ExprType,
242) -> Option<fn(Register, Register, Register) -> u32> {
243    match etype {
244        ExprType::Boolean => match relop {
245            CaseRelOp::Equal => Some(bytecode::make_equal_boolean),
246            CaseRelOp::NotEqual => Some(bytecode::make_not_equal_boolean),
247            CaseRelOp::Less
248            | CaseRelOp::LessEqual
249            | CaseRelOp::Greater
250            | CaseRelOp::GreaterEqual => None,
251        },
252
253        ExprType::Double => match relop {
254            CaseRelOp::Equal => Some(bytecode::make_equal_double),
255            CaseRelOp::NotEqual => Some(bytecode::make_not_equal_double),
256            CaseRelOp::Less => Some(bytecode::make_less_double),
257            CaseRelOp::LessEqual => Some(bytecode::make_less_equal_double),
258            CaseRelOp::Greater => Some(bytecode::make_greater_double),
259            CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_double),
260        },
261
262        ExprType::Integer => match relop {
263            CaseRelOp::Equal => Some(bytecode::make_equal_integer),
264            CaseRelOp::NotEqual => Some(bytecode::make_not_equal_integer),
265            CaseRelOp::Less => Some(bytecode::make_less_integer),
266            CaseRelOp::LessEqual => Some(bytecode::make_less_equal_integer),
267            CaseRelOp::Greater => Some(bytecode::make_greater_integer),
268            CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_integer),
269        },
270
271        ExprType::Text => match relop {
272            CaseRelOp::Equal => Some(bytecode::make_equal_text),
273            CaseRelOp::NotEqual => Some(bytecode::make_not_equal_text),
274            CaseRelOp::Less => Some(bytecode::make_less_text),
275            CaseRelOp::LessEqual => Some(bytecode::make_less_equal_text),
276            CaseRelOp::Greater => Some(bytecode::make_greater_text),
277            CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_text),
278        },
279    }
280}
281
282/// Compiles a comparison between `test` and `rhs`, leaving the boolean result in `dest`.
283fn compile_case_relop(
284    ctx: &mut Context,
285    pos: LineCol,
286    test: (Register, ExprType),
287    rhs: (Register, ExprType),
288    relop: CaseRelOp,
289    dest: Register,
290) -> Result<()> {
291    let (test_reg, test_type) = test;
292    let (rhs_reg, rhs_type) = rhs;
293    let op_name = case_relop_name(&relop);
294    let opcode = match (test_type, rhs_type) {
295        (ExprType::Double, ExprType::Integer) => {
296            ctx.codegen.emit(bytecode::make_integer_to_double(rhs_reg), pos);
297            case_relop_instr(&relop, ExprType::Double)
298        }
299
300        (ExprType::Integer, ExprType::Double) => {
301            ctx.codegen.emit(bytecode::make_integer_to_double(test_reg), pos);
302            case_relop_instr(&relop, ExprType::Double)
303        }
304
305        (lhs, rhs) if lhs == rhs => case_relop_instr(&relop, lhs),
306        _ => None,
307    };
308
309    match opcode {
310        Some(opcode) => {
311            ctx.codegen.emit(opcode(dest, test_reg, rhs_reg), pos);
312            Ok(())
313        }
314        None => Err(Error::BinaryOpType(pos, op_name, test_type, rhs_type)),
315    }
316}
317
318/// Compiles one `CASE` guard and returns the register and source position of its boolean result.
319fn compile_case_guard(
320    ctx: &mut Context,
321    symtable: &mut TempSymtable<'_, '_>,
322    test_reg: Register,
323    test_type: ExprType,
324    guard: CaseGuardSpan,
325) -> Result<(Register, LineCol)> {
326    match guard {
327        CaseGuardSpan::Is(relop, expr) => {
328            let pos = expr.start_pos();
329
330            let mut scope = symtable.temp_scope();
331            let lhs_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
332            let rhs_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
333            let cond_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
334
335            ctx.codegen.emit(bytecode::make_move(lhs_reg, test_reg), pos);
336            let rhs_type = compile_expr(&mut ctx.codegen, symtable, rhs_reg, expr)?;
337            compile_case_relop(
338                ctx,
339                pos,
340                (lhs_reg, test_type),
341                (rhs_reg, rhs_type),
342                relop,
343                cond_reg,
344            )?;
345
346            Ok((cond_reg, pos))
347        }
348
349        CaseGuardSpan::To(from_expr, to_expr) => {
350            let pos = from_expr.start_pos();
351
352            let mut scope = symtable.temp_scope();
353            let lhs_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
354            let rhs_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
355            let cond_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
356
357            let lhs_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
358            let rhs_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
359            let cond_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
360
361            let cond_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?;
362
363            ctx.codegen.emit(bytecode::make_move(lhs_from_reg, test_reg), pos);
364            let rhs_from_type = compile_expr(&mut ctx.codegen, symtable, rhs_from_reg, from_expr)?;
365            compile_case_relop(
366                ctx,
367                pos,
368                (lhs_from_reg, test_type),
369                (rhs_from_reg, rhs_from_type),
370                CaseRelOp::GreaterEqual,
371                cond_from_reg,
372            )?;
373
374            ctx.codegen.emit(bytecode::make_move(lhs_to_reg, test_reg), pos);
375            let rhs_to_type = compile_expr(&mut ctx.codegen, symtable, rhs_to_reg, to_expr)?;
376            compile_case_relop(
377                ctx,
378                pos,
379                (lhs_to_reg, test_type),
380                (rhs_to_reg, rhs_to_type),
381                CaseRelOp::LessEqual,
382                cond_to_reg,
383            )?;
384
385            ctx.codegen.emit(bytecode::make_bitwise_and(cond_reg, cond_from_reg, cond_to_reg), pos);
386            Ok((cond_reg, pos))
387        }
388    }
389}
390
391/// Compiles a `SELECT` statement and emits bytecode into `ctx`.
392fn compile_select(
393    ctx: &mut Context,
394    symtable: &mut LocalSymtable<'_>,
395    span: SelectSpan,
396) -> Result<()> {
397    let end_pos = span.end_pos;
398    let ncases = span.cases.len();
399    let select_cases = span.cases;
400    let select_expr = span.expr;
401    let select_expr_pos = select_expr.start_pos();
402
403    /// Captures the data needed to materialize a `CASE` body after dispatch generation.
404    struct PendingCase {
405        body: Vec<Statement>,
406        body_jump_pcs: Vec<(usize, LineCol)>,
407        has_next_case: bool,
408    }
409
410    let mut pending_cases = Vec::with_capacity(ncases);
411    let mut pending_next_case_jump: Option<(usize, LineCol)> = None;
412
413    symtable.with_reserved_temp(
414        |e| Error::from_syms(e, select_expr_pos),
415        |test_reg, frozen| {
416            let test_type = compile_expr(&mut ctx.codegen, frozen, test_reg, select_expr)?;
417
418            for (i, case) in select_cases.into_iter().enumerate() {
419                let has_next_case = i < ncases - 1;
420                let mut body_jump_pcs = vec![];
421                let case_dispatch_addr = ctx.codegen.next_pc();
422                if let Some((jump_pc, pos)) = pending_next_case_jump.take() {
423                    let target = u16::try_from(case_dispatch_addr)
424                        .map_err(|_| Error::TargetTooFar(pos, case_dispatch_addr))?;
425                    ctx.codegen.patch(jump_pc, bytecode::make_jump(target));
426                }
427
428                if case.guards.is_empty() {
429                    let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos);
430                    body_jump_pcs.push((jump_body_pc, end_pos));
431                } else {
432                    for guard in case.guards {
433                        let (cond_reg, pos) =
434                            compile_case_guard(ctx, frozen, test_reg, test_type, guard)?;
435                        let jump_next_guard_pc = ctx.codegen.emit(bytecode::make_nop(), pos);
436                        let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), pos);
437                        body_jump_pcs.push((jump_body_pc, pos));
438
439                        let next_addr = ctx.codegen.next_pc();
440                        let target = u16::try_from(next_addr)
441                            .map_err(|_| Error::TargetTooFar(pos, next_addr))?;
442                        ctx.codegen.patch(
443                            jump_next_guard_pc,
444                            bytecode::make_jump_if_false(cond_reg, target),
445                        );
446                    }
447
448                    let jump_next_case_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos);
449                    pending_next_case_jump = Some((jump_next_case_pc, end_pos));
450                }
451
452                pending_cases.push(PendingCase { body: case.body, body_jump_pcs, has_next_case });
453            }
454
455            Ok(())
456        },
457    )?;
458
459    let dispatch_end_jump_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos);
460    if let Some((jump_pc, pos)) = pending_next_case_jump {
461        let target = u16::try_from(dispatch_end_jump_pc)
462            .map_err(|_| Error::TargetTooFar(pos, dispatch_end_jump_pc))?;
463        ctx.codegen.patch(jump_pc, bytecode::make_jump(target));
464    }
465
466    let mut end_jumps = vec![];
467    for case in pending_cases {
468        let body_addr = ctx.codegen.next_pc();
469        for (jump_body_pc, pos) in case.body_jump_pcs {
470            let target =
471                u16::try_from(body_addr).map_err(|_| Error::TargetTooFar(pos, body_addr))?;
472            ctx.codegen.patch(jump_body_pc, bytecode::make_jump(target));
473        }
474
475        for stmt in case.body {
476            compile_stmt(ctx, symtable, stmt)?;
477        }
478
479        if case.has_next_case {
480            end_jumps.push(ctx.codegen.emit(bytecode::make_nop(), end_pos));
481        }
482    }
483
484    let end_addr = ctx.codegen.next_pc();
485    let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(end_pos, end_addr))?;
486    ctx.codegen.patch(dispatch_end_jump_pc, bytecode::make_jump(end_target));
487    for end_jump in end_jumps {
488        ctx.codegen.patch(end_jump, bytecode::make_jump(end_target));
489    }
490
491    Ok(())
492}
493
494/// Compiles a `DO` loop and emits bytecode into `ctx`.
495fn compile_do(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: DoSpan) -> Result<()> {
496    /// Compiles one loop guard expression to a temporary boolean register.
497    fn compile_guard(
498        ctx: &mut Context,
499        symtable: &mut LocalSymtable<'_>,
500        guard: Expr,
501    ) -> Result<(Register, LineCol)> {
502        let guard_pos = guard.start_pos();
503        let mut frozen = symtable.frozen();
504        let mut scope = frozen.temp_scope();
505        let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?;
506        compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, guard, ExprType::Boolean)?;
507        Ok((reg, guard_pos))
508    }
509
510    ctx.do_exit_stack.push(vec![]);
511
512    let end_addr = match span.guard {
513        DoGuard::Infinite => {
514            let start_pc = ctx.codegen.next_pc();
515            for stmt in span.body {
516                compile_stmt(ctx, symtable, stmt)?;
517            }
518            let end_pos = LineCol { line: 0, col: 0 };
519            let target =
520                u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(end_pos, start_pc))?;
521            ctx.codegen.emit(bytecode::make_jump(target), end_pos);
522            ctx.codegen.next_pc()
523        }
524
525        DoGuard::PreUntil(guard) => {
526            let start_pc = ctx.codegen.next_pc();
527            let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?;
528            let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos);
529            let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos);
530            let body_addr = ctx.codegen.next_pc();
531            let body_target =
532                u16::try_from(body_addr).map_err(|_| Error::TargetTooFar(guard_pos, body_addr))?;
533            ctx.codegen.patch(jump_body_pc, bytecode::make_jump_if_false(cond_reg, body_target));
534
535            for stmt in span.body {
536                compile_stmt(ctx, symtable, stmt)?;
537            }
538            let start_target =
539                u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?;
540            ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos);
541            let end_addr = ctx.codegen.next_pc();
542            let end_target =
543                u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?;
544            ctx.codegen.patch(jump_end_pc, bytecode::make_jump(end_target));
545            end_addr
546        }
547
548        DoGuard::PreWhile(guard) => {
549            let start_pc = ctx.codegen.next_pc();
550            let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?;
551            let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos);
552
553            for stmt in span.body {
554                compile_stmt(ctx, symtable, stmt)?;
555            }
556            let start_target =
557                u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?;
558            ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos);
559            let end_addr = ctx.codegen.next_pc();
560            let end_target =
561                u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?;
562            ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target));
563            end_addr
564        }
565
566        DoGuard::PostUntil(guard) => {
567            let start_pc = ctx.codegen.next_pc();
568            for stmt in span.body {
569                compile_stmt(ctx, symtable, stmt)?;
570            }
571            let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?;
572            let start_target =
573                u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?;
574            ctx.codegen.emit(bytecode::make_jump_if_false(cond_reg, start_target), guard_pos);
575            ctx.codegen.next_pc()
576        }
577
578        DoGuard::PostWhile(guard) => {
579            let start_pc = ctx.codegen.next_pc();
580            for stmt in span.body {
581                compile_stmt(ctx, symtable, stmt)?;
582            }
583            let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?;
584            let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos);
585            let start_target =
586                u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?;
587            ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos);
588            let end_addr = ctx.codegen.next_pc();
589            let end_target =
590                u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?;
591            ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target));
592            end_addr
593        }
594    };
595
596    let exit_jumps = ctx.do_exit_stack.pop().expect("Must have a matching DO scope");
597    for (addr, pos) in exit_jumps {
598        let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(pos, end_addr))?;
599        ctx.codegen.patch(addr, bytecode::make_jump(end_target));
600    }
601
602    Ok(())
603}
604
605/// Compiles a `FOR` loop and emits bytecode into `ctx`.
606fn compile_for(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: ForSpan) -> Result<()> {
607    if span.iter_double && span.iter.ref_type.is_none() {
608        match symtable.get_local_or_global(&span.iter) {
609            Ok(..) => {
610                // Keep existing iterators as-is.  This mirrors core behavior where implicit
611                // widening to DOUBLE only happens when the iterator does not exist yet.
612            }
613
614            Err(syms::Error::UndefinedSymbol(..)) => {
615                let key = SymbolKey::from(&span.iter.name);
616                if symtable.get_callable(&key).is_some() {
617                    return Err(Error::from_syms(
618                        syms::Error::AlreadyDefined(span.iter.clone()),
619                        span.iter_pos,
620                    ));
621                }
622                symtable
623                    .put_local(key, SymbolPrototype::Scalar(ExprType::Double))
624                    .map_err(|e| Error::from_syms(e, span.iter_pos))?;
625            }
626
627            Err(e) => return Err(Error::from_syms(e, span.iter_pos)),
628        }
629    }
630
631    compile_assignment(
632        &mut ctx.codegen,
633        symtable,
634        AssignmentSpan { vref: span.iter.clone(), vref_pos: span.iter_pos, expr: span.start },
635    )?;
636
637    let start_pc = ctx.codegen.next_pc();
638    let (jump_end_pc, cond_reg, cond_pos) = {
639        let cond_pos = span.end.start_pos();
640        let mut frozen = symtable.frozen();
641        let mut scope = frozen.temp_scope();
642        let reg = scope.alloc().map_err(|e| Error::from_syms(e, cond_pos))?;
643        compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, span.end, ExprType::Boolean)?;
644        (ctx.codegen.emit(bytecode::make_nop(), cond_pos), reg, cond_pos)
645    };
646    ctx.codegen.mark_statement_start(start_pc);
647
648    ctx.for_exit_stack.push(vec![]);
649
650    for stmt in span.body {
651        compile_stmt(ctx, symtable, stmt)?;
652    }
653
654    compile_assignment(
655        &mut ctx.codegen,
656        symtable,
657        AssignmentSpan { vref: span.iter, vref_pos: span.iter_pos, expr: span.next },
658    )?;
659
660    let start_target =
661        u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(cond_pos, start_pc))?;
662    ctx.codegen.emit(bytecode::make_jump(start_target), cond_pos);
663
664    let end_addr = ctx.codegen.next_pc();
665    let end_target =
666        u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(cond_pos, end_addr))?;
667    ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target));
668
669    let exit_jumps = ctx.for_exit_stack.pop().expect("Must have a matching FOR scope");
670    for (addr, pos) in exit_jumps {
671        let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(pos, end_addr))?;
672        ctx.codegen.patch(addr, bytecode::make_jump(end_target));
673    }
674
675    Ok(())
676}
677
678/// Compiles an `IF` statement `span` into the `ctx`.
679fn compile_if(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: IfSpan) -> Result<()> {
680    let mut end_pcs: Vec<usize> = vec![];
681    let nbranches = span.branches.len();
682
683    for (i, branch) in span.branches.into_iter().enumerate() {
684        let is_last = i == nbranches - 1;
685        let guard_pos = branch.guard.start_pos();
686
687        let (jump_pc, cond_reg) = {
688            let mut frozen = symtable.frozen();
689            let mut scope = frozen.temp_scope();
690            let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?;
691            compile_expr_as_type(
692                &mut ctx.codegen,
693                &mut frozen,
694                reg,
695                branch.guard,
696                ExprType::Boolean,
697            )?;
698            (ctx.codegen.emit(bytecode::make_nop(), guard_pos), reg)
699        };
700
701        for stmt in branch.body {
702            compile_stmt(ctx, symtable, stmt)?;
703        }
704
705        if !is_last {
706            let end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos);
707            end_pcs.push(end_pc);
708        }
709
710        let next_addr = ctx.codegen.next_pc();
711        let target =
712            u16::try_from(next_addr).map_err(|_| Error::TargetTooFar(guard_pos, next_addr))?;
713        ctx.codegen.patch(jump_pc, bytecode::make_jump_if_false(cond_reg, target));
714    }
715
716    let end_addr = ctx.codegen.next_pc();
717    for end_pc in end_pcs {
718        let end_target = u16::try_from(end_addr)
719            .map_err(|_| Error::TargetTooFar(LineCol { line: 0, col: 0 }, end_addr))?;
720        ctx.codegen.patch(end_pc, bytecode::make_jump(end_target));
721    }
722
723    Ok(())
724}
725
726/// Compiles a `WHILE` loop and emits bytecode into `ctx`.
727fn compile_while(
728    ctx: &mut Context,
729    symtable: &mut LocalSymtable<'_>,
730    span: WhileSpan,
731) -> Result<()> {
732    let start_pc = ctx.codegen.next_pc();
733
734    let (jump_end_pc, cond_reg, guard_pos) = {
735        let guard_pos = span.expr.start_pos();
736        let mut frozen = symtable.frozen();
737        let mut scope = frozen.temp_scope();
738        let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?;
739        compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, span.expr, ExprType::Boolean)?;
740        (ctx.codegen.emit(bytecode::make_nop(), guard_pos), reg, guard_pos)
741    };
742
743    for stmt in span.body {
744        compile_stmt(ctx, symtable, stmt)?;
745    }
746
747    let start_target =
748        u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?;
749    ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos);
750
751    let end_addr = ctx.codegen.next_pc();
752    let end_target =
753        u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?;
754    ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target));
755
756    Ok(())
757}
758
759/// Compiles a single `stmt` into the `ctx`.
760fn compile_stmt(
761    ctx: &mut Context,
762    symtable: &mut LocalSymtable<'_>,
763    stmt: Statement,
764) -> Result<()> {
765    let start_pc = ctx.codegen.next_pc();
766    let mut mark_start = true;
767    match stmt {
768        Statement::ArrayAssignment(span) => {
769            let key_pos = span.vref_pos;
770
771            let (arr_reg, info) = match symtable.get_local_or_global(&span.vref) {
772                Ok((reg, SymbolPrototype::Array(info))) => (reg, info),
773
774                Ok((_, SymbolPrototype::Scalar(_))) | Err(syms::Error::UndefinedSymbol(..)) => {
775                    return Err(Error::NotAnArray(key_pos, span.vref));
776                }
777
778                Err(e) => return Err(Error::from_syms(e, key_pos)),
779            };
780
781            if span.subscripts.len() != info.ndims {
782                return Err(Error::WrongNumberOfSubscripts(
783                    key_pos,
784                    info.ndims,
785                    span.subscripts.len(),
786                ));
787            }
788
789            let mut symtable = symtable.frozen();
790            let mut outer_scope = symtable.temp_scope();
791
792            let val_reg = outer_scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?;
793            compile_expr_as_type(
794                &mut ctx.codegen,
795                &mut symtable,
796                val_reg,
797                span.expr,
798                info.subtype,
799            )?;
800
801            let first_sub_reg = compile_integer_exprs(
802                &mut ctx.codegen,
803                &mut symtable,
804                &mut outer_scope,
805                key_pos,
806                span.subscripts.into_iter(),
807            )?;
808            ctx.codegen.emit(bytecode::make_store_array(arr_reg, val_reg, first_sub_reg), key_pos);
809        }
810
811        Statement::Assignment(span) => {
812            compile_assignment(&mut ctx.codegen, symtable, span)?;
813        }
814
815        Statement::Call(span) => {
816            let key = SymbolKey::from(&span.vref.name);
817            let key_pos = span.vref_pos;
818
819            let Some(md) = symtable.get_callable(&key) else {
820                return Err(Error::UndefinedSymbol(key_pos, span.vref.clone()));
821            };
822            if md.return_type().is_some() {
823                return Err(Error::NotAFunction(span.vref_pos, span.vref));
824            }
825            let is_user_defined = md.is_user_defined();
826            let md = md.clone();
827
828            define_new_args(&span, &md, symtable, &mut ctx.codegen)?;
829            let (first_temp, arg_linecols) = {
830                let mut symtable = symtable.frozen();
831                compile_args(span, md.clone(), &mut symtable, &mut ctx.codegen)?
832            };
833
834            if is_user_defined {
835                let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos);
836                ctx.codegen.set_arg_linecols(addr, arg_linecols);
837                ctx.codegen.add_fixup(addr, Fixup::Call(first_temp, key));
838            } else {
839                let upcall = ctx.codegen.get_upcall(key, None, key_pos)?;
840                let op = if md.is_async() {
841                    bytecode::make_upcall_async(upcall, first_temp)
842                } else {
843                    bytecode::make_upcall(upcall, first_temp)
844                };
845                let addr = ctx.codegen.emit(op, key_pos);
846                ctx.codegen.set_arg_linecols(addr, arg_linecols);
847            }
848        }
849
850        Statement::Callable(span) => {
851            mark_start = false;
852            declare_callable(symtable, &span.name, span.name_pos, &span.params)?;
853
854            let key = SymbolKey::from(&span.name.name);
855            // If declaration succeeds, we still have to check for callable redefinition.
856            // This linear scan is not the most efficient, but it's fine for now.
857            if ctx.defined_callables.contains(&key) {
858                return Err(Error::AlreadyDefined(span.name_pos, span.name));
859            }
860            compile_user_callable(ctx, symtable.global(), span)?;
861            ctx.defined_callables.insert(key);
862        }
863
864        Statement::Data(span) => {
865            ctx.data.extend(span.values.into_iter().map(|expr| expr.map(data_expr_to_constant)));
866        }
867
868        Statement::Declare(span) => {
869            mark_start = false;
870            if ctx.current_callable.is_some() {
871                return Err(Error::CannotNestUserCallables(span.name_pos));
872            }
873            declare_callable(symtable, &span.name, span.name_pos, &span.params)?;
874        }
875
876        Statement::Dim(span) => {
877            let name_pos = span.name_pos;
878            let key = SymbolKey::from(&span.name);
879
880            if symtable.get_callable(&key).is_some() {
881                return Err(Error::from_syms(
882                    syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
883                    name_pos,
884                ));
885            }
886
887            let reg = if span.shared {
888                if symtable.contains_global(&key) {
889                    return Err(Error::from_syms(
890                        syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
891                        name_pos,
892                    ));
893                }
894                symtable.put_global(key, SymbolPrototype::Scalar(span.vtype))
895            } else {
896                if symtable.contains_local(&key) {
897                    return Err(Error::from_syms(
898                        syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
899                        name_pos,
900                    ));
901                }
902                symtable.put_local(key, SymbolPrototype::Scalar(span.vtype))
903            }
904            .map_err(|e| Error::from_syms(e, name_pos))?;
905            ctx.codegen.emit_default(reg, span.vtype, name_pos);
906        }
907
908        Statement::DimArray(span) => {
909            let name_pos = span.name_pos;
910            let key = SymbolKey::from(&span.name);
911            let ndims = span.dimensions.len();
912
913            if symtable.get_callable(&key).is_some() {
914                return Err(Error::from_syms(
915                    syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
916                    name_pos,
917                ));
918            }
919
920            let info = syms::ArrayInfo { subtype: span.subtype, ndims };
921            let reg = if span.shared {
922                if symtable.contains_global(&key) {
923                    return Err(Error::from_syms(
924                        syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
925                        name_pos,
926                    ));
927                }
928                symtable.put_global(key, SymbolPrototype::Array(info))
929            } else {
930                if symtable.contains_local(&key) {
931                    return Err(Error::from_syms(
932                        syms::Error::AlreadyDefined(VarRef::new(&span.name, None)),
933                        name_pos,
934                    ));
935                }
936                symtable.put_local(key, SymbolPrototype::Array(info))
937            }
938            .map_err(|e| Error::from_syms(e, name_pos))?;
939
940            let mut symtable = symtable.frozen();
941            let mut outer_scope = symtable.temp_scope();
942
943            let first_dim_reg = compile_integer_exprs(
944                &mut ctx.codegen,
945                &mut symtable,
946                &mut outer_scope,
947                name_pos,
948                span.dimensions.into_iter(),
949            )?;
950            let packed = PackedArrayType::new(span.subtype, ndims)
951                .map_err(|_| Error::TooManyArrayDimensions(span.name_pos, ndims))?;
952            ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), name_pos);
953        }
954
955        Statement::Do(span) => {
956            compile_do(ctx, symtable, span)?;
957        }
958
959        Statement::End(span) => {
960            if let Some(expr) = span.code.as_ref()
961                && let Some((code, code_pos)) = static_end_code(expr)
962                && let Err(e) = bytecode::ExitCode::try_from(code)
963            {
964                return Err(Error::from_bytecode_invalid_exit_code(e, code_pos));
965            }
966
967            let mut symtable = symtable.frozen();
968            let mut scope = symtable.temp_scope();
969            let reg = scope.alloc().map_err(|e| Error::from_syms(e, span.pos))?;
970            match span.code {
971                Some(expr) => {
972                    compile_expr_as_type(
973                        &mut ctx.codegen,
974                        &mut symtable,
975                        reg,
976                        expr,
977                        ExprType::Integer,
978                    )?;
979                }
980                None => {
981                    ctx.codegen.emit(bytecode::make_load_integer(reg, 0), span.pos);
982                }
983            }
984            ctx.codegen.emit(bytecode::make_end(reg), span.pos);
985        }
986
987        Statement::ExitDo(span) => {
988            let Some(exit_stack) = ctx.do_exit_stack.last_mut() else {
989                return Err(Error::MisplacedExit(span.pos, "DO"));
990            };
991            let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos);
992            exit_stack.push((addr, span.pos));
993        }
994
995        Statement::ExitFor(span) => {
996            let Some(exit_stack) = ctx.for_exit_stack.last_mut() else {
997                return Err(Error::MisplacedExit(span.pos, "FOR"));
998            };
999            let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos);
1000            exit_stack.push((addr, span.pos));
1001        }
1002
1003        Statement::ExitFunction(span) => {
1004            if ctx.current_callable != Some(CallableKind::Function) {
1005                return Err(Error::MisplacedExit(span.pos, "FUNCTION"));
1006            }
1007            let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos);
1008            ctx.callable_exit_jumps.push((addr, span.pos));
1009        }
1010
1011        Statement::ExitSub(span) => {
1012            if ctx.current_callable != Some(CallableKind::Sub) {
1013                return Err(Error::MisplacedExit(span.pos, "SUB"));
1014            }
1015            let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos);
1016            ctx.callable_exit_jumps.push((addr, span.pos));
1017        }
1018
1019        Statement::For(span) => {
1020            compile_for(ctx, symtable, span)?;
1021        }
1022
1023        Statement::Gosub(span) => {
1024            let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos);
1025            ctx.codegen.add_fixup(addr, Fixup::Gosub(span.target));
1026        }
1027
1028        Statement::Goto(span) => {
1029            let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos);
1030            ctx.codegen.add_fixup(addr, Fixup::Goto(span.target));
1031        }
1032
1033        Statement::Label(span) => {
1034            mark_start = false;
1035            if !ctx.codegen.define_label(SymbolKey::from(&span.name), ctx.codegen.next_pc()) {
1036                return Err(Error::DuplicateLabel(span.name_pos, span.name));
1037            }
1038        }
1039
1040        Statement::Return(span) => {
1041            ctx.codegen.emit(bytecode::make_return(), span.pos);
1042        }
1043
1044        Statement::If(span) => {
1045            compile_if(ctx, symtable, span)?;
1046        }
1047
1048        Statement::OnError(span) => {
1049            match span {
1050                OnErrorSpan::Goto(span, pos) => {
1051                    let addr = ctx.codegen.emit(bytecode::make_nop(), pos);
1052                    ctx.codegen.add_fixup(addr, Fixup::OnErrorGoto(span.target));
1053                }
1054                OnErrorSpan::Reset(pos) => {
1055                    ctx.codegen
1056                        .emit(bytecode::make_set_error_handler(ErrorHandlerMode::None, 0), pos);
1057                }
1058                OnErrorSpan::ResumeNext(pos) => {
1059                    ctx.codegen.emit(
1060                        bytecode::make_set_error_handler(ErrorHandlerMode::ResumeNext, 0),
1061                        pos,
1062                    );
1063                }
1064            };
1065        }
1066
1067        Statement::Select(span) => {
1068            compile_select(ctx, symtable, span)?;
1069        }
1070
1071        Statement::While(span) => {
1072            compile_while(ctx, symtable, span)?;
1073        }
1074    }
1075    if mark_start && start_pc != ctx.codegen.next_pc() {
1076        ctx.codegen.mark_statement_start(start_pc);
1077    }
1078    Ok(())
1079}
1080
1081/// Declares a callable.
1082///
1083/// If the callable is already defined, this ensures the new declaration matches the previous one
1084/// and raises an error if not.
1085fn declare_callable(
1086    symtable: &mut LocalSymtable,
1087    name: &VarRef,
1088    name_pos: LineCol,
1089    params: &[VarRef],
1090) -> Result<()> {
1091    let mut syntax = vec![];
1092    for (i, param) in params.iter().enumerate() {
1093        let sep = if i == params.len() - 1 {
1094            ArgSepSyntax::End
1095        } else {
1096            ArgSepSyntax::Exactly(ArgSep::Long)
1097        };
1098        syntax.push(SingularArgSyntax::RequiredValue(
1099            RequiredValueSyntax {
1100                name: Cow::Owned(param.name.to_owned()),
1101                vtype: param.ref_type.unwrap_or(ExprType::Integer),
1102            },
1103            sep,
1104        ));
1105    }
1106
1107    let mut builder = CallableMetadataBuilder::new_dynamic(name.name.to_owned())
1108        .with_dynamic_syntax(vec![(syntax, None)]);
1109    if let Some(ctype) = name.ref_type {
1110        builder = builder.with_return_type(ctype);
1111    }
1112
1113    symtable.declare_user_callable(name, builder.build()).map_err(|e| Error::from_syms(e, name_pos))
1114}
1115
1116/// Compiles a single user-defined callable.
1117fn compile_user_callable(
1118    ctx: &mut Context,
1119    symtable: &mut GlobalSymtable,
1120    callable: CallableSpan,
1121) -> Result<()> {
1122    if ctx.current_callable.is_some() {
1123        return Err(Error::CannotNestUserCallables(callable.name_pos));
1124    }
1125
1126    let skip_pc = ctx.codegen.emit(bytecode::make_nop(), callable.name_pos);
1127
1128    let start_pc = ctx.codegen.next_pc();
1129
1130    let key_pos = callable.name_pos;
1131    let key = SymbolKey::from(callable.name.name);
1132    ctx.current_callable = Some(if callable.name.ref_type.is_some() {
1133        CallableKind::Function
1134    } else {
1135        CallableKind::Sub
1136    });
1137    debug_assert!(ctx.callable_exit_jumps.is_empty());
1138
1139    let mut symtable = symtable.enter_scope();
1140
1141    // The call protocol expects the return value to be in the first local variable
1142    // so allocate it early, and then all arguments follow in order from left to right.
1143    if let Some(vtype) = callable.name.ref_type {
1144        let ret_reg = symtable
1145            .put_local(key.clone(), SymbolPrototype::Scalar(vtype))
1146            .map_err(|e| Error::from_syms(e, key_pos))?;
1147
1148        // Set the default value of the function result.  We could instead try to do this
1149        // at runtime by clearning the return register... but the problem is that we need
1150        // to handle non-primitive types like strings and the runtime doesn't know the type
1151        // of the result to properly allocate it.
1152        let value = match vtype {
1153            ExprType::Boolean | ExprType::Integer => 0,
1154            ExprType::Double => ctx.codegen.get_constant(ConstantDatum::Double(0.0), key_pos)?,
1155            ExprType::Text => {
1156                ctx.codegen.get_constant(ConstantDatum::Text(String::new()), key_pos)?
1157            }
1158        };
1159        ctx.codegen.emit(bytecode::make_load_integer(ret_reg, value), key_pos);
1160    }
1161    for param in callable.params {
1162        let key = SymbolKey::from(param.name);
1163        symtable
1164            .put_local(
1165                key.clone(),
1166                SymbolPrototype::Scalar(param.ref_type.unwrap_or(ExprType::Integer)),
1167            )
1168            .map_err(|e| Error::from_syms(e, key_pos))?;
1169    }
1170
1171    for stmt in callable.body {
1172        compile_stmt(ctx, &mut symtable, stmt)?;
1173    }
1174
1175    let return_addr = ctx.codegen.emit(bytecode::make_return(), callable.end_pos);
1176    for (addr, pos) in ctx.callable_exit_jumps.drain(..) {
1177        let target =
1178            u16::try_from(return_addr).map_err(|_| Error::TargetTooFar(pos, return_addr))?;
1179        ctx.codegen.patch(addr, bytecode::make_jump(target));
1180    }
1181    ctx.current_callable = None;
1182    let skip_addr = ctx.codegen.next_pc();
1183    ctx.codegen.define_user_callable(key, start_pc, skip_addr);
1184
1185    let target =
1186        u16::try_from(skip_addr).map_err(|_| Error::TargetTooFar(callable.end_pos, skip_addr))?;
1187    ctx.codegen.patch(skip_pc, bytecode::make_jump(target));
1188
1189    Ok(())
1190}
1191
1192/// Extracts the metadata of all provided `upcalls`.
1193pub fn only_metadata(
1194    upcalls_by_name: &HashMap<SymbolKey, Rc<dyn Callable>>,
1195) -> HashMap<SymbolKey, Rc<CallableMetadata>> {
1196    let mut upcalls = HashMap::with_capacity(upcalls_by_name.len());
1197    for (name, callable) in upcalls_by_name {
1198        upcalls.insert(name.clone(), callable.metadata());
1199    }
1200    upcalls
1201}
1202
1203/// Descriptor for a single global variable to be pre-defined before compilation.
1204#[derive(Clone)]
1205pub struct GlobalDef {
1206    /// Name of the variable (case-insensitive, as in EndBASIC).
1207    pub name: String,
1208
1209    /// Kind and type information for the variable.
1210    pub kind: GlobalDefKind,
1211}
1212
1213/// Kind of a pre-defined global variable.
1214#[derive(Clone)]
1215pub enum GlobalDefKind {
1216    /// A scalar (non-array) variable.
1217    Scalar {
1218        /// Type of the scalar variable.
1219        etype: ExprType,
1220
1221        /// Initial value for the variable.  If `None`, the variable is initialized to its default
1222        /// value: `0` for numeric types and an empty string for `Text`.  The type of the datum
1223        /// must match `etype`.
1224        initial_value: Option<ConstantDatum>,
1225    },
1226
1227    /// A multidimensional array with the given element type and fixed dimension sizes.
1228    ///
1229    /// Each dimension size must be positive and must fit in a `u16`.
1230    Array {
1231        /// Element type of the array.
1232        subtype: ExprType,
1233
1234        /// Size of each dimension, in order from outermost to innermost.
1235        dimensions: Vec<usize>,
1236    },
1237}
1238
1239/// Prepares global variables injected from outside of the compiled program.
1240///
1241/// Pre-defined scalar globals are initialized to their default values (0 for numeric types,
1242/// empty string for text).  Pre-defined array globals are allocated with all elements set to
1243/// their default values.  The compiled program may read or write any of these globals.
1244///
1245/// After execution, use `Vm::get_global*` methods to query the values of these globals (and
1246/// any globals declared via `DIM SHARED` in the program itself).
1247pub(super) fn prepare_globals(
1248    ctx: &mut Context,
1249    symtable: &mut GlobalSymtable,
1250    global_defs: &[GlobalDef],
1251) -> Result<()> {
1252    let preamble_pos = LineCol { line: 0, col: 0 };
1253
1254    // Register all global defs in the symbol table and collect array globals for the preamble.
1255    let mut max_ndims: u8 = 0;
1256    let mut array_globals: Vec<(Register, &GlobalDef)> = vec![];
1257    for def in global_defs {
1258        let key = SymbolKey::from(&def.name);
1259        match &def.kind {
1260            GlobalDefKind::Array { subtype, dimensions } => {
1261                let ndims =
1262                    u8::try_from(dimensions.len()).expect("Array must have at most 255 dimensions");
1263                let info = syms::ArrayInfo { subtype: *subtype, ndims: usize::from(ndims) };
1264                let reg = symtable
1265                    .put_global(key, SymbolPrototype::Array(info))
1266                    .map_err(|e| Error::from_syms(e, preamble_pos))?;
1267                max_ndims = max(max_ndims, ndims);
1268                array_globals.push((reg, def));
1269            }
1270
1271            GlobalDefKind::Scalar { etype, initial_value } => {
1272                let reg = symtable
1273                    .put_global(key, SymbolPrototype::Scalar(*etype))
1274                    .map_err(|e| Error::from_syms(e, preamble_pos))?;
1275                match initial_value {
1276                    Some(datum) => {
1277                        if datum.etype() != *etype {
1278                            return Err(Error::TypeMismatch(preamble_pos, datum.etype(), *etype));
1279                        }
1280                        ctx.codegen.emit_value(reg, datum.clone(), preamble_pos)?;
1281                    }
1282                    None => ctx.codegen.emit_default(reg, *etype, preamble_pos),
1283                }
1284            }
1285        }
1286    }
1287
1288    // Emit the array initialization preamble, but only if any arrays were defined.
1289    //
1290    // We use a short-lived `ENTER/LEAVE` scope to borrow local registers for the dimension
1291    // temporaries without permanently consuming global register slots.
1292    if array_globals.is_empty() {
1293        // `compile` starts by popping an EOF, so make sure there is one.
1294        ctx.codegen.emit(bytecode::make_eof(), preamble_pos);
1295        return Ok(());
1296    }
1297    for (reg, def) in array_globals {
1298        let GlobalDefKind::Array { subtype, dimensions } = &def.kind else {
1299            unreachable!("array_globals only contains array defs per the loop above")
1300        };
1301
1302        let ndims = u8::try_from(dimensions.len()).unwrap();
1303        for (i, &dim) in dimensions.iter().enumerate() {
1304            let dim_u16 =
1305                u16::try_from(dim).expect("Array dimension must fit in u16 for LOADI instruction");
1306            let local_reg =
1307                Register::local(u8::try_from(i).unwrap()).expect("Dimension index fits in u8");
1308            ctx.codegen.emit(bytecode::make_load_integer(local_reg, dim_u16), preamble_pos);
1309        }
1310
1311        let first_dim_reg = Register::local(0).expect("Local register 0 is always valid");
1312        let packed = PackedArrayType::new(*subtype, usize::from(ndims))
1313            .map_err(|_| Error::TooManyArrayDimensions(preamble_pos, usize::from(ndims)))?;
1314        ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), preamble_pos);
1315    }
1316
1317    // `compile` starts by popping an EOF, so make sure there is one.
1318    ctx.codegen.emit(bytecode::make_eof(), preamble_pos);
1319    Ok(())
1320}
1321
1322/// Compiles the `input` into an `Image` that can be executed by the VM.
1323pub fn compile(
1324    input: &mut dyn io::Read,
1325    image: &Image,
1326    ctx: &mut Context,
1327    mut symtable: LocalSymtable,
1328) -> Result<(ImageDelta, LocalSymtableSnapshot)> {
1329    ctx.codegen.pop_eof();
1330    {
1331        for stmt in parser::parse(input) {
1332            compile_stmt(ctx, &mut symtable, stmt?)?;
1333        }
1334    }
1335    ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 });
1336
1337    let global_vars = symtable
1338        .iter_globals()
1339        .map(|(key, proto, reg)| {
1340            let (subtype, ndims) = match proto {
1341                SymbolPrototype::Array(info) => (info.subtype, info.ndims),
1342                SymbolPrototype::Scalar(etype) => (etype, 0),
1343            };
1344            (key.clone(), GlobalVarInfo { reg, subtype, ndims })
1345        })
1346        .collect();
1347    let program_vars = symtable
1348        .iter_locals()
1349        .map(|(key, proto, reg)| {
1350            let (subtype, ndims) = match proto {
1351                SymbolPrototype::Array(info) => (info.subtype, info.ndims),
1352                SymbolPrototype::Scalar(etype) => (etype, 0),
1353            };
1354            (key.clone(), GlobalVarInfo { reg, subtype, ndims })
1355        })
1356        .collect();
1357    let delta = ctx.codegen.build_image_delta(image, global_vars, program_vars, &ctx.data)?;
1358    Ok((delta, symtable.save()))
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363    use super::*;
1364    use crate::Compiler;
1365    use crate::ast::ExprType;
1366    use crate::mem::ConstantDatum;
1367    use crate::vm::{StopReason, Vm};
1368
1369    fn compile_and_get_global(defs: &[GlobalDef], name: &str) -> ConstantDatum {
1370        let compiler = Compiler::new(&HashMap::default(), defs)
1371            .expect("constants initialization must succeed");
1372        let image = compiler.compile(&mut "".as_bytes()).expect("compilation should succeed");
1373        let mut vm = Vm::new(HashMap::default());
1374        match vm.exec(&image) {
1375            StopReason::End(code) if code.is_success() => {}
1376            StopReason::End(code) => panic!("unexpected exit code: {}", code.to_i32()),
1377            StopReason::Eof => {}
1378            StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"),
1379            StopReason::UpcallAsync(_) => panic!("unexpected upcall"),
1380            StopReason::Yield => unreachable!(),
1381        }
1382        let key = SymbolKey::from(name);
1383        vm.get_global(&image, &key).expect("get_global failed").expect("global not found")
1384    }
1385
1386    #[test]
1387    fn test_inject_boolean() {
1388        let defs = vec![GlobalDef {
1389            name: "b".to_owned(),
1390            kind: GlobalDefKind::Scalar {
1391                etype: ExprType::Boolean,
1392                initial_value: Some(ConstantDatum::Boolean(true)),
1393            },
1394        }];
1395        assert_eq!(ConstantDatum::Boolean(true), compile_and_get_global(&defs, "b"));
1396    }
1397
1398    #[test]
1399    fn test_inject_integer() {
1400        let defs = vec![GlobalDef {
1401            name: "n".to_owned(),
1402            kind: GlobalDefKind::Scalar {
1403                etype: ExprType::Integer,
1404                initial_value: Some(ConstantDatum::Integer(42)),
1405            },
1406        }];
1407        assert_eq!(ConstantDatum::Integer(42), compile_and_get_global(&defs, "n"));
1408    }
1409
1410    #[test]
1411    fn test_inject_integer_large() {
1412        let defs = vec![GlobalDef {
1413            name: "n".to_owned(),
1414            kind: GlobalDefKind::Scalar {
1415                etype: ExprType::Integer,
1416                initial_value: Some(ConstantDatum::Integer(70000)),
1417            },
1418        }];
1419        assert_eq!(ConstantDatum::Integer(70000), compile_and_get_global(&defs, "n"));
1420    }
1421
1422    #[test]
1423    fn test_inject_double() {
1424        let defs = vec![GlobalDef {
1425            name: "d".to_owned(),
1426            kind: GlobalDefKind::Scalar {
1427                etype: ExprType::Double,
1428                initial_value: Some(ConstantDatum::Double(1.5)),
1429            },
1430        }];
1431        assert_eq!(ConstantDatum::Double(1.5), compile_and_get_global(&defs, "d"));
1432    }
1433
1434    #[test]
1435    fn test_inject_text() {
1436        let defs = vec![GlobalDef {
1437            name: "s".to_owned(),
1438            kind: GlobalDefKind::Scalar {
1439                etype: ExprType::Text,
1440                initial_value: Some(ConstantDatum::Text("hello".to_owned())),
1441            },
1442        }];
1443        assert_eq!(ConstantDatum::Text("hello".to_owned()), compile_and_get_global(&defs, "s"),);
1444    }
1445
1446    #[test]
1447    fn test_inject_type_mismatch() {
1448        let defs = vec![GlobalDef {
1449            name: "n".to_owned(),
1450            kind: GlobalDefKind::Scalar {
1451                etype: ExprType::Integer,
1452                initial_value: Some(ConstantDatum::Double(1.5)),
1453            },
1454        }];
1455        let result = Compiler::new(&HashMap::default(), &defs);
1456        assert!(matches!(result, Err(Error::TypeMismatch(..))));
1457    }
1458}