//! 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, labels: HashMap, line_labels: HashMap, next_label: u16, /// Frame-Slots (leer im Modul-Scope: dort liegen Variablen global). locals: Vec, 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 { lower(module).1 } /// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren /// Diagnosen) und alle Diagnosen. pub fn lower(module: &Module) -> (Option, Vec) { lower_with_forms(module, &FormCatalog::default()) } pub fn lower_with_forms( module: &Module, catalog: &FormCatalog, ) -> (Option, Vec) { 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, /// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE). deftypes: [Option; 26], explicit: bool, /// `OPTION BASE` (0 oder 1). option_base: u8, procs: HashMap, next_proc_id: u16, /// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet. udt_ids: HashMap, udt_defs: Vec, /// 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, /// Konstanten: Typ und gefalteter Wert. consts: HashMap)>, /// Modulvariablen (Schlüssel wie `var_key`). module_vars: HashMap, /// Mit `DIM SHARED`/`COMMON SHARED` für Prozeduren sichtbare Modulvariablen. shared_vars: HashSet, /// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in /// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds). module_labels: HashMap, module_line_labels: HashMap, /// Globale Slots (Modulvariablen, STATICs, versteckte Temps). globals: Vec, /// DATA-Konstanten des Moduls (aus dem Prescan, statisch). data: Vec, data_marks_name: HashMap, data_marks_line: HashMap, /// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm). hir_procs: Vec>, forms: FormCatalog, event_procs: Vec, } impl Sema { fn err(&mut self, pos: SourcePos, msg: impl Into) { self.diags.push(Diagnostic { pos, message: msg.into(), }); } fn run(&mut self, module: &Module) -> Option { // 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 = 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::>(); let mut locals = params .iter() .map(|param| hir::HVar { name: param.name.clone(), ty: param.ty.clone(), array: param.array, }) .collect::>(); 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 { 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::>() .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 { 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, ()> { 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>, scope: &mut Scope, pos: SourcePos, ) -> Result, ()> { 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) -> Ty { match suffix { Some(s) => suffix_ty(*s), None => self.default_ty(name), } } fn var_key(&self, name: &str, suffix: &Option, 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, "", ty, false); if global { VarSlot::Global(slot) } else { VarSlot::Local(slot) } } fn vars_of<'a>(&'a self, scope: &'a Scope) -> &'a HashMap { 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) { 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, array: bool, pos: SourcePos, ) -> Option { // 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 { 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 { let mut out = Vec::new(); for stmt in stmts { self.lower_stmt(stmt, scope, &mut out); } out } fn push(&mut self, out: &mut Vec, 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) { // 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::>() .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, ) { 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)> = Vec::new(); let mut else_body: Vec = 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 = 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, 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) { 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 { 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 { 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, ) { 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 { 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, ) { // 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 = 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 = 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, ) { 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, 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 { 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, 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(<) && is_str(&rt) { (HExpr::Concat(Box::new(le), Box::new(re)), Ty::Str) } else if is_num(<) && is_num(&rt) { self.arith(HArith::Add, le, <, re, &rt) } else { self.err(pos, "Type mismatch"); (HExpr::Int(0), Ty::Unknown) } } BinOp::Sub | BinOp::Mul => { if !is_num(<) || !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, <, re, &rt) } BinOp::Div => { if !is_num(<) || !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(<), NumTy::Dbl | NumTy::Cur) || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur) { NumTy::Dbl } else { NumTy::Sng }; let l = self.conv_num(le, <, 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(<) || !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(<), NumTy::Dbl | NumTy::Cur) || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur) { NumTy::Dbl } else { NumTy::Sng }; let l = self.conv_num(le, <, 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(<) || !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(<) == NumTy::Int && num_ty(&rt) == NumTy::Int { NumTy::Int } else { NumTy::Lng }; let l = self.conv_num(le, <, 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(<) && 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(<) && is_num(&rt) { let common = promote_num(num_ty(<), num_ty(&rt)); let l = self.conv_num(le, <, 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(<) || !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(<) == 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, <, 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, args: &Option>, 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, 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 = (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 = (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 = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect(); b(Builtin::Sln, a, Ty::Dbl, self) } "SYD" | "SYD#" | "DDB" | "DDB#" => { let a: Vec = (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 { 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 { 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 { 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 { 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) ); } }