Files
TerminalBasic/crates/tb-frontend/src/sema.rs

5873 lines
222 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Semantische Analyse **und** HIR-Lowering (Design-Entscheidung D1 der
//! Phase-2-Änderung): Symboltabellen, implizite Deklaration,
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl.
//! UDT-Feldtypen, Konstantenfaltung, Builtin-Signaturen, Label-Prüfung,
//! Compile-Zeit-Abweisung deklarierter Non-Features — und in demselben
//! Durchlauf die Erzeugung des typisierten HIR (Slots statt Namen,
//! explizite Konvertierungsknoten nach der Matrix in docs/tbvm-design.md).
//!
//! Ein vollständiges HIR ist nur bei diagnose-freier Analyse garantiert;
//! bei Fehlern werden betroffene Konstrukte bestmöglich übersprungen.
use crate::ast::*;
use crate::forms::{self, FormCatalog, ObjectClass, PropertyType};
use crate::hir::{
self, Builtin, HArg, HExpr, HPlace, HPrintItem, HStmt, HStmtKind, HTy, IntKind, LabelId, NumTy,
VarSlot,
};
use crate::lexer::Suffix;
use crate::{Diagnostic, SourcePos};
use std::collections::{hash_map::Entry, HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)]
pub enum Ty {
Int,
Lng,
Cur,
Sng,
Dbl,
Str,
FixedStr(u32),
Udt(String),
Form,
Control,
Unknown,
}
fn is_num(t: &Ty) -> bool {
matches!(
t,
Ty::Int | Ty::Lng | Ty::Cur | Ty::Sng | Ty::Dbl | Ty::Unknown
)
}
fn is_str(t: &Ty) -> bool {
matches!(t, Ty::Str | Ty::FixedStr(_) | Ty::Unknown)
}
/// Numerischer HIR-Typ eines `Ty` (Fallback SINGLE bei `Unknown`).
fn num_ty(t: &Ty) -> NumTy {
match t {
Ty::Int => NumTy::Int,
Ty::Lng => NumTy::Lng,
Ty::Cur => NumTy::Cur,
Ty::Dbl => NumTy::Dbl,
_ => NumTy::Sng,
}
}
fn ty_of_num(n: NumTy) -> Ty {
match n {
NumTy::Int => Ty::Int,
NumTy::Lng => Ty::Lng,
NumTy::Cur => Ty::Cur,
NumTy::Sng => Ty::Sng,
NumTy::Dbl => Ty::Dbl,
}
}
fn rank(n: NumTy) -> u8 {
match n {
NumTy::Int => 1,
NumTy::Lng => 2,
NumTy::Cur => 3,
NumTy::Sng => 4,
NumTy::Dbl => 5,
}
}
/// Promotion nach Matrix: Rangfolge INT < LNG < CUR < SNG < DBL,
/// CURRENCY gemischt mit Gleitkomma ergibt DOUBLE.
fn promote_num(a: NumTy, b: NumTy) -> NumTy {
if a == b {
return a;
}
let hi = if rank(a) >= rank(b) { a } else { b };
let has_cur = a == NumTy::Cur || b == NumTy::Cur;
if has_cur && matches!(hi, NumTy::Sng | NumTy::Dbl) {
NumTy::Dbl
} else {
hi
}
}
fn suffix_ty(s: Suffix) -> Ty {
match s {
Suffix::Integer => Ty::Int,
Suffix::Long => Ty::Lng,
Suffix::Single => Ty::Sng,
Suffix::Double => Ty::Dbl,
Suffix::Str => Ty::Str,
Suffix::Currency => Ty::Cur,
}
}
fn type_name_ty(t: &TypeName) -> Ty {
match t {
TypeName::Integer => Ty::Int,
TypeName::Long => Ty::Lng,
TypeName::Single => Ty::Sng,
TypeName::Double => Ty::Dbl,
TypeName::Currency => Ty::Cur,
TypeName::Str => Ty::Str,
TypeName::FixedStr(n) => Ty::FixedStr((*n).max(0) as u32),
TypeName::Form => Ty::Form,
TypeName::Control => Ty::Control,
TypeName::Udt(n) => Ty::Udt(n.clone()),
}
}
/// Gefalteter Konstantenwert (`CONST`).
#[derive(Debug, Clone, PartialEq)]
pub enum ConstVal {
Num(f64),
Str(String),
}
#[derive(Debug, Clone)]
struct VarInfo {
ty: Ty,
array: bool,
explicit: bool,
slot: u16,
global: bool,
/// Skalar-BYREF-Parameter (Slot enthält eine Referenz).
by_ref: bool,
}
#[derive(Default)]
struct Scope {
vars: HashMap<String, VarInfo>,
labels: HashMap<String, LabelId>,
line_labels: HashMap<u32, LabelId>,
next_label: u16,
/// Frame-Slots (leer im Modul-Scope: dort liegen Variablen global).
locals: Vec<hir::HVar>,
is_module: bool,
/// `DEF FN`: freie Namen binden an Modulvariablen.
is_def_fn: bool,
/// `STATIC`-Prozedur: Locals wandern in globale Slots.
is_static: bool,
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
loop_exits: Vec<(LoopKind, LabelId)>,
current_line: u32,
}
#[derive(PartialEq, Clone, Copy)]
enum LoopKind {
For,
Do,
}
impl Scope {
fn new_label(&mut self) -> LabelId {
let id = self.next_label;
self.next_label += 1;
id
}
}
#[derive(Debug, Clone)]
struct ProcInfo {
kind: ProcKind,
ret: Ty,
params: Vec<(Ty, bool)>, // (Typ, ist Array)
id: u16,
def_fn: bool,
}
// ---- Builtin-Signaturen ----------------------------------------------------
#[derive(Clone, Copy)]
enum ArgK {
N, // numerisch
S, // String
A, // beliebig
R, // Satzvariable eines benutzerdefinierten Typs (ISAM)
}
#[derive(Clone, Copy)]
enum RetK {
I,
L,
Cu,
Sg,
Db,
St,
}
fn ret_ty(r: RetK) -> Ty {
match r {
RetK::I => Ty::Int,
RetK::L => Ty::Lng,
RetK::Cu => Ty::Cur,
RetK::Sg => Ty::Sng,
RetK::Db => Ty::Dbl,
RetK::St => Ty::Str,
}
}
/// Builtin-Funktionen: Name (inkl. Suffix) → (min, max, Argtypen, Rückgabe).
fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
use ArgK::*;
use RetK::*;
Some(match name {
"ABS" | "FIX" | "INT" => (1, 1, &[N], Db),
"SGN" => (1, 1, &[N], I),
"SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => (1, 1, &[N], Db),
"CINT" => (1, 1, &[N], I),
"CLNG" => (1, 1, &[N], L),
"CSNG" => (1, 1, &[N], Sg),
"CDBL" => (1, 1, &[N], Db),
"CCUR" => (1, 1, &[N], Cu),
"RND" => (0, 1, &[N], Sg),
"ASC" => (1, 1, &[S], I),
"CHR$" => (1, 1, &[N], St),
"LEN" => (1, 1, &[A], I),
"LEFT$" | "RIGHT$" => (2, 2, &[S, N], St),
"MID$" => (2, 3, &[S, N, N], St),
"INSTR" => (2, 3, &[A, A, A], I), // Sonderfall, s. lower_builtin_fn
"UCASE$" | "LCASE$" | "LTRIM$" | "RTRIM$" => (1, 1, &[S], St),
"SPACE$" => (1, 1, &[N], St),
"STRING$" => (2, 2, &[N, A], St),
"STR$" => (1, 1, &[N], St),
"VAL" => (1, 1, &[S], Db),
"HEX$" | "OCT$" => (1, 1, &[N], St),
"INKEY$" => (0, 0, &[], St),
"INPUT$" => (1, 2, &[N, N], St),
"LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
"CSRLIN" => (0, 0, &[], I),
"POS" => (1, 1, &[N], I),
"SCREEN" => (2, 3, &[N, N, N], I),
"TAB" | "SPC" => (1, 1, &[N], St),
"DATE$" | "TIME$" => (0, 0, &[], St),
"TIMER" => (0, 0, &[], Sg),
"NOW" => (0, 0, &[], Db),
"DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
"DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
"FORMAT$" => (1, 2, &[A, S], St),
"ERR" | "ERL" => (0, 0, &[], L),
"FRE" => (1, 1, &[A], L),
"EOF" => (1, 1, &[N], I),
"LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
"FREEFILE" => (0, 0, &[], I),
"FILEATTR" => (2, 2, &[N, N], L),
"ENVIRON$" => (1, 1, &[A], St),
"COMMAND$" => (0, 0, &[], St),
"DOEVENTS" => (0, 0, &[], I),
"MSGBOX" => (1, 3, &[S, N, S], I),
"INPUTBOX$" => (1, 5, &[S, S, S, N, N], St),
// Finanzmathematik (Original-Hilfe schreibt sie mit `#`; beide
// Schreibweisen werden angenommen, gerechnet wird in DOUBLE).
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
(5, 5, &[N, N, N, N, N], Db)
}
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db),
"NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db),
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
"SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, N, N, N], Db),
// Record-Konvertierung (Zahl ↔ Bytedarstellung im Feldpuffer)
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" => (1, 1, &[N], St),
"MKSMBF$" | "MKDMBF$" => (1, 1, &[N], St),
"CVI" => (1, 1, &[S], I),
"CVL" => (1, 1, &[S], L),
"CVS" | "CVSMBF" => (1, 1, &[S], Sg),
"CVD" | "CVDMBF" => (1, 1, &[S], Db),
"CVC" => (1, 1, &[S], Cu),
// Dateisystem und System
"CURDIR$" => (0, 1, &[S], St),
"DIR$" => (0, 1, &[S], St),
"LPOS" => (1, 1, &[N], I),
"STACK" => (0, 0, &[], L),
"ERDEV" => (0, 0, &[], I),
"ERDEV$" => (0, 0, &[], St),
// ISAM-Funktionen (Formen siehe umfang-und-signaturen.md des Changes)
"GETINDEX$" => (1, 1, &[N], St),
"BOF" => (1, 1, &[N], I),
"SAVEPOINT" => (0, 0, &[], I),
"SETMEM" => (1, 1, &[N], L),
_ => return None,
})
}
/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
fn banned_feature(name: &str) -> bool {
matches!(
name,
// Hardware-Nähe
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
// Interlanguage-Schnittstelle
| "CALLS" | "SSEG" | "SSEGADD"
| "STRINGADDRESS" | "STRINGASSIGN" | "STRINGLENGTH" | "STRINGRELEASE"
// Overlay-Mechanismus
| "CHAIN"
// Grafik
| "PSET" | "PRESET" | "CIRCLE" | "DRAW" | "PALETTE"
| "PCOPY" | "PMAP" | "WINDOW" | "POINT"
// Event-Geräte (auch als Funktion in `ON COM(1) GOSUB …`)
| "COM" | "PEN" | "STRIG" | "STICK"
// Klang (außer BEEP)
| "SOUND" | "PLAY"
)
}
/// Non-Features, die nicht über ihren Namen abgewiesen werden können, weil
/// sie ihr Schlüsselwort mit einer unterstützten Form teilen. Sie werden an
/// der Syntax erkannt: `GET`/`PUT` mit `(` (Grafik) im Parser und `OPEN` mit
/// einem `COMn:`-Gerätenamen hier in der Semantik.
/// Der Inventar-Abgleich liest diese Liste.
pub const SYNTAKTISCH_ABGEWIESEN: &[&str] =
&["GET (Grafik)", "PUT (Grafik)", "OPEN COM", "DEF SEG"];
/// Ist `s` ein serieller Gerätename (`COM1:` …)? Nur für Stringliterale.
fn ist_com_geraet(s: &str) -> bool {
let up = s.to_ascii_uppercase();
up.strip_prefix("COM")
.and_then(|r| r.chars().next())
.is_some_and(|c| c.is_ascii_digit())
}
/// Builtin-Anweisungen (Bibliothek, keine Keywords).
fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
use ArgK::*;
Some(match name {
"CLS" => (0, 1, &[N]),
"BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
"COLOR" => (0, 3, &[N, N, N]),
"LOCATE" => (0, 5, &[N, N, N, N, N]),
"RANDOMIZE" => (0, 1, &[N]),
"SLEEP" => (0, 1, &[N]),
"WIDTH" => (0, 2, &[N, N]),
"SCREEN" => (1, 4, &[N, N, N, N]),
"SWAP" => (2, 2, &[A, A]),
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
"FILES" => (0, 1, &[S]),
"SHELL" => (0, 1, &[S]),
"RUN" => (0, 1, &[A]),
"CLEAR" => (0, 3, &[N, N, N]),
"KEY" => (1, 2, &[A, S]),
"ENVIRON" => (1, 1, &[S]),
"MSGBOX" => (1, 3, &[S, N, S]),
"SETUEVENT" => (0, 0, &[]),
"CHDRIVE" => (1, 1, &[S]),
"STACK" => (0, 1, &[N]),
// `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
"SETFORMATCC" => (1, 1, &[N]),
// ---- ISAM-Anweisungen -------------------------------------------
// Argumentformen wortgetreu aus der Original-Hilfe; belegt in
// openspec/changes/phase-3-isam/umfang-und-signaturen.md.
//
// CREATEINDEX ist variadisch: Dateinummer, Indexname,
// Eindeutigkeitskennzeichen, dann ein Stringargument je Spalte.
// Die Original-Hilfe nennt keine Obergrenze für die Spaltenzahl, also
// steht hier auch keine; die Argumenttypen ab dem vierten prüft
// `check_and_lower_builtin_args` gesondert.
"CREATEINDEX" => (4, u8::MAX, &[N, S, N, S]),
"DELETEINDEX" => (2, 2, &[N, S]),
// Ohne Indexnamen wird der NULL-Index (Einfügereihenfolge) gesetzt.
"SETINDEX" => (1, 2, &[N, S]),
"INSERT" | "RETRIEVE" | "UPDATE" => (2, 2, &[N, R]),
"DELETE" => (1, 1, &[N]),
"DELETETABLE" => (2, 2, &[S, S]),
"MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS" => (1, 1, &[N]),
// Dateinummer, dann ein Schlüsselwert je Indexspalte.
"SEEKEQ" | "SEEKGT" | "SEEKGE" => (2, 10, &[N, A, A, A, A, A, A, A, A, A]),
"BEGINTRANS" | "COMMITTRANS" => (0, 0, &[]),
"ROLLBACK" => (0, 1, &[N]),
_ => return None,
})
}
// ---- Einstiegspunkte -------------------------------------------------------
/// Nur Diagnosen (Kompatibilitäts-API; nutzt intern das Lowering).
pub fn check(module: &Module) -> Vec<Diagnostic> {
lower(module).1
}
/// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren
/// Diagnosen) und alle Diagnosen.
pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
lower_with_forms(module, &FormCatalog::default())
}
pub fn lower_with_forms(
module: &Module,
catalog: &FormCatalog,
) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
let mut catalog = catalog.clone();
if catalog.find("SCREEN").is_none() {
catalog.add("SCREEN", ObjectClass::Screen, None, false);
}
if module
.body
.iter()
.any(|s| matches!(s, Stmt::MetaForm { .. }))
&& catalog.find(&module.name).is_none()
{
catalog.add(&module.name, ObjectClass::Form, None, false);
}
let mut s = Sema {
diags: Vec::new(),
deftypes: [const { None }; 26],
explicit: false,
option_base: 0,
procs: HashMap::new(),
next_proc_id: 1,
udt_ids: HashMap::new(),
udt_defs: Vec::new(),
consts: HashMap::new(),
module_vars: HashMap::new(),
shared_vars: HashSet::new(),
module_labels: HashMap::new(),
module_line_labels: HashMap::new(),
globals: Vec::new(),
data: Vec::new(),
data_marks_name: HashMap::new(),
data_marks_line: HashMap::new(),
hir_procs: Vec::new(),
isam_typ: HashMap::new(),
forms: catalog,
event_procs: Vec::new(),
};
let hir = s.run(module);
(hir, s.diags)
}
struct Sema {
diags: Vec<Diagnostic>,
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
deftypes: [Option<Ty>; 26],
explicit: bool,
/// `OPTION BASE` (0 oder 1).
option_base: u8,
procs: HashMap<String, ProcInfo>,
next_proc_id: u16,
/// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet.
udt_ids: HashMap<String, u16>,
udt_defs: Vec<hir::HUdt>,
/// Satztyp je ISAM-Dateinummer aus `OPEN … FOR ISAM typ tabelle AS #n`
/// mit literaler Nummer — Grundlage der Satztypprüfung von `INSERT`,
/// `RETRIEVE` und `UPDATE`.
isam_typ: HashMap<i32, String>,
/// Konstanten: Typ und gefalteter Wert.
consts: HashMap<String, (Ty, Option<ConstVal>)>,
/// Modulvariablen (Schlüssel wie `var_key`).
module_vars: HashMap<String, VarInfo>,
/// Mit `DIM SHARED`/`COMMON SHARED` für Prozeduren sichtbare Modulvariablen.
shared_vars: HashSet<String>,
/// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in
/// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds).
module_labels: HashMap<String, LabelId>,
module_line_labels: HashMap<u32, LabelId>,
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
globals: Vec<hir::HVar>,
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
data: Vec<hir::DataItem>,
data_marks_name: HashMap<String, u32>,
data_marks_line: HashMap<u32, u32>,
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
hir_procs: Vec<Option<hir::HProc>>,
forms: FormCatalog,
event_procs: Vec<hir::HEventProc>,
}
impl Sema {
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
self.diags.push(Diagnostic {
pos,
message: msg.into(),
});
}
fn run(&mut self, module: &Module) -> Option<hir::HirModule> {
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
for stmt in &module.body {
match stmt {
Stmt::Declare { sig, .. } => self.register_proc(sig),
Stmt::TypeDecl { name, fields, pos } => {
let mut hfields = Vec::new();
for (fname, ftype) in fields {
if let TypeName::Udt(n) = ftype {
if !self.udt_ids.contains_key(n) {
self.err(*pos, "Type not defined");
}
}
hfields.push((fname.clone(), self.h_ty(&type_name_ty(ftype))));
}
if self.udt_ids.contains_key(name) {
self.err(*pos, "Duplicate definition");
} else {
self.udt_ids
.insert(name.clone(), self.udt_defs.len() as u16);
self.udt_defs.push(hir::HUdt {
name: name.clone(),
fields: hfields,
});
}
}
_ => {}
}
}
for proc in &module.procs {
self.register_proc(&proc.sig);
}
for proc in &module.procs {
self.register_event_proc(proc);
}
self.hir_procs = Vec::new();
self.hir_procs
.resize_with(self.next_proc_id as usize, || None);
// Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
// werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
let mut scope = Scope {
is_module: true,
..Scope::default()
};
self.prescan(&module.body, &mut scope, true);
let body = self.lower_body(&module.body, &mut scope);
let main = hir::HProc {
name: module.name.clone(),
kind: hir::HProcKind::Main,
params: Vec::new(),
locals: Vec::new(),
ret_slot: None,
ret_ty: None,
body,
label_count: scope.next_label,
};
self.hir_procs
.resize_with(self.next_proc_id as usize, || None);
self.hir_procs[0] = Some(main);
self.module_labels = scope.labels.clone();
self.module_line_labels = scope.line_labels.clone();
// Pass 3: Prozedurrümpfe.
for proc in &module.procs {
self.lower_proc(proc);
}
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
// Signatur, damit externe Bibliotheksaufrufe dieselben BYREF-Slots
// und Funktionsrückgaben wie lokale Implementierungen besitzen.
let mut declared = vec![None; self.next_proc_id as usize];
for (name, info) in self.procs.iter() {
declared[info.id as usize] = Some((name.clone(), info.clone()));
}
let procs: Vec<hir::HProc> = std::mem::take(&mut self.hir_procs)
.into_iter()
.enumerate()
.map(|(i, p)| {
p.unwrap_or_else(|| {
let (name, info) = declared[i].clone().unwrap();
let params = info
.params
.iter()
.enumerate()
.map(|(index, (ty, array))| hir::HParam {
name: format!("ARG{index}"),
ty: self.h_ty(ty),
array: *array,
by_ref: !array && !matches!(ty, Ty::Udt(_)),
})
.collect::<Vec<_>>();
let mut locals = params
.iter()
.map(|param| hir::HVar {
name: param.name.clone(),
ty: param.ty.clone(),
array: param.array,
})
.collect::<Vec<_>>();
let (kind, ret_slot, ret_ty) = if info.kind == ProcKind::Function {
let ty = self.h_ty(&info.ret);
let slot = VarSlot::Local(locals.len() as u16);
locals.push(hir::HVar {
name: name.clone(),
ty: ty.clone(),
array: false,
});
(hir::HProcKind::Function, Some(slot), Some(ty))
} else {
(hir::HProcKind::Sub, None, None)
};
hir::HProc {
name,
kind,
params,
locals,
ret_slot,
ret_ty,
body: Vec::new(),
label_count: 0,
}
})
})
.collect();
Some(hir::HirModule {
name: module.name.clone(),
globals: std::mem::take(&mut self.globals),
udts: std::mem::take(&mut self.udt_defs),
procs,
data: std::mem::take(&mut self.data),
option_base: self.option_base,
objects: self.forms.objects.clone(),
event_procs: std::mem::take(&mut self.event_procs),
})
}
fn lower_proc(&mut self, proc: &Proc) {
let info = self.procs.get(&proc.sig.name).cloned();
let Some(info) = info else { return };
let mut scope = Scope {
is_static: proc.is_static,
..Scope::default()
};
self.prescan(&proc.body, &mut scope, false);
let mut params = Vec::new();
for p in &proc.sig.params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
let hty = self.h_ty(&ty);
let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar {
name: p.name.clone(),
ty: hty.clone(),
array: p.array,
});
let by_ref = !p.array && !matches!(ty, Ty::Udt(_));
scope.vars.insert(
key,
VarInfo {
ty: ty.clone(),
array: p.array,
explicit: true,
slot,
global: false,
by_ref,
},
);
params.push(hir::HParam {
name: p.name.clone(),
ty: hty,
array: p.array,
by_ref,
});
}
// Funktionsname als Rückgabe-Variable (immer frame-lokal).
let mut ret_slot = None;
let mut ret_ty = None;
if proc.sig.kind == ProcKind::Function {
let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar {
name: proc.sig.name.clone(),
ty: self.h_ty(&ret),
array: false,
});
scope.vars.insert(
key,
VarInfo {
ty: ret.clone(),
array: false,
explicit: true,
slot,
global: false,
by_ref: false,
},
);
ret_slot = Some(VarSlot::Local(slot));
ret_ty = Some(self.h_ty(&ret));
}
let body = self.lower_body(&proc.body, &mut scope);
let hproc = hir::HProc {
name: proc.sig.name.clone(),
kind: match proc.sig.kind {
ProcKind::Sub => hir::HProcKind::Sub,
ProcKind::Function => hir::HProcKind::Function,
},
params,
locals: std::mem::take(&mut scope.locals),
ret_slot,
ret_ty,
body,
label_count: scope.next_label,
};
let idx = info.id as usize;
if idx < self.hir_procs.len() {
self.hir_procs[idx] = Some(hproc);
}
}
/// Prescan eines Rumpfs: Labels/Zeilennummern erhalten `LabelId`s;
/// im Modulrumpf werden zusätzlich DATA-Konstanten (statisch, in
/// Quellreihenfolge) und RESTORE-Marken eingesammelt.
fn prescan(&mut self, stmts: &[Stmt], scope: &mut Scope, module_data: bool) {
for s in stmts {
match s {
Stmt::Label(n) => {
let id = scope.new_label();
scope.labels.insert(n.clone(), id);
if module_data {
self.data_marks_name
.insert(n.clone(), self.data.len() as u32);
}
}
Stmt::LineNumber(n) => {
let id = scope.new_label();
scope.line_labels.insert(*n, id);
if module_data {
self.data_marks_line.insert(*n, self.data.len() as u32);
}
}
Stmt::Data { items, pos } => {
if module_data {
for it in items {
self.data.push(hir::DataItem {
text: it.clone(),
line: pos.line,
});
}
}
}
Stmt::If {
then_body,
elseifs,
else_body,
..
} => {
self.prescan(then_body, scope, module_data);
for (_, b) in elseifs {
self.prescan(b, scope, module_data);
}
if let Some(b) = else_body {
self.prescan(b, scope, module_data);
}
}
Stmt::Select { arms, .. } => {
for a in arms {
self.prescan(&a.body, scope, module_data);
}
}
Stmt::For { body, .. } | Stmt::DoLoop { body, .. } | Stmt::While { body, .. } => {
self.prescan(body, scope, module_data)
}
_ => {}
}
}
}
fn param_ty(&self, p: &Param) -> Ty {
if let Some(t) = &p.as_type {
type_name_ty(t)
} else {
self.name_ty(&p.name, &p.suffix)
}
}
fn register_proc(&mut self, sig: &ProcSig) {
let ret = match sig.kind {
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
ProcKind::Sub => Ty::Unknown,
};
let params = sig
.params
.iter()
.map(|p| (self.param_ty(p), p.array))
.collect();
let id = match self.procs.get(&sig.name) {
Some(p) => p.id,
None => {
let id = self.next_proc_id;
self.next_proc_id += 1;
id
}
};
self.procs.insert(
sig.name.clone(),
ProcInfo {
kind: sig.kind,
ret,
params,
id,
def_fn: false,
},
);
}
fn h_ty(&self, t: &Ty) -> HTy {
match t {
Ty::Int => HTy::Num(NumTy::Int),
Ty::Lng => HTy::Num(NumTy::Lng),
Ty::Cur => HTy::Num(NumTy::Cur),
Ty::Sng => HTy::Num(NumTy::Sng),
Ty::Dbl => HTy::Num(NumTy::Dbl),
Ty::Str => HTy::Str,
Ty::FixedStr(n) => HTy::FixedStr(*n),
Ty::Udt(name) => HTy::Udt(*self.udt_ids.get(name).unwrap_or(&0)),
Ty::Form => HTy::Form,
Ty::Control => HTy::Control,
Ty::Unknown => HTy::Num(NumTy::Sng),
}
}
fn fixed_width(&self, ty: &HTy) -> Option<i16> {
let bytes = match ty {
HTy::Num(NumTy::Int) => 2,
HTy::Num(NumTy::Lng | NumTy::Sng) => 4,
HTy::Num(NumTy::Cur | NumTy::Dbl) => 8,
HTy::FixedStr(length) => usize::try_from(*length).ok()?.checked_mul(4)?,
HTy::Udt(id) => self
.udt_defs
.get(*id as usize)?
.fields
.iter()
.try_fold(0usize, |sum, (_, field)| {
sum.checked_add(self.fixed_width(field)? as usize)
})?,
_ => return None,
};
i16::try_from(bytes).ok()
}
fn register_event_proc(&mut self, proc: &Proc) {
let Some((object, class, event)) = self.event_binding(&proc.sig.name) else {
return;
};
if !forms::events(class)
.iter()
.any(|e| e.eq_ignore_ascii_case(&event))
{
return;
}
let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new();
if self.forms.objects[object as usize].array {
expected.push(("INDEX", forms::EventParamType::Integer));
}
expected.extend(forms::event_params(&event).unwrap_or(&[]).iter().copied());
let valid = proc.sig.kind == ProcKind::Sub
&& proc.sig.params.len() == expected.len()
&& proc.sig.params.iter().zip(&expected).all(|(p, (name, t))| {
let actual = self.param_ty(p);
p.name.eq_ignore_ascii_case(name)
&& matches!(
(actual, t),
(Ty::Int, forms::EventParamType::Integer)
| (Ty::Sng, forms::EventParamType::Single)
| (Ty::Control, forms::EventParamType::Control)
)
});
if !valid {
let args = expected
.iter()
.map(|(n, t)| {
let ty = match t {
forms::EventParamType::Integer => "INTEGER",
forms::EventParamType::Single => "SINGLE",
forms::EventParamType::Control => "CONTROL",
};
format!("{n} AS {ty}")
})
.collect::<Vec<_>>()
.join(", ");
self.err(
proc.pos,
format!("Event procedure {} expects ({args})", proc.sig.name),
);
}
if let Some(info) = self.procs.get(&proc.sig.name) {
self.event_procs.push(hir::HEventProc {
object,
event,
proc: info.id,
});
}
}
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
let (base, event) = name.rsplit_once('_')?;
let found = if base == "FORM" {
self.forms
.objects
.iter()
.enumerate()
.find(|(_, o)| o.class == ObjectClass::Form)
.map(|(i, o)| (i as u16, o))
} else {
self.forms.find(base)
}?;
Some((found.0, found.1.class, event.to_string()))
}
fn property_ty(spec: forms::PropertySpec) -> Ty {
match spec.ty {
PropertyType::String => Ty::Str,
PropertyType::Single => Ty::Sng,
PropertyType::Object => Ty::Control,
PropertyType::Integer | PropertyType::Boolean | PropertyType::IntegerArray => Ty::Int,
}
}
fn object_by_name(
&mut self,
name: &str,
pos: SourcePos,
) -> Option<(u16, crate::forms::FormObject)> {
if let Some((id, object)) = self.forms.find(name) {
return Some((id, object.clone()));
}
self.err(pos, format!("Unknown object '{name}'"));
None
}
fn implicit_form_property(
&self,
scope: &Scope,
name: &str,
) -> Option<(u16, u16, forms::PropertySpec)> {
if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) {
return None;
}
let (object, form) = self
.forms
.objects
.iter()
.enumerate()
.find(|(_, object)| object.class == ObjectClass::Form)?;
let (property, spec) = forms::property(form.class, name)?;
Some((object as u16, property, spec))
}
fn object_property(
&mut self,
path: &str,
pos: SourcePos,
) -> Option<(u16, u16, forms::PropertySpec, ObjectClass)> {
let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') {
let (object, member) = tail.split_once('.').unwrap_or((tail, ""));
(object, member, Some(form))
} else {
let (object, member) = path.split_once('.')?;
(object, member, None)
};
let (id, object) = self.object_by_name(object_name, pos)?;
if let Some(parent) = parent {
let parent_ok = self.forms.belongs_to(&object, parent);
if !parent_ok {
self.err(
pos,
format!("Control '{object_name}' not found in form '{parent}'"),
);
return None;
}
}
let Some((property, spec)) = forms::property(object.class, member) else {
self.err(
pos,
format!("Unknown property '{member}' of {}", object.class.name()),
);
return None;
};
Some((id, property, spec, object.class))
}
fn object_member_target(
&mut self,
path: &str,
pos: SourcePos,
) -> Option<(u16, crate::forms::FormObject, String)> {
let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') {
let (object, member) = tail.split_once('.').unwrap_or((tail, ""));
(object, member, Some(form))
} else {
let (object, member) = path.split_once('.')?;
(object, member, None)
};
let (id, object) = self.object_by_name(object_name, pos)?;
if let Some(parent) = parent {
if !self.forms.belongs_to(&object, parent) {
self.err(
pos,
format!("Control '{object_name}' not found in form '{parent}'"),
);
return None;
}
}
Some((id, object, member.to_string()))
}
fn is_udt_path(&self, scope: &Scope, path: &str) -> bool {
let base = path.split('.').next().unwrap_or(path);
let as_key = format!("{base}\u{1}AS");
let plain = self.var_key(base, &None, false);
self.visible_var(scope, &as_key)
.or_else(|| self.visible_var(scope, &plain))
.is_some_and(|v| matches!(v.ty, Ty::Udt(_)))
}
fn declared_var(&self, scope: &Scope, name: &str) -> Option<VarInfo> {
let as_key = format!("{name}\u{1}AS");
let plain = self.var_key(name, &None, false);
self.visible_var(scope, &as_key)
.or_else(|| self.visible_var(scope, &plain))
.cloned()
}
fn dynamic_property(
&mut self,
scope: &mut Scope,
path: &str,
pos: SourcePos,
) -> Result<Option<(HExpr, forms::PropertySpec)>, ()> {
let Some((base, member)) = path.split_once('.') else {
return Ok(None);
};
let Some(info) = self.declared_var(scope, base) else {
return Ok(None);
};
let spec = match info.ty {
Ty::Form => forms::property(ObjectClass::Form, member).map(|(_, spec)| spec),
Ty::Control => ObjectClass::ALL
.into_iter()
.filter(|class| !matches!(class, ObjectClass::Form | ObjectClass::Screen))
.find_map(|class| forms::property(class, member).map(|(_, spec)| spec)),
_ => return Ok(None),
};
let Some(spec) = spec else {
self.err(pos, format!("Unknown property '{member}'"));
return Err(());
};
let base = Expr::Name {
name: base.to_string(),
suffix: None,
args: None,
pos,
};
let (place, _) = self.lower_place(&base, scope);
Ok(place.map(|place| (HExpr::Load(Box::new(place)), spec)))
}
fn object_index(
&mut self,
object: u16,
args: &Option<Vec<Expr>>,
scope: &mut Scope,
pos: SourcePos,
) -> Result<Option<HExpr>, ()> {
let Some(args) = args else { return Ok(None) };
let info = &self.forms.objects[object as usize];
if !info.array {
self.err(pos, format!("Object '{}' is not an array", info.name));
return Err(());
}
if args.len() != 1 {
self.err(pos, "Argument-count mismatch");
return Err(());
}
Ok(Some(self.lower_num_as(&args[0], scope, NumTy::Lng)))
}
/// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE).
fn default_ty(&self, name: &str) -> Ty {
let first = name.chars().next().unwrap_or('A');
if first.is_ascii_alphabetic() {
let idx = (first.to_ascii_uppercase() as u8 - b'A') as usize;
if let Some(t) = &self.deftypes[idx] {
return t.clone();
}
}
Ty::Sng
}
fn name_ty(&self, name: &str, suffix: &Option<Suffix>) -> Ty {
match suffix {
Some(s) => suffix_ty(*s),
None => self.default_ty(name),
}
}
fn var_key(&self, name: &str, suffix: &Option<Suffix>, as_decl: bool) -> String {
if as_decl {
format!("{name}\u{1}AS")
} else {
let c = match suffix {
Some(s) => s.as_char(),
None => match self.default_ty(name) {
Ty::Int => '%',
Ty::Lng => '&',
Ty::Cur => '@',
Ty::Dbl => '#',
Ty::Str => '$',
_ => '!',
},
};
format!("{name}{c}")
}
}
// ---- Slot-Verwaltung ---------------------------------------------------
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
let slot = self.globals.len() as u16;
self.globals.push(hir::HVar {
name: name.to_string(),
ty: self.h_ty(ty),
array,
});
slot
}
/// Variable im gegebenen Scope anlegen (Modul → global; Prozedur →
/// lokal, außer STATIC-Prozedur → globaler Slot mit Mangel-Namen).
fn alloc_var(&mut self, scope: &mut Scope, name: &str, ty: &Ty, array: bool) -> (u16, bool) {
if scope.is_module {
(self.alloc_global(name, ty, array), true)
} else if scope.is_static {
(
self.alloc_global(&format!("STATIC.{name}"), ty, array),
true,
)
} else {
let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar {
name: name.to_string(),
ty: self.h_ty(ty),
array,
});
(slot, false)
}
}
/// Versteckter Temp-Slot (SELECT-Selektor, FOR-Grenzen, SWAP).
fn alloc_temp(&mut self, scope: &mut Scope, ty: &Ty) -> VarSlot {
let (slot, global) = self.alloc_var(scope, "<temp>", ty, false);
if global {
VarSlot::Global(slot)
} else {
VarSlot::Local(slot)
}
}
fn vars_of<'a>(&'a self, scope: &'a Scope) -> &'a HashMap<String, VarInfo> {
if scope.is_module {
&self.module_vars
} else {
&scope.vars
}
}
fn visible_var<'a>(&'a self, scope: &'a Scope, key: &str) -> Option<&'a VarInfo> {
self.vars_of(scope).get(key).or_else(|| {
(!scope.is_module && (scope.is_def_fn || self.shared_vars.contains(key)))
.then(|| self.module_vars.get(key))
.flatten()
})
}
fn insert_var(&mut self, scope: &mut Scope, key: String, info: VarInfo) {
if scope.is_module {
self.module_vars.insert(key, info);
} else {
scope.vars.insert(key, info);
}
}
/// Feldpfad eines UDT-Zugriffs auflösen: liefert Typ und Feldindizes.
fn member_path(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> (Ty, Vec<u16>) {
let mut cur = base_ty.clone();
let mut idxs = Vec::new();
for seg in path {
match cur.clone() {
Ty::Udt(udt_name) => {
let found = self.udt_ids.get(&udt_name).and_then(|id| {
self.udt_defs[*id as usize]
.fields
.iter()
.position(|(n, _)| n == seg)
.map(|i| (i as u16, self.udt_defs[*id as usize].fields[i].1.clone()))
});
match found {
Some((i, hty)) => {
idxs.push(i);
cur = match hty {
HTy::Num(n) => ty_of_num(n),
HTy::Str => Ty::Str,
HTy::FixedStr(n) => Ty::FixedStr(n),
HTy::Udt(id) => Ty::Udt(self.udt_defs[id as usize].name.clone()),
HTy::Form => Ty::Form,
HTy::Control => Ty::Control,
};
}
None => {
self.err(pos, "Element not defined");
return (Ty::Unknown, idxs);
}
}
}
Ty::Unknown => return (Ty::Unknown, idxs),
_ => {
self.err(pos, "Type mismatch");
return (Ty::Unknown, idxs);
}
}
}
(cur, idxs)
}
/// Variable nachschlagen bzw. implizit deklarieren.
fn resolve_var(
&mut self,
scope: &mut Scope,
name: &str,
suffix: &Option<Suffix>,
array: bool,
pos: SourcePos,
) -> Option<VarInfo> {
// AS-deklarierte Variable hat Vorrang, wenn kein Suffix angegeben
let as_key = format!("{name}\u{1}AS");
if suffix.is_none() {
if let Some(v) = self.visible_var(scope, &as_key) {
return Some(v.clone());
}
} else if let Some(v) = self.visible_var(scope, &as_key).cloned() {
if v.ty == suffix_ty(suffix.unwrap()) {
return Some(v);
}
self.err(pos, "Duplicate definition");
return None;
}
let key = self.var_key(name, suffix, false);
if let Some(v) = self.visible_var(scope, &key) {
return Some(v.clone());
}
if self.explicit {
self.err(pos, "Variable not defined");
return None;
}
let ty = self.name_ty(name, suffix);
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
let info = if scope.is_def_fn {
let slot = self.alloc_global(name, &ty, array);
let info = VarInfo {
ty,
array,
explicit: false,
slot,
global: true,
by_ref: false,
};
self.module_vars.insert(key, info.clone());
info
} else {
let (slot, global) = self.alloc_var(scope, name, &ty, array);
let info = VarInfo {
ty,
array,
explicit: false,
slot,
global,
by_ref: false,
};
self.insert_var(scope, key, info.clone());
info
};
Some(info)
}
/// Konstantenfaltung für `CONST`-Ausdrücke.
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
match e {
Expr::IntLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::LongLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::SingleLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::DoubleLit(v) => Some(ConstVal::Num(*v)),
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
Expr::Paren(e) => self.fold_const(e),
Expr::Name {
name, args: None, ..
} => self.consts.get(name).and_then(|(_, v)| v.clone()),
Expr::Unary {
op: UnOp::Neg,
operand,
..
} => match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
ConstVal::Str(_) => None,
},
Expr::Unary {
op: UnOp::Not,
operand,
..
} => match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
ConstVal::Str(_) => None,
},
Expr::Binary { op, lhs, rhs, .. } => {
let l = self.fold_const(lhs)?;
let r = self.fold_const(rhs)?;
match (l, r) {
(ConstVal::Num(a), ConstVal::Num(b)) => {
let v = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
BinOp::Pow => a.powf(b),
BinOp::IntDiv => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) / bi) as f64
}
BinOp::Mod => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) % bi) as f64
}
_ => return None,
};
Some(ConstVal::Num(v))
}
(ConstVal::Str(a), ConstVal::Str(b)) if *op == BinOp::Add => {
Some(ConstVal::Str(format!("{a}{b}")))
}
_ => None,
}
}
_ => None,
}
}
// ---- Konvertierungen ---------------------------------------------------
/// Numerische Konvertierung als expliziter HIR-Knoten (Matrix).
fn conv_num(&mut self, e: HExpr, from: &Ty, to: NumTy) -> HExpr {
let f = num_ty(from);
if f == to {
return e;
}
HExpr::Conv {
from: f,
to,
arg: Box::new(e),
}
}
/// Wert an Zieltyp anpassen (Zuweisung, BYVAL-Argument).
fn coerce(&mut self, e: HExpr, from: &Ty, to: &Ty, pos: SourcePos) -> HExpr {
if matches!(
(from, to),
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
) {
return e;
}
match (is_num(to), is_str(to)) {
(true, _) if is_num(from) => {
if matches!(to, Ty::Unknown) || matches!(from, Ty::Unknown) {
e
} else {
self.conv_num(e, from, num_ty(to))
}
}
(_, true) if is_str(from) => match to {
Ty::FixedStr(n) => HExpr::FixStr {
len: *n,
arg: Box::new(e),
},
_ => e,
},
_ => {
if !matches!(to, Ty::Udt(_)) && !matches!(from, Ty::Udt(_)) {
self.err(pos, "Type mismatch");
}
e
}
}
}
// ---- Anweisungen -------------------------------------------------------
fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
let mut out = Vec::new();
for stmt in stmts {
self.lower_stmt(stmt, scope, &mut out);
}
out
}
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
out.push(HStmt {
line: scope.current_line,
kind,
});
}
fn lower_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, out: &mut Vec<HStmt>) {
// Quellzeile für Anweisungsgrenzen/Fehlerortung mitführen.
let pos = stmt_pos(stmt);
if pos.line > 0 {
scope.current_line = pos.line;
}
match stmt {
Stmt::Data { .. }
| Stmt::Include { .. }
| Stmt::MetaArrays { .. }
| Stmt::MetaForm { .. } => {}
Stmt::Label(n) => {
if let Some(id) = scope.labels.get(n).copied() {
self.push(out, scope, HStmtKind::Label(id));
}
}
Stmt::LineNumber(n) => {
if let Some(id) = scope.line_labels.get(n).copied() {
self.push(out, scope, HStmtKind::Label(id));
}
self.push(out, scope, HStmtKind::SetErl(*n));
}
Stmt::End(_) => self.push(out, scope, HStmtKind::End),
Stmt::StopStmt(_) => self.push(out, scope, HStmtKind::Stop),
Stmt::System(_) => self.push(out, scope, HStmtKind::System),
Stmt::Exit { kind, pos } => match kind {
ExitKind::For => {
match scope
.loop_exits
.iter()
.rev()
.find(|(k, _)| *k == LoopKind::For)
{
Some((_, id)) => {
let id = *id;
self.push(out, scope, HStmtKind::Goto(id));
}
None => self.err(*pos, "EXIT not within FOR...NEXT"),
}
}
ExitKind::Do => {
match scope
.loop_exits
.iter()
.rev()
.find(|(k, _)| *k == LoopKind::Do)
{
Some((_, id)) => {
let id = *id;
self.push(out, scope, HStmtKind::Goto(id));
}
None => self.err(*pos, "EXIT DO not within DO...LOOP"),
}
}
ExitKind::Sub | ExitKind::Function | ExitKind::Def => {
self.push(out, scope, HStmtKind::ExitProc);
}
},
Stmt::Assign { target, value, pos } => {
if let Expr::Name {
name,
suffix: None,
args,
..
} = target
{
if args.is_none() && !name.contains('.') && !name.contains('!') {
if let Some((object, property, spec)) =
self.implicit_form_property(scope, name)
{
if !spec.writable {
self.err(
*pos,
format!("Property '{}' is read-only at run time", spec.name),
);
self.lower_expr(value, scope);
return;
}
let (value, actual) = self.lower_expr(value, scope);
let expected = Self::property_ty(spec);
self.check_assign(&expected, &actual, *pos);
let value = self.coerce_silent(value, &actual, &expected);
self.push(
out,
scope,
HStmtKind::SetObjectProperty {
object,
index: None,
property,
value,
},
);
return;
}
}
if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name)
{
if name.contains('!') && !name.contains('.') {
let object_name = name.split_once('!').unwrap().1;
self.err(
*pos,
format!("Object '{object_name}' has no default property"),
);
self.lower_expr(value, scope);
return;
}
if args.is_none() {
match self.dynamic_property(scope, name, *pos) {
Ok(Some((object_expr, spec))) => {
if !spec.writable {
self.err(
*pos,
format!(
"Property '{}' is read-only at run time",
spec.name
),
);
self.lower_expr(value, scope);
return;
}
let (value, actual) = self.lower_expr(value, scope);
let expected = Self::property_ty(spec);
if !((is_num(&expected) && is_num(&actual))
|| (is_str(&expected) && is_str(&actual)))
{
self.err(
*pos,
format!("Type mismatch for property '{}'", spec.name),
);
}
let value = self.coerce_silent(value, &actual, &expected);
self.push(
out,
scope,
HStmtKind::SetDynamicObjectProperty {
object: object_expr,
property: spec.name.to_string(),
value,
},
);
return;
}
Err(()) => {
self.lower_expr(value, scope);
return;
}
Ok(None) => {}
}
}
if let Some((object, property, spec, _)) = self.object_property(name, *pos)
{
if spec.ty == PropertyType::IntegerArray {
let Some(indices) = args else {
self.err(*pos, "Argument-count mismatch");
self.lower_expr(value, scope);
return;
};
let expected = if self.forms.objects[object as usize].array {
2
} else {
1
};
if indices.len() != expected {
self.err(*pos, "Argument-count mismatch");
self.lower_expr(value, scope);
return;
}
let object_index = self.forms.objects[object as usize]
.array
.then(|| self.lower_num_as(&indices[0], scope, NumTy::Lng));
let index =
self.lower_num_as(&indices[expected - 1], scope, NumTy::Lng);
let value = self.lower_num_as(value, scope, NumTy::Lng);
self.push(
out,
scope,
HStmtKind::SetObjectIndexedProperty {
object,
object_index,
property,
index,
value,
},
);
return;
}
let Ok(index) = self.object_index(object, args, scope, *pos) else {
self.lower_expr(value, scope);
return;
};
if !spec.writable {
self.err(
*pos,
format!("Property '{}' is read-only at run time", spec.name),
);
self.lower_expr(value, scope);
return;
}
let (value, actual) = self.lower_expr(value, scope);
let expected = Self::property_ty(spec);
if !((is_num(&expected) && is_num(&actual))
|| (is_str(&expected) && is_str(&actual)))
{
self.err(
*pos,
format!("Type mismatch for property '{}'", spec.name),
);
}
let value = self.coerce_silent(value, &actual, &expected);
self.push(
out,
scope,
HStmtKind::SetObjectProperty {
object,
index,
property,
value,
},
);
}
return;
}
if self.forms.find(name).is_some() {
self.err(*pos, format!("Object '{name}' has no default property"));
self.lower_expr(value, scope);
return;
}
}
// MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck
if let Expr::Name {
name,
suffix: Some(Suffix::Str),
args: Some(args),
..
} = target
{
if name == "MID" {
if args.is_empty() || args.len() > 3 {
self.err(*pos, "Argument-count mismatch");
}
let mut place = None;
if let Some(sv) = args.first() {
let (p, t) = self.lower_place(sv, scope);
if !is_str(&t) {
self.err(sv.pos(), "Type mismatch");
}
place = p;
}
let start = args.get(1).map(|a| self.lower_num_as(a, scope, NumTy::Lng));
let len = args.get(2).map(|a| self.lower_num_as(a, scope, NumTy::Lng));
let (ve, vt) = self.lower_expr(value, scope);
if !is_str(&vt) {
self.err(value.pos(), "Type mismatch");
}
if let Some(place) = place {
let cur = HExpr::Load(Box::new(place.clone()));
let call = HExpr::Builtin {
b: Builtin::MidAssign,
args: vec![
cur,
start.unwrap_or(HExpr::Lng(1)),
len.unwrap_or(HExpr::Lng(-1)),
ve,
],
ret: HTy::Str,
};
self.push(out, scope, HStmtKind::Assign { place, value: call });
}
return;
}
}
// `DATE$ = "..."` / `TIME$ = "..."` sind Anweisungen, keine
// Zuweisungen an eine Variable.
// `ERR = n` setzt den Fehlercode, ohne einen Fehler
// auszulösen (Anweisungsform von ERR).
if let Expr::Name {
name,
suffix: None,
args: None,
..
} = target
{
if name == "ERR" {
let (e, t) = self.want_num(value, scope);
let e = self.conv_num(e, &t, NumTy::Lng);
self.push(out, scope, HStmtKind::SetErr(e));
return;
}
}
if let Expr::Name {
name,
suffix: Some(Suffix::Str),
args: None,
..
} = target
{
if name == "DATE" || name == "TIME" {
let (ve, _) = self.want_str(value, scope);
let bt = if name == "DATE" {
Builtin::DateSet
} else {
Builtin::TimeSet
};
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: bt,
args: vec![ve],
},
);
return;
}
}
let (place, tt) = self.lower_place(target, scope);
let (ve, vt) = self.lower_expr(value, scope);
self.check_assign(&tt, &vt, *pos);
if let Some(place) = place {
let value = self.coerce_silent(ve, &vt, &tt);
self.push(out, scope, HStmtKind::Assign { place, value });
}
}
Stmt::Print {
printer,
file,
using,
items,
..
} => {
if let Some(f) = file {
self.want_num(f, scope);
}
if let Some(u) = using {
self.want_str(u, scope);
}
let mut hitems = Vec::new();
let mut trailing = false;
for (i, item) in items.iter().enumerate() {
let last = i + 1 == items.len();
match item {
PrintItem::Expr(e) => {
trailing = false;
// TAB(n)/SPC(n) sind Positionssteuerungen.
if let Expr::Name {
name,
suffix: None,
args: Some(a),
..
} = e
{
if (name == "TAB" || name == "SPC") && a.len() == 1 {
let n = self.lower_num_as(&a[0], scope, NumTy::Lng);
hitems.push(if name == "TAB" {
HPrintItem::Tab(n)
} else {
HPrintItem::Spc(n)
});
// TAB/SPC wirken wie `;` danach
trailing = last;
continue;
}
}
let (he, _t) = self.lower_expr(e, scope);
hitems.push(HPrintItem::Val(he));
}
PrintItem::Comma => {
hitems.push(HPrintItem::Comma);
trailing = last;
}
PrintItem::Semicolon => {
trailing = last;
}
}
}
// `PRINT #n` und `LPRINT` schreiben dieselben Elemente an ein
// anderes Ziel; danach steht der Bildschirm wieder.
let ziel = if *printer {
Some(HExpr::Lng(-2))
} else if let Some(f) = file {
let (e, t) = self.want_num(f, scope);
Some(self.conv_num(e, &t, NumTy::Lng))
} else {
None
};
if let Some(z) = ziel.clone() {
self.push_ziel(out, scope, z);
}
if let Some(u) = using {
// `PRINT USING fmt$; a; b` — die Semikolons trennen nur
// die Werte; Druckzonen gibt es hier nicht.
let (fe, _) = self.want_str(u, scope);
let mut args = vec![fe];
for it in &hitems {
match it {
HPrintItem::Val(e) => args.push(e.clone()),
_ => self.err(pos, "Illegal function call: PRINT USING"),
}
}
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::PrintUsing,
args,
},
);
if !trailing {
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::PrintNewline,
args: vec![],
},
);
}
} else {
self.push(
out,
scope,
HStmtKind::Print {
items: hitems,
trailing,
},
);
}
if ziel.is_some() {
self.push_ziel(out, scope, HExpr::Lng(-1));
}
}
Stmt::Input {
line,
file,
keep_cursor: _,
prompt,
vars,
..
} => {
let datei = file.as_ref().map(|f| {
let (e, t) = self.want_num(f, scope);
self.conv_num(e, &t, NumTy::Lng)
});
let mut targets = Vec::new();
for v in vars {
if let (Some(p), _) = self.lower_place(v, scope) {
targets.push(p);
}
}
// `INPUT #n` kennt weder Eingabeaufforderung noch Fragezeichen.
let (ptext, question) = match (&datei, prompt) {
(Some(_), _) => (None, false),
(None, Some((t, q))) => (Some(t.clone()), *q),
(None, None) => (None, true),
};
self.push(
out,
scope,
HStmtKind::Input {
file: datei,
line_mode: *line,
prompt: ptext,
question,
targets,
},
);
}
Stmt::If {
cond,
then_body,
elseifs,
else_body,
..
} => {
let c = self.lower_cond(cond, scope);
let then = self.lower_body(then_body, scope);
// ELSEIF-Kette zu verschachteltem If absenken (von hinten).
let mut els = match else_body {
Some(b) => self.lower_body(b, scope),
None => Vec::new(),
};
for (ec, eb) in elseifs.iter().rev() {
let c2 = self.lower_cond(ec, scope);
let body2 = self.lower_body(eb, scope);
let stmt = HStmt {
line: scope.current_line,
kind: HStmtKind::If {
cond: c2,
then: body2,
els,
},
};
els = vec![stmt];
}
self.push(out, scope, HStmtKind::If { cond: c, then, els });
}
Stmt::Select { expr, arms, .. } => {
self.lower_select(expr, arms, scope, out);
}
Stmt::For {
var,
from,
to,
step,
body,
pos,
} => {
let (place, vt) = self.lower_place(var, scope);
if !is_num(&vt) {
self.err(*pos, "Type mismatch");
}
let ty = num_ty(&vt);
let (fe, ft) = self.lower_expr_num(from, scope);
let (te, tt2) = self.lower_expr_num(to, scope);
let from_e = self.conv_num(fe, &ft, ty);
let to_e = self.conv_num(te, &tt2, ty);
let step_e = step.as_ref().map(|s| {
let (se, st) = self.lower_expr_num(s, scope);
self.conv_num(se, &st, ty)
});
let limit_slot = self.alloc_temp(scope, &ty_of_num(ty));
// Dynamischer STEP braucht einen Slot; konstante Literale
// bettet der Codegen direkt ein.
let step_slot = match &step_e {
Some(e) if literal_value(e).is_none() => {
Some(self.alloc_temp(scope, &ty_of_num(ty)))
}
_ => None,
};
let exit_label = scope.new_label();
scope.loop_exits.push((LoopKind::For, exit_label));
let hbody = self.lower_body(body, scope);
scope.loop_exits.pop();
if let Some(place) = place {
self.push(
out,
scope,
HStmtKind::For {
var: place,
ty,
from: from_e,
to: to_e,
step: step_e,
limit_slot,
step_slot,
body: hbody,
exit_label,
},
);
}
}
Stmt::DoLoop {
pre, post, body, ..
} => {
let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
let exit_label = scope.new_label();
scope.loop_exits.push((LoopKind::Do, exit_label));
let hbody = self.lower_body(body, scope);
scope.loop_exits.pop();
let post_c = post.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
self.push(
out,
scope,
HStmtKind::Loop {
pre: pre_c,
post: post_c,
body: hbody,
exit_label,
},
);
}
Stmt::While { cond, body, .. } => {
let c = self.lower_cond(cond, scope);
let exit_label = scope.new_label();
let hbody = self.lower_body(body, scope);
self.push(
out,
scope,
HStmtKind::Loop {
pre: Some((false, c)),
post: None,
body: hbody,
exit_label,
},
);
}
Stmt::Goto { target, pos } => {
if let Some(id) = self.label_id(target, scope, *pos) {
self.push(out, scope, HStmtKind::Goto(id));
}
}
Stmt::Gosub { target, pos } => {
if let Some(id) = self.label_id(target, scope, *pos) {
self.push(out, scope, HStmtKind::Gosub(id));
}
}
Stmt::OnGoto {
expr,
targets,
gosub,
pos,
} => {
let sel = self.lower_num_as(expr, scope, NumTy::Int);
let mut ids = Vec::new();
for t in targets {
if let Some(id) = self.label_id(t, scope, *pos) {
ids.push(id);
}
}
self.push(
out,
scope,
HStmtKind::OnGoto {
sel,
gosub: *gosub,
targets: ids,
},
);
}
Stmt::Return { target, pos } => {
let id = match target {
Some(t) => match self.label_id(t, scope, *pos) {
Some(id) => Some(id),
None => return,
},
None => None,
};
self.push(out, scope, HStmtKind::ReturnGosub(id));
}
Stmt::OnError { local, action, pos } => {
// `ON ERROR GOTO label` ohne `LOCAL` setzt den modulweiten
// Handler; sein Sprungziel liegt im Modulrumpf, auch wenn die
// Anweisung in einer Prozedur steht. Nur `ON LOCAL ERROR`
// verlangt ein Label im eigenen Rumpf.
let local = *local;
match action {
OnErrorAction::Goto(t) => {
let id = if local || scope.is_module {
self.label_id(t, scope, *pos)
} else {
self.module_label_id(t, *pos)
};
if let Some(id) = id {
self.push(
out,
scope,
HStmtKind::OnError {
local,
target: Some(id),
},
);
}
}
OnErrorAction::Disable => {
self.push(
out,
scope,
HStmtKind::OnError {
local,
target: None,
},
);
}
OnErrorAction::ResumeNext => {
self.push(out, scope, HStmtKind::OnErrorResumeNext { local });
}
}
}
Stmt::Resume { kind, pos } => {
let k = match kind {
ResumeKind::Retry => hir::HResume::Retry,
ResumeKind::Next => hir::HResume::Next,
ResumeKind::Label(t) => match self.label_id(t, scope, *pos) {
Some(id) => hir::HResume::Label(id),
None => return,
},
};
self.push(out, scope, HStmtKind::Resume(k));
}
Stmt::ErrorStmt { code, .. } => {
let c = self.lower_num_as(code, scope, NumTy::Int);
self.push(out, scope, HStmtKind::RaiseError(c));
}
Stmt::Dim {
shared,
redim,
decls,
..
} => {
for d in decls {
if *shared && scope.is_module {
self.shared_vars.insert(self.var_key(
&d.name,
&d.suffix,
d.as_type.is_some(),
));
}
self.declare(d, *redim, scope, out);
}
}
Stmt::StaticDecl { decls, .. } => {
// STATIC-Variablen leben in globalen Slots (eine Instanz).
for d in decls {
let ty = self.decl_ty(d);
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
if let Entry::Vacant(entry) = scope.vars.entry(key) {
let slot =
self.alloc_global(&format!("STATIC.{}", d.name), &ty, d.dims.is_some());
entry.insert(VarInfo {
ty,
array: d.dims.is_some(),
explicit: true,
slot,
global: true,
by_ref: false,
});
}
}
}
Stmt::CommonDecl { shared, decls, .. } => {
for d in decls {
if *shared && scope.is_module {
self.shared_vars.insert(self.var_key(
&d.name,
&d.suffix,
d.as_type.is_some(),
));
}
self.declare(d, false, scope, out);
}
}
Stmt::SharedDecl { decls, .. } => {
// Zugriff auf Modulvariablen aus einer Prozedur heraus.
for d in decls {
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
let info = match self.module_vars.get(&key) {
Some(v) => v.clone(),
None => {
let ty = self.decl_ty(d);
let slot = self.alloc_global(&d.name, &ty, d.dims.is_some());
let v = VarInfo {
ty,
array: d.dims.is_some(),
explicit: true,
slot,
global: true,
by_ref: false,
};
self.module_vars.insert(key.clone(), v.clone());
v
}
};
scope.vars.insert(key, info);
}
}
Stmt::Erase { names, .. } => {
let mut slots = Vec::new();
for n in names {
if let Expr::Name {
name, suffix, pos, ..
} = n
{
if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) {
slots.push(if v.global {
VarSlot::Global(v.slot)
} else {
VarSlot::Local(v.slot)
});
}
}
}
self.push(out, scope, HStmtKind::Erase(slots));
}
Stmt::ConstDecl { items, pos } => {
for (name, suffix, value) in items {
let ty = match suffix {
Some(s) => suffix_ty(*s),
None => match self.fold_const(value) {
Some(ConstVal::Str(_)) => Ty::Str,
_ => Ty::Dbl,
},
};
let folded = self.fold_const(value);
if folded.is_none() {
self.err(*pos, "Invalid constant");
}
if self.consts.insert(name.clone(), (ty, folded)).is_some() {
self.err(*pos, "Duplicate definition");
}
}
}
Stmt::DefType { ty, ranges, .. } => {
for (a, b) in ranges {
let (a, b) = (a.to_ascii_uppercase(), b.to_ascii_uppercase());
for c in a..=b {
if c.is_ascii_uppercase() {
self.deftypes[(c as u8 - b'A') as usize] = Some(type_name_ty(ty));
}
}
}
}
Stmt::OptionStmt { kind, .. } => match kind {
OptionKind::Explicit => self.explicit = true,
OptionKind::Base(b) => self.option_base = *b,
},
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
Stmt::Call {
name, args, pos, ..
} => {
self.lower_call_stmt(name, args, scope, *pos, out);
}
Stmt::ReadStmt { vars, .. } => {
let mut places = Vec::new();
for v in vars {
if let (Some(p), _) = self.lower_place(v, scope) {
places.push(p);
}
}
self.push(out, scope, HStmtKind::Read(places));
}
Stmt::Restore { target, pos } => {
let idx = match target {
None => 0,
Some(t) => {
self.check_label_exists(t, scope, *pos);
match t {
LabelRef::Name(n) => self.data_marks_name.get(n).copied().unwrap_or(0),
LabelRef::Line(n) => self.data_marks_line.get(n).copied().unwrap_or(0),
}
}
};
self.push(out, scope, HStmtKind::Restore(idx));
}
Stmt::DefFn {
name,
suffix,
params,
body,
pos,
} => {
self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos);
}
Stmt::DefFnBlock {
name,
suffix,
params,
body,
pos,
} => {
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
}
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
Stmt::Open {
file,
mode,
isam,
number,
len,
pos,
..
} => {
let (fe, _) = self.want_str(file, scope);
self.reject_com_device(file, *pos);
let (ne, nt) = self.want_num(number, scope);
let ne = self.conv_num(ne, &nt, NumTy::Lng);
let le = match len {
Some(l) => {
let (e, t) = self.want_num(l, scope);
self.conv_num(e, &t, NumTy::Lng)
}
// Vorgabe-Recordlänge des Vorbilds.
None => HExpr::Lng(128),
};
if let Some((ty_name, table)) = isam {
let Some(&udt) = self.udt_ids.get(ty_name) else {
self.err(*pos, "Type not defined");
return;
};
// Die Laufzeit braucht die Spaltennamen; `UdtLayout`
// führt nur die Feldtypen. Sie reisen deshalb als
// Stringargument mit — ohne Änderung am `.tbc`-Format.
let spalten = self.udt_defs[udt as usize]
.fields
.iter()
.map(|(n, _)| n.clone())
.collect::<Vec<_>>()
.join(",");
// Für die Satztypprüfung (Aufgabe 2.3) merken, welcher
// Typ an dieser Dateinummer hängt — nur bei literaler
// Nummer, sonst ist sie erst zur Laufzeit bekannt.
if let Some(ConstVal::Num(n)) = self.fold_const(number) {
self.isam_typ.insert(n as i32, ty_name.clone());
}
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::IsamOpen,
args: vec![
fe,
ne,
HExpr::Str(table.clone()),
HExpr::Str(spalten),
HExpr::Lng(udt as i32),
],
},
);
return;
}
// Ohne `FOR`-Klausel gilt RANDOM (Vorbild).
let m = match mode {
Some(OpenMode::Input) => "I",
Some(OpenMode::Output) => "O",
Some(OpenMode::Append) => "A",
Some(OpenMode::Binary) => "B",
_ => "R",
};
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Open,
args: vec![fe, ne, HExpr::Str(m.to_string()), le],
},
);
}
Stmt::OpenLegacy {
mode,
number,
file,
len,
pos,
..
} => {
let (me, _) = self.want_str(mode, scope);
let (ne, nt) = self.want_num(number, scope);
let ne = self.conv_num(ne, &nt, NumTy::Lng);
let (fe, _) = self.want_str(file, scope);
self.reject_com_device(file, *pos);
let le = match len {
Some(l) => {
let (e, t) = self.want_num(l, scope);
self.conv_num(e, &t, NumTy::Lng)
}
None => HExpr::Lng(128),
};
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Open,
args: vec![fe, ne, me, le],
},
);
}
Stmt::CloseStmt { files, .. } => {
if files.is_empty() {
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::CloseAll,
args: vec![],
},
);
}
for f in files {
let (e, t) = self.want_num(f, scope);
let e = self.conv_num(e, &t, NumTy::Lng);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Close,
args: vec![e],
},
);
}
}
Stmt::FieldStmt { file, fields, .. } => {
let (fe, ft) = self.want_num(file, scope);
let fe = self.conv_num(fe, &ft, NumTy::Lng);
let mut hf = Vec::new();
for (width, var) in fields {
let (we, wt) = self.want_num(width, scope);
let we = self.conv_num(we, &wt, NumTy::Lng);
let (place, t) = self.lower_place(var, scope);
if !is_str(&t) && t != Ty::Unknown {
self.err(var.pos(), "Type mismatch");
}
if let Some(p) = place {
hf.push((we, p));
}
}
self.push(
out,
scope,
HStmtKind::Field {
file: fe,
fields: hf,
},
);
}
Stmt::GetPut {
put,
file,
recnum,
var,
..
} => {
let (fe, ft) = self.want_num(file, scope);
let fe = self.conv_num(fe, &ft, NumTy::Lng);
let re = recnum.as_ref().map(|r| {
let (e, t) = self.want_num(r, scope);
self.conv_num(e, &t, NumTy::Lng)
});
let ve = var.as_ref().and_then(|v| self.lower_place(v, scope).0);
self.push(
out,
scope,
HStmtKind::GetPut {
put: *put,
file: fe,
recnum: re,
var: ve,
},
);
}
Stmt::LsetRset {
rset,
target,
value,
pos,
} => {
let (_, tt) = self.lower_place(target, scope);
let (_, vt) = self.lower_expr(value, scope);
let ok = (is_str(&tt) && is_str(&vt))
|| matches!((&tt, &vt), (Ty::Udt(a), Ty::Udt(b)) if a == b)
|| tt == Ty::Unknown
|| vt == Ty::Unknown;
if !ok {
self.err(*pos, "Type mismatch");
}
let (place, _) = self.lower_place(target, scope);
let (ve, _) = self.lower_expr(value, scope);
if let Some(place) = place {
self.push(
out,
scope,
HStmtKind::LsetRset {
rset: *rset,
target: place,
value: ve,
},
);
}
}
Stmt::WriteStmt { file, items, .. } => {
if let Some(f) = file {
self.want_num(f, scope);
}
for e in items {
self.lower_expr(e, scope);
}
let mut args = Vec::new();
for it in items {
args.push(self.lower_expr(it, scope).0);
}
if let Some(f) = file {
let (e, t) = self.want_num(f, scope);
let e = self.conv_num(e, &t, NumTy::Lng);
self.push_ziel(out, scope, e);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::WriteFile,
args,
},
);
self.push_ziel(out, scope, HExpr::Lng(-1));
} else {
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::WriteFile,
args,
},
);
}
}
Stmt::SeekStmt { file, position, .. } => {
self.want_num(file, scope);
self.want_num(position, scope);
let (fe, ft) = self.want_num(file, scope);
let fe = self.conv_num(fe, &ft, NumTy::Lng);
let (pe, pt) = self.want_num(position, scope);
let pe = self.conv_num(pe, &pt, NumTy::Lng);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::SeekStmt,
args: vec![fe, pe],
},
);
}
Stmt::LockStmt { file, from, to, .. } => {
self.want_num(file, scope);
if let Some(f) = from {
self.want_num(f, scope);
}
if let Some(t) = to {
self.want_num(t, scope);
}
let (fe, ft) = self.want_num(file, scope);
let fe = self.conv_num(fe, &ft, NumTy::Lng);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::LockStmt,
args: vec![fe],
},
);
}
Stmt::NameStmt { old, new, .. } => {
self.want_str(old, scope);
self.want_str(new, scope);
let (a, _) = self.want_str(old, scope);
let (b2, _) = self.want_str(new, scope);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::NameStmt,
args: vec![a, b2],
},
);
}
// ---- Bildschirm/Ereignisse ----
Stmt::ViewPrint { top, bottom, .. } => {
if let Some(t) = top {
self.want_num(t, scope);
}
if let Some(b) = bottom {
self.want_num(b, scope);
}
let a = match (top, bottom) {
(Some(t), Some(b)) => {
let t = self.want_num(t, scope);
let b = self.want_num(b, scope);
vec![
self.conv_num(t.0, &t.1, NumTy::Lng),
self.conv_num(b.0, &b.1, NumTy::Lng),
]
}
_ => vec![HExpr::Lng(-1), HExpr::Lng(-1)],
};
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::ViewPrint,
args: a,
},
);
}
Stmt::GraphicsLine {
from,
to,
relative,
color,
fill,
..
} => {
let mut args = Vec::new();
if let Some((x, y)) = from {
for expr in [x, y] {
let (value, ty) = self.want_num(expr, scope);
args.push(self.conv_num(value, &ty, NumTy::Lng));
}
} else {
args.extend([HExpr::Lng(i32::MIN), HExpr::Lng(i32::MIN)]);
}
for expr in [&to.0, &to.1] {
let (value, ty) = self.want_num(expr, scope);
args.push(self.conv_num(value, &ty, NumTy::Lng));
}
let color = color.as_ref().map_or(HExpr::Lng(-1), |expr| {
let (value, ty) = self.want_num(expr, scope);
self.conv_num(value, &ty, NumTy::Lng)
});
args.extend([
color,
HExpr::Lng(i32::from(*relative)),
HExpr::Lng(i32::from(*fill)),
]);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::GraphicsLine,
args,
},
);
}
Stmt::GraphicsPaint {
point,
paint,
border,
..
} => {
let mut args = Vec::new();
for expr in [&point.0, &point.1] {
let (value, ty) = self.want_num(expr, scope);
args.push(self.conv_num(value, &ty, NumTy::Lng));
}
args.push(
paint
.as_ref()
.map_or(HExpr::Lng(-1), |expr| self.lower_expr(expr, scope).0),
);
args.push(border.as_ref().map_or(HExpr::Lng(-1), |expr| {
let (value, ty) = self.want_num(expr, scope);
self.conv_num(value, &ty, NumTy::Lng)
}));
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::GraphicsPaint,
args,
},
);
}
Stmt::GraphicsView {
rect, fill, border, ..
} => {
let mut args = Vec::new();
if let Some((x1, y1, x2, y2)) = rect {
for expr in [x1, y1, x2, y2] {
let (value, ty) = self.want_num(expr, scope);
args.push(self.conv_num(value, &ty, NumTy::Lng));
}
} else {
args.extend([
HExpr::Lng(-1),
HExpr::Lng(-1),
HExpr::Lng(-1),
HExpr::Lng(-1),
]);
}
for expr in [fill.as_ref(), border.as_ref()] {
args.push(expr.map_or(HExpr::Lng(-1), |expr| {
let (value, ty) = self.want_num(expr, scope);
self.conv_num(value, &ty, NumTy::Lng)
}));
}
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::GraphicsView,
args,
},
);
}
Stmt::TrapDef {
device,
index,
target,
pos,
} => {
let Some(art) = trap_art(device) else {
// COM/PEN/PLAY/STRIG sind Non-Feature — namentlich.
self.err(*pos, "Feature unavailable");
return;
};
// Wertebereich prüfen, soweit die Kennung konstant ist.
if let Some(i) = index {
if let Some(n) = const_zahl(i) {
if !trap_bereich_ok(art, n) {
self.err(*pos, trap_bereich_text(art));
}
}
} else if art != TRAP_UEVENT {
self.err(*pos, trap_bereich_text(art));
}
if index.is_some() && art == TRAP_UEVENT {
self.err(*pos, "ON UEVENT nimmt keine Kennung");
}
let hindex = match index {
Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
None => HExpr::Lng(0),
};
// `GOSUB 0` schaltet den Trap ab und meint nicht Zeile 0.
let ziel = if matches!(target, LabelRef::Line(0)) {
None
} else {
match self.label_id(target, scope, *pos) {
Some(id) => Some(id),
None => return,
}
};
self.push(
out,
scope,
HStmtKind::TrapDef {
art,
index: hindex,
ziel,
},
);
}
Stmt::EventControl {
device,
index,
action,
pos,
} => {
match device.as_str() {
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
_ => self.err(*pos, "Feature unavailable"),
}
if let Some(i) = index {
self.want_num(i, scope);
}
// `KEY ON`/`KEY OFF` ohne Index blendet die Softkey-Zeile
// ein bzw. aus — das ist keine Ereignissteuerung
// (die hieße `KEY(n) ON`).
if device == "KEY" && index.is_none() && *action != EventAction::Stop {
let ein = i32::from(*action == EventAction::On);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::KeyDisplay,
args: vec![HExpr::Lng(ein)],
},
);
return;
}
if device == "EVENT" {
// Das Vorbild kennt nur ON und OFF — kein STOP.
if *action == EventAction::Stop {
self.err(*pos, "EVENT kennt nur ON und OFF");
return;
}
let an = *action == EventAction::On;
self.push(out, scope, HStmtKind::EventSwitch(an));
return;
}
let Some(art) = trap_art(device) else {
self.err(*pos, "Feature unavailable");
return;
};
if let Some(i) = index {
if let Some(n) = const_zahl(i) {
if !trap_bereich_ok(art, n) {
self.err(*pos, trap_bereich_text(art));
}
}
}
let hindex = match index {
Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
None => HExpr::Lng(0),
};
let zustand = match action {
EventAction::On => 0,
EventAction::Off => 1,
EventAction::Stop => 2,
};
self.push(
out,
scope,
HStmtKind::TrapSet {
art,
index: hindex,
zustand,
},
);
}
}
}
fn decl_ty(&mut self, d: &VarDecl) -> Ty {
if let Some(t) = &d.as_type {
if let TypeName::Udt(n) = t {
if !self.udt_ids.contains_key(n) {
self.err(d.pos, "Type not defined");
}
}
type_name_ty(t)
} else {
self.name_ty(&d.name, &d.suffix)
}
}
fn lower_select(
&mut self,
expr: &Expr,
arms: &[CaseArm],
scope: &mut Scope,
out: &mut Vec<HStmt>,
) {
let (se, st) = self.lower_expr(expr, scope);
// Selektor einmal auswerten (versteckter Temp-Slot).
let sel_ty = if st == Ty::Unknown {
Ty::Sng
} else {
st.clone()
};
let temp = self.alloc_temp(scope, &sel_ty);
let temp_place = HPlace {
base: temp,
base_is_ref: false,
indices: Vec::new(),
fields: Vec::new(),
ty: self.h_ty(&sel_ty),
array_elem: None,
};
self.push(
out,
scope,
HStmtKind::Assign {
place: temp_place.clone(),
value: se,
},
);
// Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
let mut cases: Vec<(HExpr, Vec<HStmt>)> = Vec::new();
let mut else_body: Vec<HStmt> = Vec::new();
for arm in arms {
let body = self.lower_body(&arm.body, scope);
if arm.specs.is_empty() {
else_body = body;
continue;
}
let mut cond: Option<HExpr> = None;
for spec in &arm.specs {
let c = self.lower_case_spec(spec, &temp_place, &st, scope);
cond = Some(match cond {
None => c,
Some(prev) => HExpr::Logic {
op: hir::HLogic::Or,
ty: IntKind::I2,
l: Box::new(prev),
r: Box::new(c),
},
});
}
cases.push((cond.unwrap(), body));
}
// Von hinten zu verschachteltem If zusammensetzen.
let mut els = else_body;
for (cond, body) in cases.into_iter().rev() {
let stmt = HStmt {
line: scope.current_line,
kind: HStmtKind::If {
cond,
then: body,
els,
},
};
els = vec![stmt];
}
out.extend(els);
}
fn lower_case_spec(
&mut self,
spec: &CaseSpec,
temp: &HPlace,
sel_ty: &Ty,
scope: &mut Scope,
) -> HExpr {
let load = || HExpr::Load(Box::new(temp.clone()));
let cmp = |s: &mut Self, op: hir::HCmp, e: &Expr, scope: &mut Scope| -> HExpr {
let (he, et) = s.lower_expr(e, scope);
let ok = (is_num(sel_ty) && is_num(&et)) || (is_str(sel_ty) && is_str(&et));
if !ok {
s.err(e.pos(), "Type mismatch");
}
if is_str(sel_ty) && is_str(&et) {
HExpr::Cmp {
op,
ty: hir::CmpKind::Str,
l: Box::new(load()),
r: Box::new(he),
}
} else {
let common = promote_num(num_ty(sel_ty), num_ty(&et));
let l = s.conv_num(load(), sel_ty, common);
let r = s.conv_num(he, &et, common);
HExpr::Cmp {
op,
ty: hir::CmpKind::Num(common),
l: Box::new(l),
r: Box::new(r),
}
}
};
match spec {
CaseSpec::Expr(e) => cmp(self, hir::HCmp::Eq, e, scope),
CaseSpec::Is(op, e) => {
let hop = match op {
BinOp::Eq => hir::HCmp::Eq,
BinOp::Ne => hir::HCmp::Ne,
BinOp::Lt => hir::HCmp::Lt,
BinOp::Le => hir::HCmp::Le,
BinOp::Gt => hir::HCmp::Gt,
BinOp::Ge => hir::HCmp::Ge,
_ => hir::HCmp::Eq,
};
cmp(self, hop, e, scope)
}
CaseSpec::Range(a, b) => {
let lo = cmp(self, hir::HCmp::Ge, a, scope);
let hi = cmp(self, hir::HCmp::Le, b, scope);
HExpr::Logic {
op: hir::HLogic::And,
ty: IntKind::I2,
l: Box::new(lo),
r: Box::new(hi),
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn lower_def_fn(
&mut self,
name: &str,
suffix: &Option<Suffix>,
params: &[Param],
block: Option<&[Stmt]>,
single: Option<&Expr>,
scope: &mut Scope,
pos: SourcePos,
) {
// Registrierung (mit Prozedur-Id).
let ret = self.name_ty(name, suffix);
let param_infos: Vec<(Ty, bool)> =
params.iter().map(|p| (self.param_ty(p), p.array)).collect();
let id = match self.procs.get(name) {
Some(p) => p.id,
None => {
let id = self.next_proc_id;
self.next_proc_id += 1;
self.hir_procs.push(None);
id
}
};
self.procs.insert(
name.to_string(),
ProcInfo {
kind: ProcKind::Function,
ret: ret.clone(),
params: param_infos,
id,
def_fn: true,
},
);
let _ = pos;
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
// freie Namen binden an Modulvariablen.
let mut fscope = Scope {
is_def_fn: true,
..Scope::default()
};
let mut hparams = Vec::new();
for p in params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, false);
let slot = fscope.locals.len() as u16;
fscope.locals.push(hir::HVar {
name: p.name.clone(),
ty: self.h_ty(&ty),
array: false,
});
fscope.vars.insert(
key,
VarInfo {
ty: ty.clone(),
array: false,
explicit: true,
slot,
global: false,
by_ref: false,
},
);
hparams.push(hir::HParam {
name: p.name.clone(),
ty: self.h_ty(&ty),
array: false,
by_ref: false,
});
}
let ret_key = self.var_key(name, suffix, false);
let ret_slot_idx = fscope.locals.len() as u16;
fscope.locals.push(hir::HVar {
name: name.to_string(),
ty: self.h_ty(&ret),
array: false,
});
fscope.vars.insert(
ret_key,
VarInfo {
ty: ret.clone(),
array: false,
explicit: true,
slot: ret_slot_idx,
global: false,
by_ref: false,
},
);
let body = match (block, single) {
(Some(stmts), _) => {
self.prescan(stmts, &mut fscope, false);
self.lower_body(stmts, &mut fscope)
}
(_, Some(expr)) => {
let (e, et) = self.lower_expr(expr, &mut fscope);
let value = self.coerce(e, &et, &ret, expr.pos());
vec![HStmt {
line: fscope.current_line,
kind: HStmtKind::Assign {
place: HPlace {
base: VarSlot::Local(ret_slot_idx),
base_is_ref: false,
indices: Vec::new(),
fields: Vec::new(),
ty: self.h_ty(&ret),
array_elem: None,
},
value,
},
}]
}
_ => Vec::new(),
};
let hproc = hir::HProc {
name: name.to_string(),
kind: hir::HProcKind::DefFn,
params: hparams,
locals: std::mem::take(&mut fscope.locals),
ret_slot: Some(VarSlot::Local(ret_slot_idx)),
ret_ty: Some(self.h_ty(&ret)),
body,
label_count: fscope.next_label,
};
// Modul-Scope-Zeile weiterführen.
scope.current_line = scope.current_line.max(fscope.current_line);
while self.hir_procs.len() <= id as usize {
self.hir_procs.push(None);
}
self.hir_procs[id as usize] = Some(hproc);
}
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
let ty = self.decl_ty(d);
let mut hdims = Vec::new();
if let Some(dims) = &d.dims {
for (lo, hi) in dims {
let lo_e = match lo {
Some(l) => self.lower_num_as(l, scope, NumTy::Lng),
None => HExpr::Lng(self.option_base as i32),
};
let hi_e = self.lower_num_as(hi, scope, NumTy::Lng);
hdims.push((lo_e, hi_e));
}
}
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
let is_array = d.dims.is_some();
if let Some(existing) = self.vars_of(scope).get(&key).cloned() {
let redim_ok = redim && existing.array && is_array;
if existing.explicit && !redim_ok {
self.err(d.pos, "Duplicate definition");
return;
}
if redim_ok && !hdims.is_empty() {
let slot = if existing.global {
VarSlot::Global(existing.slot)
} else {
VarSlot::Local(existing.slot)
};
self.push(
out,
scope,
HStmtKind::Dim {
slot,
elem: self.h_ty(&ty),
dims: hdims,
redim: true,
},
);
return;
}
}
let (slot, global) = self.alloc_var(scope, &d.name, &ty, is_array);
self.insert_var(
scope,
key,
VarInfo {
ty: ty.clone(),
array: is_array,
explicit: true,
slot,
global,
by_ref: false,
},
);
if is_array && !hdims.is_empty() {
let vslot = if global {
VarSlot::Global(slot)
} else {
VarSlot::Local(slot)
};
self.push(
out,
scope,
HStmtKind::Dim {
slot: vslot,
elem: self.h_ty(&ty),
dims: hdims,
redim,
},
);
}
}
fn check_label_exists(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) {
let found = match target {
LabelRef::Name(n) => scope.labels.contains_key(n),
LabelRef::Line(n) => scope.line_labels.contains_key(n),
};
if !found {
self.err(pos, "Label not defined");
}
}
fn label_id(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) -> Option<LabelId> {
let id = match target {
LabelRef::Name(n) => scope.labels.get(n).copied(),
LabelRef::Line(n) => scope.line_labels.get(n).copied(),
};
if id.is_none() {
self.err(pos, "Label not defined");
}
id
}
/// Sprungziel im Modulrumpf auflösen (für `ON ERROR GOTO` aus einer
/// Prozedur heraus).
fn module_label_id(&mut self, target: &LabelRef, pos: SourcePos) -> Option<LabelId> {
let id = match target {
LabelRef::Name(n) => self.module_labels.get(n).copied(),
LabelRef::Line(n) => self.module_line_labels.get(n).copied(),
};
if id.is_none() {
self.err(pos, "Label not defined");
}
id
}
// ---- Aufrufe -----------------------------------------------------------
fn lower_call_stmt(
&mut self,
name: &str,
args: &[Expr],
scope: &mut Scope,
pos: SourcePos,
out: &mut Vec<HStmt>,
) {
if name == "CLIPBOARD.ADDITEM" {
if args.len() != 1 {
self.err(pos, "Argument-count mismatch for CLIPBOARD.ADDITEM");
return;
}
let (value, _) = self.want_str(&args[0], scope);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::ClipboardAdd,
args: vec![value],
},
);
return;
}
if name == "PRINTER.PRINT" {
self.push_ziel(out, scope, HExpr::Lng(-2));
let items = args
.iter()
.map(|arg| HPrintItem::Val(self.lower_expr(arg, scope).0))
.collect();
self.push(
out,
scope,
HStmtKind::Print {
items,
trailing: false,
},
);
self.push_ziel(out, scope, HExpr::Lng(-1));
return;
}
if matches!(name, "PRINTER.NEWPAGE" | "PRINTER.ENDDOC") {
if !args.is_empty() {
self.err(pos, format!("Argument-count mismatch for {name}"));
return;
}
if name == "PRINTER.NEWPAGE" {
self.push_ziel(out, scope, HExpr::Lng(-2));
self.push(
out,
scope,
HStmtKind::Print {
items: vec![HPrintItem::Val(HExpr::Str("\u{c}".into()))],
trailing: true,
},
);
self.push_ziel(out, scope, HExpr::Lng(-1));
}
return;
}
if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
if let Some((object, _)) = self
.forms
.objects
.iter()
.enumerate()
.find(|(_, object)| object.class == ObjectClass::Form)
{
let method = forms::methods(ObjectClass::Form)
.iter()
.position(|method| *method == name)
.unwrap() as u16;
self.push(
out,
scope,
HStmtKind::ObjectMethod {
object: object as u16,
index: None,
method,
args: Vec::new(),
},
);
return;
}
}
if name.contains('.') || name.contains('!') {
if let Some((object, info, member)) = self.object_member_target(name, pos) {
let Some(method) = forms::methods(info.class)
.iter()
.position(|m| m.eq_ignore_ascii_case(&member))
else {
self.err(
pos,
format!("Unknown method '{member}' of {}", info.class.name()),
);
return;
};
if !forms::method_is_implemented(info.class, forms::methods(info.class)[method]) {
self.err(
pos,
format!("Feature unavailable: {}.{member}", info.class.name()),
);
return;
}
let member = forms::methods(info.class)[method];
let (index, actual) = if info.array {
let Some((index, actual)) = args.split_first() else {
self.err(pos, format!("Object '{}' requires an index", info.name));
return;
};
(Some(self.lower_num_as(index, scope, NumTy::Lng)), actual)
} else {
(None, args)
};
let (min, max) = forms::method_arity(info.class, member).unwrap();
if !(min..=max).contains(&actual.len()) {
self.err(
pos,
format!("Argument-count mismatch for {}.{member}", info.class.name()),
);
for arg in actual {
self.lower_expr(arg, scope);
}
return;
}
let hargs = if info.class == ObjectClass::Form && member == "SHOW" {
actual
.iter()
.map(|a| self.lower_num_as(a, scope, NumTy::Lng))
.collect()
} else {
actual.iter().map(|a| self.lower_expr(a, scope).0).collect()
};
self.push(
out,
scope,
HStmtKind::ObjectMethod {
object,
index,
method: method as u16,
args: hargs,
},
);
return;
}
return;
}
if matches!(name, "LOAD" | "UNLOAD") && args.len() == 1 {
if let Expr::Name {
name: object_name,
suffix: None,
args: index,
pos: object_pos,
} = &args[0]
{
if let Some((object, info)) =
self.forms.find(object_name).map(|(i, o)| (i, o.clone()))
{
if index.is_some() && !info.array {
self.err(
*object_pos,
format!("Object '{object_name}' is not an array"),
);
return;
}
let index = index
.as_ref()
.and_then(|a| a.first())
.map(|e| self.lower_num_as(e, scope, NumTy::Lng));
self.push(
out,
scope,
HStmtKind::ObjectLoad {
object,
index,
unload: name == "UNLOAD",
},
);
return;
}
self.err(*object_pos, format!("Unknown object '{object_name}'"));
return;
}
}
if let Some(info) = self.procs.get(name).cloned() {
if info.kind != ProcKind::Sub {
self.err(pos, "Duplicate definition");
return;
}
if args.len() != info.params.len() {
self.err(pos, "Argument-count mismatch");
}
let hargs = self.lower_call_args(&info, args, scope);
self.push(
out,
scope,
HStmtKind::CallSub {
proc: info.id,
args: hargs,
},
);
return;
}
if let Some((min, max, spec)) = builtin_stmt(name) {
self.lower_builtin_stmt(name, args, min, max, spec, scope, pos, out);
return;
}
if banned_feature(name) {
for a in args {
self.lower_expr(a, scope);
}
self.err(pos, "Feature unavailable");
return;
}
self.err(pos, "Subprogram not defined");
}
/// Argumente eines SUB-/FUNCTION-Aufrufs: Variablen BYREF (exakter
/// Typ), Ausdrücke/Klammern BYVAL mit Konvertierung.
fn lower_call_args(&mut self, info: &ProcInfo, args: &[Expr], scope: &mut Scope) -> Vec<HArg> {
let mut out = Vec::new();
for (i, a) in args.iter().enumerate() {
let (pt, p_array) = match info.params.get(i) {
Some(p) => (p.0.clone(), p.1),
None => (Ty::Unknown, false),
};
// DEF FN: alle Parameter BYVAL.
let force_byval = info.def_fn;
// Array-Parameter: ganzes Array als Referenz.
if p_array {
if let Expr::Name {
name,
suffix,
args: idx,
pos,
} = a
{
if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) {
let base = if v.global {
VarSlot::Global(v.slot)
} else {
VarSlot::Local(v.slot)
};
out.push(HArg::ArrayRef(HPlace {
base,
base_is_ref: false,
indices: Vec::new(),
fields: Vec::new(),
ty: self.h_ty(&v.ty),
array_elem: None,
}));
continue;
}
}
}
self.err(a.pos(), "Parameter type mismatch");
continue;
}
// BYREF-fähig: einfacher Variablen-/Element-/Feldzugriff.
let byref_candidate = !force_byval && self.is_place_expr(a, scope);
if byref_candidate {
let (place, at) = self.lower_place(a, scope);
let kind_ok = (is_num(&pt) && is_num(&at))
|| (is_str(&pt) && is_str(&at))
|| matches!((&pt, &at), (Ty::Udt(x), Ty::Udt(y)) if x == y)
|| matches!(
(&pt, &at),
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
)
|| pt == Ty::Unknown
|| at == Ty::Unknown;
if !kind_ok {
self.err(a.pos(), "Parameter type mismatch");
continue;
}
// BYREF verlangt exakten Typ (Vorbild: „Parameter type
// mismatch" bei abweichendem numerischen Typ).
if pt != Ty::Unknown && at != Ty::Unknown && pt != at {
if is_num(&pt) && is_num(&at) {
self.err(a.pos(), "Parameter type mismatch");
continue;
}
// FixedStr → STRING-Parameter: BYVAL-Kopie.
if let Some(place) = place {
let e = HExpr::Load(Box::new(place));
let e = self.coerce(e, &at, &pt, a.pos());
out.push(HArg::ByVal(e));
}
continue;
}
if let Some(place) = place {
out.push(HArg::ByRef(place));
}
continue;
}
// BYVAL: Ausdruck auswerten und konvertieren.
let (e, at) = self.lower_expr(a, scope);
let kind_ok = (is_num(&pt) && is_num(&at))
|| (is_str(&pt) && is_str(&at))
|| matches!(
(&pt, &at),
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
)
|| pt == Ty::Unknown
|| at == Ty::Unknown;
if !kind_ok {
self.err(a.pos(), "Parameter type mismatch");
continue;
}
let e = self.coerce(e, &at, &pt, a.pos());
out.push(HArg::ByVal(e));
}
out
}
/// Ist der Ausdruck ein L-Wert (Variable/Element/Feld), keine
/// Konstante, Funktion oder Klammerung?
fn is_place_expr(&self, e: &Expr, scope: &Scope) -> bool {
match e {
Expr::Name {
name, suffix, args, ..
} => {
if self.consts.contains_key(name) {
return false;
}
if self.forms.find(name).is_some()
|| (name.contains('!') && !self.is_udt_path(scope, name))
{
return false;
}
match args {
None => {
// Variable oder parameterlose Funktion/Builtin?
let as_key = format!("{name}\u{1}AS");
let key = self.var_key(name, suffix, false);
let declared = self.visible_var(scope, &key).is_some()
|| self
.visible_var(scope, &as_key)
.is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
if declared {
return true;
}
let full_name = match suffix {
Some(s) => format!("{name}{}", s.as_char()),
None => name.to_string(),
};
if banned_feature(&full_name) || builtin_fn(&full_name).is_some() {
return false;
}
if let Some(info) = self.procs.get(name) {
if info.kind == ProcKind::Function && info.params.is_empty() {
return false;
}
}
true // implizite Variable
}
Some(_) => {
// Nur deklarierte Arrays sind L-Werte.
let as_key = format!("{name}\u{1}AS");
let key = self.var_key(name, suffix, false);
let v = if suffix.is_none() {
self.visible_var(scope, &as_key)
.or_else(|| self.visible_var(scope, &key))
} else {
self.visible_var(scope, &key).or_else(|| {
self.visible_var(scope, &as_key)
.filter(|v| v.ty == suffix_ty(suffix.unwrap()))
})
};
v.map(|v| v.array).unwrap_or(false)
}
}
}
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
fn lower_builtin_stmt(
&mut self,
name: &str,
args: &[Expr],
min: u8,
max: u8,
spec: &[ArgK],
scope: &mut Scope,
pos: SourcePos,
out: &mut Vec<HStmt>,
) {
// Argumentprüfung wie bisher.
let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos);
match name {
"BEEP" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Beep,
args: vec![],
},
),
"DOEVENTS" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Doevents,
args: vec![],
},
),
"RANDOMIZE" => {
let a = lowered
.into_iter()
.next()
.map(|(e, t)| {
let conv = self.conv_num(e, &t, NumTy::Dbl);
vec![conv]
})
.unwrap_or_default();
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Randomize,
args: a,
},
);
}
"SLEEP" => {
let a = lowered
.into_iter()
.next()
.map(|(e, t)| {
let conv = self.conv_num(e, &t, NumTy::Dbl);
vec![conv]
})
.unwrap_or_default();
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Sleep,
args: a,
},
);
}
"SWAP" => {
// SWAP a, b → t = a : a = b : b = t (versteckter Temp).
if args.len() == 2 {
let (pa, ta) = self.lower_place(&args[0], scope);
let (pb, tb) = self.lower_place(&args[1], scope);
let ok = (is_num(&ta) && is_num(&tb))
|| (is_str(&ta) && is_str(&tb))
|| matches!((&ta, &tb), (Ty::Udt(x), Ty::Udt(y)) if x == y)
|| ta == Ty::Unknown
|| tb == Ty::Unknown;
if !ok {
self.err(pos, "Type mismatch");
return;
}
if let (Some(pa), Some(pb)) = (pa, pb) {
let tmp = self.alloc_temp(scope, &ta);
let tmp_place = HPlace {
base: tmp,
base_is_ref: false,
indices: Vec::new(),
fields: Vec::new(),
ty: self.h_ty(&ta),
array_elem: None,
};
let load_a = HExpr::Load(Box::new(pa.clone()));
let load_b = HExpr::Load(Box::new(pb.clone()));
let b_conv = self.coerce_silent(load_b, &tb, &ta);
let t_load = HExpr::Load(Box::new(tmp_place.clone()));
let t_conv = self.coerce_silent(t_load, &ta, &tb);
self.push(
out,
scope,
HStmtKind::Assign {
place: tmp_place,
value: load_a,
},
);
self.push(
out,
scope,
HStmtKind::Assign {
place: pa,
value: b_conv,
},
);
self.push(
out,
scope,
HStmtKind::Assign {
place: pb,
value: t_conv,
},
);
}
}
}
// Bildschirmanweisungen: ausgelassene Argumente kommen als -1
// durch, damit die Laufzeit „weglassen" von „null" unterscheidet.
"ENVIRON" | "CLEAR" | "TRON" | "TROFF" | "STACK" | "MSGBOX" => {
let bt = match name {
"ENVIRON" => Builtin::EnvironSet,
"CLEAR" => Builtin::Clear,
"TRON" => Builtin::Tron,
"TROFF" => Builtin::Troff,
"MSGBOX" => Builtin::MsgBox,
_ => Builtin::StackStmt,
};
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
}
"KILL" | "CHDIR" | "CHDRIVE" | "MKDIR" | "RMDIR" | "FILES" | "SHELL" => {
let bt = match name {
"KILL" => Builtin::Kill,
"CHDIR" => Builtin::Chdir,
"CHDRIVE" => Builtin::Chdrive,
"MKDIR" => Builtin::Mkdir,
"RMDIR" => Builtin::Rmdir,
"FILES" => Builtin::Files,
_ => Builtin::ShellStmt,
};
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
}
"RESET" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::CloseAll,
args: vec![],
},
),
"SETFORMATCC" => {
let (e, t) = lowered[0].clone();
let n = self.conv_num(e, &t, NumTy::Lng);
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::SetFormatCc,
args: vec![n],
},
);
}
"KEY" => {
// `KEY LIST` (Makros auflisten) vs. `KEY n, text$` (zuweisen).
let ist_list = matches!(
args.first(),
Some(Expr::Name { name, args: None, suffix: None, .. }) if name == "LIST"
);
if ist_list {
if args.len() > 1 {
self.err(pos, "Argument-count mismatch: KEY");
}
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::KeyList,
args: vec![],
},
);
} else {
if lowered.len() != 2 {
self.err(pos, "Argument-count mismatch: KEY");
}
let (ne, nt) = lowered[0].clone();
let n = self.conv_num(ne, &nt, NumTy::Lng);
let t = lowered[1].0.clone();
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::KeyAssign,
args: vec![n, t],
},
);
}
}
"CLS" | "COLOR" | "LOCATE" | "WIDTH" | "SCREEN" => {
let b = match name {
"CLS" => Builtin::Cls,
"COLOR" => Builtin::Color,
"LOCATE" => Builtin::Locate,
"SCREEN" => Builtin::ScreenStmt,
_ => Builtin::Width,
};
let a = self.lower_opt_args(args, &lowered);
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
}
// ---- ISAM ------------------------------------------------------
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE"
| "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS"
| "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
self.lower_isam_stmt(name, args, lowered, scope, out)
}
"SETUEVENT" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::SetUEvent,
args: vec![],
},
),
"RUN" => {
let (target, string) = match lowered.first() {
None => (None, false),
Some((expr, ty)) if is_str(ty) => (Some(expr.clone()), true),
Some((expr, ty)) => (Some(self.conv_num(expr.clone(), ty, NumTy::Lng)), false),
};
self.push(out, scope, HStmtKind::Run { target, string });
}
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
_ => self.push(out, scope, HStmtKind::Unsupported("Anweisung")),
}
}
/// ISAM-Anweisungen absenken. Die Dateinummer ist überall das erste
/// Argument (bei `DELETETABLE` stattdessen der Datenbankname); alle
/// weiteren Argumente reisen unverändert durch, weil die Laufzeit sie
/// gegen das Tabellenlayout prüfen muss.
#[allow(clippy::too_many_arguments)]
fn lower_isam_stmt(
&mut self,
name: &str,
args: &[Expr],
lowered: Vec<(HExpr, Ty)>,
scope: &mut Scope,
out: &mut Vec<HStmt>,
) {
let b = match name {
"CREATEINDEX" => Builtin::IsamCreateIndex,
"DELETEINDEX" => Builtin::IsamDeleteIndex,
"SETINDEX" => Builtin::IsamSetIndex,
"INSERT" => Builtin::IsamInsert,
"RETRIEVE" => Builtin::IsamRetrieve,
"UPDATE" => Builtin::IsamUpdate,
"DELETE" => Builtin::IsamDelete,
"DELETETABLE" => Builtin::IsamDeleteTable,
"MOVEFIRST" => Builtin::IsamMoveFirst,
"MOVELAST" => Builtin::IsamMoveLast,
"MOVENEXT" => Builtin::IsamMoveNext,
"MOVEPREVIOUS" => Builtin::IsamMovePrevious,
"SEEKEQ" => Builtin::IsamSeekEq,
"SEEKGT" => Builtin::IsamSeekGt,
"SEEKGE" => Builtin::IsamSeekGe,
"BEGINTRANS" => Builtin::IsamBeginTrans,
"COMMITTRANS" => Builtin::IsamCommitTrans,
_ => Builtin::IsamRollback,
};
// Satzargument gegen den Typ der Dateinummer prüfen, sofern beide
// literal bekannt sind (Aufgabe 2.3). Ist die Nummer erst zur
// Laufzeit bekannt, prüft die Laufzeit gegen das Tabellenlayout.
if matches!(name, "INSERT" | "RETRIEVE" | "UPDATE") {
if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) = (
args.first().and_then(|a| self.fold_const(a)),
lowered.get(1),
) {
if let Some(soll) = self.isam_typ.get(&(n as i32)) {
if soll != hat {
self.err(args[1].pos(), "Type mismatch");
}
}
}
}
let mut hargs = Vec::with_capacity(lowered.len());
for (i, (e, t)) in lowered.into_iter().enumerate() {
// Erstes Argument ist die Dateinummer (bei DELETETABLE ein Name).
let ist_nummer = i == 0 && name != "DELETETABLE";
if ist_nummer || (name == "ROLLBACK" && i == 0) {
hargs.push(self.conv_num(e, &t, NumTy::Lng));
} else {
hargs.push(e);
}
}
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: hargs });
}
/// Ausgabeziel von `PRINT` umschalten (1 Bildschirm, 2 Drucker,
/// sonst Dateinummer).
fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) {
self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::PrintZiel,
args: vec![n],
},
);
}
/// Argumente einer Bildschirmanweisung: ausgelassene (`LOCATE , 5`)
/// werden zu -1, damit die Laufzeit „weglassen" erkennt (0 ist bei
/// `COLOR` eine gültige Farbe).
fn lower_opt_args(&mut self, args: &[Expr], lowered: &[(HExpr, Ty)]) -> Vec<HExpr> {
args.iter()
.enumerate()
.map(|(i, a)| {
if matches!(a, Expr::Missing) {
HExpr::Lng(-1)
} else {
let (e, t) = lowered[i].clone();
if t == Ty::Unknown {
e
} else {
self.conv_num(e, &t, NumTy::Lng)
}
}
})
.collect()
}
/// Zählt/prüft Builtin-Argumente und liefert die abgesenkten Ausdrücke.
#[allow(clippy::too_many_arguments)]
fn check_and_lower_builtin_args(
&mut self,
name: &str,
args: &[Expr],
min: u8,
max: u8,
spec: &[ArgK],
scope: &mut Scope,
pos: SourcePos,
) -> Vec<(HExpr, Ty)> {
let real: Vec<&Expr> = args
.iter()
.filter(|a| !matches!(a, Expr::Missing))
.collect();
if real.len() < min as usize || args.len() > max as usize {
// Katalogtext des Vorbilds, ergänzt um das Element: eine Diagnose
// zu einem dokumentierten Element muss es benennen (Guiding
// Principle) — sonst ist bei mehreren Aufrufen in einer Zeile
// nicht erkennbar, welcher gemeint ist.
self.err(pos, format!("Argument-count mismatch: {name}"));
}
// Sonderfall INSTR([start%,] s$, such$)
let instr_with_start = name == "INSTR" && args.len() == 3;
// Sonderfall CREATEINDEX: beliebig viele Spaltennamen ab Argument 4.
let createindex = name == "CREATEINDEX";
let mut out = Vec::new();
for (i, a) in args.iter().enumerate() {
if matches!(a, Expr::Missing) {
out.push((HExpr::Int(0), Ty::Unknown));
continue;
}
let (he, at) = self.lower_expr(a, scope);
let kind = if instr_with_start {
match i {
0 => ArgK::N,
_ => ArgK::S,
}
} else if name == "INSTR" || (createindex && i >= 3) {
ArgK::S
} else {
*spec.get(i).unwrap_or(&ArgK::A)
};
let ok = match kind {
ArgK::N => is_num(&at),
ArgK::S => is_str(&at),
ArgK::A => true,
ArgK::R => matches!(at, Ty::Udt(_) | Ty::Unknown),
};
if !ok {
self.err(a.pos(), "Type mismatch");
}
out.push((he, at));
}
out
}
// ---- Ausdrücke ---------------------------------------------------------
fn want_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
let (he, t) = self.lower_expr(e, scope);
if !is_num(&t) {
self.err(e.pos(), "Type mismatch");
}
(he, t)
}
/// `OPEN "COM1:" …` — serielle Schnittstelle ist deklariertes Non-Feature.
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
/// bleibt der Datei-E/A überlassen.
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
if let Expr::StrLit(s) = file {
if ist_com_geraet(s) {
self.err(pos, "Feature unavailable");
}
}
}
fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
let (he, t) = self.lower_expr(e, scope);
if !is_str(&t) {
self.err(e.pos(), "Type mismatch");
}
(he, t)
}
/// Numerischer Ausdruck, konvertiert auf Zieltyp.
fn lower_num_as(&mut self, e: &Expr, scope: &mut Scope, to: NumTy) -> HExpr {
let (he, t) = self.want_num(e, scope);
if t == Ty::Unknown {
he
} else {
self.conv_num(he, &t, to)
}
}
fn lower_expr_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
self.want_num(e, scope)
}
/// Bedingung: numerisch; 0 = falsch. Der Codegen testet auf ≠ 0 im
/// jeweiligen Typ (kein Konvertierungszwang nötig — auf INTEGER
/// konvertieren würde bei großen Werten fälschlich Fehler 6 auslösen,
/// daher Vergleich im Operandentyp).
fn lower_cond(&mut self, e: &Expr, scope: &mut Scope) -> HExpr {
let (he, t) = self.want_num(e, scope);
if t == Ty::Unknown || num_ty(&t) == NumTy::Int {
return he;
}
// vergleiche ≠ 0 im Operandentyp → INTEGER-Ergebnis
let nt = num_ty(&t);
let zero = match nt {
NumTy::Int => HExpr::Int(0),
NumTy::Lng => HExpr::Lng(0),
NumTy::Cur => HExpr::Cur(0),
NumTy::Sng => HExpr::Sng(0.0),
NumTy::Dbl => HExpr::Dbl(0.0),
};
HExpr::Cmp {
op: hir::HCmp::Ne,
ty: hir::CmpKind::Num(nt),
l: Box::new(he),
r: Box::new(zero),
}
}
fn check_assign(&mut self, target: &Ty, value: &Ty, pos: SourcePos) {
let ok = (is_num(target) && is_num(value))
|| (is_str(target) && is_str(value))
|| matches!((target, value), (Ty::Udt(a), Ty::Udt(b)) if a == b)
|| matches!(
(target, value),
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
)
|| *target == Ty::Unknown
|| *value == Ty::Unknown;
if !ok {
self.err(pos, "Type mismatch");
}
}
/// Wie `coerce`, aber ohne neue Diagnose (Prüfung lief bereits).
fn coerce_silent(&mut self, e: HExpr, from: &Ty, to: &Ty) -> HExpr {
if is_num(to) && is_num(from) && *to != Ty::Unknown && *from != Ty::Unknown {
return self.conv_num(e, from, num_ty(to));
}
if let Ty::FixedStr(n) = to {
if is_str(from) {
return HExpr::FixStr {
len: *n,
arg: Box::new(e),
};
}
}
e
}
/// L-Wert absenken: liefert Platz und Typ.
fn lower_place(&mut self, e: &Expr, scope: &mut Scope) -> (Option<HPlace>, Ty) {
match e {
Expr::Name {
name,
suffix,
args,
pos,
} => {
let indices = match args {
Some(idx) => {
let mut v = Vec::new();
for a in idx {
v.push(self.lower_num_as(a, scope, NumTy::Lng));
}
Some(v)
}
None => None,
};
// UDT-Feldzugriff über Punktpfad, auch auf UDT-Arrays.
if name.contains('.') {
let parts: Vec<&str> = name.split('.').collect();
let base = parts[0];
let base_as = format!("{base}\u{1}AS");
let base_info = self
.visible_var(scope, &base_as)
.or_else(|| self.visible_var(scope, &self.var_key(base, &None, false)))
.cloned();
if let Some(info) = base_info {
if matches!(info.ty, Ty::Udt(_)) {
if info.array != indices.is_some() {
self.err(*pos, "Duplicate definition");
return (None, Ty::Unknown);
}
let (fty, fpath) = self.member_path(&info.ty, &parts[1..], *pos);
let hty = self.h_ty(&fty);
let base_slot = if info.global {
VarSlot::Global(info.slot)
} else {
VarSlot::Local(info.slot)
};
return (
Some(HPlace {
base: base_slot,
base_is_ref: false,
indices: indices.unwrap_or_default(),
fields: fpath,
ty: hty,
array_elem: None,
}),
fty,
);
}
}
}
let is_array_access = indices.is_some();
let info = self.resolve_var(scope, name, suffix, is_array_access, *pos);
let Some(info) = info else {
return (None, self.name_ty(name, suffix));
};
let base = if info.global {
VarSlot::Global(info.slot)
} else {
VarSlot::Local(info.slot)
};
let hty = self.h_ty(&info.ty);
let idx_vec = indices.unwrap_or_default();
let array_elem = if !idx_vec.is_empty() {
Some((hty.clone(), idx_vec.len() as u8))
} else {
None
};
(
Some(HPlace {
base,
base_is_ref: info.by_ref,
indices: idx_vec,
fields: Vec::new(),
ty: hty,
array_elem,
}),
info.ty,
)
}
_ => {
self.err(e.pos(), "Variable required");
(None, Ty::Unknown)
}
}
}
fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
match e {
Expr::IntLit(v) => (HExpr::Int(*v), Ty::Int),
Expr::LongLit(v) => (HExpr::Lng(*v), Ty::Lng),
Expr::SingleLit(v) => (HExpr::Sng(*v), Ty::Sng),
Expr::DoubleLit(v) => (HExpr::Dbl(*v), Ty::Dbl),
Expr::CurrencyLit(v) => (HExpr::Cur(*v), Ty::Cur),
Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str),
Expr::Paren(inner) => self.lower_expr(inner, scope),
Expr::Missing => (HExpr::Int(0), Ty::Unknown),
Expr::TypeOf { value, class, pos } => {
let Some(class) = ObjectClass::parse(class) else {
self.err(*pos, format!("Unknown control class '{class}'"));
self.lower_expr(value, scope);
return (HExpr::Int(0), Ty::Int);
};
let (value, ty) = self.lower_expr(value, scope);
if !matches!(ty, Ty::Form | Ty::Control | Ty::Unknown) {
self.err(*pos, "Object required");
}
(
HExpr::TypeOf {
value: Box::new(value),
class,
},
Ty::Int,
)
}
Expr::Unary { op, operand, pos } => {
let (he, t) = self.lower_expr(operand, scope);
if !is_num(&t) {
self.err(*pos, "Type mismatch");
}
match op {
UnOp::Neg => {
let nt = num_ty(&t);
// Literal direkt negieren (hilft FOR STEP -1).
match (&he, nt) {
(HExpr::Int(v), NumTy::Int) if *v != i16::MIN => {
(HExpr::Int(-v), t.clone())
}
(HExpr::Lng(v), NumTy::Lng) if *v != i32::MIN => {
(HExpr::Lng(-v), t.clone())
}
(HExpr::Sng(v), NumTy::Sng) => (HExpr::Sng(-v), t.clone()),
(HExpr::Dbl(v), NumTy::Dbl) => (HExpr::Dbl(-v), t.clone()),
_ => (
HExpr::Neg {
ty: nt,
arg: Box::new(he),
},
t.clone(),
),
}
}
UnOp::Not => {
let kind = if num_ty(&t) == NumTy::Int {
IntKind::I2
} else {
IntKind::I4
};
let target = if kind == IntKind::I2 {
NumTy::Int
} else {
NumTy::Lng
};
let conv = if t == Ty::Unknown {
he
} else {
self.conv_num(he, &t, target)
};
(
HExpr::Not {
ty: kind,
arg: Box::new(conv),
},
if kind == IntKind::I2 {
Ty::Int
} else {
Ty::Lng
},
)
}
}
}
Expr::Binary { op, lhs, rhs, pos } => {
let (le, lt) = self.lower_expr(lhs, scope);
let (re, rt) = self.lower_expr(rhs, scope);
self.lower_binary(*op, le, lt, re, rt, *pos)
}
Expr::Name {
name,
suffix,
args,
pos,
} => self.lower_name_expr(name, suffix, args, *pos, scope),
}
}
fn lower_binary(
&mut self,
op: BinOp,
le: HExpr,
lt: Ty,
re: HExpr,
rt: Ty,
pos: SourcePos,
) -> (HExpr, Ty) {
use hir::{HArith, HCmp, HLogic};
match op {
BinOp::Add => {
if is_str(&lt) && is_str(&rt) {
(HExpr::Concat(Box::new(le), Box::new(re)), Ty::Str)
} else if is_num(&lt) && is_num(&rt) {
self.arith(HArith::Add, le, &lt, re, &rt)
} else {
self.err(pos, "Type mismatch");
(HExpr::Int(0), Ty::Unknown)
}
}
BinOp::Sub | BinOp::Mul => {
if !is_num(&lt) || !is_num(&rt) {
self.err(pos, "Type mismatch");
return (HExpr::Int(0), Ty::Unknown);
}
let a = if op == BinOp::Sub {
HArith::Sub
} else {
HArith::Mul
};
self.arith(a, le, &lt, re, &rt)
}
BinOp::Div => {
if !is_num(&lt) || !is_num(&rt) {
self.err(pos, "Type mismatch");
return (HExpr::Int(0), Ty::Unknown);
}
// `/`: DOUBLE bei DOUBLE-/CURRENCY-Operand, sonst SINGLE.
let out = if matches!(num_ty(&lt), NumTy::Dbl | NumTy::Cur)
|| matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur)
{
NumTy::Dbl
} else {
NumTy::Sng
};
let l = self.conv_num(le, &lt, out);
let r = self.conv_num(re, &rt, out);
(
HExpr::Bin {
op: HArith::Div,
ty: out,
l: Box::new(l),
r: Box::new(r),
},
ty_of_num(out),
)
}
BinOp::Pow => {
if !is_num(&lt) || !is_num(&rt) {
self.err(pos, "Type mismatch");
return (HExpr::Int(0), Ty::Unknown);
}
// `^` rechnet in DOUBLE; Ergebnis SINGLE außer bei
// DOUBLE-/CURRENCY-Operand (Festlegung: Matrix in
// docs/tbvm-design.md; Verifikation gegen die
// Original-Hilfe ist in PLAN.md Phase 3 eingeplant).
let out = if matches!(num_ty(&lt), NumTy::Dbl | NumTy::Cur)
|| matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur)
{
NumTy::Dbl
} else {
NumTy::Sng
};
let l = self.conv_num(le, &lt, NumTy::Dbl);
let r = self.conv_num(re, &rt, NumTy::Dbl);
let p = HExpr::Bin {
op: HArith::Pow,
ty: NumTy::Dbl,
l: Box::new(l),
r: Box::new(r),
};
if out == NumTy::Dbl {
(p, Ty::Dbl)
} else {
(
HExpr::Conv {
from: NumTy::Dbl,
to: NumTy::Sng,
arg: Box::new(p),
},
Ty::Sng,
)
}
}
BinOp::IntDiv | BinOp::Mod => {
if !is_num(&lt) || !is_num(&rt) {
self.err(pos, "Type mismatch");
return (HExpr::Int(0), Ty::Unknown);
}
// Operanden vorab auf Ganzzahl gerundet; INTEGER nur wenn
// beide Operanden INTEGER sind.
let out = if num_ty(&lt) == NumTy::Int && num_ty(&rt) == NumTy::Int {
NumTy::Int
} else {
NumTy::Lng
};
let l = self.conv_num(le, &lt, out);
let r = self.conv_num(re, &rt, out);
let a = if op == BinOp::IntDiv {
HArith::IDiv
} else {
HArith::Mod
};
(
HExpr::Bin {
op: a,
ty: out,
l: Box::new(l),
r: Box::new(r),
},
ty_of_num(out),
)
}
BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
let hop = match op {
BinOp::Eq => HCmp::Eq,
BinOp::Ne => HCmp::Ne,
BinOp::Lt => HCmp::Lt,
BinOp::Le => HCmp::Le,
BinOp::Gt => HCmp::Gt,
_ => HCmp::Ge,
};
if is_str(&lt) && is_str(&rt) {
(
HExpr::Cmp {
op: hop,
ty: hir::CmpKind::Str,
l: Box::new(le),
r: Box::new(re),
},
Ty::Int,
)
} else if is_num(&lt) && is_num(&rt) {
let common = promote_num(num_ty(&lt), num_ty(&rt));
let l = self.conv_num(le, &lt, common);
let r = self.conv_num(re, &rt, common);
(
HExpr::Cmp {
op: hop,
ty: hir::CmpKind::Num(common),
l: Box::new(l),
r: Box::new(r),
},
Ty::Int,
)
} else {
self.err(pos, "Type mismatch");
(HExpr::Int(0), Ty::Int)
}
}
BinOp::And | BinOp::Or | BinOp::Xor | BinOp::Eqv | BinOp::Imp => {
if !is_num(&lt) || !is_num(&rt) {
self.err(pos, "Type mismatch");
return (HExpr::Int(0), Ty::Unknown);
}
let hop = match op {
BinOp::And => HLogic::And,
BinOp::Or => HLogic::Or,
BinOp::Xor => HLogic::Xor,
BinOp::Eqv => HLogic::Eqv,
_ => HLogic::Imp,
};
// INTEGER-Ergebnis nur wenn beide Operanden INTEGER sind.
let kind = if num_ty(&lt) == NumTy::Int && num_ty(&rt) == NumTy::Int {
IntKind::I2
} else {
IntKind::I4
};
let target = if kind == IntKind::I2 {
NumTy::Int
} else {
NumTy::Lng
};
let l = self.conv_num(le, &lt, target);
let r = self.conv_num(re, &rt, target);
(
HExpr::Logic {
op: hop,
ty: kind,
l: Box::new(l),
r: Box::new(r),
},
if kind == IntKind::I2 {
Ty::Int
} else {
Ty::Lng
},
)
}
}
}
/// Arithmetik `+ - *` im promoteten Typ.
fn arith(&mut self, op: hir::HArith, le: HExpr, lt: &Ty, re: HExpr, rt: &Ty) -> (HExpr, Ty) {
if *lt == Ty::Unknown || *rt == Ty::Unknown {
return (le, Ty::Unknown);
}
let out = promote_num(num_ty(lt), num_ty(rt));
let l = self.conv_num(le, lt, out);
let r = self.conv_num(re, rt, out);
(
HExpr::Bin {
op,
ty: out,
l: Box::new(l),
r: Box::new(r),
},
ty_of_num(out),
)
}
fn lower_name_expr(
&mut self,
name: &str,
suffix: &Option<Suffix>,
args: &Option<Vec<Expr>>,
pos: SourcePos,
scope: &mut Scope,
) -> (HExpr, Ty) {
if name == "CLIPBOARD.GETTEXT" && args.as_ref().is_none_or(Vec::is_empty) {
return (
HExpr::Builtin {
b: Builtin::ClipboardGet,
args: Vec::new(),
ret: HTy::Str,
},
Ty::Str,
);
}
if name == "SCREEN" && args.is_some() {
return self.lower_builtin_fn(name, args.as_deref().unwrap(), scope, pos);
}
if suffix.is_none() && args.is_none() && !name.contains('.') && !name.contains('!') {
if let Some((object, property, spec)) = self.implicit_form_property(scope, name) {
return (
HExpr::ObjectProperty {
object,
index: None,
property,
ty: self.h_ty(&Self::property_ty(spec)),
},
Self::property_ty(spec),
);
}
}
if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name) {
if name.contains('!') && !name.contains('.') {
return match self.object_member_target(name, pos) {
Some((object, info, _)) => {
let Ok(index) = self.object_index(object, args, scope, pos) else {
return (HExpr::Int(0), Ty::Unknown);
};
let ty = if info.class == ObjectClass::Form {
Ty::Form
} else {
Ty::Control
};
(
HExpr::ObjectRef {
object,
index: index.map(Box::new),
class: info.class,
},
ty,
)
}
None => (HExpr::Int(0), Ty::Unknown),
};
}
if args.is_none() {
match self.dynamic_property(scope, name, pos) {
Ok(Some((object, spec))) => {
let ty = Self::property_ty(spec);
return (
HExpr::DynamicObjectProperty {
object: Box::new(object),
property: spec.name.to_string(),
},
ty,
);
}
Err(()) => return (HExpr::Int(0), Ty::Unknown),
Ok(None) => {}
}
}
let Some((object, info, member)) = self.object_member_target(name, pos) else {
return (HExpr::Int(0), Ty::Unknown);
};
if let Some((property, spec)) = forms::property(info.class, &member) {
if spec.name == "LIST" || spec.ty == PropertyType::IntegerArray {
let Some(indices) = args else {
self.err(pos, "Argument-count mismatch for LIST");
return (HExpr::Int(0), Ty::Unknown);
};
let expected = if info.array { 2 } else { 1 };
if indices.len() != expected {
self.err(pos, "Argument-count mismatch for LIST");
return (HExpr::Int(0), Ty::Unknown);
}
let object_index = info
.array
.then(|| Box::new(self.lower_num_as(&indices[0], scope, NumTy::Lng)));
let index = self.lower_num_as(&indices[expected - 1], scope, NumTy::Lng);
let ty = Self::property_ty(spec);
return (
HExpr::ObjectIndexedProperty {
object,
object_index,
property,
index: Box::new(index),
ty: self.h_ty(&ty),
},
ty,
);
}
return {
let Ok(index) = self.object_index(object, args, scope, pos) else {
return (HExpr::Int(0), Ty::Unknown);
};
(
HExpr::ObjectProperty {
object,
index: index.map(Box::new),
property,
ty: self.h_ty(&Self::property_ty(spec)),
},
Self::property_ty(spec),
)
};
}
if let Some(method) = forms::methods(info.class)
.iter()
.position(|m| m.eq_ignore_ascii_case(&member))
{
let Some(return_type) =
forms::method_return_type(info.class, forms::methods(info.class)[method])
else {
self.err(pos, format!("Method '{member}' has no return value"));
return (HExpr::Int(0), Ty::Unknown);
};
let supplied = args.as_deref().unwrap_or(&[]);
let (index, actual) = if info.array {
let Some((index, actual)) = supplied.split_first() else {
self.err(pos, format!("Object '{}' requires an index", info.name));
return (HExpr::Int(0), Ty::Unknown);
};
(
Some(Box::new(self.lower_num_as(index, scope, NumTy::Lng))),
actual,
)
} else {
(None, supplied)
};
let (min, max) =
forms::method_arity(info.class, forms::methods(info.class)[method]).unwrap();
if !(min..=max).contains(&actual.len()) {
self.err(
pos,
format!("Argument-count mismatch for {}.{member}", info.class.name()),
);
return (HExpr::Int(0), Ty::Unknown);
}
let hargs = actual
.iter()
.map(|arg| self.lower_expr(arg, scope).0)
.collect();
let ty = Self::property_ty(forms::PropertySpec {
name: "",
ty: return_type,
default: forms::PropertyDefault::Empty,
min: None,
max: None,
writable: false,
});
return (
HExpr::ObjectMethodCall {
object,
index,
method: method as u16,
args: hargs,
ty: self.h_ty(&ty),
},
ty,
);
}
self.err(
pos,
format!(
"Unknown property or method '{member}' of {}",
info.class.name()
),
);
return (HExpr::Int(0), Ty::Unknown);
}
if let Some((object, info)) = self.forms.find(name).map(|(id, info)| (id, info.clone())) {
if args.is_none() || info.array {
let Ok(index) = self.object_index(object, args, scope, pos) else {
return (HExpr::Int(0), Ty::Unknown);
};
let ty = if info.class == ObjectClass::Form {
Ty::Form
} else {
Ty::Control
};
return (
HExpr::ObjectRef {
object,
index: index.map(Box::new),
class: info.class,
},
ty,
);
}
}
let full_name = match suffix {
Some(s) => format!("{name}{}", s.as_char()),
None => name.to_string(),
};
// 1. Konstante
if let Some((t, v)) = self.consts.get(name).cloned() {
if args.is_some() {
self.err(pos, "Syntax error");
}
let e = match (&t, v) {
(_, Some(ConstVal::Str(s))) => HExpr::Str(s),
(Ty::Int, Some(ConstVal::Num(n))) => HExpr::Int(n as i16),
(Ty::Lng, Some(ConstVal::Num(n))) => HExpr::Lng(n as i32),
(Ty::Sng, Some(ConstVal::Num(n))) => HExpr::Sng(n as f32),
(Ty::Cur, Some(ConstVal::Num(n))) => HExpr::Cur((n * 10_000.0).round() as i64),
(_, Some(ConstVal::Num(n))) => HExpr::Dbl(n),
_ => HExpr::Int(0),
};
return (e, t);
}
match args {
Some(idx) => {
// 2. Deklariertes Array
let as_key = format!("{name}\u{1}AS");
let key = self.var_key(name, suffix, false);
let existing = if suffix.is_none() {
self.visible_var(scope, &as_key)
.or_else(|| self.visible_var(scope, &key))
} else {
self.visible_var(scope, &key).or_else(|| {
self.visible_var(scope, &as_key)
.filter(|v| v.ty == suffix_ty(suffix.unwrap()))
})
};
let existing = existing.cloned();
if let Some(v) = existing {
if v.array {
let (place, ty) = self.lower_place(
&Expr::Name {
name: name.to_string(),
suffix: *suffix,
args: Some(idx.clone()),
pos,
},
scope,
);
let e = match place {
Some(p) => HExpr::Load(Box::new(p)),
None => HExpr::Int(0),
};
return (e, ty);
}
// Rekursion: In einer FUNCTION ist der Name mit
// Argumentliste der (rekursive) Aufruf, ohne Argumente
// die Rückgabevariable.
let is_fn_call = self
.procs
.get(name)
.map(|p| p.kind == ProcKind::Function)
.unwrap_or(false);
if !is_fn_call {
self.err(pos, "Duplicate definition");
return (HExpr::Int(0), Ty::Unknown);
}
}
// 3. Nicht unterstützte Features → Compile-Fehler
if banned_feature(&full_name) {
for a in idx {
self.lower_expr(a, scope);
}
self.err(pos, "Feature unavailable");
return (HExpr::Int(0), Ty::Unknown);
}
// 4. Builtin-Funktion
if builtin_fn(&full_name).is_some() {
return self.lower_builtin_fn(&full_name, idx, scope, pos);
}
// 5. FUNCTION / DEF FN
if let Some(info) = self.procs.get(name).cloned() {
if info.kind == ProcKind::Function {
if idx.len() != info.params.len() {
self.err(pos, "Argument-count mismatch");
}
let hargs = self.lower_call_args(&info, idx, scope);
let ret = info.ret.clone();
return (
HExpr::FnCall {
proc: info.id,
args: hargs,
ret: self.h_ty(&ret),
},
ret,
);
}
self.err(pos, "Duplicate definition");
return (HExpr::Int(0), Ty::Unknown);
}
// 6. Implizites Array (klassisch: DIM x(10) implizit)
if self.explicit {
for a in idx {
self.want_num(a, scope);
}
self.err(pos, "Variable not defined");
return (HExpr::Int(0), self.name_ty(name, suffix));
}
let (place, ty) = self.lower_place(
&Expr::Name {
name: name.to_string(),
suffix: *suffix,
args: Some(idx.clone()),
pos,
},
scope,
);
let e = match place {
Some(p) => HExpr::Load(Box::new(p)),
None => HExpr::Int(0),
};
(e, ty)
}
None => {
// 2. Parameterlose Builtins (RND, ERR, INKEY$, TIMER …)
let as_key = format!("{name}\u{1}AS");
let key = self.var_key(name, suffix, false);
let declared = self.visible_var(scope, &key).is_some()
|| self
.visible_var(scope, &as_key)
.is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
if !declared {
if banned_feature(&full_name) {
self.err(pos, "Feature unavailable");
return (HExpr::Int(0), Ty::Unknown);
}
if let Some((0, _, _, _)) = builtin_fn(&full_name) {
return self.lower_builtin_fn(&full_name, &[], scope, pos);
}
if let Some(info) = self.procs.get(name).cloned() {
if info.kind == ProcKind::Function && info.params.is_empty() {
let ret = info.ret.clone();
return (
HExpr::FnCall {
proc: info.id,
args: Vec::new(),
ret: self.h_ty(&ret),
},
ret,
);
}
}
}
// 3. Variable (ggf. implizit deklarieren)
let (place, ty) = self.lower_place(
&Expr::Name {
name: name.to_string(),
suffix: *suffix,
args: None,
pos,
},
scope,
);
let e = match place {
Some(p) => HExpr::Load(Box::new(p)),
None => HExpr::Int(0),
};
(e, ty)
}
}
}
/// Builtin-Funktionsaufruf absenken (inkl. Argument-Konvertierungen).
fn lower_builtin_fn(
&mut self,
full_name: &str,
args: &[Expr],
scope: &mut Scope,
pos: SourcePos,
) -> (HExpr, Ty) {
let (min, max, spec, retk) = builtin_fn(full_name).unwrap();
// LBOUND/UBOUND brauchen das Array selbst.
if full_name == "LBOUND" || full_name == "UBOUND" {
if args.is_empty() || args.len() > 2 {
self.err(pos, "Argument-count mismatch");
return (HExpr::Lng(0), Ty::Lng);
}
let place = if let Expr::Name {
name,
suffix,
args: idx,
pos,
} = &args[0]
{
if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
self.resolve_var(scope, name, suffix, true, *pos)
.map(|v| HPlace {
base: if v.global {
VarSlot::Global(v.slot)
} else {
VarSlot::Local(v.slot)
},
base_is_ref: false,
indices: Vec::new(),
fields: Vec::new(),
ty: self.h_ty(&v.ty),
array_elem: None,
})
} else {
None
}
} else {
None
};
let dim = match args.get(1) {
Some(d) => self.lower_num_as(d, scope, NumTy::Lng),
None => HExpr::Lng(1),
};
match place {
Some(p) => {
return (
HExpr::ArrayBound {
lower: full_name == "LBOUND",
place: Box::new(p),
dim: Box::new(dim),
},
Ty::Lng,
)
}
None => {
self.err(args[0].pos(), "Type mismatch");
return (HExpr::Lng(0), Ty::Lng);
}
}
}
let lowered =
self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos);
let get = |i: usize| -> Option<&(HExpr, Ty)> { lowered.get(i) };
let take = |l: &[(HExpr, Ty)], i: usize| -> (HExpr, Ty) {
l.get(i).cloned().unwrap_or((HExpr::Int(0), Ty::Unknown))
};
macro_rules! conv_arg {
($i:expr, $to:expr) => {{
let (e, t) = take(&lowered, $i);
if t == Ty::Unknown {
e
} else {
self.conv_num(e, &t, $to)
}
}};
}
let b = |b: Builtin, args: Vec<HExpr>, ret: Ty, s: &Sema| -> (HExpr, Ty) {
let rty = s.h_ty(&ret);
(HExpr::Builtin { b, args, ret: rty }, ret)
};
match full_name {
// --- Strings ---
"LEN" => {
let (e, t) = take(&lowered, 0);
if is_str(&t) && t != Ty::Unknown {
b(Builtin::Len, vec![e], Ty::Int, self)
} else if let Some(size) = self.fixed_width(&self.h_ty(&t)) {
(HExpr::Int(size), Ty::Int)
} else {
// LEN(zahlvariable) = Bytegröße des Typs
let size: i16 = match num_ty(&t) {
NumTy::Int => 2,
NumTy::Lng | NumTy::Sng => 4,
NumTy::Cur | NumTy::Dbl => 8,
};
(HExpr::Int(size), Ty::Int)
}
}
"LEFT$" => {
let (s, _) = take(&lowered, 0);
let n = conv_arg!(1, NumTy::Lng);
b(Builtin::LeftS, vec![s, n], Ty::Str, self)
}
"RIGHT$" => {
let (s, _) = take(&lowered, 0);
let n = conv_arg!(1, NumTy::Lng);
b(Builtin::RightS, vec![s, n], Ty::Str, self)
}
"MID$" => {
let (s, _) = take(&lowered, 0);
let start = conv_arg!(1, NumTy::Lng);
let len = if get(2).is_some() {
conv_arg!(2, NumTy::Lng)
} else {
HExpr::Lng(-1)
};
b(Builtin::MidS, vec![s, start, len], Ty::Str, self)
}
"INSTR" => {
let (start, s, t) = if args.len() == 3 {
let st = conv_arg!(0, NumTy::Lng);
(st, take(&lowered, 1).0, take(&lowered, 2).0)
} else {
(HExpr::Lng(1), take(&lowered, 0).0, take(&lowered, 1).0)
};
b(Builtin::InstrF, vec![start, s, t], Ty::Int, self)
}
"UCASE$" => b(Builtin::UcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
"LCASE$" => b(Builtin::LcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
"LTRIM$" => b(Builtin::LtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
"RTRIM$" => b(Builtin::RtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
"SPACE$" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::SpaceS, vec![n], Ty::Str, self)
}
"STRING$" => {
let n = conv_arg!(0, NumTy::Lng);
let (ch, cht) = take(&lowered, 1);
let ch = if is_num(&cht) && cht != Ty::Unknown {
self.conv_num(ch, &cht, NumTy::Lng)
} else {
ch
};
b(Builtin::StringS, vec![n, ch], Ty::Str, self)
}
"CHR$" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::ChrS, vec![n], Ty::Str, self)
}
"ASC" => b(Builtin::Asc, vec![take(&lowered, 0).0], Ty::Int, self),
"STR$" => {
// Typ bleibt erhalten (Formatierung je Tag).
b(Builtin::StrS, vec![take(&lowered, 0).0], Ty::Str, self)
}
"VAL" => b(Builtin::Val, vec![take(&lowered, 0).0], Ty::Dbl, self),
"HEX$" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::HexS, vec![n], Ty::Str, self)
}
"OCT$" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::OctS, vec![n], Ty::Str, self)
}
// --- Konvertierungsfunktionen → Conv-Knoten ---
"CINT" | "CLNG" | "CSNG" | "CDBL" | "CCUR" => {
let (e, t) = take(&lowered, 0);
let to = match full_name {
"CINT" => NumTy::Int,
"CLNG" => NumTy::Lng,
"CSNG" => NumTy::Sng,
"CDBL" => NumTy::Dbl,
_ => NumTy::Cur,
};
let e = if t == Ty::Unknown {
e
} else {
self.conv_num(e, &t, to)
};
(e, ty_of_num(to))
}
// --- Mathematik ---
"ABS" | "FIX" | "INT" => {
let (e, t) = take(&lowered, 0);
let bt = match full_name {
"ABS" => Builtin::Abs,
"FIX" => Builtin::Fix,
_ => Builtin::IntF,
};
// Ergebnistyp = Operandentyp (Vorbild).
let rt = if t == Ty::Unknown {
Ty::Sng
} else {
ty_of_num(num_ty(&t))
};
b(bt, vec![e], rt, self)
}
"SGN" => b(Builtin::Sgn, vec![take(&lowered, 0).0], Ty::Int, self),
"SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => {
let e = conv_arg!(0, NumTy::Dbl);
let bt = match full_name {
"SQR" => Builtin::Sqr,
"EXP" => Builtin::Exp,
"LOG" => Builtin::Log,
"SIN" => Builtin::Sin,
"COS" => Builtin::Cos,
"TAN" => Builtin::Tan,
_ => Builtin::Atn,
};
b(bt, vec![e], Ty::Dbl, self)
}
"RND" => {
let a = if !lowered.is_empty() {
vec![conv_arg!(0, NumTy::Sng)]
} else {
vec![]
};
b(Builtin::Rnd, a, Ty::Sng, self)
}
// --- Datei-E/A ---
"EOF" | "LOF" | "LOC" | "SEEK" | "FILEATTR" => {
let f = conv_arg!(0, NumTy::Lng);
match full_name {
"EOF" => b(Builtin::EofF, vec![f], Ty::Int, self),
"LOF" => b(Builtin::LofF, vec![f], Ty::Lng, self),
"LOC" => b(Builtin::LocF, vec![f], Ty::Lng, self),
"SEEK" => b(Builtin::SeekF, vec![f], Ty::Lng, self),
_ => {
let art = conv_arg!(1, NumTy::Lng);
b(Builtin::Fileattr, vec![f, art], Ty::Lng, self)
}
}
}
"FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self),
// --- ISAM-Funktionen ---
"GETINDEX$" => {
let f = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamGetIndexS, vec![f], Ty::Str, self)
}
"BOF" => {
let f = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamBof, vec![f], Ty::Int, self)
}
"SAVEPOINT" => b(Builtin::IsamSavepoint, vec![], Ty::Int, self),
"SETMEM" => {
let n = conv_arg!(0, NumTy::Lng);
b(Builtin::IsamSetmem, vec![n], Ty::Lng, self)
}
"ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
"ERDEV" => b(Builtin::Erdev, vec![], Ty::Int, self),
"ERDEV$" => b(Builtin::ErdevS, vec![], Ty::Str, self),
"CURDIR$" => b(Builtin::CurdirS, vec![], Ty::Str, self),
"DIR$" => {
let m = if lowered.is_empty() {
HExpr::Str(String::new())
} else {
take(&lowered, 0).0
};
b(Builtin::DirS, vec![m], Ty::Str, self)
}
"LPOS" => b(Builtin::Lpos, vec![], Ty::Int, self),
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" | "MKSMBF$" | "MKDMBF$" => {
// Zweites Argument wählt Breite und Typ.
let art = match full_name {
"MKI$" => 0,
"MKL$" => 1,
"MKS$" | "MKSMBF$" => 2,
"MKD$" | "MKDMBF$" => 3,
_ => 4,
};
let wert = take(&lowered, 0).0;
b(Builtin::MkS, vec![wert, HExpr::Lng(art)], Ty::Str, self)
}
"CVI" | "CVL" | "CVS" | "CVD" | "CVC" | "CVSMBF" | "CVDMBF" => {
let art = match full_name {
"CVI" => 0,
"CVL" => 1,
"CVS" | "CVSMBF" => 2,
"CVD" | "CVDMBF" => 3,
_ => 4,
};
let ret = match art {
0 => Ty::Int,
1 => Ty::Lng,
2 => Ty::Sng,
3 => Ty::Dbl,
_ => Ty::Cur,
};
let s = take(&lowered, 0).0;
b(Builtin::CvF, vec![s, HExpr::Lng(art)], ret, self)
}
"SHELL" => {
let c = take(&lowered, 0).0;
b(Builtin::ShellFn, vec![c], Ty::Lng, self)
}
// --- Finanzmathematik ---
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
let a: Vec<HExpr> = (0..5).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
let bt = match full_name.trim_end_matches('#') {
"FV" => Builtin::Fv,
"PV" => Builtin::Pv,
"PMT" => Builtin::Pmt,
_ => Builtin::NPer,
};
b(bt, a, Ty::Dbl, self)
}
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
let a: Vec<HExpr> = (0..6).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
let bt = match full_name.trim_end_matches('#') {
"IPMT" => Builtin::IPmt,
"PPMT" => Builtin::PPmt,
_ => Builtin::Rate,
};
b(bt, a, Ty::Dbl, self)
}
"NPV" | "NPV#" => {
let zins = conv_arg!(0, NumTy::Dbl);
let reihe = take(&lowered, 1).0;
b(Builtin::Npv, vec![zins, reihe], Ty::Dbl, self)
}
"IRR" | "IRR#" => {
let reihe = take(&lowered, 0).0;
let schaetzung = conv_arg!(1, NumTy::Dbl);
b(Builtin::Irr, vec![reihe, schaetzung], Ty::Dbl, self)
}
"MIRR" | "MIRR#" => {
let reihe = take(&lowered, 0).0;
let f = conv_arg!(1, NumTy::Dbl);
let w = conv_arg!(2, NumTy::Dbl);
b(Builtin::Mirr, vec![reihe, f, w], Ty::Dbl, self)
}
"SLN" | "SLN#" => {
let a: Vec<HExpr> = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
b(Builtin::Sln, a, Ty::Dbl, self)
}
"SYD" | "SYD#" | "DDB" | "DDB#" => {
let a: Vec<HExpr> = (0..4).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
let bt = if full_name.starts_with("SYD") {
Builtin::Syd
} else {
Builtin::Ddb
};
b(bt, a, Ty::Dbl, self)
}
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
"DATESERIAL" | "TIMESERIAL" => {
let x = conv_arg!(0, NumTy::Lng);
let y = conv_arg!(1, NumTy::Lng);
let z = conv_arg!(2, NumTy::Lng);
let bt = if full_name == "DATESERIAL" {
Builtin::DateSerial
} else {
Builtin::TimeSerial
};
b(bt, vec![x, y, z], Ty::Dbl, self)
}
"DATEVALUE" => b(Builtin::DateValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
"TIMEVALUE" => b(Builtin::TimeValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => {
let e = conv_arg!(0, NumTy::Dbl);
let bt = match full_name {
"DAY" => Builtin::DayF,
"MONTH" => Builtin::MonthF,
"YEAR" => Builtin::YearF,
"WEEKDAY" => Builtin::WeekdayF,
"HOUR" => Builtin::HourF,
"MINUTE" => Builtin::MinuteF,
_ => Builtin::SecondF,
};
b(bt, vec![e], Ty::Int, self)
}
"FORMAT$" => {
let mut a = vec![take(&lowered, 0).0];
if get(1).is_some() {
a.push(take(&lowered, 1).0);
}
b(Builtin::FormatS, a, Ty::Str, self)
}
// --- Bildschirm ---
"TAB" | "SPC" => {
// In einer PRINT-Liste werden sie eigens abgesenkt; hier
// stehen sie außerhalb und sind dort nicht zulässig.
self.err(pos, format!("Illegal function call: {full_name}"));
(HExpr::Str(String::new()), Ty::Str)
}
"CSRLIN" => b(Builtin::Csrlin, vec![], Ty::Int, self),
"POS" => {
// `POS(0)` — das Argument ist im Vorbild ein Dummy.
b(Builtin::PosFn, vec![], Ty::Int, self)
}
"SCREEN" => {
let zeile = conv_arg!(0, NumTy::Lng);
let spalte = conv_arg!(1, NumTy::Lng);
let farbe = if get(2).is_some() {
conv_arg!(2, NumTy::Lng)
} else {
HExpr::Lng(0)
};
b(Builtin::ScreenFn, vec![zeile, spalte, farbe], Ty::Int, self)
}
"INKEY$" => b(Builtin::InkeyS, vec![], Ty::Str, self),
"INPUT$" => {
let n = conv_arg!(0, NumTy::Lng);
let mut args = vec![n];
if get(1).is_some() {
args.push(conv_arg!(1, NumTy::Lng));
}
b(Builtin::InputS, args, Ty::Str, self)
}
// --- Fehlerstatus ---
"ERR" => (HExpr::Err, Ty::Lng),
"ERL" => (HExpr::Erl, Ty::Lng),
// --- Sonstiges (Phase-2-Scheibe) ---
"TIMER" => b(Builtin::Timer, vec![], Ty::Sng, self),
"DATE$" => b(Builtin::DateS, vec![], Ty::Str, self),
"TIME$" => b(Builtin::TimeS, vec![], Ty::Str, self),
"COMMAND$" => b(Builtin::CommandS, vec![], Ty::Str, self),
"DOEVENTS" => b(Builtin::Doevents, vec![], Ty::Int, self),
"MSGBOX" => b(
Builtin::MsgBox,
lowered.into_iter().map(|(expr, _)| expr).collect(),
Ty::Int,
self,
),
"INPUTBOX$" => b(
Builtin::InputBoxS,
lowered.into_iter().map(|(expr, _)| expr).collect(),
Ty::Str,
self,
),
// --- Spätere Phasen: dokumentiert, aber noch nicht verfügbar ---
_ => (HExpr::Unsupported("Funktion"), ret_ty(retk)),
}
}
}
/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
fn stmt_pos(stmt: &Stmt) -> SourcePos {
match stmt {
Stmt::Assign { pos, .. }
| Stmt::Print { pos, .. }
| Stmt::Input { pos, .. }
| Stmt::If { pos, .. }
| Stmt::Select { pos, .. }
| Stmt::For { pos, .. }
| Stmt::DoLoop { pos, .. }
| Stmt::While { pos, .. }
| Stmt::Goto { pos, .. }
| Stmt::Gosub { pos, .. }
| Stmt::OnGoto { pos, .. }
| Stmt::Return { pos, .. }
| Stmt::Exit { pos, .. }
| Stmt::Dim { pos, .. }
| Stmt::SharedDecl { pos, .. }
| Stmt::StaticDecl { pos, .. }
| Stmt::CommonDecl { pos, .. }
| Stmt::Erase { pos, .. }
| Stmt::ConstDecl { pos, .. }
| Stmt::DefType { pos, .. }
| Stmt::OptionStmt { pos, .. }
| Stmt::TypeDecl { pos, .. }
| Stmt::Declare { pos, .. }
| Stmt::Call { pos, .. }
| Stmt::OnError { pos, .. }
| Stmt::Resume { pos, .. }
| Stmt::ErrorStmt { pos, .. }
| Stmt::Data { pos, .. }
| Stmt::ReadStmt { pos, .. }
| Stmt::Restore { pos, .. }
| Stmt::DefFn { pos, .. }
| Stmt::DefFnBlock { pos, .. }
| Stmt::Open { pos, .. }
| Stmt::OpenLegacy { pos, .. }
| Stmt::CloseStmt { pos, .. }
| Stmt::FieldStmt { pos, .. }
| Stmt::GetPut { pos, .. }
| Stmt::LsetRset { pos, .. }
| Stmt::WriteStmt { pos, .. }
| Stmt::SeekStmt { pos, .. }
| Stmt::LockStmt { pos, .. }
| Stmt::NameStmt { pos, .. }
| Stmt::ViewPrint { pos, .. }
| Stmt::GraphicsLine { pos, .. }
| Stmt::GraphicsPaint { pos, .. }
| Stmt::GraphicsView { pos, .. }
| Stmt::TrapDef { pos, .. }
| Stmt::EventControl { pos, .. }
| Stmt::Include { pos, .. }
| Stmt::MetaArrays { pos, .. }
| Stmt::MetaForm { pos }
| Stmt::End(pos)
| Stmt::StopStmt(pos)
| Stmt::System(pos) => *pos,
Stmt::Label(_) | Stmt::LineNumber(_) => SourcePos::default(),
}
}
// ---- Ereignis-Traps (Sprachreferenz §8) -------------------------------------
pub const TRAP_KEY: u8 = 0;
pub const TRAP_TIMER: u8 = 1;
pub const TRAP_UEVENT: u8 = 2;
pub const TRAP_SIGNAL: u8 = 3;
/// Quellenname → Art. `None` für die Non-Features.
fn trap_art(device: &str) -> Option<u8> {
Some(match device {
"KEY" => TRAP_KEY,
"TIMER" => TRAP_TIMER,
"UEVENT" => TRAP_UEVENT,
"SIGNAL" => TRAP_SIGNAL,
_ => return None,
})
}
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
fn const_zahl(e: &Expr) -> Option<i64> {
match e {
Expr::IntLit(n) => Some(*n as i64),
Expr::LongLit(n) => Some(*n as i64),
Expr::SingleLit(n) => Some(*n as i64),
Expr::DoubleLit(n) => Some(*n as i64),
Expr::Paren(inner) => const_zahl(inner),
_ => None,
}
}
/// Wertebereiche nach der Original-Hilfe (Belege in `umfang-und-form.md`).
fn trap_bereich_ok(art: u8, n: i64) -> bool {
match art {
TRAP_KEY => matches!(n, 0..=25 | 30 | 31),
TRAP_TIMER => (1..=86_400).contains(&n),
TRAP_SIGNAL => (1..=2).contains(&n),
_ => true,
}
}
fn trap_bereich_text(art: u8) -> &'static str {
match art {
TRAP_KEY => "ON KEY: Kennung 0, 1-25 oder 30-31 erwartet",
TRAP_TIMER => "ON TIMER: Intervall 1 bis 86400 Sekunden erwartet",
TRAP_SIGNAL => "ON SIGNAL: Kennung 1 (SIGINT) oder 2 (SIGTERM) erwartet",
_ => "Kennung erwartet",
}
}
use crate::hir::literal_value;
#[cfg(test)]
mod tests {
use crate::forms::{FormCatalog, ObjectClass};
use crate::{analyze_source, analyze_source_with_forms};
fn diags(src: &str) -> Vec<String> {
analyze_source("TEST", src)
.diagnostics
.into_iter()
.map(|d| d.message)
.collect()
}
#[test]
fn typkonflikt_wird_erkannt() {
assert!(diags("a$ = 1").contains(&"Type mismatch".to_string()));
assert!(diags("a% = \"x\"").contains(&"Type mismatch".to_string()));
assert!(diags("a = 1 + \"x\"").contains(&"Type mismatch".to_string()));
assert!(diags("s$ = \"a\" + \"b\"").is_empty());
}
#[test]
fn option_explicit() {
assert!(diags("OPTION EXPLICIT\nx = 1").contains(&"Variable not defined".to_string()));
assert!(diags("OPTION EXPLICIT\nDIM x AS INTEGER\nx = 1").is_empty());
}
#[test]
fn deftype_regeln() {
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
assert!(diags("DEFINT I\ni = 5").is_empty());
}
#[test]
fn unbekanntes_unterprogramm() {
assert!(diags("Foo 1").contains(&"Subprogram not defined".to_string()));
assert!(diags("SUB Foo (a%)\nEND SUB\n' Aufruf\nFoo 1").is_empty());
}
#[test]
fn argumentanzahl() {
assert!(diags("PRINT LEFT$(\"a\")").contains(&"Argument-count mismatch: LEFT$".to_string()));
assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1")
.contains(&"Argument-count mismatch".to_string()));
}
#[test]
fn hardware_features_zur_compilezeit_abgelehnt() {
assert!(diags("POKE 100, 1").contains(&"Feature unavailable".to_string()));
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable".to_string()));
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable".to_string()));
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable".to_string()));
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable".to_string()));
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable".to_string()));
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable".to_string()));
}
#[test]
fn label_pruefung() {
assert!(diags("GOTO Nirwana").contains(&"Label not defined".to_string()));
assert!(diags("Ziel:\nGOTO Ziel").is_empty());
assert!(diags("10 PRINT 1\nGOTO 10").is_empty());
}
#[test]
fn funktionsaufruf_und_rueckgabe() {
let d = diags("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
assert!(d.is_empty(), "{d:?}");
}
#[test]
fn arrays_implizit_und_explizit() {
assert!(diags("DIM a(10)\na(1) = 2\nPRINT a(1)").is_empty());
assert!(diags("b(3) = 1").is_empty()); // implizites Array
assert!(diags("DIM c(5)\nDIM c(5)").contains(&"Duplicate definition".to_string()));
assert!(diags("REDIM d(5)\nREDIM d(9)").is_empty());
}
#[test]
fn builtins_typen() {
assert!(diags("PRINT LEN(\"abc\")").is_empty());
assert!(diags("PRINT MID$(\"abc\", 2, 1)").is_empty());
assert!(diags("PRINT CHR$(\"x\")").contains(&"Type mismatch".to_string()));
assert!(diags("x$ = INKEY$").is_empty());
assert!(diags("t! = TIMER").is_empty());
}
#[test]
fn mid_anweisung() {
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"").is_empty());
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5").contains(&"Type mismatch".to_string()));
}
#[test]
fn def_fn_blockform() {
let d = diags("DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)");
assert!(d.is_empty(), "{d:?}");
}
#[test]
fn udt_feldtypen() {
let src = "TYPE Kunde\nName AS STRING * 30\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Umsatz = 5\nk.Name = \"x\"";
assert!(diags(src).is_empty(), "{:?}", diags(src));
let bad = "TYPE Kunde\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Gibtsnicht = 1";
assert!(diags(bad).contains(&"Element not defined".to_string()));
let bad2 = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM k AS Kunde\nk.Name = 5";
assert!(diags(bad2).contains(&"Type mismatch".to_string()));
let array = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM SHARED k(1 TO 2) AS Kunde\nSUB Setzen\nk(1).Name = \"Ada\"\nPRINT k(1).Name\nEND SUB";
assert!(diags(array).is_empty(), "{:?}", diags(array));
}
#[test]
fn shared_und_common() {
let src =
"DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB";
assert!(diags(src).is_empty(), "{:?}", diags(src));
let src = "TYPE Satz\nWert AS INTEGER\nEND TYPE\nDIM SHARED r AS Satz\nSUB Hoch\nr.Wert = r.Wert + 1\nEND SUB";
assert!(diags(src).is_empty(), "{:?}", diags(src));
let src = "DIM SHARED datei AS INTEGER, name AS STRING\nSUB Nutzen\ndatei% = 1\nname$ = \"x\"\nEND SUB";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("COMMON SHARED /blk/ a%, b$()").is_empty());
}
#[test]
fn konstantenfaltung() {
assert!(diags("CONST PI = 3.14159, ZWEI.PI = PI * 2").is_empty());
assert!(diags("x = 1\nCONST K = x + 1").contains(&"Invalid constant".to_string()));
}
#[test]
fn datei_ea_und_events() {
let src = "OPEN \"test.dat\" FOR RANDOM AS #1 LEN = 64\nCLOSE #1\nOPEN \"o\", #2, \"f.txt\"\nCLOSE\nTIMER ON\nKEY(5) OFF";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("PEN ON").contains(&"Feature unavailable".to_string()));
}
fn form_catalog() -> FormCatalog {
let mut c = FormCatalog::default();
c.add("Form1", ObjectClass::Form, None, false);
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
c.add("List1", ObjectClass::ListBox, Some("Form1"), false);
c.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
c
}
fn form_diags(src: &str) -> Vec<String> {
analyze_source_with_forms("FORM1", src, &form_catalog())
.diagnostics
.into_iter()
.map(|d| d.message)
.collect()
}
#[test]
fn objektpfade_werden_namentlich_geprueft_und_udt_bleibt_unveraendert() {
assert!(diags("Text9.Text = \"a\"")
.iter()
.any(|d| d.contains("TEXT9")));
assert!(form_diags("Text1.Farbe = 3")
.iter()
.any(|d| d.contains("FARBE")));
assert!(form_diags("Text1.Text = 5")
.iter()
.any(|d| d.contains("TEXT")));
assert!(form_diags("x = Text1.ListCount")
.iter()
.any(|d| d.contains("LISTCOUNT")));
assert!(diags("TYPE T\nF AS INTEGER\nEND TYPE\nDIM x AS T\nx.F = 1").is_empty());
}
#[test]
fn eigenschaftstyp_schreibbarkeit_bang_und_hir_index() {
let d = form_diags("Text1.Text = 5\nx = Text1.SelLength");
assert!(
d.iter()
.any(|m| m.contains("Type mismatch for property 'TEXT'")),
"{d:?}"
);
let d = form_diags("SCREEN.Width = 80");
assert!(d.iter().any(|m| m.contains("read-only")), "{d:?}");
assert!(form_diags("Form1!Text1.Text = \"a\"").is_empty());
let d = form_diags("Text1 = \"x\"");
assert!(
d.iter().any(|m| m.contains("Text1") || m.contains("TEXT1")),
"{d:?}"
);
assert!(form_diags("Command1(3).Caption = \"drei\"").is_empty());
assert!(form_diags("zeichen% = SCREEN(1, 1)").is_empty());
assert!(form_diags("SHOW\nHIDE").is_empty());
assert!(form_diags("Caption = \"Titel\"\nx% = Width").is_empty());
assert!(
form_diags("CALL Aus(Text1)\nSUB Aus(c AS CONTROL)\nc.Visible = 0\nEND SUB").is_empty()
);
let a = analyze_source_with_forms("FORM1", "Text1.Text = \"a\"", &form_catalog());
let h = a.hir.unwrap();
assert!(matches!(
h.procs[0].body[0].kind,
HStmtKind::SetObjectProperty { object: 1, .. }
));
let d = form_diags("Text1.Move 1, 2");
assert!(d.is_empty(), "{d:?}");
for src in ["Form1.Hide 1", "Form1.Show 1, 2", "SCREEN.Show 1"] {
let d = form_diags(src);
assert!(
d.iter().any(|m| m.contains("Argument-count mismatch")),
"{src}: {d:?}"
);
}
let d = form_diags("Form1.Show \"modal\"");
assert!(d.iter().any(|m| m.contains("Type mismatch")), "{d:?}");
}
#[test]
fn typeof_form_control_und_form_metabefehl() {
let d = form_diags("DIM Ziel AS CONTROL\nIF TYPEOF Ziel IS CommandButton THEN PRINT 1");
assert!(d.is_empty(), "{d:?}");
let a = analyze_source("MeinForm", "'$FORM\nMeinForm.Caption = \"x\"");
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
assert!(a
.hir
.unwrap()
.objects
.iter()
.any(|o| o.name == "MEINFORM" && o.class == ObjectClass::Form));
}
#[test]
fn dialoge_listindex_und_zeichenmessung_werden_abgesenkt() {
let source = "MSGBOX \"Hinweis\"\nantwort% = MSGBOX(\"Weiter?\", 4, \"Frage\")\nname$ = INPUTBOX$(\"Name\")\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT Picture1.TEXTWIDTH(\"abc\")";
let diagnostics = form_diags(source);
assert!(diagnostics.is_empty(), "{diagnostics:?}");
let arrays = form_diags("Command1(1).SETFOCUS\nCommand1(1).MOVE 1, 2, 3, 4");
assert!(arrays.is_empty(), "{arrays:?}");
}
#[test]
fn ereignisprozedur_signatur_und_normale_unterstrich_sub() {
let ok = "SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nEND SUB";
assert!(form_diags(ok).is_empty(), "{:?}", form_diags(ok));
let bad = form_diags("SUB Form_MouseDown(X AS SINGLE)\nEND SUB");
assert!(
bad.iter().any(|d| d.contains("BUTTON AS INTEGER")),
"{bad:?}"
);
assert!(form_diags("SUB Zins_Berechnen(n AS INTEGER)\nEND SUB").is_empty());
let array_bad = form_diags("SUB Command1_Click()\nEND SUB");
assert!(
array_bad.iter().any(|d| d.contains("INDEX AS INTEGER")),
"{array_bad:?}"
);
}
#[test]
fn dreizeiler_wird_namentlich_statt_falsch_abgewiesen() {
let d = diags("Text1.Text = \"hallo\"\nForm1.Show 1\nUNLOAD Form1");
assert_eq!(d.len(), 3, "{d:?}");
assert!(d[0].contains("TEXT1"));
assert!(d[1].contains("FORM1"));
assert!(d[2].contains("FORM1"));
}
// ---- HIR-Lowering ------------------------------------------------------
use crate::hir::{HExpr, HStmtKind, NumTy};
fn hir_of(src: &str) -> crate::hir::HirModule {
let a = analyze_source("TEST", src);
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
a.hir.expect("HIR erwartet")
}
#[test]
fn hir_konvertierung_materialisiert() {
// d# = i% + 1.5# → Conv(INTEGER→DOUBLE) unter der DOUBLE-Addition
let h = hir_of("i% = 2\nd# = i% + 1.5#");
let main = &h.procs[0];
let HStmtKind::Assign { place, value } = &main.body[1].kind else {
panic!("Assign erwartet: {:?}", main.body[1].kind);
};
assert_eq!(place.ty, crate::hir::HTy::Num(NumTy::Dbl));
let HExpr::Bin { ty, l, .. } = value else {
panic!("Bin erwartet: {value:?}");
};
assert_eq!(*ty, NumTy::Dbl);
assert!(
matches!(
**l,
HExpr::Conv {
from: NumTy::Int,
to: NumTy::Dbl,
..
}
),
"Conv INTEGER→DOUBLE erwartet: {l:?}"
);
}
#[test]
fn hir_slots_aufgeloest() {
let h = hir_of("a% = 1\nb% = a%");
assert_eq!(h.globals.len(), 2);
let HStmtKind::Assign { value, .. } = &h.procs[0].body[1].kind else {
panic!();
};
assert!(matches!(value, HExpr::Load(_)));
}
#[test]
fn hir_intdiv_rundet_operanden() {
// 7.5 \ 2 → beide Operanden Conv → LONG (SINGLE-Operand)
let h = hir_of("x& = 7.5 \\ 2");
let HStmtKind::Assign { value, .. } = &h.procs[0].body[0].kind else {
panic!();
};
let HExpr::Bin { op, ty, l, .. } = value else {
panic!("Bin erwartet: {value:?}");
};
assert_eq!(*op, crate::hir::HArith::IDiv);
assert_eq!(*ty, NumTy::Lng);
assert!(matches!(**l, HExpr::Conv { to: NumTy::Lng, .. }));
}
#[test]
fn hir_select_wird_abgesenkt() {
let h = hir_of("SELECT CASE 2\nCASE 1\nPRINT \"a\"\nCASE ELSE\nPRINT \"b\"\nEND SELECT");
// Temp-Zuweisung + If-Kette
let main = &h.procs[0];
assert!(matches!(main.body[0].kind, HStmtKind::Assign { .. }));
assert!(matches!(main.body[1].kind, HStmtKind::If { .. }));
}
#[test]
fn hir_byref_und_byval() {
let h = hir_of("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
let main = &h.procs[0];
let calls: Vec<_> = main
.body
.iter()
.filter_map(|s| match &s.kind {
HStmtKind::CallSub { args, .. } => Some(args),
_ => None,
})
.collect();
assert_eq!(calls.len(), 2);
assert!(matches!(calls[0][0], crate::hir::HArg::ByRef(_)));
assert!(matches!(calls[1][0], crate::hir::HArg::ByVal(_)));
}
#[test]
fn byref_verlangt_exakten_typ() {
let d = diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo n%");
assert!(
d.contains(&"Parameter type mismatch".to_string()),
"BYREF mit abweichendem Typ muss abgelehnt werden: {d:?}"
);
// BYVAL (Klammern) konvertiert stattdessen
assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty());
}
// ---- ISAM (Change `phase-3-isam`) -----------------------------------
/// Satztyp und geöffnete Tabelle als Vorspann für die ISAM-Tests.
const ISAM_KOPF: &str = "TYPE KundeTyp\n Nummer AS LONG\n Name AS STRING * 20\n END TYPE\n DIM k AS KundeTyp\n OPEN \"db\" FOR ISAM KundeTyp \"Kunden\" AS #1\n";
/// Aufgabe 2.1: die Anweisung parst mit `#`-Dateinummer diagnose-frei.
#[test]
fn isam_anweisung_parst_ohne_diagnose() {
let d = diags(&format!("{ISAM_KOPF}SETINDEX #1, \"Name\""));
assert!(d.is_empty(), "SETINDEX soll diagnose-frei parsen: {d:?}");
// Auch ohne `#` (die Original-Hilfe schreibt es optional).
let d = diags(&format!("{ISAM_KOPF}SETINDEX 1, \"Name\""));
assert!(d.is_empty(), "{d:?}");
}
/// Aufgabe 2.2: falsche Argumentanzahl wird gemeldet und nennt das
/// Element (Guiding Principle).
#[test]
fn isam_argumentanzahl_nennt_das_element() {
let d = diags(&format!("{ISAM_KOPF}SEEKGT #1"));
assert!(
d.iter().any(|m| m.contains("SEEKGT")),
"Diagnose muss SEEKGT nennen: {d:?}"
);
assert!(
d.iter().any(|m| m.contains("Argument-count mismatch")),
"{d:?}"
);
}
/// Aufgabe 2.3: das Satzargument wird gegen den Tabellentyp geprüft.
#[test]
fn isam_satzargument_wird_typgeprueft() {
let d = diags(&format!("{ISAM_KOPF}DIM x AS STRING\nRETRIEVE #1, x$"));
assert!(
d.contains(&"Type mismatch".to_string()),
"String statt Satzvariable muss auffallen: {d:?}"
);
// Ein anderer benutzerdefinierter Typ passt ebenfalls nicht.
let quelle = format!(
"TYPE Anderer\n z AS INTEGER\nEND TYPE\n{ISAM_KOPF} DIM a AS Anderer\nRETRIEVE #1, a"
);
assert!(
diags(&quelle).contains(&"Type mismatch".to_string()),
"fremder Satztyp muss auffallen: {:?}",
diags(&quelle)
);
// Der richtige Typ ist diagnose-frei.
let d = diags(&format!("{ISAM_KOPF}RETRIEVE #1, k"));
assert!(d.is_empty(), "{d:?}");
}
/// Aufgabe 2.5: kein ISAM-Element endet als unbekannter Bezeichner —
/// ein Programm, das jedes Element genau einmal verwendet.
#[test]
fn kein_isam_element_ist_unbekannter_bezeichner() {
let quelle = format!(
"{ISAM_KOPF} CREATEINDEX #1, \"NachName\", 0, \"Name\"\n CREATEINDEX #1, \"Zwei\", 1, \"Name\", \"-Nummer\"\n SETINDEX #1, \"NachName\"\n i$ = GETINDEX$(1)\n INSERT #1, k\n MOVEFIRST #1\n MOVELAST #1\n MOVENEXT #1\n MOVEPREVIOUS #1\n SEEKEQ #1, \"a\"\n SEEKGT #1, \"a\"\n SEEKGE #1, \"a\"\n RETRIEVE #1, k\n UPDATE #1, k\n DELETE #1\n e% = EOF(1)\n b% = BOF(1)\n BEGINTRANS\n s% = SAVEPOINT\n ROLLBACK s%\n ROLLBACK\n ROLLBACK ALL\n COMMITTRANS\n m& = SETMEM(1024)\n DELETEINDEX #1, \"NachName\"\n CLOSE #1\n DELETETABLE \"db\", \"Kunden\"\n"
);
let d = diags(&quelle);
assert!(
!d.iter().any(|m| m.contains("not defined")),
"ISAM-Element wird als unbekannter Bezeichner abgewiesen: {d:?}"
);
assert!(d.is_empty(), "unerwartete Diagnosen: {d:?}");
}
/// `ROLLBACK ALL` senkt auf die Sentinel-Kennung ab und ist damit von
/// `ROLLBACK kennung` unterscheidbar.
#[test]
fn rollback_all_wird_als_sentinel_abgesenkt() {
let a = crate::analyze_source("TEST", "BEGINTRANS\nROLLBACK ALL");
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
// `ALL` darf keine Variable werden — sonst zaehlte OPTION EXPLICIT es an.
let d = diags("OPTION EXPLICIT\nBEGINTRANS\nROLLBACK ALL");
assert!(d.is_empty(), "{d:?}");
}
/// `CREATEINDEX` ist in der Spaltenzahl unbegrenzt — die Original-Hilfe
/// nennt keine Obergrenze, also darf die Signatur auch keine setzen.
#[test]
fn createindex_ohne_obergrenze_der_spaltenzahl() {
let felder: String = (1..=12).map(|i| format!(" F{i} AS LONG\n")).collect();
let spalten: String = (1..=12).map(|i| format!(", \"F{i}\"")).collect();
let quelle = format!(
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0{spalten}"
);
let d = diags(&quelle);
assert!(
d.is_empty(),
"zwölfspaltiger Index muss zulässig sein: {d:?}"
);
// Ein Spaltenname muss trotzdem ein String sein.
let quelle = format!(
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0, \"F1\", 42"
);
assert!(
diags(&quelle).contains(&"Type mismatch".to_string()),
"Zahl als Spaltenname muss auffallen: {:?}",
diags(&quelle)
);
}
}