1592 lines
56 KiB
Rust
1592 lines
56 KiB
Rust
//! Codegenerator: typisiertes HIR → Bytecode.
|
|
//!
|
|
//! Dummer Tree-Walk (Design D1): Typen und Konvertierungen sind im HIR
|
|
//! bereits explizit, hier passiert nur noch Instruktionsauswahl und
|
|
//! Label-Fixup (Vorwärtsziele über Fixup-Listen, kein zweiter Pass).
|
|
|
|
use crate::bytecode::{CmpOp, CompiledModule, DataItem, Instr, ProcCode};
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
use tb_frontend::hir::{
|
|
self, Builtin, CmpKind, HArg, HArith, HCmp, HExpr, HLogic, HPlace, HPrintItem, HProcKind,
|
|
HResume, HStmt, HStmtKind, HTy, HirModule, IntKind, NumTy, VarSlot,
|
|
};
|
|
use tb_runtime::builtins::ids;
|
|
use tb_runtime::value::TypeInit;
|
|
|
|
pub fn compile(hir: &HirModule) -> CompiledModule {
|
|
let mut cg = Codegen {
|
|
common_arrays: hir
|
|
.commons
|
|
.iter()
|
|
.filter(|c| c.dims.is_some())
|
|
.map(|c| c.slot)
|
|
.collect(),
|
|
strings: Vec::new(),
|
|
string_ids: HashMap::new(),
|
|
jump_tables: Vec::new(),
|
|
modul_label_pc: Vec::new(),
|
|
initialize_main: hir
|
|
.objects
|
|
.iter()
|
|
.any(|object| object.class == tb_frontend::forms::ObjectClass::Form),
|
|
};
|
|
let mut procs = Vec::new();
|
|
for proc in &hir.procs {
|
|
procs.push(cg.compile_proc(proc));
|
|
}
|
|
CompiledModule {
|
|
modules: vec![(hir.name.clone(), hir.option_base)],
|
|
sources: vec![tb_frontend::source::SourceFile {
|
|
module: 0,
|
|
path: hir.name.clone(),
|
|
}],
|
|
form_initial: vec![],
|
|
startup_form: None,
|
|
name: hir.name.clone(),
|
|
option_base: hir.option_base,
|
|
strings: cg.strings,
|
|
globals_init: hir.globals.iter().map(slot_init).collect(),
|
|
global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
|
|
udts: hir
|
|
.udts
|
|
.iter()
|
|
.map(|u| tb_runtime::value::UdtLayout {
|
|
name: u.name.clone(),
|
|
fields: u.fields.iter().map(|(_, t)| type_init(t)).collect(),
|
|
})
|
|
.collect(),
|
|
procs,
|
|
data: hir
|
|
.data
|
|
.iter()
|
|
.map(|d| DataItem {
|
|
text: d.text.clone(),
|
|
line: d.line,
|
|
})
|
|
.collect(),
|
|
jump_tables: cg.jump_tables,
|
|
objects: hir.objects.clone(),
|
|
event_procs: hir.event_procs.clone(),
|
|
}
|
|
}
|
|
|
|
/// Slot-Vorbelegung: Arrays und UDT-Handles starten leer (Auto-Init).
|
|
fn slot_init(v: &hir::HVar) -> TypeInit {
|
|
if v.array {
|
|
TypeInit::Empty
|
|
} else {
|
|
type_init(&v.ty)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn type_init(t: &HTy) -> TypeInit {
|
|
match t {
|
|
HTy::Num(NumTy::Int) => TypeInit::Int,
|
|
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
|
HTy::Num(NumTy::Sng) => TypeInit::Sng,
|
|
HTy::Num(NumTy::Dbl) => TypeInit::Dbl,
|
|
HTy::Num(NumTy::Cur) => TypeInit::Cur,
|
|
HTy::Str => TypeInit::Str,
|
|
HTy::FixedStr(n) => TypeInit::FixedStr(*n),
|
|
HTy::Udt(id) => TypeInit::Udt(*id),
|
|
HTy::Form | HTy::Control => TypeInit::Empty,
|
|
}
|
|
}
|
|
|
|
struct Codegen {
|
|
common_arrays: std::collections::HashSet<u16>,
|
|
strings: Vec<Rc<str>>,
|
|
string_ids: HashMap<String, u16>,
|
|
jump_tables: Vec<Vec<u32>>,
|
|
/// Sprungziele des Modulrumpfs (Prozedur 0). Ein modulweites
|
|
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
|
|
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
|
|
modul_label_pc: Vec<Option<u32>>,
|
|
/// Formulare müssen erst nach den globalen DIM-Anweisungen Ereignisse
|
|
/// zustellen; reine Textprogramme behalten ihre bisherigen Grenzen.
|
|
initialize_main: bool,
|
|
}
|
|
|
|
struct ProcCtx {
|
|
pos: tb_frontend::SourcePos,
|
|
code: Vec<Instr>,
|
|
/// LabelId → Instruktionsindex.
|
|
label_pc: Vec<Option<u32>>,
|
|
/// (Instruktionsindex, LabelId) — nach dem Emit gepatcht.
|
|
fixups: Vec<(usize, u16)>,
|
|
/// (Tabellenindex, Labels) — Sprungtabellen nach dem Emit auflösen.
|
|
table_fixups: Vec<(usize, Vec<u16>)>,
|
|
}
|
|
|
|
impl ProcCtx {
|
|
fn here(&self) -> u32 {
|
|
self.code.len() as u32
|
|
}
|
|
/// Label auf die unmittelbar zuvor emittierte `Stmt`-Grenze binden.
|
|
///
|
|
/// Schleifen brauchen im Kreis eine Anweisungsgrenze — sonst wird dort
|
|
/// nie ein Ereignis zugestellt, kein Breakpoint erreicht und kein
|
|
/// Abbruch bemerkt (`FOR i = 1 TO 10000: NEXT` als Warteschleife).
|
|
/// Wo die Grenze ohnehin direkt vor dem Schleifenkopf liegt, zeigt der
|
|
/// Rücksprung einfach auf sie, statt eine zweite zu emittieren.
|
|
fn bind_auf_stmt(&mut self, label: u16) {
|
|
let pc = self.code.len().saturating_sub(1) as u32;
|
|
debug_assert!(
|
|
matches!(self.code.last(), Some(Instr::Stmt(_))),
|
|
"bind_auf_stmt ohne vorangehende Anweisungsgrenze"
|
|
);
|
|
if (label as usize) >= self.label_pc.len() {
|
|
self.label_pc.resize(label as usize + 1, None);
|
|
}
|
|
self.label_pc[label as usize] = Some(pc);
|
|
}
|
|
fn bind(&mut self, label: u16) {
|
|
let pc = self.here();
|
|
if (label as usize) >= self.label_pc.len() {
|
|
self.label_pc.resize(label as usize + 1, None);
|
|
}
|
|
self.label_pc[label as usize] = Some(pc);
|
|
}
|
|
fn emit(&mut self, i: Instr) {
|
|
if matches!(i, Instr::Stmt(_) | Instr::InitStmt(_)) {
|
|
self.code
|
|
.push(Instr::Source(self.pos.source, self.pos.column));
|
|
}
|
|
self.code.push(i);
|
|
}
|
|
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
|
fn emit_jump(&mut self, i: Instr, label: u16) {
|
|
self.fixups.push((self.code.len(), label));
|
|
self.code.push(i);
|
|
}
|
|
/// Frisches internes Label (zusätzlich zu den HIR-Labels).
|
|
fn new_label(&mut self) -> u16 {
|
|
let id = self.label_pc.len() as u16;
|
|
self.label_pc.push(None);
|
|
id
|
|
}
|
|
}
|
|
|
|
impl Codegen {
|
|
fn pool(&mut self, s: &str) -> u16 {
|
|
if let Some(id) = self.string_ids.get(s) {
|
|
return *id;
|
|
}
|
|
let id = self.strings.len() as u16;
|
|
self.strings.push(Rc::from(s));
|
|
self.string_ids.insert(s.to_string(), id);
|
|
id
|
|
}
|
|
|
|
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
|
let mut ctx = ProcCtx {
|
|
pos: tb_frontend::SourcePos::default(),
|
|
code: Vec::new(),
|
|
label_pc: vec![None; proc.label_count as usize],
|
|
fixups: Vec::new(),
|
|
table_fixups: Vec::new(),
|
|
};
|
|
if proc.kind == hir::HProcKind::Main {
|
|
let declarations = proc
|
|
.body
|
|
.iter()
|
|
.filter(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
|
|
.collect::<Vec<_>>();
|
|
for stmt in declarations {
|
|
self.stmt_inner(&mut ctx, proc, stmt, false);
|
|
}
|
|
if self.initialize_main
|
|
|| proc
|
|
.body
|
|
.iter()
|
|
.any(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
|
|
{
|
|
ctx.emit(Instr::Stmt(0));
|
|
}
|
|
}
|
|
for stmt in &proc.body {
|
|
if proc.kind == hir::HProcKind::Main
|
|
&& matches!(stmt.kind, HStmtKind::Dim { redim: false, .. })
|
|
{
|
|
continue;
|
|
}
|
|
self.stmt(&mut ctx, proc, stmt);
|
|
}
|
|
// Rumpfende
|
|
self.emit_proc_exit(&mut ctx, proc);
|
|
|
|
// Prozedur 0 (Modulrumpf) gibt ihre Sprungziele weiter.
|
|
if proc.kind == hir::HProcKind::Main {
|
|
self.modul_label_pc = ctx.label_pc.clone();
|
|
}
|
|
|
|
// Fixups patchen
|
|
for (idx, label) in std::mem::take(&mut ctx.fixups) {
|
|
// Modulweites `ON ERROR GOTO` in einer Prozedur: das Label lebt
|
|
// im Modulrumpf, nicht im eigenen.
|
|
let modulweit =
|
|
proc.kind != hir::HProcKind::Main && matches!(ctx.code[idx], Instr::OnErrorGoto(_));
|
|
let pc = if modulweit {
|
|
self.modul_label_pc
|
|
.get(label as usize)
|
|
.copied()
|
|
.flatten()
|
|
.expect("Modul-Label ohne Position")
|
|
} else {
|
|
ctx.label_pc[label as usize].expect("Label ohne Position")
|
|
};
|
|
match &mut ctx.code[idx] {
|
|
Instr::Jump(t)
|
|
| Instr::JumpIfFalse(t)
|
|
| Instr::JumpIfTrue(t)
|
|
| Instr::Gosub(t)
|
|
| Instr::RetGosubTo(t)
|
|
| Instr::ResumeLabel(t)
|
|
| Instr::OnErrorGoto(t)
|
|
| Instr::OnErrorLocal(t)
|
|
| Instr::TrapDefine(_, t) => *t = pc,
|
|
other => unreachable!("Fixup auf {other:?}"),
|
|
}
|
|
}
|
|
for (table, labels) in std::mem::take(&mut ctx.table_fixups) {
|
|
let pcs: Vec<u32> = labels
|
|
.iter()
|
|
.map(|l| ctx.label_pc[*l as usize].expect("Label ohne Position"))
|
|
.collect();
|
|
self.jump_tables[table] = pcs;
|
|
}
|
|
|
|
ProcCode {
|
|
module: 0,
|
|
kind: proc.kind,
|
|
params: proc.params.clone(),
|
|
ret_ty: proc.ret_ty.clone(),
|
|
name: proc.name.clone(),
|
|
n_params: proc.params.len() as u16,
|
|
locals_init: proc.locals.iter().map(slot_init).collect(),
|
|
local_names: proc.locals.iter().map(|l| l.name.clone()).collect(),
|
|
code: ctx.code,
|
|
}
|
|
}
|
|
|
|
fn emit_proc_exit(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc) {
|
|
match proc.kind {
|
|
HProcKind::Main => ctx.emit(Instr::End),
|
|
HProcKind::Sub => ctx.emit(Instr::RetProc),
|
|
HProcKind::Function | HProcKind::DefFn => {
|
|
if let Some(VarSlot::Local(slot)) = proc.ret_slot {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
} else {
|
|
ctx.emit(Instr::PushInt(0));
|
|
}
|
|
ctx.emit(Instr::RetFn);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Anweisungen -------------------------------------------------------
|
|
|
|
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) {
|
|
self.stmt_inner(ctx, proc, stmt, true);
|
|
}
|
|
|
|
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
|
|
ctx.pos = stmt.pos;
|
|
match &stmt.kind {
|
|
HStmtKind::Label(l) => {
|
|
ctx.bind(*l);
|
|
return;
|
|
}
|
|
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
|
_ => ctx.emit(Instr::InitStmt(stmt.line)),
|
|
}
|
|
match &stmt.kind {
|
|
HStmtKind::Label(_) => unreachable!(),
|
|
HStmtKind::SetErl(n) => ctx.emit(Instr::SetErl(*n)),
|
|
HStmtKind::Assign { place, value } => {
|
|
self.store_place(ctx, place, |cg, ctx| cg.expr(ctx, value));
|
|
}
|
|
HStmtKind::SetObjectProperty {
|
|
object,
|
|
index,
|
|
property,
|
|
value,
|
|
} => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
self.expr(ctx, value);
|
|
ctx.emit(Instr::StoreObjectProperty(
|
|
*object,
|
|
*property,
|
|
index.is_some(),
|
|
));
|
|
}
|
|
HStmtKind::SetObjectIndexedProperty {
|
|
object,
|
|
object_index,
|
|
property,
|
|
index,
|
|
value,
|
|
} => {
|
|
if let Some(object_index) = object_index {
|
|
self.expr(ctx, object_index);
|
|
}
|
|
self.expr(ctx, index);
|
|
self.expr(ctx, value);
|
|
ctx.emit(Instr::StoreObjectIndexedProperty(
|
|
*object,
|
|
*property | if object_index.is_some() { 0x8000 } else { 0 },
|
|
));
|
|
}
|
|
HStmtKind::SetDynamicObjectProperty {
|
|
object,
|
|
property,
|
|
value,
|
|
} => {
|
|
self.expr(ctx, object);
|
|
self.expr(ctx, value);
|
|
let property = self.pool(property);
|
|
ctx.emit(Instr::StoreDynamicObjectProperty(property));
|
|
}
|
|
HStmtKind::ObjectMethod {
|
|
object,
|
|
index,
|
|
method,
|
|
args,
|
|
} => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
for arg in args {
|
|
self.expr(ctx, arg);
|
|
}
|
|
ctx.emit(Instr::ObjectMethod(
|
|
*object,
|
|
*method,
|
|
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
|
|
));
|
|
}
|
|
HStmtKind::ObjectLoad {
|
|
object,
|
|
index,
|
|
unload,
|
|
} => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
ctx.emit(Instr::ObjectLoad(*object, *unload, index.is_some()));
|
|
}
|
|
HStmtKind::Print { items, trailing } => {
|
|
for item in items {
|
|
match item {
|
|
HPrintItem::Val(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_VAL, 1));
|
|
}
|
|
HPrintItem::Tab(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_TAB, 1));
|
|
}
|
|
HPrintItem::Spc(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_SPC, 1));
|
|
}
|
|
HPrintItem::Comma => {
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_COMMA, 0));
|
|
}
|
|
}
|
|
}
|
|
if !trailing {
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_NEWLINE, 0));
|
|
}
|
|
}
|
|
HStmtKind::SetErr(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::SetErr);
|
|
}
|
|
HStmtKind::Field { file, fields } => {
|
|
self.expr(ctx, file);
|
|
for (len, place) in fields {
|
|
self.expr(ctx, len);
|
|
self.make_ref(ctx, place);
|
|
}
|
|
ctx.emit(Instr::Field(fields.len() as u8));
|
|
}
|
|
HStmtKind::LsetRset {
|
|
rset,
|
|
target,
|
|
value,
|
|
} => {
|
|
self.make_ref(ctx, target);
|
|
self.expr(ctx, value);
|
|
ctx.emit(Instr::LsetRset(*rset));
|
|
}
|
|
HStmtKind::GetPut {
|
|
put,
|
|
file,
|
|
recnum,
|
|
var,
|
|
} => {
|
|
self.expr(ctx, file);
|
|
if let Some(r) = recnum {
|
|
self.expr(ctx, r);
|
|
}
|
|
let (art, zusatz) = match var {
|
|
None => (0u8, 0u16),
|
|
Some(v) => {
|
|
self.make_ref(ctx, v);
|
|
match &v.ty {
|
|
HTy::Num(NumTy::Int) => (1, 0),
|
|
HTy::Num(NumTy::Lng) => (2, 0),
|
|
HTy::Num(NumTy::Sng) => (3, 0),
|
|
HTy::Num(NumTy::Dbl) => (4, 0),
|
|
HTy::Num(NumTy::Cur) => (5, 0),
|
|
HTy::FixedStr(n) => (6, *n as u16),
|
|
HTy::Udt(i) => (7, *i),
|
|
// Variable Strings: Länge erst zur Laufzeit.
|
|
HTy::Str => (8, 0),
|
|
HTy::Form | HTy::Control => (8, 0),
|
|
}
|
|
}
|
|
};
|
|
ctx.emit(Instr::GetPut(*put, recnum.is_some(), art, zusatz));
|
|
}
|
|
HStmtKind::Input {
|
|
file,
|
|
line_mode,
|
|
prompt,
|
|
question,
|
|
targets,
|
|
} => {
|
|
if let Some(f) = file {
|
|
// Dateinummer zuerst, dann die Referenzen darüber.
|
|
self.expr(ctx, f);
|
|
for t in targets {
|
|
self.make_ref(ctx, t);
|
|
}
|
|
ctx.emit(Instr::InputFile(targets.len() as u8, *line_mode));
|
|
return;
|
|
}
|
|
for t in targets {
|
|
self.make_ref(ctx, t);
|
|
}
|
|
let prompt_idx = match prompt {
|
|
Some(p) => self.pool(p),
|
|
None => 0xFFFF,
|
|
};
|
|
ctx.emit(Instr::Input(
|
|
targets.len() as u8,
|
|
*line_mode,
|
|
prompt_idx,
|
|
*question,
|
|
));
|
|
}
|
|
HStmtKind::If { cond, then, els } => {
|
|
self.expr(ctx, cond);
|
|
let l_else = ctx.new_label();
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_else);
|
|
for s in then {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
if els.is_empty() {
|
|
ctx.bind(l_else);
|
|
} else {
|
|
let l_end = ctx.new_label();
|
|
ctx.emit_jump(Instr::Jump(0), l_end);
|
|
ctx.bind(l_else);
|
|
for s in els {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
ctx.bind(l_end);
|
|
}
|
|
}
|
|
HStmtKind::Loop {
|
|
end_pos,
|
|
pre,
|
|
post,
|
|
body,
|
|
exit_label,
|
|
} => {
|
|
let l_start = ctx.new_label();
|
|
ctx.bind_auf_stmt(l_start);
|
|
if let Some((is_until, cond)) = pre {
|
|
self.expr(ctx, cond);
|
|
if *is_until {
|
|
ctx.emit_jump(Instr::JumpIfTrue(0), *exit_label);
|
|
} else {
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), *exit_label);
|
|
}
|
|
}
|
|
for s in body {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
ctx.pos = *end_pos;
|
|
ctx.emit(Instr::Stmt(end_pos.line));
|
|
match post {
|
|
Some((is_until, cond)) => {
|
|
self.expr(ctx, cond);
|
|
if *is_until {
|
|
// LOOP UNTIL: weiter, solange falsch
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_start);
|
|
} else {
|
|
ctx.emit_jump(Instr::JumpIfTrue(0), l_start);
|
|
}
|
|
}
|
|
None => ctx.emit_jump(Instr::Jump(0), l_start),
|
|
}
|
|
ctx.bind(*exit_label);
|
|
}
|
|
HStmtKind::For {
|
|
end_pos,
|
|
var,
|
|
ty,
|
|
from,
|
|
to,
|
|
step,
|
|
limit_slot,
|
|
step_slot,
|
|
body,
|
|
exit_label,
|
|
} => {
|
|
self.gen_for(
|
|
ctx,
|
|
proc,
|
|
var,
|
|
*ty,
|
|
from,
|
|
to,
|
|
step.as_ref(),
|
|
*limit_slot,
|
|
*step_slot,
|
|
body,
|
|
*exit_label,
|
|
*end_pos,
|
|
);
|
|
}
|
|
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
|
HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(0), *l),
|
|
HStmtKind::OnGoto {
|
|
sel,
|
|
gosub,
|
|
targets,
|
|
} => {
|
|
self.expr(ctx, sel);
|
|
let table = self.jump_tables.len();
|
|
self.jump_tables.push(Vec::new());
|
|
ctx.table_fixups.push((table, targets.clone()));
|
|
ctx.emit(Instr::OnJump(table as u16, *gosub));
|
|
}
|
|
HStmtKind::TrapDef { art, index, ziel } => {
|
|
self.expr(ctx, index);
|
|
match ziel {
|
|
Some(l) => ctx.emit_jump(Instr::TrapDefine(*art, 0), *l),
|
|
None => ctx.emit(Instr::TrapDisable(*art)),
|
|
}
|
|
}
|
|
HStmtKind::TrapSet {
|
|
art,
|
|
index,
|
|
zustand,
|
|
} => {
|
|
self.expr(ctx, index);
|
|
ctx.emit(Instr::TrapSet(*art, *zustand));
|
|
}
|
|
HStmtKind::EventSwitch(an) => ctx.emit(Instr::EventSwitch(*an)),
|
|
HStmtKind::ReturnGosub(target) => match target {
|
|
None => ctx.emit(Instr::RetGosub),
|
|
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
|
|
},
|
|
HStmtKind::Run { target, string } => {
|
|
if let Some(target) = target {
|
|
self.expr(ctx, target);
|
|
}
|
|
ctx.emit(Instr::Run(if target.is_none() {
|
|
0
|
|
} else if *string {
|
|
2
|
|
} else {
|
|
1
|
|
}));
|
|
}
|
|
HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc),
|
|
HStmtKind::CallSub { proc: id, args } => {
|
|
for a in args {
|
|
self.arg(ctx, a);
|
|
}
|
|
ctx.emit(Instr::Call(*id, args.len() as u8));
|
|
}
|
|
HStmtKind::BuiltinStmt { b, args } => {
|
|
for a in args {
|
|
self.expr(ctx, a);
|
|
}
|
|
// `DOEVENTS` und `SLEEP` sind Zustellpunkte und damit Sache
|
|
// der Ausführung, nicht der Bibliothek: ein Builtin kann
|
|
// keine Ereignisprozedur des Programms aufrufen
|
|
// (design.md, D1).
|
|
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
|
|
ctx.emit(i);
|
|
if matches!(b, Builtin::Doevents) {
|
|
ctx.emit(Instr::Pop);
|
|
}
|
|
return;
|
|
}
|
|
let id = builtin_id(*b);
|
|
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
|
|
if matches!(b, Builtin::MsgBox) {
|
|
ctx.emit(Instr::Pop);
|
|
}
|
|
if builtin_returns_value(*b) {
|
|
ctx.emit(Instr::Pop);
|
|
}
|
|
}
|
|
HStmtKind::OnError { local, target } => match (local, target) {
|
|
(false, Some(l)) => ctx.emit_jump(Instr::OnErrorGoto(0), *l),
|
|
(true, Some(l)) => ctx.emit_jump(Instr::OnErrorLocal(0), *l),
|
|
(false, None) => ctx.emit(Instr::OnErrorDisable),
|
|
(true, None) => ctx.emit(Instr::OnErrorLocalDisable),
|
|
},
|
|
HStmtKind::OnErrorResumeNext { local } => {
|
|
ctx.emit(Instr::OnErrorResumeNext(*local));
|
|
}
|
|
HStmtKind::Resume(kind) => match kind {
|
|
HResume::Retry => ctx.emit(Instr::Resume0),
|
|
HResume::Next => ctx.emit(Instr::ResumeNext),
|
|
HResume::Label(l) => ctx.emit_jump(Instr::ResumeLabel(0), *l),
|
|
},
|
|
HStmtKind::RaiseError(code) => {
|
|
self.expr(ctx, code);
|
|
ctx.emit(Instr::RaiseError);
|
|
}
|
|
HStmtKind::Read(places) => {
|
|
for p in places {
|
|
let numeric = matches!(p.ty, HTy::Num(_));
|
|
if numeric {
|
|
self.store_place(ctx, p, |_cg, ctx| {
|
|
ctx.emit(Instr::ReadData(1));
|
|
emit_conv(ctx, NumTy::Dbl, p.ty.num().unwrap_or(NumTy::Dbl));
|
|
});
|
|
} else {
|
|
self.store_place(ctx, p, |_cg, ctx| {
|
|
ctx.emit(Instr::ReadData(0));
|
|
if let HTy::FixedStr(n) = &p.ty {
|
|
ctx.emit(Instr::FixStr(*n));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
HStmtKind::Restore(idx) => ctx.emit(Instr::Restore(*idx)),
|
|
HStmtKind::Dim {
|
|
slot,
|
|
elem,
|
|
dims,
|
|
redim,
|
|
} => {
|
|
for (lo, hi) in dims {
|
|
self.expr(ctx, lo);
|
|
self.expr(ctx, hi);
|
|
}
|
|
let (global, s) = slot_parts(*slot);
|
|
let init = type_init(elem);
|
|
if *redim {
|
|
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
|
|
} else if global && self.common_arrays.contains(&s) {
|
|
ctx.emit(Instr::CommonArr(global, s, dims.len() as u8, init));
|
|
} else {
|
|
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
|
}
|
|
}
|
|
HStmtKind::Erase(slots) => {
|
|
for slot in slots {
|
|
let (global, s) = slot_parts(*slot);
|
|
ctx.emit(Instr::EraseSlot(global, s));
|
|
}
|
|
}
|
|
HStmtKind::End => ctx.emit(Instr::SystemInstr),
|
|
HStmtKind::Stop => ctx.emit(Instr::StopInstr),
|
|
HStmtKind::System => ctx.emit(Instr::SystemInstr),
|
|
HStmtKind::Unsupported(name) => {
|
|
let idx = self.pool(name);
|
|
ctx.emit(Instr::Unsupported(idx));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn gen_for(
|
|
&mut self,
|
|
ctx: &mut ProcCtx,
|
|
proc: &hir::HProc,
|
|
var: &HPlace,
|
|
ty: NumTy,
|
|
from: &HExpr,
|
|
to: &HExpr,
|
|
step: Option<&HExpr>,
|
|
limit_slot: VarSlot,
|
|
step_slot: Option<VarSlot>,
|
|
body: &[HStmt],
|
|
exit_label: u16,
|
|
end_pos: tb_frontend::SourcePos,
|
|
) {
|
|
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
|
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
|
self.expr(ctx, to);
|
|
emit_store_slot(ctx, limit_slot);
|
|
if let (Some(step_e), Some(sslot)) = (step, step_slot) {
|
|
self.expr(ctx, step_e);
|
|
emit_store_slot(ctx, sslot);
|
|
}
|
|
let const_step = match step {
|
|
None => Some(1.0),
|
|
Some(e) => hir::literal_value(e),
|
|
};
|
|
|
|
let l_test = ctx.new_label();
|
|
let l_body = ctx.new_label();
|
|
ctx.bind(l_test);
|
|
match const_step {
|
|
Some(s) => {
|
|
// Vergleichsrichtung zur Compilezeit.
|
|
let op = if s >= 0.0 { CmpOp::Le } else { CmpOp::Ge };
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, op));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
}
|
|
None => {
|
|
// Vorzeichen des Schritts zur Laufzeit prüfen.
|
|
let sslot = step_slot.expect("dynamischer STEP ohne Slot");
|
|
let l_neg = ctx.new_label();
|
|
emit_load_slot(ctx, sslot);
|
|
push_zero(ctx, ty);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Ge));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_neg);
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Le));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
ctx.emit_jump(Instr::Jump(0), l_body);
|
|
ctx.bind(l_neg);
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Ge));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
}
|
|
}
|
|
ctx.bind(l_body);
|
|
for s in body {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
// NEXT: eigene Quellgrenze für Fehler, RESUME und Debugger.
|
|
ctx.pos = end_pos;
|
|
ctx.emit(Instr::Stmt(end_pos.line));
|
|
self.store_place(ctx, var, |cg, ctx| {
|
|
cg.load_place(ctx, var);
|
|
match (step, step_slot) {
|
|
(Some(e), None) => cg.expr(ctx, e), // konstanter STEP
|
|
(Some(_), Some(sslot)) => emit_load_slot(ctx, sslot),
|
|
(None, _) => push_one(ctx, ty),
|
|
}
|
|
ctx.emit(add_instr(ty));
|
|
});
|
|
ctx.emit_jump(Instr::Jump(0), l_test);
|
|
ctx.bind(exit_label);
|
|
}
|
|
|
|
// ---- Ausdrücke ---------------------------------------------------------
|
|
|
|
fn expr(&mut self, ctx: &mut ProcCtx, e: &HExpr) {
|
|
match e {
|
|
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
|
|
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
|
|
HExpr::UdtId(id) => ctx.emit(Instr::PushUdtId(*id)),
|
|
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
|
|
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
|
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
|
HExpr::Str(s) => {
|
|
let idx = self.pool(s);
|
|
ctx.emit(Instr::PushStr(idx));
|
|
}
|
|
HExpr::Load(p) => self.load_place(ctx, p),
|
|
HExpr::ObjectProperty {
|
|
object,
|
|
index,
|
|
property,
|
|
..
|
|
} => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
ctx.emit(Instr::LoadObjectProperty(
|
|
*object,
|
|
*property,
|
|
index.is_some(),
|
|
));
|
|
}
|
|
HExpr::ObjectIndexedProperty {
|
|
object,
|
|
object_index,
|
|
property,
|
|
index,
|
|
..
|
|
} => {
|
|
if let Some(object_index) = object_index {
|
|
self.expr(ctx, object_index);
|
|
}
|
|
self.expr(ctx, index);
|
|
ctx.emit(Instr::LoadObjectIndexedProperty(
|
|
*object,
|
|
*property | if object_index.is_some() { 0x8000 } else { 0 },
|
|
));
|
|
}
|
|
HExpr::ObjectMethodCall {
|
|
object,
|
|
index,
|
|
method,
|
|
args,
|
|
..
|
|
} => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
for arg in args {
|
|
self.expr(ctx, arg);
|
|
}
|
|
ctx.emit(Instr::ObjectMethodFn(
|
|
*object,
|
|
*method,
|
|
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
|
|
));
|
|
}
|
|
HExpr::DynamicObjectProperty { object, property } => {
|
|
self.expr(ctx, object);
|
|
let property = self.pool(property);
|
|
ctx.emit(Instr::LoadDynamicObjectProperty(property));
|
|
}
|
|
HExpr::ObjectRef { object, index, .. } => {
|
|
if let Some(index) = index {
|
|
self.expr(ctx, index);
|
|
}
|
|
ctx.emit(Instr::PushObject(*object, index.is_some()));
|
|
}
|
|
HExpr::TypeOf { value, class } => {
|
|
self.expr(ctx, value);
|
|
ctx.emit(Instr::TypeOf(class.id()));
|
|
}
|
|
HExpr::Conv { from, to, arg } => {
|
|
self.expr(ctx, arg);
|
|
emit_conv(ctx, *from, *to);
|
|
}
|
|
HExpr::FixStr { len, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(Instr::FixStr(*len));
|
|
}
|
|
HExpr::Neg { ty, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::NegI2,
|
|
NumTy::Lng => Instr::NegI4,
|
|
NumTy::Sng => Instr::NegR4,
|
|
NumTy::Dbl => Instr::NegR8,
|
|
NumTy::Cur => Instr::NegCy,
|
|
});
|
|
}
|
|
HExpr::Bin { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(arith_instr(*op, *ty));
|
|
}
|
|
HExpr::Not { ty, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(match ty {
|
|
IntKind::I2 => Instr::NotI2,
|
|
IntKind::I4 => Instr::NotI4,
|
|
});
|
|
}
|
|
HExpr::Logic { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(logic_instr(*op, *ty));
|
|
}
|
|
HExpr::Cmp { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
let cop = cmp_op(*op);
|
|
ctx.emit(match ty {
|
|
CmpKind::Num(NumTy::Int) => Instr::CmpI2(cop),
|
|
CmpKind::Num(NumTy::Lng) => Instr::CmpI4(cop),
|
|
CmpKind::Num(NumTy::Sng) => Instr::CmpR4(cop),
|
|
CmpKind::Num(NumTy::Dbl) => Instr::CmpR8(cop),
|
|
CmpKind::Num(NumTy::Cur) => Instr::CmpCy(cop),
|
|
CmpKind::Str => Instr::CmpStr(cop),
|
|
});
|
|
}
|
|
HExpr::Concat(l, r) => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(Instr::Concat);
|
|
}
|
|
HExpr::FnCall { proc, args, .. } => {
|
|
for a in args {
|
|
self.arg(ctx, a);
|
|
}
|
|
ctx.emit(Instr::Call(*proc, args.len() as u8));
|
|
}
|
|
HExpr::Builtin { b, args, .. } => {
|
|
for a in args {
|
|
self.expr(ctx, a);
|
|
}
|
|
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
|
|
ctx.emit(i);
|
|
return;
|
|
}
|
|
ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8));
|
|
}
|
|
HExpr::ArrayBound { lower, place, dim } => {
|
|
self.load_array_handle(ctx, place);
|
|
self.expr(ctx, dim);
|
|
ctx.emit(Instr::ArrBound(*lower));
|
|
}
|
|
HExpr::Err => ctx.emit(Instr::LoadErr),
|
|
HExpr::Erl => ctx.emit(Instr::LoadErl),
|
|
HExpr::Unsupported(name) => {
|
|
let idx = self.pool(name);
|
|
ctx.emit(Instr::Unsupported(idx));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn arg(&mut self, ctx: &mut ProcCtx, a: &HArg) {
|
|
match a {
|
|
HArg::ByVal(e) => self.expr(ctx, e),
|
|
HArg::ByRef(p) => self.make_ref(ctx, p),
|
|
HArg::ArrayRef(p) => self.load_array_handle(ctx, p),
|
|
}
|
|
}
|
|
|
|
// ---- Plätze (L-Werte) ----------------------------------------------------
|
|
|
|
/// Array-Handle eines Platzes laden (mit Auto-DIM-Information).
|
|
fn load_array_handle(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
let (elem, dims) = match &p.array_elem {
|
|
Some((t, d)) => (type_init(t), *d),
|
|
None => (type_init(&p.ty), 1),
|
|
};
|
|
ctx.emit(Instr::LoadArr(global, slot, dims, elem));
|
|
}
|
|
|
|
fn load_place(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields {
|
|
ctx.emit(Instr::LoadField(*f));
|
|
}
|
|
}
|
|
|
|
/// Platz speichern; `value` emittiert den Wert auf den Stack.
|
|
fn store_place(
|
|
&mut self,
|
|
ctx: &mut ProcCtx,
|
|
p: &HPlace,
|
|
value: impl FnOnce(&mut Self, &mut ProcCtx),
|
|
) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.fields.is_empty() {
|
|
// Basis-Handle (ggf. Element) laden, Feldpfad bis vorletzte Ebene.
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields[..p.fields.len() - 1] {
|
|
ctx.emit(Instr::LoadField(*f));
|
|
}
|
|
value(self, ctx);
|
|
ctx.emit(Instr::StoreField(*p.fields.last().unwrap()));
|
|
return;
|
|
}
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
value(self, ctx);
|
|
ctx.emit(Instr::StoreElem(p.indices.len() as u8));
|
|
return;
|
|
}
|
|
value(self, ctx);
|
|
if matches!(p.ty, HTy::Udt(_)) {
|
|
// UDT-Zuweisung kopiert Inhalte (Wertsemantik): Ziel-Handle
|
|
// laden und Quellfelder hineinkopieren.
|
|
if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
ctx.emit(Instr::CopyRec);
|
|
return;
|
|
}
|
|
if p.base_is_ref {
|
|
ctx.emit(Instr::StoreRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::StoreGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::StoreLocal(slot));
|
|
}
|
|
}
|
|
|
|
/// Referenz auf einen Platz erzeugen (BYREF-Argumente, INPUT-Ziele).
|
|
fn make_ref(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.indices.is_empty() && p.fields.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::MakeRefElem(p.indices.len() as u8));
|
|
return;
|
|
}
|
|
if !p.fields.is_empty() {
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields {
|
|
ctx.emit(Instr::MakeRefField(*f));
|
|
}
|
|
return;
|
|
}
|
|
if p.base_is_ref {
|
|
// Referenz weiterreichen: der Slot enthält bereits eine Referenz.
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::MakeRefGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::MakeRefLocal(slot));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Instruktionsauswahl-Hilfen ----------------------------------------------
|
|
|
|
fn slot_parts(s: VarSlot) -> (bool, u16) {
|
|
match s {
|
|
VarSlot::Global(i) => (true, i),
|
|
VarSlot::Local(i) => (false, i),
|
|
}
|
|
}
|
|
|
|
fn emit_store_slot(ctx: &mut ProcCtx, s: VarSlot) {
|
|
match s {
|
|
VarSlot::Global(i) => ctx.emit(Instr::StoreGlobal(i)),
|
|
VarSlot::Local(i) => ctx.emit(Instr::StoreLocal(i)),
|
|
}
|
|
}
|
|
|
|
fn emit_load_slot(ctx: &mut ProcCtx, s: VarSlot) {
|
|
match s {
|
|
VarSlot::Global(i) => ctx.emit(Instr::LoadGlobal(i)),
|
|
VarSlot::Local(i) => ctx.emit(Instr::LoadLocal(i)),
|
|
}
|
|
}
|
|
|
|
fn push_zero(ctx: &mut ProcCtx, ty: NumTy) {
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::PushInt(0),
|
|
NumTy::Lng => Instr::PushLng(0),
|
|
NumTy::Sng => Instr::PushSng(0.0),
|
|
NumTy::Dbl => Instr::PushDbl(0.0),
|
|
NumTy::Cur => Instr::PushCur(0),
|
|
});
|
|
}
|
|
|
|
fn push_one(ctx: &mut ProcCtx, ty: NumTy) {
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::PushInt(1),
|
|
NumTy::Lng => Instr::PushLng(1),
|
|
NumTy::Sng => Instr::PushSng(1.0),
|
|
NumTy::Dbl => Instr::PushDbl(1.0),
|
|
NumTy::Cur => Instr::PushCur(10_000),
|
|
});
|
|
}
|
|
|
|
fn add_instr(ty: NumTy) -> Instr {
|
|
match ty {
|
|
NumTy::Int => Instr::AddI2,
|
|
NumTy::Lng => Instr::AddI4,
|
|
NumTy::Sng => Instr::AddR4,
|
|
NumTy::Dbl => Instr::AddR8,
|
|
NumTy::Cur => Instr::AddCy,
|
|
}
|
|
}
|
|
|
|
fn arith_instr(op: HArith, ty: NumTy) -> Instr {
|
|
use NumTy::*;
|
|
match (op, ty) {
|
|
(HArith::Add, Int) => Instr::AddI2,
|
|
(HArith::Add, Lng) => Instr::AddI4,
|
|
(HArith::Add, Sng) => Instr::AddR4,
|
|
(HArith::Add, Dbl) => Instr::AddR8,
|
|
(HArith::Add, Cur) => Instr::AddCy,
|
|
(HArith::Sub, Int) => Instr::SubI2,
|
|
(HArith::Sub, Lng) => Instr::SubI4,
|
|
(HArith::Sub, Sng) => Instr::SubR4,
|
|
(HArith::Sub, Dbl) => Instr::SubR8,
|
|
(HArith::Sub, Cur) => Instr::SubCy,
|
|
(HArith::Mul, Int) => Instr::MulI2,
|
|
(HArith::Mul, Lng) => Instr::MulI4,
|
|
(HArith::Mul, Sng) => Instr::MulR4,
|
|
(HArith::Mul, Dbl) => Instr::MulR8,
|
|
(HArith::Mul, Cur) => Instr::MulCy,
|
|
(HArith::Div, Sng) => Instr::DivR4,
|
|
(HArith::Div, Dbl) => Instr::DivR8,
|
|
(HArith::IDiv, Int) => Instr::IDivI2,
|
|
(HArith::IDiv, Lng) => Instr::IDivI4,
|
|
(HArith::Mod, Int) => Instr::ModI2,
|
|
(HArith::Mod, Lng) => Instr::ModI4,
|
|
(HArith::Pow, Dbl) => Instr::PowR8,
|
|
(op, ty) => unreachable!("arith {op:?} auf {ty:?}"),
|
|
}
|
|
}
|
|
|
|
fn logic_instr(op: HLogic, ty: IntKind) -> Instr {
|
|
match (op, ty) {
|
|
(HLogic::And, IntKind::I2) => Instr::AndI2,
|
|
(HLogic::And, IntKind::I4) => Instr::AndI4,
|
|
(HLogic::Or, IntKind::I2) => Instr::OrI2,
|
|
(HLogic::Or, IntKind::I4) => Instr::OrI4,
|
|
(HLogic::Xor, IntKind::I2) => Instr::XorI2,
|
|
(HLogic::Xor, IntKind::I4) => Instr::XorI4,
|
|
(HLogic::Eqv, IntKind::I2) => Instr::EqvI2,
|
|
(HLogic::Eqv, IntKind::I4) => Instr::EqvI4,
|
|
(HLogic::Imp, IntKind::I2) => Instr::ImpI2,
|
|
(HLogic::Imp, IntKind::I4) => Instr::ImpI4,
|
|
}
|
|
}
|
|
|
|
fn cmp_op(op: HCmp) -> CmpOp {
|
|
match op {
|
|
HCmp::Eq => CmpOp::Eq,
|
|
HCmp::Ne => CmpOp::Ne,
|
|
HCmp::Lt => CmpOp::Lt,
|
|
HCmp::Le => CmpOp::Le,
|
|
HCmp::Gt => CmpOp::Gt,
|
|
HCmp::Ge => CmpOp::Ge,
|
|
}
|
|
}
|
|
|
|
fn cmp_instr(ty: NumTy, op: CmpOp) -> Instr {
|
|
match ty {
|
|
NumTy::Int => Instr::CmpI2(op),
|
|
NumTy::Lng => Instr::CmpI4(op),
|
|
NumTy::Sng => Instr::CmpR4(op),
|
|
NumTy::Dbl => Instr::CmpR8(op),
|
|
NumTy::Cur => Instr::CmpCy(op),
|
|
}
|
|
}
|
|
|
|
fn emit_conv(ctx: &mut ProcCtx, from: NumTy, to: NumTy) {
|
|
use NumTy::*;
|
|
if from == to {
|
|
return;
|
|
}
|
|
ctx.emit(match (from, to) {
|
|
(Int, Lng) => Instr::ConvI2I4,
|
|
(Int, Sng) => Instr::ConvI2R4,
|
|
(Int, Dbl) => Instr::ConvI2R8,
|
|
(Int, Cur) => Instr::ConvI2Cy,
|
|
(Lng, Int) => Instr::ConvI4I2,
|
|
(Lng, Sng) => Instr::ConvI4R4,
|
|
(Lng, Dbl) => Instr::ConvI4R8,
|
|
(Lng, Cur) => Instr::ConvI4Cy,
|
|
(Sng, Int) => Instr::ConvR4I2,
|
|
(Sng, Lng) => Instr::ConvR4I4,
|
|
(Sng, Dbl) => Instr::ConvR4R8,
|
|
(Sng, Cur) => Instr::ConvR4Cy,
|
|
(Dbl, Int) => Instr::ConvR8I2,
|
|
(Dbl, Lng) => Instr::ConvR8I4,
|
|
(Dbl, Sng) => Instr::ConvR8R4,
|
|
(Dbl, Cur) => Instr::ConvR8Cy,
|
|
(Cur, Int) => Instr::ConvCyI2,
|
|
(Cur, Lng) => Instr::ConvCyI4,
|
|
(Cur, Sng) => Instr::ConvCyR4,
|
|
(Cur, Dbl) => Instr::ConvCyR8,
|
|
_ => unreachable!(),
|
|
});
|
|
}
|
|
|
|
/// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit.
|
|
/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege.
|
|
/// `DOEVENTS`/`SLEEP` → eigene Instruktion statt Builtin-Aufruf.
|
|
fn zustellpunkt_instr(b: Builtin, argc: usize) -> Option<Instr> {
|
|
match b {
|
|
Builtin::Doevents => Some(Instr::Doevents),
|
|
Builtin::Sleep => Some(Instr::Sleep(argc > 0)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn builtin_id(b: Builtin) -> u16 {
|
|
match b {
|
|
Builtin::Len => ids::LEN,
|
|
Builtin::LeftS => ids::LEFT_S,
|
|
Builtin::RightS => ids::RIGHT_S,
|
|
Builtin::MidS => ids::MID_S,
|
|
Builtin::InstrF => ids::INSTR,
|
|
Builtin::UcaseS => ids::UCASE_S,
|
|
Builtin::LcaseS => ids::LCASE_S,
|
|
Builtin::LtrimS => ids::LTRIM_S,
|
|
Builtin::RtrimS => ids::RTRIM_S,
|
|
Builtin::SpaceS => ids::SPACE_S,
|
|
Builtin::StringS => ids::STRING_S,
|
|
Builtin::ChrS => ids::CHR_S,
|
|
Builtin::Asc => ids::ASC,
|
|
Builtin::StrS => ids::STR_S,
|
|
Builtin::Val => ids::VAL,
|
|
Builtin::HexS => ids::HEX_S,
|
|
Builtin::OctS => ids::OCT_S,
|
|
Builtin::MidAssign => ids::MID_ASSIGN,
|
|
Builtin::Abs => ids::ABS,
|
|
Builtin::Sgn => ids::SGN,
|
|
Builtin::IntF => ids::INT_F,
|
|
Builtin::Fix => ids::FIX,
|
|
Builtin::Sqr => ids::SQR,
|
|
Builtin::Exp => ids::EXP,
|
|
Builtin::Log => ids::LOG,
|
|
Builtin::Sin => ids::SIN,
|
|
Builtin::Cos => ids::COS,
|
|
Builtin::Tan => ids::TAN,
|
|
Builtin::Atn => ids::ATN,
|
|
Builtin::Rnd => ids::RND,
|
|
Builtin::Randomize => ids::RANDOMIZE,
|
|
Builtin::PrintVal => ids::PRINT_VAL,
|
|
Builtin::PrintStrLit => ids::PRINT_STR_LIT,
|
|
Builtin::PrintComma => ids::PRINT_COMMA,
|
|
Builtin::PrintTab => ids::PRINT_TAB,
|
|
Builtin::PrintSpc => ids::PRINT_SPC,
|
|
Builtin::PrintNewline => ids::PRINT_NEWLINE,
|
|
Builtin::PrintUsing => ids::PRINT_USING,
|
|
Builtin::FormatS => ids::FORMAT_S,
|
|
Builtin::SetFormatCc => ids::SET_FORMAT_CC,
|
|
Builtin::Cls => ids::CLS,
|
|
Builtin::Color => ids::COLOR,
|
|
Builtin::Locate => ids::LOCATE,
|
|
Builtin::Width => ids::WIDTH,
|
|
Builtin::ViewPrint => ids::VIEW_PRINT,
|
|
Builtin::ScreenStmt => ids::SCREEN_STMT,
|
|
Builtin::GraphicsLine => ids::GRAPHICS_LINE,
|
|
Builtin::GraphicsPaint => ids::GRAPHICS_PAINT,
|
|
Builtin::GraphicsView => ids::GRAPHICS_VIEW,
|
|
Builtin::KeyAssign => ids::KEY_ASSIGN,
|
|
Builtin::KeyList => ids::KEY_LIST,
|
|
Builtin::KeyDisplay => ids::KEY_DISPLAY,
|
|
Builtin::Csrlin => ids::CSRLIN,
|
|
Builtin::PosFn => ids::POS_FN,
|
|
Builtin::ScreenFn => ids::SCREEN_FN,
|
|
Builtin::InkeyS => ids::INKEY_S,
|
|
Builtin::InputS => ids::INPUT_S,
|
|
Builtin::EnvironS => ids::ENVIRON_S,
|
|
Builtin::EnvironSet => ids::ENVIRON_SET,
|
|
Builtin::Fre => ids::FRE,
|
|
Builtin::Clear => ids::CLEAR,
|
|
Builtin::Tron => ids::TRON,
|
|
Builtin::Troff => ids::TROFF,
|
|
Builtin::StackFn => ids::STACK_FN,
|
|
Builtin::StackStmt => ids::STACK_STMT,
|
|
Builtin::Erdev => ids::ERDEV,
|
|
Builtin::ErdevS => ids::ERDEV_S,
|
|
Builtin::Open => ids::OPEN,
|
|
Builtin::Close => ids::CLOSE,
|
|
Builtin::CloseAll => ids::CLOSE_ALL,
|
|
Builtin::PrintZiel => ids::PRINT_ZIEL,
|
|
Builtin::WriteFile => ids::WRITE_FILE,
|
|
Builtin::EofF => ids::EOF_F,
|
|
Builtin::LofF => ids::LOF_F,
|
|
Builtin::LocF => ids::LOC_F,
|
|
Builtin::SeekF => ids::SEEK_F,
|
|
Builtin::SeekStmt => ids::SEEK_STMT,
|
|
Builtin::Freefile => ids::FREEFILE,
|
|
Builtin::Fileattr => ids::FILEATTR,
|
|
Builtin::LockStmt => ids::LOCK_STMT,
|
|
Builtin::Kill => ids::KILL,
|
|
Builtin::NameStmt => ids::NAME_STMT,
|
|
Builtin::Files => ids::FILES,
|
|
Builtin::Chdir => ids::CHDIR,
|
|
Builtin::Chdrive => ids::CHDRIVE,
|
|
Builtin::Mkdir => ids::MKDIR,
|
|
Builtin::Rmdir => ids::RMDIR,
|
|
Builtin::CurdirS => ids::CURDIR_S,
|
|
Builtin::DirS => ids::DIR_S,
|
|
Builtin::Lpos => ids::LPOS,
|
|
Builtin::ShellStmt => ids::SHELL_STMT,
|
|
Builtin::ShellFn => ids::SHELL_FN,
|
|
Builtin::MkS => ids::MK_S,
|
|
Builtin::CvF => ids::CV_F,
|
|
Builtin::Fv => ids::FV,
|
|
Builtin::Pv => ids::PV,
|
|
Builtin::Pmt => ids::PMT,
|
|
Builtin::NPer => ids::NPER,
|
|
Builtin::IPmt => ids::IPMT,
|
|
Builtin::PPmt => ids::PPMT,
|
|
Builtin::Rate => ids::RATE,
|
|
Builtin::Npv => ids::NPV,
|
|
Builtin::Irr => ids::IRR,
|
|
Builtin::Mirr => ids::MIRR,
|
|
Builtin::Sln => ids::SLN,
|
|
Builtin::Syd => ids::SYD,
|
|
Builtin::Ddb => ids::DDB,
|
|
Builtin::Timer => ids::TIMER,
|
|
Builtin::DateS => ids::DATE_S,
|
|
Builtin::TimeS => ids::TIME_S,
|
|
Builtin::DateSet => ids::DATE_SET,
|
|
Builtin::TimeSet => ids::TIME_SET,
|
|
Builtin::Now => ids::NOW,
|
|
Builtin::TimezoneKnown => ids::TIMEZONEKNOWN,
|
|
Builtin::DateSerial => ids::DATE_SERIAL,
|
|
Builtin::TimeSerial => ids::TIME_SERIAL,
|
|
Builtin::DateValue => ids::DATE_VALUE,
|
|
Builtin::TimeValue => ids::TIME_VALUE,
|
|
Builtin::DayF => ids::DAY_F,
|
|
Builtin::MonthF => ids::MONTH_F,
|
|
Builtin::YearF => ids::YEAR_F,
|
|
Builtin::WeekdayF => ids::WEEKDAY_F,
|
|
Builtin::HourF => ids::HOUR_F,
|
|
Builtin::MinuteF => ids::MINUTE_F,
|
|
Builtin::SecondF => ids::SECOND_F,
|
|
Builtin::CommandS => ids::COMMAND_S,
|
|
Builtin::Doevents => ids::DOEVENTS,
|
|
Builtin::Sleep => ids::SLEEP,
|
|
Builtin::SetUEvent => ids::SETUEVENT,
|
|
Builtin::Beep => ids::BEEP,
|
|
// ISAM
|
|
Builtin::IsamOpen => ids::ISAM_OPEN,
|
|
Builtin::IsamCreateIndex => ids::ISAM_CREATE_INDEX,
|
|
Builtin::IsamDeleteIndex => ids::ISAM_DELETE_INDEX,
|
|
Builtin::IsamSetIndex => ids::ISAM_SET_INDEX,
|
|
Builtin::IsamGetIndexS => ids::ISAM_GET_INDEX_S,
|
|
Builtin::IsamInsert => ids::ISAM_INSERT,
|
|
Builtin::IsamRetrieve => ids::ISAM_RETRIEVE,
|
|
Builtin::IsamUpdate => ids::ISAM_UPDATE,
|
|
Builtin::IsamDelete => ids::ISAM_DELETE,
|
|
Builtin::IsamDeleteTable => ids::ISAM_DELETE_TABLE,
|
|
Builtin::IsamMoveFirst => ids::ISAM_MOVE_FIRST,
|
|
Builtin::IsamMoveLast => ids::ISAM_MOVE_LAST,
|
|
Builtin::IsamMoveNext => ids::ISAM_MOVE_NEXT,
|
|
Builtin::IsamMovePrevious => ids::ISAM_MOVE_PREVIOUS,
|
|
Builtin::IsamSeekEq => ids::ISAM_SEEK_EQ,
|
|
Builtin::IsamSeekGt => ids::ISAM_SEEK_GT,
|
|
Builtin::IsamSeekGe => ids::ISAM_SEEK_GE,
|
|
Builtin::IsamBeginTrans => ids::ISAM_BEGIN_TRANS,
|
|
Builtin::IsamCommitTrans => ids::ISAM_COMMIT_TRANS,
|
|
Builtin::IsamRollback => ids::ISAM_ROLLBACK,
|
|
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
|
|
Builtin::IsamSetmem => ids::ISAM_SETMEM,
|
|
Builtin::IsamBof => ids::ISAM_BOF,
|
|
Builtin::MsgBox => ids::MSGBOX,
|
|
Builtin::InputBoxS => ids::INPUTBOX_S,
|
|
Builtin::ClipboardAdd => ids::CLIPBOARD_ADD,
|
|
Builtin::ClipboardGet => ids::CLIPBOARD_GET,
|
|
}
|
|
}
|
|
|
|
/// Liefert der Builtin in Anweisungsposition einen Wert (→ `Pop`)?
|
|
fn builtin_returns_value(b: Builtin) -> bool {
|
|
matches!(b, Builtin::Doevents)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn compile_src(src: &str) -> CompiledModule {
|
|
let a = tb_frontend::analyze_source("TEST", src);
|
|
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
|
|
compile(&a.hir.unwrap())
|
|
}
|
|
|
|
fn main_code(m: &CompiledModule) -> &[Instr] {
|
|
&m.procs[0].code
|
|
}
|
|
|
|
#[test]
|
|
fn ausdruck_monomorph_mit_conv() {
|
|
// d# = i% + 1.5# → LoadGlobal, ConvI2R8, PushDbl, AddR8, StoreGlobal
|
|
let m = compile_src("i% = 2\nd# = i% + 1.5#");
|
|
let code = main_code(&m);
|
|
let want = [
|
|
Instr::LoadGlobal(0),
|
|
Instr::ConvI2R8,
|
|
Instr::PushDbl(1.5),
|
|
Instr::AddR8,
|
|
Instr::StoreGlobal(1),
|
|
];
|
|
assert!(
|
|
code.windows(want.len()).any(|w| w == want),
|
|
"erwartete Sequenz nicht gefunden: {code:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vergleich_und_logik() {
|
|
let m = compile_src("a% = 1\nb% = a% > 0 AND a% < 5");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Gt)));
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Lt)));
|
|
assert!(code.contains(&Instr::AndI2));
|
|
}
|
|
|
|
#[test]
|
|
fn string_konkatenation() {
|
|
let m = compile_src("s$ = \"a\" + \"b\"");
|
|
assert!(main_code(&m).contains(&Instr::Concat));
|
|
}
|
|
|
|
#[test]
|
|
fn if_mit_fixup() {
|
|
let m = compile_src("IF 1 THEN\nPRINT \"a\"\nELSE\nPRINT \"b\"\nEND IF");
|
|
let code = main_code(&m);
|
|
// Es gibt einen bedingten Sprung und einen unbedingten, beide gepatcht (≠ 0).
|
|
let jf = code.iter().find_map(|i| match i {
|
|
Instr::JumpIfFalse(t) => Some(*t),
|
|
_ => None,
|
|
});
|
|
assert!(jf.is_some() && jf.unwrap() > 0, "{code:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn for_mit_konstantem_step() {
|
|
let m = compile_src("FOR i% = 1 TO 3\nPRINT i%\nNEXT");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Le)), "{code:?}");
|
|
assert!(code.contains(&Instr::AddI2));
|
|
// Kein Laufzeit-Vorzeichentest bei konstantem Schritt:
|
|
assert!(!code.contains(&Instr::CmpI2(CmpOp::Ge)));
|
|
}
|
|
|
|
#[test]
|
|
fn for_mit_dynamischem_step() {
|
|
let m = compile_src("s% = -1\nFOR i% = 3 TO 1 STEP s%\nNEXT");
|
|
let code = main_code(&m);
|
|
// Vorzeichentest → beide Vergleichsrichtungen vorhanden
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Le)));
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Ge)));
|
|
}
|
|
|
|
#[test]
|
|
fn gosub_und_on_goto() {
|
|
let m = compile_src("GOSUB U\nON 2 GOTO A, B\nA:\nB:\nU:\nRETURN");
|
|
let code = main_code(&m);
|
|
assert!(code.iter().any(|i| matches!(i, Instr::Gosub(_))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::OnJump(0, false))));
|
|
assert_eq!(m.jump_tables.len(), 1);
|
|
assert_eq!(m.jump_tables[0].len(), 2);
|
|
assert!(code.contains(&Instr::RetGosub));
|
|
}
|
|
|
|
#[test]
|
|
fn prozedur_und_byref() {
|
|
let m = compile_src("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::MakeRefGlobal(0)));
|
|
assert!(
|
|
code.iter()
|
|
.filter(|i| matches!(i, Instr::Call(1, 1)))
|
|
.count()
|
|
== 2
|
|
);
|
|
// Prozedurrumpf liest/schreibt über Referenz
|
|
let sub = &m.procs[1].code;
|
|
assert!(sub.contains(&Instr::LoadRef(0)));
|
|
assert!(sub.contains(&Instr::StoreRef(0)));
|
|
assert!(sub.last() == Some(&Instr::RetProc));
|
|
}
|
|
|
|
#[test]
|
|
fn function_liefert_wert() {
|
|
let m = compile_src("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
|
|
let f = &m.procs[1].code;
|
|
assert!(f.contains(&Instr::RetFn));
|
|
let code = main_code(&m);
|
|
assert!(code.iter().any(|i| matches!(i, Instr::Call(1, 1))));
|
|
}
|
|
|
|
#[test]
|
|
fn arrays_dim_und_zugriff() {
|
|
let m = compile_src("DIM a%(10)\na%(3) = 7\nPRINT a%(3)");
|
|
let code = main_code(&m);
|
|
assert!(code
|
|
.iter()
|
|
.any(|i| matches!(i, Instr::DimArr(true, 0, 1, TypeInit::Int))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::StoreElem(1))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::LoadElem(1))));
|
|
}
|
|
|
|
#[test]
|
|
fn data_read_restore() {
|
|
let m = compile_src("DATA 1, 2\nREAD a%, b%\nRESTORE\nREAD c%");
|
|
let code = main_code(&m);
|
|
assert_eq!(m.data.len(), 2);
|
|
assert!(
|
|
code.iter()
|
|
.filter(|i| matches!(i, Instr::ReadData(1)))
|
|
.count()
|
|
== 3
|
|
);
|
|
assert!(code.contains(&Instr::Restore(0)));
|
|
assert!(code.contains(&Instr::ConvR8I2));
|
|
}
|
|
|
|
#[test]
|
|
fn fehlerbehandlung_emit() {
|
|
let m = compile_src("ON ERROR GOTO H\nERROR 5\nEND\nH:\nRESUME NEXT");
|
|
let code = main_code(&m);
|
|
assert!(code
|
|
.iter()
|
|
.any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
|
|
assert!(code.contains(&Instr::RaiseError));
|
|
assert!(code.contains(&Instr::ResumeNext));
|
|
}
|
|
|
|
#[test]
|
|
fn tbc_roundtrip_ueber_codegen() {
|
|
let m = compile_src("PRINT \"Hallo\"");
|
|
let bytes = m.to_tbc();
|
|
let back = CompiledModule::from_tbc(&bytes).unwrap();
|
|
assert_eq!(back.procs[0].code, m.procs[0].code);
|
|
}
|
|
}
|