6332 lines
239 KiB
Rust
6332 lines
239 KiB
Rust
//! 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, Clone)]
|
||
struct Scope {
|
||
vars: HashMap<String, VarInfo>,
|
||
labels: HashMap<String, LabelId>,
|
||
line_labels: HashMap<u32, LabelId>,
|
||
next_label: u16,
|
||
/// Frame-Slots (leer im Modul-Scope: dort liegen Variablen global).
|
||
locals: Vec<hir::HVar>,
|
||
is_module: bool,
|
||
/// `DEF FN`: freie Namen binden an Modulvariablen.
|
||
is_def_fn: bool,
|
||
/// `STATIC`-Prozedur: Locals wandern in globale Slots.
|
||
is_static: bool,
|
||
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
|
||
loop_exits: Vec<(LoopKind, LabelId)>,
|
||
current_line: u32,
|
||
current_pos: SourcePos,
|
||
}
|
||
|
||
#[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),
|
||
"TIMEZONEKNOWN" => (0, 0, &[], I),
|
||
"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#" => (2, 2, &[N, A], Db),
|
||
"IRR" | "IRR#" => (2, 2, &[A, N], 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
|
||
"SHELL" => (1, 1, &[S], L),
|
||
"CURDIR$" => (0, 1, &[S], St),
|
||
"DIR$" => (0, 1, &[S], St),
|
||
"LPOS" => (1, 1, &[N], I),
|
||
"STACK" => (0, 0, &[], L),
|
||
"ERDEV" => (0, 0, &[], I),
|
||
"ERDEV$" => (0, 0, &[], St),
|
||
// ISAM-Funktionen (Formen siehe umfang-und-signaturen.md des Changes)
|
||
"GETINDEX$" => (1, 1, &[N], St),
|
||
"BOF" => (1, 1, &[N], I),
|
||
"SAVEPOINT" => (0, 0, &[], I),
|
||
"SETMEM" => (1, 1, &[N], L),
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
|
||
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
|
||
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
|
||
fn banned_feature(name: &str) -> bool {
|
||
matches!(
|
||
name,
|
||
// Hardware-Nähe
|
||
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
|
||
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
|
||
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
|
||
// Interlanguage-Schnittstelle
|
||
| "CALLS" | "SSEG" | "SSEGADD"
|
||
| "STRINGADDRESS" | "STRINGASSIGN" | "STRINGLENGTH" | "STRINGRELEASE"
|
||
// Overlay-Mechanismus
|
||
| "CHAIN"
|
||
// Grafik
|
||
| "PSET" | "PRESET" | "CIRCLE" | "DRAW" | "PALETTE"
|
||
| "PCOPY" | "PMAP" | "WINDOW" | "POINT"
|
||
// Event-Geräte (auch als Funktion in `ON COM(1) GOSUB …`)
|
||
| "COM" | "PEN" | "STRIG" | "STICK"
|
||
// Klang (außer BEEP)
|
||
| "SOUND" | "PLAY"
|
||
)
|
||
}
|
||
|
||
/// Non-Features, die nicht über ihren Namen abgewiesen werden können, weil
|
||
/// sie ihr Schlüsselwort mit einer unterstützten Form teilen. Sie werden an
|
||
/// der Syntax erkannt: `GET`/`PUT` mit `(` (Grafik) im Parser und `OPEN` mit
|
||
/// einem `COMn:`-Gerätenamen hier in der Semantik.
|
||
/// Der Inventar-Abgleich liest diese Liste.
|
||
pub const SYNTAKTISCH_ABGEWIESEN: &[&str] =
|
||
&["GET (Grafik)", "PUT (Grafik)", "OPEN COM", "DEF SEG"];
|
||
|
||
/// Ist `s` ein serieller Gerätename (`COM1:` …)? Nur für Stringliterale.
|
||
fn ist_com_geraet(s: &str) -> bool {
|
||
let up = s.to_ascii_uppercase();
|
||
up.strip_prefix("COM")
|
||
.and_then(|r| r.chars().next())
|
||
.is_some_and(|c| c.is_ascii_digit())
|
||
}
|
||
|
||
/// Builtin-Anweisungen (Bibliothek, keine Keywords).
|
||
fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
|
||
use ArgK::*;
|
||
Some(match name {
|
||
"CLS" => (0, 1, &[N]),
|
||
"BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
|
||
"COLOR" => (0, 3, &[N, N, N]),
|
||
"LOCATE" => (0, 5, &[N, N, N, N, N]),
|
||
"RANDOMIZE" => (0, 1, &[N]),
|
||
"SLEEP" => (0, 1, &[N]),
|
||
"WIDTH" => (0, 2, &[N, N]),
|
||
"SCREEN" => (1, 4, &[N, N, N, N]),
|
||
"SWAP" => (2, 2, &[A, A]),
|
||
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
|
||
"FILES" => (0, 1, &[S]),
|
||
"SHELL" => (0, 1, &[S]),
|
||
"RUN" => (0, 1, &[A]),
|
||
"CLEAR" => (0, 3, &[N, N, N]),
|
||
"KEY" => (1, 2, &[A, S]),
|
||
"ENVIRON" => (1, 1, &[S]),
|
||
"MSGBOX" => (1, 3, &[S, N, S]),
|
||
"SETUEVENT" => (0, 0, &[]),
|
||
"CHDRIVE" => (1, 1, &[S]),
|
||
"STACK" => (0, 1, &[N]),
|
||
// `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
|
||
"SETFORMATCC" => (1, 1, &[N]),
|
||
// ---- ISAM-Anweisungen -------------------------------------------
|
||
// Argumentformen wortgetreu aus der Original-Hilfe; belegt in
|
||
// openspec/changes/phase-3-isam/umfang-und-signaturen.md.
|
||
//
|
||
// CREATEINDEX ist variadisch: Dateinummer, Indexname,
|
||
// Eindeutigkeitskennzeichen, dann ein Stringargument je Spalte.
|
||
// Die Original-Hilfe nennt keine Obergrenze für die Spaltenzahl, also
|
||
// steht hier auch keine; die Argumenttypen ab dem vierten prüft
|
||
// `check_and_lower_builtin_args` gesondert.
|
||
"CREATEINDEX" => (4, u8::MAX, &[N, S, N, S]),
|
||
"DELETEINDEX" => (2, 2, &[N, S]),
|
||
// Ohne Indexnamen wird der NULL-Index (Einfügereihenfolge) gesetzt.
|
||
"SETINDEX" => (1, 2, &[N, S]),
|
||
"INSERT" | "RETRIEVE" | "UPDATE" => (2, 2, &[N, R]),
|
||
"DELETE" => (1, 1, &[N]),
|
||
"DELETETABLE" => (2, 2, &[S, S]),
|
||
"MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS" => (1, 1, &[N]),
|
||
// Dateinummer, dann ein Schlüsselwert je Indexspalte.
|
||
"SEEKEQ" | "SEEKGT" | "SEEKGE" => (2, 10, &[N, A, A, A, A, A, A, A, A, A]),
|
||
"BEGINTRANS" | "COMMITTRANS" => (0, 0, &[]),
|
||
"ROLLBACK" => (0, 1, &[N]),
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
// ---- Einstiegspunkte -------------------------------------------------------
|
||
|
||
/// Nur Diagnosen (Kompatibilitäts-API; nutzt intern das Lowering).
|
||
pub fn check(module: &Module) -> Vec<Diagnostic> {
|
||
lower(module).1
|
||
}
|
||
|
||
/// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren
|
||
/// Diagnosen) und alle Diagnosen.
|
||
pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
|
||
lower_with_forms(module, &FormCatalog::default())
|
||
}
|
||
|
||
pub fn lower_with_forms(
|
||
module: &Module,
|
||
catalog: &FormCatalog,
|
||
) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
|
||
let mut catalog = catalog.clone();
|
||
if catalog.find("SCREEN").is_none() {
|
||
catalog.add("SCREEN", ObjectClass::Screen, None, false);
|
||
}
|
||
if module
|
||
.body
|
||
.iter()
|
||
.any(|s| matches!(s, Stmt::MetaForm { .. }))
|
||
&& catalog.find(&module.name).is_none()
|
||
{
|
||
catalog.add(&module.name, ObjectClass::Form, None, false);
|
||
}
|
||
let mut s = new_sema(module, &catalog);
|
||
let hir = s.run(module);
|
||
(hir, s.diags)
|
||
}
|
||
|
||
/// Kompiliert Debugcode mit denselben Symbolen, DEFtype-Regeln und Slots wie der Originalrumpf.
|
||
/// Der neue Rumpf ist getrennt; der Projektquelltext wird nicht verändert.
|
||
#[derive(Default, Clone)]
|
||
pub struct DebugSymbols {
|
||
pub globals: Vec<hir::HVar>,
|
||
pub udts: Vec<hir::HUdt>,
|
||
pub original_globals: usize,
|
||
}
|
||
|
||
pub fn lower_debug(
|
||
module: &Module,
|
||
catalog: &FormCatalog,
|
||
procedure: &str,
|
||
text: &str,
|
||
expression: bool,
|
||
symbols: DebugSymbols,
|
||
) -> Result<(hir::HirModule, hir::HProc), Vec<Diagnostic>> {
|
||
let text = if expression {
|
||
format!("PRINT {text}\n")
|
||
} else {
|
||
format!("{text}\n")
|
||
};
|
||
let lexed = crate::lexer::lex(&text);
|
||
let parsed = crate::parser::parse("<Immediate>", &lexed.tokens);
|
||
let mut errors = lexed.diagnostics;
|
||
errors.extend(parsed.diagnostics);
|
||
if !parsed.module.procs.is_empty() {
|
||
errors.push(Diagnostic {
|
||
file: None,
|
||
pos: SourcePos::default(),
|
||
message: "Keine Prozedurdefinition im Direktfenster".into(),
|
||
});
|
||
}
|
||
if !errors.is_empty() {
|
||
return Err(errors);
|
||
}
|
||
let mut sema = new_sema(module, catalog);
|
||
sema.debug_symbols = Some(symbols);
|
||
sema.debug_request = Some((procedure.into(), parsed.module.body, expression));
|
||
let hir = sema.run(module);
|
||
if sema.debug_proc.is_none() {
|
||
sema.err(SourcePos::default(), "Kein erreichbarer Debug-Kontext");
|
||
}
|
||
if !sema.diags.is_empty() {
|
||
return Err(sema.diags);
|
||
}
|
||
Ok((hir.unwrap(), sema.debug_proc.unwrap()))
|
||
}
|
||
|
||
/// Semantisch gebundene Objekt-/Ereignisnamen für transaktionale IDE-Umbenennungen.
|
||
/// Aufrufer validieren die gesamte Übersetzungseinheit einschließlich ihrer Imports.
|
||
#[derive(Default)]
|
||
pub struct BoundFormReferences {
|
||
pub objects: Vec<(SourcePos, String, u16)>,
|
||
pub procedures: Vec<(SourcePos, String)>,
|
||
pub diagnostics: Vec<Diagnostic>,
|
||
}
|
||
pub fn bound_form_references(module: &Module, catalog: &FormCatalog) -> BoundFormReferences {
|
||
let mut s = new_sema(module, catalog);
|
||
s.references = Some(BoundFormReferences::default());
|
||
s.run(module);
|
||
let mut references = s.references.unwrap();
|
||
references.diagnostics = s.diags;
|
||
references
|
||
}
|
||
|
||
fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
||
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(),
|
||
commons: 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.clone(),
|
||
module_name: module.name.clone(),
|
||
event_procs: Vec::new(),
|
||
references: None,
|
||
debug_request: None,
|
||
debug_proc: None,
|
||
debug_symbols: None,
|
||
}
|
||
}
|
||
|
||
/// Exportdeklarationen mit den Typen und Konstanten ihres Ursprungsmoduls.
|
||
/// Rümpfe werden hier nicht übersetzt; dafür bleibt der reguläre Sema-Durchlauf zuständig.
|
||
pub fn export_declarations(module: &Module, constants: &[Stmt]) -> Vec<Stmt> {
|
||
let mut s = new_sema(module, &FormCatalog::default());
|
||
let mut scope = Scope::default();
|
||
let mut declarations = Vec::new();
|
||
let local: HashSet<_> = module
|
||
.body
|
||
.iter()
|
||
.flat_map(|stmt| match stmt {
|
||
Stmt::ConstDecl { items, .. } => items.iter().map(|i| i.0.as_str()).collect(),
|
||
_ => Vec::new(),
|
||
})
|
||
.collect();
|
||
for stmt in constants {
|
||
if let Stmt::ConstDecl { items, .. } = stmt {
|
||
if !local.contains(items[0].0.as_str()) {
|
||
s.lower_stmt(stmt, &mut scope, &mut Vec::new());
|
||
}
|
||
}
|
||
}
|
||
for stmt in &module.body {
|
||
match stmt {
|
||
Stmt::DefType { .. } => s.lower_stmt(stmt, &mut scope, &mut Vec::new()),
|
||
Stmt::ConstDecl { items, pos } => {
|
||
for item in items {
|
||
let declaration = Stmt::ConstDecl {
|
||
items: vec![item.clone()],
|
||
pos: *pos,
|
||
};
|
||
s.lower_stmt(&declaration, &mut scope, &mut Vec::new());
|
||
let value = match s.consts.get(&item.0).and_then(|(_, value)| value.as_ref()) {
|
||
Some(ConstVal::Num(n)) => Expr::DoubleLit(*n, item.2.pos()),
|
||
Some(ConstVal::Str(value)) => Expr::StrLit(value.clone(), item.2.pos()),
|
||
None => item.2.clone(),
|
||
};
|
||
declarations.push(Stmt::ConstDecl {
|
||
items: vec![(item.0.clone(), item.1, value)],
|
||
pos: *pos,
|
||
});
|
||
}
|
||
}
|
||
Stmt::TypeDecl { .. } => declarations.push(stmt.clone()),
|
||
_ => {}
|
||
}
|
||
}
|
||
for proc in &module.procs {
|
||
let mut sig = proc.sig.clone();
|
||
if sig.suffix.is_none() && sig.kind == ProcKind::Function {
|
||
sig.suffix =
|
||
Suffix::from_char(s.var_key(&sig.name, &None, false).chars().last().unwrap());
|
||
}
|
||
for param in &mut sig.params {
|
||
if param.suffix.is_none() && param.as_type.is_none() {
|
||
param.suffix =
|
||
Suffix::from_char(s.var_key(¶m.name, &None, false).chars().last().unwrap());
|
||
}
|
||
}
|
||
declarations.push(Stmt::Declare { sig, pos: proc.pos });
|
||
}
|
||
declarations
|
||
}
|
||
|
||
struct Sema {
|
||
diags: Vec<Diagnostic>,
|
||
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
|
||
deftypes: [Option<Ty>; 26],
|
||
explicit: bool,
|
||
/// `OPTION BASE` (0 oder 1).
|
||
option_base: u8,
|
||
procs: HashMap<String, ProcInfo>,
|
||
next_proc_id: u16,
|
||
/// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet.
|
||
udt_ids: HashMap<String, u16>,
|
||
udt_defs: Vec<hir::HUdt>,
|
||
/// Satztyp je ISAM-Dateinummer aus `OPEN … FOR ISAM typ tabelle AS #n`
|
||
/// mit literaler Nummer — Grundlage der Satztypprüfung von `INSERT`,
|
||
/// `RETRIEVE` und `UPDATE`.
|
||
isam_typ: HashMap<i32, String>,
|
||
/// Konstanten: Typ und gefalteter Wert.
|
||
consts: HashMap<String, (Ty, Option<ConstVal>)>,
|
||
/// Modulvariablen (Schlüssel wie `var_key`).
|
||
module_vars: HashMap<String, VarInfo>,
|
||
/// Mit `DIM SHARED`/`COMMON SHARED` für Prozeduren sichtbare Modulvariablen.
|
||
shared_vars: HashSet<String>,
|
||
/// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in
|
||
/// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds).
|
||
module_labels: HashMap<String, LabelId>,
|
||
module_line_labels: HashMap<u32, LabelId>,
|
||
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
||
globals: Vec<hir::HVar>,
|
||
commons: Vec<hir::HCommon>,
|
||
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
||
data: Vec<hir::DataItem>,
|
||
data_marks_name: HashMap<String, u32>,
|
||
data_marks_line: HashMap<u32, u32>,
|
||
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
|
||
hir_procs: Vec<Option<hir::HProc>>,
|
||
forms: FormCatalog,
|
||
module_name: String,
|
||
event_procs: Vec<hir::HEventProc>,
|
||
references: Option<BoundFormReferences>,
|
||
debug_request: Option<(String, Vec<Stmt>, bool)>,
|
||
debug_proc: Option<hir::HProc>,
|
||
debug_symbols: Option<DebugSymbols>,
|
||
}
|
||
|
||
impl Sema {
|
||
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
|
||
self.diags.push(Diagnostic {
|
||
file: None,
|
||
pos,
|
||
message: msg.into(),
|
||
});
|
||
}
|
||
|
||
fn run(&mut self, module: &Module) -> Option<hir::HirModule> {
|
||
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
|
||
for stmt in &module.body {
|
||
match stmt {
|
||
Stmt::Declare { sig, pos } => self.register_proc(sig, *pos),
|
||
Stmt::DefType { .. } => {
|
||
self.lower_stmt(stmt, &mut Scope::default(), &mut Vec::new())
|
||
}
|
||
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,
|
||
});
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
let mut defined = HashSet::new();
|
||
for proc in &module.procs {
|
||
if !defined.insert(proc.sig.name.clone()) {
|
||
self.err(proc.pos, "Duplicate definition");
|
||
}
|
||
self.register_proc(&proc.sig, proc.pos);
|
||
}
|
||
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);
|
||
|
||
self.deftypes = [const { None }; 26];
|
||
|
||
// 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);
|
||
self.lower_debug_scope(&module.name, &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 {
|
||
let deftypes = self.deftypes.clone();
|
||
self.lower_proc(proc);
|
||
self.deftypes = deftypes;
|
||
}
|
||
|
||
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
|
||
// Signatur, damit externe Bibliotheksaufrufe dieselben BYREF-Slots
|
||
// und Funktionsrückgaben wie lokale Implementierungen besitzen.
|
||
let mut declared = vec![None; self.next_proc_id as usize];
|
||
for (name, info) in self.procs.iter() {
|
||
declared[info.id as usize] = Some((name.clone(), info.clone()));
|
||
}
|
||
let procs: Vec<hir::HProc> = std::mem::take(&mut self.hir_procs)
|
||
.into_iter()
|
||
.enumerate()
|
||
.map(|(i, p)| {
|
||
p.unwrap_or_else(|| {
|
||
let (name, info) = declared[i].clone().unwrap();
|
||
let params = info
|
||
.params
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(index, (ty, array))| hir::HParam {
|
||
name: format!("ARG{index}"),
|
||
ty: self.h_ty(ty),
|
||
array: *array,
|
||
by_ref: !array,
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let mut locals = params
|
||
.iter()
|
||
.map(|param| hir::HVar {
|
||
name: param.name.clone(),
|
||
ty: param.ty.clone(),
|
||
array: param.array,
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let (kind, ret_slot, ret_ty) = if info.kind == ProcKind::Function {
|
||
let ty = self.h_ty(&info.ret);
|
||
let slot = VarSlot::Local(locals.len() as u16);
|
||
locals.push(hir::HVar {
|
||
name: name.clone(),
|
||
ty: ty.clone(),
|
||
array: false,
|
||
});
|
||
(hir::HProcKind::Function, Some(slot), Some(ty))
|
||
} else {
|
||
(hir::HProcKind::Sub, None, None)
|
||
};
|
||
hir::HProc {
|
||
name,
|
||
kind,
|
||
params,
|
||
locals,
|
||
ret_slot,
|
||
ret_ty,
|
||
body: Vec::new(),
|
||
label_count: 0,
|
||
}
|
||
})
|
||
})
|
||
.collect();
|
||
|
||
Some(hir::HirModule {
|
||
name: module.name.clone(),
|
||
globals: std::mem::take(&mut self.globals),
|
||
commons: std::mem::take(&mut self.commons),
|
||
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: key.trim_end_matches("\u{1}AS").to_string(),
|
||
ty: hty.clone(),
|
||
array: p.array,
|
||
});
|
||
let by_ref = !p.array;
|
||
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: key.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);
|
||
self.lower_debug_scope(&proc.sig.name, &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);
|
||
}
|
||
}
|
||
|
||
fn lower_debug_scope(&mut self, name: &str, original: &Scope) {
|
||
let Some((target, statements, expression)) = self.debug_request.clone() else {
|
||
return;
|
||
};
|
||
if !name.eq_ignore_ascii_case(&target) {
|
||
return;
|
||
}
|
||
if original.locals.len() >= u16::MAX as usize {
|
||
self.err(SourcePos::default(), "Kein freier temporärer Debug-Slot");
|
||
return;
|
||
}
|
||
let mut scope = original.clone();
|
||
if let Some(symbols) = self.debug_symbols.take() {
|
||
let offset = self.udt_defs.len() as u16;
|
||
let mut udts = symbols.udts;
|
||
for udt in &mut udts {
|
||
for (_, ty) in &mut udt.fields {
|
||
if let HTy::Udt(id) = ty {
|
||
*id += offset;
|
||
}
|
||
}
|
||
}
|
||
for (i, udt) in udts.iter().enumerate() {
|
||
self.udt_ids.insert(udt.name.clone(), offset + i as u16);
|
||
}
|
||
self.udt_defs.extend(udts);
|
||
self.globals
|
||
.resize_with(symbols.original_globals, || hir::HVar {
|
||
name: "<DebugPad>".into(),
|
||
ty: HTy::Num(NumTy::Int),
|
||
array: false,
|
||
});
|
||
for mut var in symbols.globals {
|
||
if let HTy::Udt(id) = &mut var.ty {
|
||
*id += offset;
|
||
}
|
||
let ty = match &var.ty {
|
||
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,
|
||
};
|
||
let key = if var.name.ends_with(['%', '&', '!', '#', '$', '@']) {
|
||
var.name.clone()
|
||
} else {
|
||
format!("{}\u{1}AS", var.name)
|
||
};
|
||
scope.vars.insert(
|
||
key,
|
||
VarInfo {
|
||
ty,
|
||
array: var.array,
|
||
explicit: true,
|
||
slot: self.globals.len() as u16,
|
||
global: true,
|
||
by_ref: false,
|
||
},
|
||
);
|
||
self.globals.push(var);
|
||
}
|
||
}
|
||
let explicit = self.explicit;
|
||
self.explicit = true;
|
||
let globals = self.globals.len();
|
||
let locals = scope.locals.len();
|
||
let mut proc = hir::HProc {
|
||
name: "<Immediate>".into(),
|
||
kind: hir::HProcKind::Sub,
|
||
params: vec![],
|
||
locals: vec![],
|
||
ret_slot: None,
|
||
ret_ty: None,
|
||
body: vec![],
|
||
label_count: 0,
|
||
};
|
||
if expression {
|
||
if let [Stmt::Print { items, .. }] = statements.as_slice() {
|
||
if let [PrintItem::Expr(expr)] = items.as_slice() {
|
||
let (value, ty) = self.lower_expr(expr, &mut scope);
|
||
let ty = self.h_ty(&ty);
|
||
let slot = VarSlot::Local(scope.locals.len() as u16);
|
||
proc.body.push(HStmt {
|
||
pos: SourcePos::default(),
|
||
line: 0,
|
||
kind: HStmtKind::Assign {
|
||
place: HPlace {
|
||
base: slot,
|
||
base_is_ref: false,
|
||
indices: vec![],
|
||
fields: vec![],
|
||
ty: ty.clone(),
|
||
array_elem: None,
|
||
},
|
||
value,
|
||
},
|
||
});
|
||
proc.ret_ty = Some(ty.clone());
|
||
proc.ret_slot = Some(slot);
|
||
proc.kind = hir::HProcKind::Function;
|
||
proc.locals = scope.locals.clone();
|
||
proc.locals.push(hir::HVar {
|
||
name: "<Watch>".into(),
|
||
ty,
|
||
array: false,
|
||
});
|
||
} else {
|
||
self.err(
|
||
SourcePos::default(),
|
||
"Genau ein Watch-Ausdruck erforderlich",
|
||
);
|
||
}
|
||
} else {
|
||
self.err(
|
||
SourcePos::default(),
|
||
"Genau ein Watch-Ausdruck erforderlich",
|
||
);
|
||
}
|
||
} else {
|
||
if statements.iter().any(|s| {
|
||
!matches!(
|
||
s,
|
||
Stmt::Assign { .. }
|
||
| Stmt::Print { .. }
|
||
| Stmt::Call { .. }
|
||
| Stmt::ErrorStmt { .. }
|
||
)
|
||
}) {
|
||
self.err(
|
||
SourcePos::default(),
|
||
"Direktfenster erlaubt PRINT, Zuweisungen, Prozeduraufrufe und ERROR",
|
||
);
|
||
} else {
|
||
proc.body = self.lower_body(&statements, &mut scope);
|
||
}
|
||
proc.locals = scope.locals.clone();
|
||
}
|
||
if self.globals.len() != globals || scope.locals.len() != locals {
|
||
self.err(
|
||
SourcePos::default(),
|
||
"Direktcode darf keine neuen Variablen deklarieren",
|
||
);
|
||
}
|
||
proc.label_count = scope.next_label;
|
||
self.explicit = explicit;
|
||
self.debug_proc = Some(proc);
|
||
self.debug_request = None;
|
||
}
|
||
|
||
/// 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, pos: SourcePos) {
|
||
let ret = match sig.kind {
|
||
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
|
||
ProcKind::Sub => Ty::Unknown,
|
||
};
|
||
let params: Vec<_> = sig
|
||
.params
|
||
.iter()
|
||
.map(|p| (self.param_ty(p), p.array))
|
||
.collect();
|
||
let id = match self.procs.get(&sig.name) {
|
||
Some(p) => {
|
||
let id = p.id;
|
||
if p.kind != sig.kind || p.ret != ret || p.params != params {
|
||
self.err(pos, "Parameter type mismatch");
|
||
}
|
||
id
|
||
}
|
||
None => {
|
||
let id = self.next_proc_id;
|
||
self.next_proc_id += 1;
|
||
id
|
||
}
|
||
};
|
||
self.procs.insert(
|
||
sig.name.clone(),
|
||
ProcInfo {
|
||
kind: sig.kind,
|
||
ret,
|
||
params,
|
||
id,
|
||
def_fn: false,
|
||
},
|
||
);
|
||
}
|
||
|
||
fn h_ty(&self, t: &Ty) -> HTy {
|
||
match t {
|
||
Ty::Int => HTy::Num(NumTy::Int),
|
||
Ty::Lng => HTy::Num(NumTy::Lng),
|
||
Ty::Cur => HTy::Num(NumTy::Cur),
|
||
Ty::Sng => HTy::Num(NumTy::Sng),
|
||
Ty::Dbl => HTy::Num(NumTy::Dbl),
|
||
Ty::Str => HTy::Str,
|
||
Ty::FixedStr(n) => HTy::FixedStr(*n),
|
||
Ty::Udt(name) => HTy::Udt(*self.udt_ids.get(name).unwrap_or(&0)),
|
||
Ty::Form => HTy::Form,
|
||
Ty::Control => HTy::Control,
|
||
Ty::Unknown => HTy::Num(NumTy::Sng),
|
||
}
|
||
}
|
||
|
||
fn fixed_width(&self, ty: &HTy) -> Option<i16> {
|
||
let bytes = match ty {
|
||
HTy::Num(NumTy::Int) => 2,
|
||
HTy::Num(NumTy::Lng | NumTy::Sng) => 4,
|
||
HTy::Num(NumTy::Cur | NumTy::Dbl) => 8,
|
||
HTy::FixedStr(length) => usize::try_from(*length).ok()?.checked_mul(4)?,
|
||
HTy::Udt(id) => self
|
||
.udt_defs
|
||
.get(*id as usize)?
|
||
.fields
|
||
.iter()
|
||
.try_fold(0usize, |sum, (_, field)| {
|
||
sum.checked_add(self.fixed_width(field)? as usize)
|
||
})?,
|
||
_ => return None,
|
||
};
|
||
i16::try_from(bytes).ok()
|
||
}
|
||
|
||
fn register_event_proc(&mut self, proc: &Proc) {
|
||
let Some((object, class, event)) = self.event_binding(&proc.sig.name) else {
|
||
return;
|
||
};
|
||
if !forms::events(class)
|
||
.iter()
|
||
.any(|e| e.eq_ignore_ascii_case(&event))
|
||
{
|
||
return;
|
||
}
|
||
if let Some(r) = &mut self.references {
|
||
r.objects.push((proc.pos, proc.sig.name.clone(), object));
|
||
}
|
||
let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new();
|
||
if self.forms.objects[object as usize].array {
|
||
expected.push(("INDEX", forms::EventParamType::Integer));
|
||
}
|
||
expected.extend(forms::event_params(&event).unwrap_or(&[]).iter().copied());
|
||
let valid = proc.sig.kind == ProcKind::Sub
|
||
&& proc.sig.params.len() == expected.len()
|
||
&& proc.sig.params.iter().zip(&expected).all(|(p, (name, t))| {
|
||
let actual = self.param_ty(p);
|
||
p.name.eq_ignore_ascii_case(name)
|
||
&& matches!(
|
||
(actual, t),
|
||
(Ty::Int, forms::EventParamType::Integer)
|
||
| (Ty::Sng, forms::EventParamType::Single)
|
||
| (Ty::Control, forms::EventParamType::Control)
|
||
)
|
||
});
|
||
if !valid {
|
||
let args = expected
|
||
.iter()
|
||
.map(|(n, t)| {
|
||
let ty = match t {
|
||
forms::EventParamType::Integer => "INTEGER",
|
||
forms::EventParamType::Single => "SINGLE",
|
||
forms::EventParamType::Control => "CONTROL",
|
||
};
|
||
format!("{n} AS {ty}")
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join(", ");
|
||
self.err(
|
||
proc.pos,
|
||
format!("Event procedure {} expects ({args})", proc.sig.name),
|
||
);
|
||
}
|
||
if let Some(info) = self.procs.get(&proc.sig.name) {
|
||
self.event_procs.push(hir::HEventProc {
|
||
object,
|
||
event,
|
||
proc: info.id,
|
||
});
|
||
}
|
||
}
|
||
|
||
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
|
||
let (base, event) = name.rsplit_once('_')?;
|
||
let found = if base == "FORM" {
|
||
self.current_form()
|
||
} else {
|
||
self.find_object(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 current_form(&self) -> Option<(u16, &forms::FormObject)> {
|
||
self.forms
|
||
.find(&self.module_name)
|
||
.filter(|(_, o)| o.class == ObjectClass::Form)
|
||
.or_else(|| {
|
||
let mut forms = self
|
||
.forms
|
||
.objects
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|(_, o)| o.class == ObjectClass::Form);
|
||
let (id, object) = forms.next()?;
|
||
forms.next().is_none().then_some((id as u16, object))
|
||
})
|
||
}
|
||
|
||
fn find_object(&self, name: &str) -> Option<(u16, &forms::FormObject)> {
|
||
self.forms
|
||
.find_in(name, &self.module_name)
|
||
.or_else(|| {
|
||
self.forms
|
||
.find(name)
|
||
.filter(|(_, o)| o.class == ObjectClass::Form || o.class == ObjectClass::Screen)
|
||
})
|
||
.or_else(|| {
|
||
let mut matches = self
|
||
.forms
|
||
.objects
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|(_, o)| o.name.eq_ignore_ascii_case(name));
|
||
let (id, object) = matches.next()?;
|
||
matches.next().is_none().then_some((id as u16, object))
|
||
})
|
||
}
|
||
|
||
fn scoped_object(
|
||
&mut self,
|
||
name: &str,
|
||
parent: Option<&str>,
|
||
pos: SourcePos,
|
||
) -> Option<(u16, forms::FormObject)> {
|
||
if let Some(parent) = parent {
|
||
if let Some((id, object)) = self.forms.find_in(name, parent) {
|
||
if let Some(r) = &mut self.references {
|
||
r.objects.push((pos, name.into(), id));
|
||
}
|
||
if let Some((form, _)) = self.forms.find(parent) {
|
||
if let Some(r) = &mut self.references {
|
||
r.objects.push((pos, parent.into(), form));
|
||
}
|
||
}
|
||
return Some((id, object.clone()));
|
||
}
|
||
self.err(
|
||
pos,
|
||
format!("Control '{name}' not found in form '{parent}'"),
|
||
);
|
||
None
|
||
} else {
|
||
self.object_by_name(name, pos)
|
||
}
|
||
}
|
||
|
||
fn object_by_name(
|
||
&mut self,
|
||
name: &str,
|
||
pos: SourcePos,
|
||
) -> Option<(u16, crate::forms::FormObject)> {
|
||
if let Some((id, object)) = self.find_object(name) {
|
||
let object = object.clone();
|
||
if let Some(r) = &mut self.references {
|
||
r.objects.push((pos, name.into(), id));
|
||
}
|
||
return Some((id, object));
|
||
}
|
||
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.current_form()?;
|
||
let (property, spec) = forms::property(form.class, name)?;
|
||
Some((object, 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.scoped_object(object_name, parent, pos)?;
|
||
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.scoped_object(object_name, parent, pos)?;
|
||
Some((id, object, member.to_string()))
|
||
}
|
||
|
||
fn is_debug_path(&self, scope: &Scope, path: &str) -> bool {
|
||
self.debug_request.is_some()
|
||
&& path.contains('!')
|
||
&& scope.vars.keys().any(|k| {
|
||
k.trim_end_matches("\u{1}AS")
|
||
.trim_end_matches(['%', '&', '!', '#', '$', '@'])
|
||
== path
|
||
.split('.')
|
||
.next()
|
||
.unwrap_or(path)
|
||
.trim_end_matches(['%', '&', '!', '#', '$', '@'])
|
||
})
|
||
}
|
||
fn is_udt_path(&self, scope: &Scope, path: &str) -> bool {
|
||
let base = path.split('.').next().unwrap_or(path);
|
||
let as_key = format!("{base}\u{1}AS");
|
||
let plain = self.var_key(base, &None, false);
|
||
self.visible_var(scope, &as_key)
|
||
.or_else(|| self.visible_var(scope, &plain))
|
||
.is_some_and(|v| matches!(v.ty, Ty::Udt(_)))
|
||
}
|
||
|
||
fn declared_var(&self, scope: &Scope, name: &str) -> Option<VarInfo> {
|
||
let as_key = format!("{name}\u{1}AS");
|
||
let plain = self.var_key(name, &None, false);
|
||
self.visible_var(scope, &as_key)
|
||
.or_else(|| self.visible_var(scope, &plain))
|
||
.cloned()
|
||
}
|
||
|
||
fn dynamic_property(
|
||
&mut self,
|
||
scope: &mut Scope,
|
||
path: &str,
|
||
pos: SourcePos,
|
||
) -> Result<Option<(HExpr, forms::PropertySpec)>, ()> {
|
||
let Some((base, member)) = path.split_once('.') else {
|
||
return Ok(None);
|
||
};
|
||
let Some(info) = self.declared_var(scope, base) else {
|
||
return Ok(None);
|
||
};
|
||
let spec = match info.ty {
|
||
Ty::Form => forms::property(ObjectClass::Form, member).map(|(_, spec)| spec),
|
||
Ty::Control => ObjectClass::ALL
|
||
.into_iter()
|
||
.filter(|class| !matches!(class, ObjectClass::Form | ObjectClass::Screen))
|
||
.find_map(|class| forms::property(class, member).map(|(_, spec)| spec)),
|
||
_ => return Ok(None),
|
||
};
|
||
let Some(spec) = spec else {
|
||
self.err(pos, format!("Unknown property '{member}'"));
|
||
return Err(());
|
||
};
|
||
let base = Expr::Name {
|
||
name: base.to_string(),
|
||
suffix: None,
|
||
args: None,
|
||
pos,
|
||
};
|
||
let (place, _) = self.lower_place(&base, scope);
|
||
Ok(place.map(|place| (HExpr::Load(Box::new(place)), spec)))
|
||
}
|
||
|
||
fn object_index(
|
||
&mut self,
|
||
object: u16,
|
||
args: &Option<Vec<Expr>>,
|
||
scope: &mut Scope,
|
||
pos: SourcePos,
|
||
) -> Result<Option<HExpr>, ()> {
|
||
let Some(args) = args else { return Ok(None) };
|
||
let info = &self.forms.objects[object as usize];
|
||
if !info.array {
|
||
self.err(pos, format!("Object '{}' is not an array", info.name));
|
||
return Err(());
|
||
}
|
||
if args.len() != 1 {
|
||
self.err(pos, "Argument-count mismatch");
|
||
return Err(());
|
||
}
|
||
Ok(Some(self.lower_num_as(&args[0], scope, NumTy::Lng)))
|
||
}
|
||
|
||
/// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE).
|
||
fn default_ty(&self, name: &str) -> Ty {
|
||
let first = name.chars().next().unwrap_or('A');
|
||
if first.is_ascii_alphabetic() {
|
||
let idx = (first.to_ascii_uppercase() as u8 - b'A') as usize;
|
||
if let Some(t) = &self.deftypes[idx] {
|
||
return t.clone();
|
||
}
|
||
}
|
||
Ty::Sng
|
||
}
|
||
|
||
fn name_ty(&self, name: &str, suffix: &Option<Suffix>) -> Ty {
|
||
match suffix {
|
||
Some(s) => suffix_ty(*s),
|
||
None => self.default_ty(name),
|
||
}
|
||
}
|
||
|
||
fn var_key(&self, name: &str, suffix: &Option<Suffix>, as_decl: bool) -> String {
|
||
if as_decl {
|
||
format!("{name}\u{1}AS")
|
||
} else {
|
||
let c = match suffix {
|
||
Some(s) => s.as_char(),
|
||
None => match self.default_ty(name) {
|
||
Ty::Int => '%',
|
||
Ty::Lng => '&',
|
||
Ty::Cur => '@',
|
||
Ty::Dbl => '#',
|
||
Ty::Str => '$',
|
||
_ => '!',
|
||
},
|
||
};
|
||
format!("{name}{c}")
|
||
}
|
||
}
|
||
|
||
// ---- Slot-Verwaltung ---------------------------------------------------
|
||
|
||
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
|
||
let slot = self.globals.len() as u16;
|
||
self.globals.push(hir::HVar {
|
||
name: name.trim_end_matches("\u{1}AS").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.trim_end_matches("\u{1}AS").to_string(),
|
||
ty: self.h_ty(ty),
|
||
array,
|
||
});
|
||
(slot, false)
|
||
}
|
||
}
|
||
|
||
/// Versteckter Temp-Slot (SELECT-Selektor, FOR-Grenzen, SWAP).
|
||
fn alloc_temp(&mut self, scope: &mut Scope, ty: &Ty) -> VarSlot {
|
||
let (slot, global) = self.alloc_var(scope, "<temp>", ty, false);
|
||
if global {
|
||
VarSlot::Global(slot)
|
||
} else {
|
||
VarSlot::Local(slot)
|
||
}
|
||
}
|
||
|
||
fn vars_of<'a>(&'a self, scope: &'a Scope) -> &'a HashMap<String, VarInfo> {
|
||
if scope.is_module {
|
||
&self.module_vars
|
||
} else {
|
||
&scope.vars
|
||
}
|
||
}
|
||
|
||
fn visible_var<'a>(&'a self, scope: &'a Scope, key: &str) -> Option<&'a VarInfo> {
|
||
self.vars_of(scope).get(key).or_else(|| {
|
||
(!scope.is_module && (scope.is_def_fn || self.shared_vars.contains(key)))
|
||
.then(|| self.module_vars.get(key))
|
||
.flatten()
|
||
})
|
||
}
|
||
|
||
fn insert_var(&mut self, scope: &mut Scope, key: String, info: VarInfo) {
|
||
if scope.is_module {
|
||
self.module_vars.insert(key, info);
|
||
} else {
|
||
scope.vars.insert(key, info);
|
||
}
|
||
}
|
||
|
||
/// Feldpfad eines UDT-Zugriffs auflösen: liefert Typ und Feldindizes.
|
||
fn member_path(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> (Ty, Vec<u16>) {
|
||
let mut cur = base_ty.clone();
|
||
let mut idxs = Vec::new();
|
||
for seg in path {
|
||
match cur.clone() {
|
||
Ty::Udt(udt_name) => {
|
||
let found = self.udt_ids.get(&udt_name).and_then(|id| {
|
||
self.udt_defs[*id as usize]
|
||
.fields
|
||
.iter()
|
||
.position(|(n, _)| n == seg)
|
||
.map(|i| (i as u16, self.udt_defs[*id as usize].fields[i].1.clone()))
|
||
});
|
||
match found {
|
||
Some((i, hty)) => {
|
||
idxs.push(i);
|
||
cur = match hty {
|
||
HTy::Num(n) => ty_of_num(n),
|
||
HTy::Str => Ty::Str,
|
||
HTy::FixedStr(n) => Ty::FixedStr(n),
|
||
HTy::Udt(id) => Ty::Udt(self.udt_defs[id as usize].name.clone()),
|
||
HTy::Form => Ty::Form,
|
||
HTy::Control => Ty::Control,
|
||
};
|
||
}
|
||
None => {
|
||
self.err(pos, "Element not defined");
|
||
return (Ty::Unknown, idxs);
|
||
}
|
||
}
|
||
}
|
||
Ty::Unknown => return (Ty::Unknown, idxs),
|
||
_ => {
|
||
self.err(pos, "Type mismatch");
|
||
return (Ty::Unknown, idxs);
|
||
}
|
||
}
|
||
}
|
||
(cur, idxs)
|
||
}
|
||
|
||
/// Variable nachschlagen bzw. implizit deklarieren.
|
||
fn resolve_var(
|
||
&mut self,
|
||
scope: &mut Scope,
|
||
name: &str,
|
||
suffix: &Option<Suffix>,
|
||
array: bool,
|
||
pos: SourcePos,
|
||
) -> Option<VarInfo> {
|
||
// AS-deklarierte Variable hat Vorrang, wenn kein Suffix angegeben
|
||
let as_key = format!("{name}\u{1}AS");
|
||
if suffix.is_none() {
|
||
if let Some(v) = self.visible_var(scope, &as_key) {
|
||
return Some(v.clone());
|
||
}
|
||
} else if let Some(v) = self.visible_var(scope, &as_key).cloned() {
|
||
if v.ty == suffix_ty(suffix.unwrap()) {
|
||
return Some(v);
|
||
}
|
||
self.err(pos, "Duplicate definition");
|
||
return None;
|
||
}
|
||
let key = self.var_key(name, suffix, false);
|
||
if let Some(v) = self.visible_var(scope, &key) {
|
||
return Some(v.clone());
|
||
}
|
||
if self.explicit {
|
||
self.err(pos, "Variable not defined");
|
||
return None;
|
||
}
|
||
let ty = self.name_ty(name, suffix);
|
||
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
|
||
let info = if scope.is_def_fn {
|
||
let slot = self.alloc_global(&key, &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, &key, &ty, array);
|
||
let info = VarInfo {
|
||
ty,
|
||
array,
|
||
explicit: false,
|
||
slot,
|
||
global,
|
||
by_ref: false,
|
||
};
|
||
self.insert_var(scope, key, info.clone());
|
||
info
|
||
};
|
||
Some(info)
|
||
}
|
||
|
||
/// Konstantenfaltung für `CONST`-Ausdrücke.
|
||
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
|
||
match e {
|
||
Expr::IntLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||
Expr::LongLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||
Expr::SingleLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||
Expr::DoubleLit(v, _) => Some(ConstVal::Num(*v)),
|
||
Expr::CurrencyLit(v, _) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
|
||
Expr::StrLit(s, _) => Some(ConstVal::Str(s.clone())),
|
||
Expr::Paren(e) => self.fold_const(e),
|
||
Expr::Name {
|
||
name, args: None, ..
|
||
} => self.consts.get(name).and_then(|(_, v)| v.clone()),
|
||
Expr::Unary {
|
||
op: UnOp::Neg,
|
||
operand,
|
||
..
|
||
} => match self.fold_const(operand)? {
|
||
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
|
||
ConstVal::Str(_) => None,
|
||
},
|
||
Expr::Unary {
|
||
op: UnOp::Not,
|
||
operand,
|
||
..
|
||
} => match self.fold_const(operand)? {
|
||
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
|
||
ConstVal::Str(_) => None,
|
||
},
|
||
Expr::Binary { op, lhs, rhs, .. } => {
|
||
let l = self.fold_const(lhs)?;
|
||
let r = self.fold_const(rhs)?;
|
||
match (l, r) {
|
||
(ConstVal::Num(a), ConstVal::Num(b)) => {
|
||
let v = match op {
|
||
BinOp::Add => a + b,
|
||
BinOp::Sub => a - b,
|
||
BinOp::Mul => a * b,
|
||
BinOp::Div => a / b,
|
||
BinOp::Pow => a.powf(b),
|
||
BinOp::IntDiv => {
|
||
let bi = b as i64;
|
||
if bi == 0 {
|
||
return None;
|
||
}
|
||
((a as i64) / bi) as f64
|
||
}
|
||
BinOp::Mod => {
|
||
let bi = b as i64;
|
||
if bi == 0 {
|
||
return None;
|
||
}
|
||
((a as i64) % bi) as f64
|
||
}
|
||
_ => return None,
|
||
};
|
||
Some(ConstVal::Num(v))
|
||
}
|
||
(ConstVal::Str(a), ConstVal::Str(b)) if *op == BinOp::Add => {
|
||
Some(ConstVal::Str(format!("{a}{b}")))
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
// ---- Konvertierungen ---------------------------------------------------
|
||
|
||
/// Numerische Konvertierung als expliziter HIR-Knoten (Matrix).
|
||
fn conv_num(&mut self, e: HExpr, from: &Ty, to: NumTy) -> HExpr {
|
||
let f = num_ty(from);
|
||
if f == to {
|
||
return e;
|
||
}
|
||
HExpr::Conv {
|
||
from: f,
|
||
to,
|
||
arg: Box::new(e),
|
||
}
|
||
}
|
||
|
||
/// Wert an Zieltyp anpassen (Zuweisung, BYVAL-Argument).
|
||
fn coerce(&mut self, e: HExpr, from: &Ty, to: &Ty, pos: SourcePos) -> HExpr {
|
||
if matches!(
|
||
(from, to),
|
||
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
|
||
) {
|
||
return e;
|
||
}
|
||
match (is_num(to), is_str(to)) {
|
||
(true, _) if is_num(from) => {
|
||
if matches!(to, Ty::Unknown) || matches!(from, Ty::Unknown) {
|
||
e
|
||
} else {
|
||
self.conv_num(e, from, num_ty(to))
|
||
}
|
||
}
|
||
(_, true) if is_str(from) => match to {
|
||
Ty::FixedStr(n) => HExpr::FixStr {
|
||
len: *n,
|
||
arg: Box::new(e),
|
||
},
|
||
_ => e,
|
||
},
|
||
_ => {
|
||
if !matches!(to, Ty::Udt(_)) && !matches!(from, Ty::Udt(_)) {
|
||
self.err(pos, "Type mismatch");
|
||
}
|
||
e
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Anweisungen -------------------------------------------------------
|
||
|
||
fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
|
||
let enclosing = (scope.current_line, scope.current_pos);
|
||
let mut out = Vec::new();
|
||
for stmt in stmts {
|
||
self.lower_stmt(stmt, scope, &mut out);
|
||
}
|
||
(scope.current_line, scope.current_pos) = enclosing;
|
||
out
|
||
}
|
||
|
||
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
|
||
out.push(HStmt {
|
||
pos: scope.current_pos,
|
||
line: scope.current_line,
|
||
kind,
|
||
});
|
||
}
|
||
|
||
fn lower_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, out: &mut Vec<HStmt>) {
|
||
// Quellzeile für Anweisungsgrenzen/Fehlerortung mitführen.
|
||
let pos = stmt_pos(stmt);
|
||
if pos.line > 0 {
|
||
scope.current_line = pos.line;
|
||
scope.current_pos = pos;
|
||
}
|
||
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)
|
||
&& !self.is_debug_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.find_object(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, elseif_pos) in elseifs.iter().rev() {
|
||
let c2 = self.lower_cond(ec, scope);
|
||
let body2 = self.lower_body(eb, scope);
|
||
let stmt = HStmt {
|
||
pos: *elseif_pos,
|
||
line: elseif_pos.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,
|
||
end_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,
|
||
end_pos: *end_pos,
|
||
step: step_e,
|
||
limit_slot,
|
||
step_slot,
|
||
body: hbody,
|
||
exit_label,
|
||
},
|
||
);
|
||
}
|
||
}
|
||
Stmt::DoLoop {
|
||
pre,
|
||
post,
|
||
body,
|
||
end_pos,
|
||
..
|
||
} => {
|
||
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 {
|
||
end_pos: *end_pos,
|
||
pre: pre_c,
|
||
post: post_c,
|
||
body: hbody,
|
||
exit_label,
|
||
},
|
||
);
|
||
}
|
||
Stmt::While {
|
||
cond,
|
||
body,
|
||
end_pos,
|
||
..
|
||
} => {
|
||
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 {
|
||
end_pos: *end_pos,
|
||
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.{}", entry.key()),
|
||
&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,
|
||
block,
|
||
..
|
||
} => {
|
||
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);
|
||
if scope.is_module {
|
||
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
||
let slot = self.module_vars[&key].slot;
|
||
let dims = d.dims.as_ref().map(|dims| {
|
||
dims.iter()
|
||
.map(|(lo, hi)| {
|
||
let lower =
|
||
lo.as_ref().map_or(Some(self.option_base as i32), |e| {
|
||
self.common_bound(e)
|
||
});
|
||
let upper = self.common_bound(hi);
|
||
(lower, upper)
|
||
})
|
||
.collect()
|
||
});
|
||
self.commons.push(hir::HCommon {
|
||
slot,
|
||
block: block.clone(),
|
||
key,
|
||
ty: self.globals[slot as usize].ty.clone(),
|
||
dims,
|
||
pos: d.pos,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
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(&key, &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 { sig, pos } => {
|
||
if let Some(r) = &mut self.references {
|
||
r.procedures.push((*pos, sig.name.clone()));
|
||
}
|
||
} // 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), *pos);
|
||
}
|
||
Stmt::DefFnBlock {
|
||
name,
|
||
suffix,
|
||
params,
|
||
body,
|
||
pos,
|
||
} => {
|
||
self.lower_def_fn(name, suffix, params, Some(body), None, *pos);
|
||
}
|
||
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
|
||
Stmt::Open {
|
||
file,
|
||
mode,
|
||
isam,
|
||
number,
|
||
len,
|
||
pos,
|
||
..
|
||
} => {
|
||
let (fe, _) = self.want_str(file, scope);
|
||
self.reject_com_device(file, *pos);
|
||
let (ne, nt) = self.want_num(number, scope);
|
||
let ne = self.conv_num(ne, &nt, NumTy::Lng);
|
||
let le = match len {
|
||
Some(l) => {
|
||
let (e, t) = self.want_num(l, scope);
|
||
self.conv_num(e, &t, NumTy::Lng)
|
||
}
|
||
// Vorgabe-Recordlänge des Vorbilds.
|
||
None => HExpr::Lng(128),
|
||
};
|
||
if let Some((ty_name, table)) = isam {
|
||
let Some(&udt) = self.udt_ids.get(ty_name) else {
|
||
self.err(*pos, "Type not defined");
|
||
return;
|
||
};
|
||
// Die Laufzeit braucht die Spaltennamen; `UdtLayout`
|
||
// führt nur die Feldtypen. Sie reisen deshalb als
|
||
// Stringargument mit — ohne Änderung am `.tbc`-Format.
|
||
let spalten = self.udt_defs[udt as usize]
|
||
.fields
|
||
.iter()
|
||
.map(|(n, _)| n.clone())
|
||
.collect::<Vec<_>>()
|
||
.join(",");
|
||
// Für die Satztypprüfung (Aufgabe 2.3) merken, welcher
|
||
// Typ an dieser Dateinummer hängt — nur bei literaler
|
||
// Nummer, sonst ist sie erst zur Laufzeit bekannt.
|
||
if let Some(ConstVal::Num(n)) = self.fold_const(number) {
|
||
self.isam_typ.insert(n as i32, ty_name.clone());
|
||
}
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::IsamOpen,
|
||
args: vec![
|
||
fe,
|
||
ne,
|
||
HExpr::Str(table.clone()),
|
||
HExpr::Str(spalten),
|
||
HExpr::UdtId(udt),
|
||
],
|
||
},
|
||
);
|
||
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, format!("Feature unavailable: {device}"));
|
||
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, format!("Feature unavailable: {device}")),
|
||
}
|
||
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, format!("Feature unavailable: {device}"));
|
||
return;
|
||
};
|
||
if let Some(i) = index {
|
||
if let Some(n) = const_zahl(i) {
|
||
if !trap_bereich_ok(art, n) {
|
||
self.err(*pos, trap_bereich_text(art));
|
||
}
|
||
}
|
||
}
|
||
let hindex = match index {
|
||
Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
|
||
None => HExpr::Lng(0),
|
||
};
|
||
let zustand = match action {
|
||
EventAction::On => 0,
|
||
EventAction::Off => 1,
|
||
EventAction::Stop => 2,
|
||
};
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::TrapSet {
|
||
art,
|
||
index: hindex,
|
||
zustand,
|
||
},
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn decl_ty(&mut self, d: &VarDecl) -> Ty {
|
||
if let Some(t) = &d.as_type {
|
||
if let TypeName::Udt(n) = t {
|
||
if !self.udt_ids.contains_key(n) {
|
||
self.err(d.pos, "Type not defined");
|
||
}
|
||
}
|
||
type_name_ty(t)
|
||
} else {
|
||
self.name_ty(&d.name, &d.suffix)
|
||
}
|
||
}
|
||
|
||
fn lower_select(
|
||
&mut self,
|
||
expr: &Expr,
|
||
arms: &[CaseArm],
|
||
scope: &mut Scope,
|
||
out: &mut Vec<HStmt>,
|
||
) {
|
||
let (se, st) = self.lower_expr(expr, scope);
|
||
// Selektor einmal auswerten (versteckter Temp-Slot).
|
||
let sel_ty = if st == Ty::Unknown {
|
||
Ty::Sng
|
||
} else {
|
||
st.clone()
|
||
};
|
||
let temp = self.alloc_temp(scope, &sel_ty);
|
||
let temp_place = HPlace {
|
||
base: temp,
|
||
base_is_ref: false,
|
||
indices: Vec::new(),
|
||
fields: Vec::new(),
|
||
ty: self.h_ty(&sel_ty),
|
||
array_elem: None,
|
||
};
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Assign {
|
||
place: temp_place.clone(),
|
||
value: se,
|
||
},
|
||
);
|
||
|
||
// Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
|
||
let mut cases: Vec<(HExpr, Vec<HStmt>, SourcePos)> = Vec::new();
|
||
let mut else_body: Vec<HStmt> = Vec::new();
|
||
for arm in arms {
|
||
let body = self.lower_body(&arm.body, scope);
|
||
if arm.specs.is_empty() {
|
||
else_body = body;
|
||
continue;
|
||
}
|
||
let mut cond: Option<HExpr> = None;
|
||
for spec in &arm.specs {
|
||
let c = self.lower_case_spec(spec, &temp_place, &st, scope);
|
||
cond = Some(match cond {
|
||
None => c,
|
||
Some(prev) => HExpr::Logic {
|
||
op: hir::HLogic::Or,
|
||
ty: IntKind::I2,
|
||
l: Box::new(prev),
|
||
r: Box::new(c),
|
||
},
|
||
});
|
||
}
|
||
cases.push((cond.unwrap(), body, arm.pos));
|
||
}
|
||
// Von hinten zu verschachteltem If zusammensetzen.
|
||
let mut els = else_body;
|
||
for (cond, body, pos) in cases.into_iter().rev() {
|
||
let stmt = HStmt {
|
||
pos,
|
||
line: pos.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),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn lower_def_fn(
|
||
&mut self,
|
||
name: &str,
|
||
suffix: &Option<Suffix>,
|
||
params: &[Param],
|
||
block: Option<&[Stmt]>,
|
||
single: Option<&Expr>,
|
||
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,
|
||
},
|
||
);
|
||
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
|
||
// freie Namen binden an Modulvariablen.
|
||
let mut fscope = Scope {
|
||
is_def_fn: true,
|
||
current_line: pos.line,
|
||
current_pos: pos,
|
||
..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: key.trim_end_matches("\u{1}AS").to_string(),
|
||
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.trim_end_matches("\u{1}AS").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 {
|
||
pos: fscope.current_pos,
|
||
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,
|
||
};
|
||
while self.hir_procs.len() <= id as usize {
|
||
self.hir_procs.push(None);
|
||
}
|
||
self.hir_procs[id as usize] = Some(hproc);
|
||
}
|
||
|
||
fn common_bound(&self, expr: &Expr) -> Option<i32> {
|
||
match self.fold_const(expr)? {
|
||
ConstVal::Num(n)
|
||
if n.is_finite()
|
||
&& (i32::MIN as f64..=i32::MAX as f64).contains(&n.round_ties_even()) =>
|
||
{
|
||
Some(n.round_ties_even() as i32)
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
|
||
let ty = self.decl_ty(d);
|
||
let mut hdims = Vec::new();
|
||
if let Some(dims) = &d.dims {
|
||
for (lo, hi) in dims {
|
||
let lo_e = match lo {
|
||
Some(l) => self.lower_num_as(l, scope, NumTy::Lng),
|
||
None => HExpr::Lng(self.option_base as i32),
|
||
};
|
||
let hi_e = self.lower_num_as(hi, scope, NumTy::Lng);
|
||
hdims.push((lo_e, hi_e));
|
||
}
|
||
}
|
||
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
||
let is_array = d.dims.is_some();
|
||
if let Some(existing) = self.vars_of(scope).get(&key).cloned() {
|
||
let redim_ok = redim && existing.array && is_array;
|
||
if existing.explicit && !redim_ok {
|
||
self.err(d.pos, "Duplicate definition");
|
||
return;
|
||
}
|
||
if redim_ok && !hdims.is_empty() {
|
||
let slot = if existing.global {
|
||
VarSlot::Global(existing.slot)
|
||
} else {
|
||
VarSlot::Local(existing.slot)
|
||
};
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Dim {
|
||
slot,
|
||
elem: self.h_ty(&ty),
|
||
dims: hdims,
|
||
redim: true,
|
||
},
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
let (slot, global) = self.alloc_var(scope, &key, &ty, is_array);
|
||
self.insert_var(
|
||
scope,
|
||
key,
|
||
VarInfo {
|
||
ty: ty.clone(),
|
||
array: is_array,
|
||
explicit: true,
|
||
slot,
|
||
global,
|
||
by_ref: false,
|
||
},
|
||
);
|
||
if is_array && !hdims.is_empty() {
|
||
let vslot = if global {
|
||
VarSlot::Global(slot)
|
||
} else {
|
||
VarSlot::Local(slot)
|
||
};
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Dim {
|
||
slot: vslot,
|
||
elem: self.h_ty(&ty),
|
||
dims: hdims,
|
||
redim,
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
fn check_label_exists(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) {
|
||
let found = match target {
|
||
LabelRef::Name(n) => scope.labels.contains_key(n),
|
||
LabelRef::Line(n) => scope.line_labels.contains_key(n),
|
||
};
|
||
if !found {
|
||
self.err(pos, "Label not defined");
|
||
}
|
||
}
|
||
|
||
fn label_id(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) -> Option<LabelId> {
|
||
let id = match target {
|
||
LabelRef::Name(n) => scope.labels.get(n).copied(),
|
||
LabelRef::Line(n) => scope.line_labels.get(n).copied(),
|
||
};
|
||
if id.is_none() {
|
||
self.err(pos, "Label not defined");
|
||
}
|
||
id
|
||
}
|
||
|
||
/// Sprungziel im Modulrumpf auflösen (für `ON ERROR GOTO` aus einer
|
||
/// Prozedur heraus).
|
||
fn module_label_id(&mut self, target: &LabelRef, pos: SourcePos) -> Option<LabelId> {
|
||
let id = match target {
|
||
LabelRef::Name(n) => self.module_labels.get(n).copied(),
|
||
LabelRef::Line(n) => self.module_line_labels.get(n).copied(),
|
||
};
|
||
if id.is_none() {
|
||
self.err(pos, "Label not defined");
|
||
}
|
||
id
|
||
}
|
||
|
||
// ---- Aufrufe -----------------------------------------------------------
|
||
|
||
fn lower_call_stmt(
|
||
&mut self,
|
||
name: &str,
|
||
args: &[Expr],
|
||
scope: &mut Scope,
|
||
pos: SourcePos,
|
||
out: &mut Vec<HStmt>,
|
||
) {
|
||
if let Some(r) = &mut self.references {
|
||
r.procedures.push((pos, name.into()));
|
||
}
|
||
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.current_form() {
|
||
let method = forms::methods(ObjectClass::Form)
|
||
.iter()
|
||
.position(|method| *method == name)
|
||
.unwrap() as u16;
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::ObjectMethod {
|
||
object,
|
||
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]
|
||
{
|
||
let (parent, object_name) = object_name
|
||
.split_once('!')
|
||
.map_or((None, object_name.as_str()), |(parent, name)| {
|
||
(Some(parent), name)
|
||
});
|
||
if let Some((object, _)) = self.scoped_object(object_name, parent, *object_pos) {
|
||
let Ok(index) = self.object_index(object, index, scope, *object_pos) else {
|
||
return;
|
||
};
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::ObjectLoad {
|
||
object,
|
||
index,
|
||
unload: name == "UNLOAD",
|
||
},
|
||
);
|
||
return;
|
||
}
|
||
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, format!("Feature unavailable: {name}"));
|
||
return;
|
||
}
|
||
self.err(pos, "Subprogram not defined");
|
||
}
|
||
|
||
/// Argumente eines SUB-/FUNCTION-Aufrufs: Variablen BYREF (exakter
|
||
/// Typ), Ausdrücke/Klammern BYVAL mit Konvertierung.
|
||
fn lower_call_args(&mut self, info: &ProcInfo, args: &[Expr], scope: &mut Scope) -> Vec<HArg> {
|
||
let mut out = Vec::new();
|
||
for (i, a) in args.iter().enumerate() {
|
||
let (pt, p_array) = match info.params.get(i) {
|
||
Some(p) => (p.0.clone(), p.1),
|
||
None => (Ty::Unknown, false),
|
||
};
|
||
// DEF FN: alle Parameter BYVAL.
|
||
let force_byval = info.def_fn;
|
||
// Array-Parameter: ganzes Array als Referenz.
|
||
if p_array {
|
||
if let Expr::Name {
|
||
name,
|
||
suffix,
|
||
args: idx,
|
||
pos,
|
||
} = a
|
||
{
|
||
if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
|
||
if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) {
|
||
let base = if v.global {
|
||
VarSlot::Global(v.slot)
|
||
} else {
|
||
VarSlot::Local(v.slot)
|
||
};
|
||
out.push(HArg::ArrayRef(HPlace {
|
||
base,
|
||
base_is_ref: false,
|
||
indices: Vec::new(),
|
||
fields: Vec::new(),
|
||
ty: self.h_ty(&v.ty),
|
||
array_elem: None,
|
||
}));
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
self.err(a.pos(), "Parameter type mismatch");
|
||
continue;
|
||
}
|
||
// BYREF-fähig: einfacher Variablen-/Element-/Feldzugriff.
|
||
let byref_candidate = !force_byval && self.is_place_expr(a, scope);
|
||
if byref_candidate {
|
||
let (place, at) = self.lower_place(a, scope);
|
||
let kind_ok = (is_num(&pt) && is_num(&at))
|
||
|| (is_str(&pt) && is_str(&at))
|
||
|| matches!((&pt, &at), (Ty::Udt(x), Ty::Udt(y)) if x == y)
|
||
|| matches!(
|
||
(&pt, &at),
|
||
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
|
||
)
|
||
|| pt == Ty::Unknown
|
||
|| at == Ty::Unknown;
|
||
if !kind_ok {
|
||
self.err(a.pos(), "Parameter type mismatch");
|
||
continue;
|
||
}
|
||
// BYREF verlangt exakten Typ (Vorbild: „Parameter type
|
||
// mismatch" bei abweichendem numerischen Typ).
|
||
if pt != Ty::Unknown && at != Ty::Unknown && pt != at {
|
||
if is_num(&pt) && is_num(&at) {
|
||
self.err(a.pos(), "Parameter type mismatch");
|
||
continue;
|
||
}
|
||
// FixedStr → STRING-Parameter: BYVAL-Kopie.
|
||
if let Some(place) = place {
|
||
let e = HExpr::Load(Box::new(place));
|
||
let e = self.coerce(e, &at, &pt, a.pos());
|
||
out.push(HArg::ByVal(e));
|
||
}
|
||
continue;
|
||
}
|
||
if let Some(place) = place {
|
||
out.push(HArg::ByRef(place));
|
||
}
|
||
continue;
|
||
}
|
||
// BYVAL: Ausdruck auswerten und konvertieren.
|
||
let (e, at) = self.lower_expr(a, scope);
|
||
let kind_ok = (is_num(&pt) && is_num(&at))
|
||
|| (is_str(&pt) && is_str(&at))
|
||
|| matches!(
|
||
(&pt, &at),
|
||
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
|
||
)
|
||
|| pt == Ty::Unknown
|
||
|| at == Ty::Unknown;
|
||
if !kind_ok {
|
||
self.err(a.pos(), "Parameter type mismatch");
|
||
continue;
|
||
}
|
||
let e = self.coerce(e, &at, &pt, a.pos());
|
||
out.push(HArg::ByVal(e));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Ist der Ausdruck ein L-Wert (Variable/Element/Feld), keine
|
||
/// Konstante, Funktion oder Klammerung?
|
||
fn is_place_expr(&self, e: &Expr, scope: &Scope) -> bool {
|
||
match e {
|
||
Expr::Name {
|
||
name, suffix, args, ..
|
||
} => {
|
||
if self.consts.contains_key(name) {
|
||
return false;
|
||
}
|
||
if (suffix.is_none() && self.find_object(name).is_some())
|
||
|| (name.contains('!')
|
||
&& !self.is_udt_path(scope, name)
|
||
&& !self.is_debug_path(scope, name))
|
||
{
|
||
return false;
|
||
}
|
||
match args {
|
||
None => {
|
||
// Variable oder parameterlose Funktion/Builtin?
|
||
let as_key = format!("{name}\u{1}AS");
|
||
let key = self.var_key(name, suffix, false);
|
||
let declared = self.visible_var(scope, &key).is_some()
|
||
|| self
|
||
.visible_var(scope, &as_key)
|
||
.is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
|
||
if declared {
|
||
return true;
|
||
}
|
||
let full_name = match suffix {
|
||
Some(s) => format!("{name}{}", s.as_char()),
|
||
None => name.to_string(),
|
||
};
|
||
if banned_feature(&full_name) || builtin_fn(&full_name).is_some() {
|
||
return false;
|
||
}
|
||
if let Some(info) = self.procs.get(name) {
|
||
if info.kind == ProcKind::Function && info.params.is_empty() {
|
||
return false;
|
||
}
|
||
}
|
||
true // implizite Variable
|
||
}
|
||
Some(_) => {
|
||
// Nur deklarierte Arrays sind L-Werte.
|
||
let as_key = format!("{name}\u{1}AS");
|
||
let key = self.var_key(name, suffix, false);
|
||
let v = if suffix.is_none() {
|
||
self.visible_var(scope, &as_key)
|
||
.or_else(|| self.visible_var(scope, &key))
|
||
} else {
|
||
self.visible_var(scope, &key).or_else(|| {
|
||
self.visible_var(scope, &as_key)
|
||
.filter(|v| v.ty == suffix_ty(suffix.unwrap()))
|
||
})
|
||
};
|
||
v.map(|v| v.array).unwrap_or(false)
|
||
}
|
||
}
|
||
}
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn lower_builtin_stmt(
|
||
&mut self,
|
||
name: &str,
|
||
args: &[Expr],
|
||
min: u8,
|
||
max: u8,
|
||
spec: &[ArgK],
|
||
scope: &mut Scope,
|
||
pos: SourcePos,
|
||
out: &mut Vec<HStmt>,
|
||
) {
|
||
// Argumentprüfung wie bisher.
|
||
let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos);
|
||
|
||
match name {
|
||
"BEEP" => self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::Beep,
|
||
args: vec![],
|
||
},
|
||
),
|
||
"DOEVENTS" => self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::Doevents,
|
||
args: vec![],
|
||
},
|
||
),
|
||
"RANDOMIZE" => {
|
||
let a = lowered
|
||
.into_iter()
|
||
.next()
|
||
.map(|(e, t)| {
|
||
let conv = self.conv_num(e, &t, NumTy::Dbl);
|
||
vec![conv]
|
||
})
|
||
.unwrap_or_default();
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::Randomize,
|
||
args: a,
|
||
},
|
||
);
|
||
}
|
||
"SLEEP" => {
|
||
let a = lowered
|
||
.into_iter()
|
||
.next()
|
||
.map(|(e, t)| {
|
||
let conv = self.conv_num(e, &t, NumTy::Dbl);
|
||
vec![conv]
|
||
})
|
||
.unwrap_or_default();
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::Sleep,
|
||
args: a,
|
||
},
|
||
);
|
||
}
|
||
"SWAP" => {
|
||
// SWAP a, b → t = a : a = b : b = t (versteckter Temp).
|
||
if args.len() == 2 {
|
||
let (pa, ta) = self.lower_place(&args[0], scope);
|
||
let (pb, tb) = self.lower_place(&args[1], scope);
|
||
let ok = (is_num(&ta) && is_num(&tb))
|
||
|| (is_str(&ta) && is_str(&tb))
|
||
|| matches!((&ta, &tb), (Ty::Udt(x), Ty::Udt(y)) if x == y)
|
||
|| ta == Ty::Unknown
|
||
|| tb == Ty::Unknown;
|
||
if !ok {
|
||
self.err(pos, "Type mismatch");
|
||
return;
|
||
}
|
||
if let (Some(pa), Some(pb)) = (pa, pb) {
|
||
let tmp = self.alloc_temp(scope, &ta);
|
||
let tmp_place = HPlace {
|
||
base: tmp,
|
||
base_is_ref: false,
|
||
indices: Vec::new(),
|
||
fields: Vec::new(),
|
||
ty: self.h_ty(&ta),
|
||
array_elem: None,
|
||
};
|
||
let load_a = HExpr::Load(Box::new(pa.clone()));
|
||
let load_b = HExpr::Load(Box::new(pb.clone()));
|
||
let b_conv = self.coerce_silent(load_b, &tb, &ta);
|
||
let t_load = HExpr::Load(Box::new(tmp_place.clone()));
|
||
let t_conv = self.coerce_silent(t_load, &ta, &tb);
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Assign {
|
||
place: tmp_place,
|
||
value: load_a,
|
||
},
|
||
);
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Assign {
|
||
place: pa,
|
||
value: b_conv,
|
||
},
|
||
);
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::Assign {
|
||
place: pb,
|
||
value: t_conv,
|
||
},
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// Bildschirmanweisungen: ausgelassene Argumente kommen als -1
|
||
// durch, damit die Laufzeit „weglassen" von „null" unterscheidet.
|
||
"ENVIRON" | "CLEAR" | "TRON" | "TROFF" | "STACK" | "MSGBOX" => {
|
||
let bt = match name {
|
||
"ENVIRON" => Builtin::EnvironSet,
|
||
"CLEAR" => Builtin::Clear,
|
||
"TRON" => Builtin::Tron,
|
||
"TROFF" => Builtin::Troff,
|
||
"MSGBOX" => Builtin::MsgBox,
|
||
_ => Builtin::StackStmt,
|
||
};
|
||
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
|
||
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
|
||
}
|
||
"KILL" | "CHDIR" | "CHDRIVE" | "MKDIR" | "RMDIR" | "FILES" | "SHELL" => {
|
||
let bt = match name {
|
||
"KILL" => Builtin::Kill,
|
||
"CHDIR" => Builtin::Chdir,
|
||
"CHDRIVE" => Builtin::Chdrive,
|
||
"MKDIR" => Builtin::Mkdir,
|
||
"RMDIR" => Builtin::Rmdir,
|
||
"FILES" => Builtin::Files,
|
||
_ => Builtin::ShellStmt,
|
||
};
|
||
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
|
||
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
|
||
}
|
||
"RESET" => self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::CloseAll,
|
||
args: vec![],
|
||
},
|
||
),
|
||
"SETFORMATCC" => {
|
||
let (e, t) = lowered[0].clone();
|
||
let n = self.conv_num(e, &t, NumTy::Lng);
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::SetFormatCc,
|
||
args: vec![n],
|
||
},
|
||
);
|
||
}
|
||
"KEY" => {
|
||
// `KEY LIST` (Makros auflisten) vs. `KEY n, text$` (zuweisen).
|
||
let ist_list = matches!(
|
||
args.first(),
|
||
Some(Expr::Name { name, args: None, suffix: None, .. }) if name == "LIST"
|
||
);
|
||
if ist_list {
|
||
if args.len() > 1 {
|
||
self.err(pos, "Argument-count mismatch: KEY");
|
||
}
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::KeyList,
|
||
args: vec![],
|
||
},
|
||
);
|
||
} else {
|
||
if lowered.len() != 2 {
|
||
self.err(pos, "Argument-count mismatch: KEY");
|
||
}
|
||
let (ne, nt) = lowered[0].clone();
|
||
let n = self.conv_num(ne, &nt, NumTy::Lng);
|
||
let t = lowered[1].0.clone();
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::KeyAssign,
|
||
args: vec![n, t],
|
||
},
|
||
);
|
||
}
|
||
}
|
||
"CLS" | "COLOR" | "LOCATE" | "WIDTH" | "SCREEN" => {
|
||
let b = match name {
|
||
"CLS" => Builtin::Cls,
|
||
"COLOR" => Builtin::Color,
|
||
"LOCATE" => Builtin::Locate,
|
||
"SCREEN" => Builtin::ScreenStmt,
|
||
_ => Builtin::Width,
|
||
};
|
||
let a = self.lower_opt_args(args, &lowered);
|
||
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
|
||
}
|
||
// ---- ISAM ------------------------------------------------------
|
||
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE"
|
||
| "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS"
|
||
| "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
|
||
self.lower_isam_stmt(name, args, lowered, scope, out)
|
||
}
|
||
"SETUEVENT" => self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::SetUEvent,
|
||
args: vec![],
|
||
},
|
||
),
|
||
"RUN" => {
|
||
let (target, string) = match lowered.first() {
|
||
None => (None, false),
|
||
Some((expr, ty)) if is_str(ty) => (Some(expr.clone()), true),
|
||
Some((expr, ty)) => (Some(self.conv_num(expr.clone(), ty, NumTy::Lng)), false),
|
||
};
|
||
self.push(out, scope, HStmtKind::Run { target, string });
|
||
}
|
||
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
|
||
_ => self.push(out, scope, HStmtKind::Unsupported("Anweisung")),
|
||
}
|
||
}
|
||
|
||
/// ISAM-Anweisungen absenken. Die Dateinummer ist überall das erste
|
||
/// Argument (bei `DELETETABLE` stattdessen der Datenbankname); alle
|
||
/// weiteren Argumente reisen unverändert durch, weil die Laufzeit sie
|
||
/// gegen das Tabellenlayout prüfen muss.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn lower_isam_stmt(
|
||
&mut self,
|
||
name: &str,
|
||
args: &[Expr],
|
||
lowered: Vec<(HExpr, Ty)>,
|
||
scope: &mut Scope,
|
||
out: &mut Vec<HStmt>,
|
||
) {
|
||
let b = match name {
|
||
"CREATEINDEX" => Builtin::IsamCreateIndex,
|
||
"DELETEINDEX" => Builtin::IsamDeleteIndex,
|
||
"SETINDEX" => Builtin::IsamSetIndex,
|
||
"INSERT" => Builtin::IsamInsert,
|
||
"RETRIEVE" => Builtin::IsamRetrieve,
|
||
"UPDATE" => Builtin::IsamUpdate,
|
||
"DELETE" => Builtin::IsamDelete,
|
||
"DELETETABLE" => Builtin::IsamDeleteTable,
|
||
"MOVEFIRST" => Builtin::IsamMoveFirst,
|
||
"MOVELAST" => Builtin::IsamMoveLast,
|
||
"MOVENEXT" => Builtin::IsamMoveNext,
|
||
"MOVEPREVIOUS" => Builtin::IsamMovePrevious,
|
||
"SEEKEQ" => Builtin::IsamSeekEq,
|
||
"SEEKGT" => Builtin::IsamSeekGt,
|
||
"SEEKGE" => Builtin::IsamSeekGe,
|
||
"BEGINTRANS" => Builtin::IsamBeginTrans,
|
||
"COMMITTRANS" => Builtin::IsamCommitTrans,
|
||
_ => Builtin::IsamRollback,
|
||
};
|
||
|
||
// Satzargument gegen den Typ der Dateinummer prüfen, sofern beide
|
||
// literal bekannt sind (Aufgabe 2.3). Ist die Nummer erst zur
|
||
// Laufzeit bekannt, prüft die Laufzeit gegen das Tabellenlayout.
|
||
if matches!(name, "INSERT" | "RETRIEVE" | "UPDATE") {
|
||
if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) = (
|
||
args.first().and_then(|a| self.fold_const(a)),
|
||
lowered.get(1),
|
||
) {
|
||
if let Some(soll) = self.isam_typ.get(&(n as i32)) {
|
||
if soll != hat {
|
||
self.err(args[1].pos(), "Type mismatch");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut hargs = Vec::with_capacity(lowered.len());
|
||
for (i, (e, t)) in lowered.into_iter().enumerate() {
|
||
// Erstes Argument ist die Dateinummer (bei DELETETABLE ein Name).
|
||
let ist_nummer = i == 0 && name != "DELETETABLE";
|
||
if ist_nummer || (name == "ROLLBACK" && i == 0) {
|
||
hargs.push(self.conv_num(e, &t, NumTy::Lng));
|
||
} else {
|
||
hargs.push(e);
|
||
}
|
||
}
|
||
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: hargs });
|
||
}
|
||
|
||
/// Ausgabeziel von `PRINT` umschalten (−1 Bildschirm, −2 Drucker,
|
||
/// sonst Dateinummer).
|
||
fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) {
|
||
self.push(
|
||
out,
|
||
scope,
|
||
HStmtKind::BuiltinStmt {
|
||
b: Builtin::PrintZiel,
|
||
args: vec![n],
|
||
},
|
||
);
|
||
}
|
||
|
||
/// Argumente einer Bildschirmanweisung: ausgelassene (`LOCATE , 5`)
|
||
/// werden zu -1, damit die Laufzeit „weglassen" erkennt (0 ist bei
|
||
/// `COLOR` eine gültige Farbe).
|
||
fn lower_opt_args(&mut self, args: &[Expr], lowered: &[(HExpr, Ty)]) -> Vec<HExpr> {
|
||
args.iter()
|
||
.enumerate()
|
||
.map(|(i, a)| {
|
||
if matches!(a, Expr::Missing) {
|
||
HExpr::Lng(-1)
|
||
} else {
|
||
let (e, t) = lowered[i].clone();
|
||
if t == Ty::Unknown {
|
||
e
|
||
} else {
|
||
self.conv_num(e, &t, NumTy::Lng)
|
||
}
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Zählt/prüft Builtin-Argumente und liefert die abgesenkten Ausdrücke.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn check_and_lower_builtin_args(
|
||
&mut self,
|
||
name: &str,
|
||
args: &[Expr],
|
||
min: u8,
|
||
max: u8,
|
||
spec: &[ArgK],
|
||
scope: &mut Scope,
|
||
pos: SourcePos,
|
||
) -> Vec<(HExpr, Ty)> {
|
||
let real: Vec<&Expr> = args
|
||
.iter()
|
||
.filter(|a| !matches!(a, Expr::Missing))
|
||
.collect();
|
||
if real.len() < min as usize || args.len() > max as usize {
|
||
// Katalogtext des Vorbilds, ergänzt um das Element: eine Diagnose
|
||
// zu einem dokumentierten Element muss es benennen (Guiding
|
||
// Principle) — sonst ist bei mehreren Aufrufen in einer Zeile
|
||
// nicht erkennbar, welcher gemeint ist.
|
||
self.err(pos, format!("Argument-count mismatch: {name}"));
|
||
}
|
||
// Sonderfall INSTR([start%,] s$, such$)
|
||
let instr_with_start = name == "INSTR" && args.len() == 3;
|
||
// Sonderfall CREATEINDEX: beliebig viele Spaltennamen ab Argument 4.
|
||
let createindex = name == "CREATEINDEX";
|
||
let mut out = Vec::new();
|
||
for (i, a) in args.iter().enumerate() {
|
||
if matches!(a, Expr::Missing) {
|
||
out.push((HExpr::Int(0), Ty::Unknown));
|
||
continue;
|
||
}
|
||
let (he, at) = self.lower_expr(a, scope);
|
||
let kind = if instr_with_start {
|
||
match i {
|
||
0 => ArgK::N,
|
||
_ => ArgK::S,
|
||
}
|
||
} else if name == "INSTR" || (createindex && i >= 3) {
|
||
ArgK::S
|
||
} else {
|
||
*spec.get(i).unwrap_or(&ArgK::A)
|
||
};
|
||
let ok = match kind {
|
||
ArgK::N => is_num(&at),
|
||
ArgK::S => is_str(&at),
|
||
ArgK::A => true,
|
||
ArgK::R => matches!(at, Ty::Udt(_) | Ty::Unknown),
|
||
};
|
||
if !ok {
|
||
self.err(a.pos(), "Type mismatch");
|
||
}
|
||
out.push((he, at));
|
||
}
|
||
out
|
||
}
|
||
|
||
// ---- Ausdrücke ---------------------------------------------------------
|
||
|
||
fn want_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||
let (he, t) = self.lower_expr(e, scope);
|
||
if !is_num(&t) {
|
||
self.err(e.pos(), "Type mismatch");
|
||
}
|
||
(he, t)
|
||
}
|
||
/// `OPEN "COM1:" …` — serielle Schnittstelle ist deklariertes Non-Feature.
|
||
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
||
/// bleibt der Datei-E/A überlassen.
|
||
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
||
if let Expr::StrLit(s, _) = file {
|
||
if ist_com_geraet(s) {
|
||
self.err(pos, format!("Feature unavailable: {s}"));
|
||
}
|
||
}
|
||
}
|
||
|
||
fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||
let (he, t) = self.lower_expr(e, scope);
|
||
if !is_str(&t) {
|
||
self.err(e.pos(), "Type mismatch");
|
||
}
|
||
(he, t)
|
||
}
|
||
|
||
/// Numerischer Ausdruck, konvertiert auf Zieltyp.
|
||
fn lower_num_as(&mut self, e: &Expr, scope: &mut Scope, to: NumTy) -> HExpr {
|
||
let (he, t) = self.want_num(e, scope);
|
||
if t == Ty::Unknown {
|
||
he
|
||
} else {
|
||
self.conv_num(he, &t, to)
|
||
}
|
||
}
|
||
|
||
fn lower_expr_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||
self.want_num(e, scope)
|
||
}
|
||
|
||
/// Bedingung: numerisch; 0 = falsch. Der Codegen testet auf ≠ 0 im
|
||
/// jeweiligen Typ (kein Konvertierungszwang nötig — auf INTEGER
|
||
/// konvertieren würde bei großen Werten fälschlich Fehler 6 auslösen,
|
||
/// daher Vergleich im Operandentyp).
|
||
fn lower_cond(&mut self, e: &Expr, scope: &mut Scope) -> HExpr {
|
||
let (he, t) = self.want_num(e, scope);
|
||
if t == Ty::Unknown || num_ty(&t) == NumTy::Int {
|
||
return he;
|
||
}
|
||
// vergleiche ≠ 0 im Operandentyp → INTEGER-Ergebnis
|
||
let nt = num_ty(&t);
|
||
let zero = match nt {
|
||
NumTy::Int => HExpr::Int(0),
|
||
NumTy::Lng => HExpr::Lng(0),
|
||
NumTy::Cur => HExpr::Cur(0),
|
||
NumTy::Sng => HExpr::Sng(0.0),
|
||
NumTy::Dbl => HExpr::Dbl(0.0),
|
||
};
|
||
HExpr::Cmp {
|
||
op: hir::HCmp::Ne,
|
||
ty: hir::CmpKind::Num(nt),
|
||
l: Box::new(he),
|
||
r: Box::new(zero),
|
||
}
|
||
}
|
||
|
||
fn check_assign(&mut self, target: &Ty, value: &Ty, pos: SourcePos) {
|
||
let ok = (is_num(target) && is_num(value))
|
||
|| (is_str(target) && is_str(value))
|
||
|| matches!((target, value), (Ty::Udt(a), Ty::Udt(b)) if a == b)
|
||
|| matches!(
|
||
(target, value),
|
||
(Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
|
||
)
|
||
|| *target == Ty::Unknown
|
||
|| *value == Ty::Unknown;
|
||
if !ok {
|
||
self.err(pos, "Type mismatch");
|
||
}
|
||
}
|
||
|
||
/// Wie `coerce`, aber ohne neue Diagnose (Prüfung lief bereits).
|
||
fn coerce_silent(&mut self, e: HExpr, from: &Ty, to: &Ty) -> HExpr {
|
||
if is_num(to) && is_num(from) && *to != Ty::Unknown && *from != Ty::Unknown {
|
||
return self.conv_num(e, from, num_ty(to));
|
||
}
|
||
if let Ty::FixedStr(n) = to {
|
||
if is_str(from) {
|
||
return HExpr::FixStr {
|
||
len: *n,
|
||
arg: Box::new(e),
|
||
};
|
||
}
|
||
}
|
||
e
|
||
}
|
||
|
||
/// L-Wert absenken: liefert Platz und Typ.
|
||
fn lower_place(&mut self, e: &Expr, scope: &mut Scope) -> (Option<HPlace>, Ty) {
|
||
match e {
|
||
Expr::Name {
|
||
name,
|
||
suffix,
|
||
args,
|
||
pos,
|
||
} => {
|
||
let indices = match args {
|
||
Some(idx) => {
|
||
let mut v = Vec::new();
|
||
for a in idx {
|
||
v.push(self.lower_num_as(a, scope, NumTy::Lng));
|
||
}
|
||
Some(v)
|
||
}
|
||
None => None,
|
||
};
|
||
// UDT-Feldzugriff über Punktpfad, auch auf UDT-Arrays.
|
||
if name.contains('.') {
|
||
let parts: Vec<&str> = name.split('.').collect();
|
||
let base = parts[0];
|
||
let base_as = format!("{base}\u{1}AS");
|
||
let base_info = self
|
||
.visible_var(scope, &base_as)
|
||
.or_else(|| self.visible_var(scope, &self.var_key(base, &None, false)))
|
||
.cloned();
|
||
if let Some(info) = base_info {
|
||
if matches!(info.ty, Ty::Udt(_)) {
|
||
if info.array != indices.is_some() {
|
||
self.err(*pos, "Duplicate definition");
|
||
return (None, Ty::Unknown);
|
||
}
|
||
let (fty, fpath) = self.member_path(&info.ty, &parts[1..], *pos);
|
||
let hty = self.h_ty(&fty);
|
||
let base_slot = if info.global {
|
||
VarSlot::Global(info.slot)
|
||
} else {
|
||
VarSlot::Local(info.slot)
|
||
};
|
||
return (
|
||
Some(HPlace {
|
||
base: base_slot,
|
||
base_is_ref: info.by_ref,
|
||
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<Suffix>,
|
||
args: &Option<Vec<Expr>>,
|
||
pos: SourcePos,
|
||
scope: &mut Scope,
|
||
) -> (HExpr, Ty) {
|
||
if name == "CLIPBOARD.GETTEXT" && args.as_ref().is_none_or(Vec::is_empty) {
|
||
return (
|
||
HExpr::Builtin {
|
||
b: Builtin::ClipboardGet,
|
||
args: Vec::new(),
|
||
ret: HTy::Str,
|
||
},
|
||
Ty::Str,
|
||
);
|
||
}
|
||
if name == "SCREEN" && args.is_some() {
|
||
return self.lower_builtin_fn(name, args.as_deref().unwrap(), scope, pos);
|
||
}
|
||
if suffix.is_none() && args.is_none() && !name.contains('.') && !name.contains('!') {
|
||
if let Some((object, property, spec)) = self.implicit_form_property(scope, name) {
|
||
return (
|
||
HExpr::ObjectProperty {
|
||
object,
|
||
index: None,
|
||
property,
|
||
ty: self.h_ty(&Self::property_ty(spec)),
|
||
},
|
||
Self::property_ty(spec),
|
||
);
|
||
}
|
||
}
|
||
if (name.contains('.') || name.contains('!'))
|
||
&& !self.is_udt_path(scope, name)
|
||
&& !self.is_debug_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
|
||
.find_object(name)
|
||
.filter(|_| suffix.is_none())
|
||
.map(|(id, info)| (id, info.clone()))
|
||
{
|
||
if args.is_none() || info.array {
|
||
if let Some(r) = &mut self.references {
|
||
r.objects.push((pos, name.into(), object));
|
||
}
|
||
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, format!("Feature unavailable: {full_name}"));
|
||
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, format!("Feature unavailable: {full_name}"));
|
||
return (HExpr::Int(0), Ty::Unknown);
|
||
}
|
||
if let Some((0, _, _, _)) = builtin_fn(&full_name) {
|
||
return self.lower_builtin_fn(&full_name, &[], scope, pos);
|
||
}
|
||
if let Some(info) = self.procs.get(name).cloned() {
|
||
if info.kind == ProcKind::Function && info.params.is_empty() {
|
||
let ret = info.ret.clone();
|
||
return (
|
||
HExpr::FnCall {
|
||
proc: info.id,
|
||
args: Vec::new(),
|
||
ret: self.h_ty(&ret),
|
||
},
|
||
ret,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// 3. Variable (ggf. implizit deklarieren)
|
||
let (place, ty) = self.lower_place(
|
||
&Expr::Name {
|
||
name: name.to_string(),
|
||
suffix: *suffix,
|
||
args: None,
|
||
pos,
|
||
},
|
||
scope,
|
||
);
|
||
let e = match place {
|
||
Some(p) => HExpr::Load(Box::new(p)),
|
||
None => HExpr::Int(0),
|
||
};
|
||
(e, ty)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Builtin-Funktionsaufruf absenken (inkl. Argument-Konvertierungen).
|
||
fn lower_builtin_fn(
|
||
&mut self,
|
||
full_name: &str,
|
||
args: &[Expr],
|
||
scope: &mut Scope,
|
||
pos: SourcePos,
|
||
) -> (HExpr, Ty) {
|
||
let (min, max, spec, retk) = builtin_fn(full_name).unwrap();
|
||
|
||
// LBOUND/UBOUND brauchen das Array selbst.
|
||
if full_name == "LBOUND" || full_name == "UBOUND" {
|
||
if args.is_empty() || args.len() > 2 {
|
||
self.err(pos, "Argument-count mismatch");
|
||
return (HExpr::Lng(0), Ty::Lng);
|
||
}
|
||
let place = if let Expr::Name {
|
||
name,
|
||
suffix,
|
||
args: idx,
|
||
pos,
|
||
} = &args[0]
|
||
{
|
||
if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
|
||
self.resolve_var(scope, name, suffix, true, *pos)
|
||
.map(|v| HPlace {
|
||
base: if v.global {
|
||
VarSlot::Global(v.slot)
|
||
} else {
|
||
VarSlot::Local(v.slot)
|
||
},
|
||
base_is_ref: false,
|
||
indices: Vec::new(),
|
||
fields: Vec::new(),
|
||
ty: self.h_ty(&v.ty),
|
||
array_elem: None,
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
let dim = match args.get(1) {
|
||
Some(d) => self.lower_num_as(d, scope, NumTy::Lng),
|
||
None => HExpr::Lng(1),
|
||
};
|
||
match place {
|
||
Some(p) => {
|
||
return (
|
||
HExpr::ArrayBound {
|
||
lower: full_name == "LBOUND",
|
||
place: Box::new(p),
|
||
dim: Box::new(dim),
|
||
},
|
||
Ty::Lng,
|
||
)
|
||
}
|
||
None => {
|
||
self.err(args[0].pos(), "Type mismatch");
|
||
return (HExpr::Lng(0), Ty::Lng);
|
||
}
|
||
}
|
||
}
|
||
|
||
let lowered =
|
||
self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos);
|
||
let get = |i: usize| -> Option<&(HExpr, Ty)> { lowered.get(i) };
|
||
let take = |l: &[(HExpr, Ty)], i: usize| -> (HExpr, Ty) {
|
||
l.get(i).cloned().unwrap_or((HExpr::Int(0), Ty::Unknown))
|
||
};
|
||
|
||
macro_rules! conv_arg {
|
||
($i:expr, $to:expr) => {{
|
||
let (e, t) = take(&lowered, $i);
|
||
if t == Ty::Unknown {
|
||
e
|
||
} else {
|
||
self.conv_num(e, &t, $to)
|
||
}
|
||
}};
|
||
}
|
||
|
||
let b = |b: Builtin, args: Vec<HExpr>, ret: Ty, s: &Sema| -> (HExpr, Ty) {
|
||
let rty = s.h_ty(&ret);
|
||
(HExpr::Builtin { b, args, ret: rty }, ret)
|
||
};
|
||
|
||
match full_name {
|
||
// --- Strings ---
|
||
"LEN" => {
|
||
let (e, t) = take(&lowered, 0);
|
||
if is_str(&t) && t != Ty::Unknown {
|
||
b(Builtin::Len, vec![e], Ty::Int, self)
|
||
} else if let Some(size) = self.fixed_width(&self.h_ty(&t)) {
|
||
(HExpr::Int(size), Ty::Int)
|
||
} else {
|
||
// LEN(zahlvariable) = Bytegröße des Typs
|
||
let size: i16 = match num_ty(&t) {
|
||
NumTy::Int => 2,
|
||
NumTy::Lng | NumTy::Sng => 4,
|
||
NumTy::Cur | NumTy::Dbl => 8,
|
||
};
|
||
(HExpr::Int(size), Ty::Int)
|
||
}
|
||
}
|
||
"LEFT$" => {
|
||
let (s, _) = take(&lowered, 0);
|
||
let n = conv_arg!(1, NumTy::Lng);
|
||
b(Builtin::LeftS, vec![s, n], Ty::Str, self)
|
||
}
|
||
"RIGHT$" => {
|
||
let (s, _) = take(&lowered, 0);
|
||
let n = conv_arg!(1, NumTy::Lng);
|
||
b(Builtin::RightS, vec![s, n], Ty::Str, self)
|
||
}
|
||
"MID$" => {
|
||
let (s, _) = take(&lowered, 0);
|
||
let start = conv_arg!(1, NumTy::Lng);
|
||
let len = if get(2).is_some() {
|
||
conv_arg!(2, NumTy::Lng)
|
||
} else {
|
||
HExpr::Lng(-1)
|
||
};
|
||
b(Builtin::MidS, vec![s, start, len], Ty::Str, self)
|
||
}
|
||
"INSTR" => {
|
||
let (start, s, t) = if args.len() == 3 {
|
||
let st = conv_arg!(0, NumTy::Lng);
|
||
(st, take(&lowered, 1).0, take(&lowered, 2).0)
|
||
} else {
|
||
(HExpr::Lng(1), take(&lowered, 0).0, take(&lowered, 1).0)
|
||
};
|
||
b(Builtin::InstrF, vec![start, s, t], Ty::Int, self)
|
||
}
|
||
"UCASE$" => b(Builtin::UcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||
"LCASE$" => b(Builtin::LcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||
"LTRIM$" => b(Builtin::LtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||
"RTRIM$" => b(Builtin::RtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||
"SPACE$" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::SpaceS, vec![n], Ty::Str, self)
|
||
}
|
||
"STRING$" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
let (ch, cht) = take(&lowered, 1);
|
||
let ch = if is_num(&cht) && cht != Ty::Unknown {
|
||
self.conv_num(ch, &cht, NumTy::Lng)
|
||
} else {
|
||
ch
|
||
};
|
||
b(Builtin::StringS, vec![n, ch], Ty::Str, self)
|
||
}
|
||
"CHR$" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::ChrS, vec![n], Ty::Str, self)
|
||
}
|
||
"ASC" => b(Builtin::Asc, vec![take(&lowered, 0).0], Ty::Int, self),
|
||
"STR$" => {
|
||
// Typ bleibt erhalten (Formatierung je Tag).
|
||
b(Builtin::StrS, vec![take(&lowered, 0).0], Ty::Str, self)
|
||
}
|
||
"VAL" => b(Builtin::Val, vec![take(&lowered, 0).0], Ty::Dbl, self),
|
||
"HEX$" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::HexS, vec![n], Ty::Str, self)
|
||
}
|
||
"OCT$" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::OctS, vec![n], Ty::Str, self)
|
||
}
|
||
// --- Konvertierungsfunktionen → Conv-Knoten ---
|
||
"CINT" | "CLNG" | "CSNG" | "CDBL" | "CCUR" => {
|
||
let (e, t) = take(&lowered, 0);
|
||
let to = match full_name {
|
||
"CINT" => NumTy::Int,
|
||
"CLNG" => NumTy::Lng,
|
||
"CSNG" => NumTy::Sng,
|
||
"CDBL" => NumTy::Dbl,
|
||
_ => NumTy::Cur,
|
||
};
|
||
let e = if t == Ty::Unknown {
|
||
e
|
||
} else {
|
||
self.conv_num(e, &t, to)
|
||
};
|
||
(e, ty_of_num(to))
|
||
}
|
||
// --- Mathematik ---
|
||
"ABS" | "FIX" | "INT" => {
|
||
let (e, t) = take(&lowered, 0);
|
||
let bt = match full_name {
|
||
"ABS" => Builtin::Abs,
|
||
"FIX" => Builtin::Fix,
|
||
_ => Builtin::IntF,
|
||
};
|
||
// Ergebnistyp = Operandentyp (Vorbild).
|
||
let rt = if t == Ty::Unknown {
|
||
Ty::Sng
|
||
} else {
|
||
ty_of_num(num_ty(&t))
|
||
};
|
||
b(bt, vec![e], rt, self)
|
||
}
|
||
"SGN" => b(Builtin::Sgn, vec![take(&lowered, 0).0], Ty::Int, self),
|
||
"SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => {
|
||
let e = conv_arg!(0, NumTy::Dbl);
|
||
let bt = match full_name {
|
||
"SQR" => Builtin::Sqr,
|
||
"EXP" => Builtin::Exp,
|
||
"LOG" => Builtin::Log,
|
||
"SIN" => Builtin::Sin,
|
||
"COS" => Builtin::Cos,
|
||
"TAN" => Builtin::Tan,
|
||
_ => Builtin::Atn,
|
||
};
|
||
b(bt, vec![e], Ty::Dbl, self)
|
||
}
|
||
"RND" => {
|
||
let a = if !lowered.is_empty() {
|
||
vec![conv_arg!(0, NumTy::Sng)]
|
||
} else {
|
||
vec![]
|
||
};
|
||
b(Builtin::Rnd, a, Ty::Sng, self)
|
||
}
|
||
// --- Datei-E/A ---
|
||
"EOF" | "LOF" | "LOC" | "SEEK" | "FILEATTR" => {
|
||
let f = conv_arg!(0, NumTy::Lng);
|
||
match full_name {
|
||
"EOF" => b(Builtin::EofF, vec![f], Ty::Int, self),
|
||
"LOF" => b(Builtin::LofF, vec![f], Ty::Lng, self),
|
||
"LOC" => b(Builtin::LocF, vec![f], Ty::Lng, self),
|
||
"SEEK" => b(Builtin::SeekF, vec![f], Ty::Lng, self),
|
||
_ => {
|
||
let art = conv_arg!(1, NumTy::Lng);
|
||
b(Builtin::Fileattr, vec![f, art], Ty::Lng, self)
|
||
}
|
||
}
|
||
}
|
||
"FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self),
|
||
// --- ISAM-Funktionen ---
|
||
"GETINDEX$" => {
|
||
let f = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::IsamGetIndexS, vec![f], Ty::Str, self)
|
||
}
|
||
"BOF" => {
|
||
let f = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::IsamBof, vec![f], Ty::Int, self)
|
||
}
|
||
"SAVEPOINT" => b(Builtin::IsamSavepoint, vec![], Ty::Int, self),
|
||
"SETMEM" => {
|
||
let n = conv_arg!(0, NumTy::Lng);
|
||
b(Builtin::IsamSetmem, vec![n], Ty::Lng, self)
|
||
}
|
||
"ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
|
||
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
|
||
"ERDEV" => b(Builtin::Erdev, vec![], Ty::Int, self),
|
||
"ERDEV$" => b(Builtin::ErdevS, vec![], Ty::Str, self),
|
||
"CURDIR$" => b(Builtin::CurdirS, vec![], Ty::Str, self),
|
||
"DIR$" => {
|
||
let m = if lowered.is_empty() {
|
||
HExpr::Str(String::new())
|
||
} else {
|
||
take(&lowered, 0).0
|
||
};
|
||
b(Builtin::DirS, vec![m], Ty::Str, self)
|
||
}
|
||
"LPOS" => b(Builtin::Lpos, vec![], Ty::Int, self),
|
||
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" | "MKSMBF$" | "MKDMBF$" => {
|
||
// Zweites Argument wählt Breite und Typ.
|
||
let art = match full_name {
|
||
"MKI$" => 0,
|
||
"MKL$" => 1,
|
||
"MKS$" | "MKSMBF$" => 2,
|
||
"MKD$" | "MKDMBF$" => 3,
|
||
_ => 4,
|
||
};
|
||
let wert = take(&lowered, 0).0;
|
||
b(Builtin::MkS, vec![wert, HExpr::Lng(art)], Ty::Str, self)
|
||
}
|
||
"CVI" | "CVL" | "CVS" | "CVD" | "CVC" | "CVSMBF" | "CVDMBF" => {
|
||
let art = match full_name {
|
||
"CVI" => 0,
|
||
"CVL" => 1,
|
||
"CVS" | "CVSMBF" => 2,
|
||
"CVD" | "CVDMBF" => 3,
|
||
_ => 4,
|
||
};
|
||
let ret = match art {
|
||
0 => Ty::Int,
|
||
1 => Ty::Lng,
|
||
2 => Ty::Sng,
|
||
3 => Ty::Dbl,
|
||
_ => Ty::Cur,
|
||
};
|
||
let s = take(&lowered, 0).0;
|
||
b(Builtin::CvF, vec![s, HExpr::Lng(art)], ret, self)
|
||
}
|
||
"SHELL" => {
|
||
let c = take(&lowered, 0).0;
|
||
b(Builtin::ShellFn, vec![c], Ty::Lng, self)
|
||
}
|
||
// --- Finanzmathematik ---
|
||
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
|
||
let a: Vec<HExpr> = (0..5).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||
let bt = match full_name.trim_end_matches('#') {
|
||
"FV" => Builtin::Fv,
|
||
"PV" => Builtin::Pv,
|
||
"PMT" => Builtin::Pmt,
|
||
_ => Builtin::NPer,
|
||
};
|
||
b(bt, a, Ty::Dbl, self)
|
||
}
|
||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
|
||
let a: Vec<HExpr> = (0..6).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||
let bt = match full_name.trim_end_matches('#') {
|
||
"IPMT" => Builtin::IPmt,
|
||
"PPMT" => Builtin::PPmt,
|
||
_ => Builtin::Rate,
|
||
};
|
||
b(bt, a, Ty::Dbl, self)
|
||
}
|
||
"NPV" | "NPV#" => {
|
||
let zins = conv_arg!(0, NumTy::Dbl);
|
||
let reihe = take(&lowered, 1).0;
|
||
b(Builtin::Npv, vec![zins, reihe], Ty::Dbl, self)
|
||
}
|
||
"IRR" | "IRR#" => {
|
||
let reihe = take(&lowered, 0).0;
|
||
let schaetzung = conv_arg!(1, NumTy::Dbl);
|
||
b(Builtin::Irr, vec![reihe, schaetzung], Ty::Dbl, self)
|
||
}
|
||
"MIRR" | "MIRR#" => {
|
||
let reihe = take(&lowered, 0).0;
|
||
let f = conv_arg!(1, NumTy::Dbl);
|
||
let w = conv_arg!(2, NumTy::Dbl);
|
||
b(Builtin::Mirr, vec![reihe, f, w], Ty::Dbl, self)
|
||
}
|
||
"SLN" | "SLN#" => {
|
||
let a: Vec<HExpr> = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||
b(Builtin::Sln, a, Ty::Dbl, self)
|
||
}
|
||
"SYD" | "SYD#" | "DDB" | "DDB#" => {
|
||
let a: Vec<HExpr> = (0..4).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||
let bt = if full_name.starts_with("SYD") {
|
||
Builtin::Syd
|
||
} else {
|
||
Builtin::Ddb
|
||
};
|
||
b(bt, a, Ty::Dbl, self)
|
||
}
|
||
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
|
||
"TIMEZONEKNOWN" => b(Builtin::TimezoneKnown, vec![], Ty::Int, 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).
|
||
pub 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::LineNumber(_, pos) => *pos,
|
||
Stmt::Label(_) => SourcePos::default(),
|
||
}
|
||
}
|
||
|
||
// ---- Ereignis-Traps (Sprachreferenz §8) -------------------------------------
|
||
|
||
pub const TRAP_KEY: u8 = 0;
|
||
pub const TRAP_TIMER: u8 = 1;
|
||
pub const TRAP_UEVENT: u8 = 2;
|
||
pub const TRAP_SIGNAL: u8 = 3;
|
||
|
||
/// Quellenname → Art. `None` für die Non-Features.
|
||
fn trap_art(device: &str) -> Option<u8> {
|
||
Some(match device {
|
||
"KEY" => TRAP_KEY,
|
||
"TIMER" => TRAP_TIMER,
|
||
"UEVENT" => TRAP_UEVENT,
|
||
"SIGNAL" => TRAP_SIGNAL,
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
|
||
fn const_zahl(e: &Expr) -> Option<i64> {
|
||
match e {
|
||
Expr::IntLit(n, _) => Some(*n as i64),
|
||
Expr::LongLit(n, _) => Some(*n as i64),
|
||
Expr::SingleLit(n, _) => Some(*n as i64),
|
||
Expr::DoubleLit(n, _) => Some(*n as i64),
|
||
Expr::Paren(inner) => const_zahl(inner),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Wertebereiche nach der Original-Hilfe (Belege in `umfang-und-form.md`).
|
||
fn trap_bereich_ok(art: u8, n: i64) -> bool {
|
||
match art {
|
||
TRAP_KEY => matches!(n, 0..=25 | 30 | 31),
|
||
TRAP_TIMER => (1..=86_400).contains(&n),
|
||
TRAP_SIGNAL => (1..=2).contains(&n),
|
||
_ => true,
|
||
}
|
||
}
|
||
|
||
fn trap_bereich_text(art: u8) -> &'static str {
|
||
match art {
|
||
TRAP_KEY => "ON KEY: Kennung 0, 1-25 oder 30-31 erwartet",
|
||
TRAP_TIMER => "ON TIMER: Intervall 1 bis 86400 Sekunden erwartet",
|
||
TRAP_SIGNAL => "ON SIGNAL: Kennung 1 (SIGINT) oder 2 (SIGTERM) erwartet",
|
||
_ => "Kennung erwartet",
|
||
}
|
||
}
|
||
|
||
use crate::hir::literal_value;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use crate::forms::{FormCatalog, ObjectClass};
|
||
use crate::{analyze_source, analyze_source_with_forms};
|
||
|
||
fn diags(src: &str) -> Vec<String> {
|
||
analyze_source("TEST", src)
|
||
.diagnostics
|
||
.into_iter()
|
||
.map(|d| d.message)
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn typkonflikt_wird_erkannt() {
|
||
assert!(diags("a$ = 1").contains(&"Type mismatch".to_string()));
|
||
assert!(diags("a% = \"x\"").contains(&"Type mismatch".to_string()));
|
||
assert!(diags("a = 1 + \"x\"").contains(&"Type mismatch".to_string()));
|
||
assert!(diags("s$ = \"a\" + \"b\"").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn option_explicit() {
|
||
assert!(diags("OPTION EXPLICIT\nx = 1").contains(&"Variable not defined".to_string()));
|
||
assert!(diags("OPTION EXPLICIT\nDIM x AS INTEGER\nx = 1").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn deftype_regeln() {
|
||
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
|
||
assert!(diags("DEFINT I\ni = 5").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn unbekanntes_unterprogramm() {
|
||
assert!(diags("Foo 1").contains(&"Subprogram not defined".to_string()));
|
||
assert!(diags("SUB Foo (a%)\nEND SUB\n' Aufruf\nFoo 1").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn argumentanzahl() {
|
||
assert!(diags("PRINT LEFT$(\"a\")").contains(&"Argument-count mismatch: LEFT$".to_string()));
|
||
assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1")
|
||
.contains(&"Argument-count mismatch".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn hardware_features_zur_compilezeit_abgelehnt() {
|
||
assert!(diags("POKE 100, 1").contains(&"Feature unavailable: POKE".to_string()));
|
||
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable: PEEK".to_string()));
|
||
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable: VARPTR".to_string()));
|
||
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable: SOUND".to_string()));
|
||
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable: CHAIN".to_string()));
|
||
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable: PLAY".to_string()));
|
||
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable: CIRCLE".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());
|
||
assert!(matches!(
|
||
super::builtin_fn("TIMEZONEKNOWN"),
|
||
Some((0, 0, [], super::RetK::I))
|
||
));
|
||
assert!(diags("PRINT TIMEZONEKNOWN").is_empty());
|
||
for name in ["TIMEZONEKNOWN(1)", "MKL$()"] {
|
||
let d = diags(&format!("PRINT {name}"));
|
||
assert!(
|
||
d.iter().any(|m| m.contains("Argument-count mismatch")
|
||
&& m.contains(name.split('(').next().unwrap())),
|
||
"{d:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[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: PEN".to_string()));
|
||
}
|
||
|
||
fn form_catalog() -> FormCatalog {
|
||
let mut c = FormCatalog::default();
|
||
c.add("Form1", ObjectClass::Form, None, false);
|
||
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
|
||
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
|
||
c.add("List1", ObjectClass::ListBox, Some("Form1"), false);
|
||
c.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
|
||
c
|
||
}
|
||
|
||
fn form_diags(src: &str) -> Vec<String> {
|
||
analyze_source_with_forms("FORM1", src, &form_catalog())
|
||
.diagnostics
|
||
.into_iter()
|
||
.map(|d| d.message)
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn objektpfade_werden_namentlich_geprueft_und_udt_bleibt_unveraendert() {
|
||
assert!(diags("Text9.Text = \"a\"")
|
||
.iter()
|
||
.any(|d| d.contains("TEXT9")));
|
||
assert!(form_diags("Text1.Farbe = 3")
|
||
.iter()
|
||
.any(|d| d.contains("FARBE")));
|
||
assert!(form_diags("Text1.Text = 5")
|
||
.iter()
|
||
.any(|d| d.contains("TEXT")));
|
||
assert!(form_diags("x = Text1.ListCount")
|
||
.iter()
|
||
.any(|d| d.contains("LISTCOUNT")));
|
||
assert!(diags("TYPE T\nF AS INTEGER\nEND TYPE\nDIM x AS T\nx.F = 1").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn eigenschaftstyp_schreibbarkeit_bang_und_hir_index() {
|
||
let d = form_diags("Text1.Text = 5\nx = Text1.SelLength");
|
||
assert!(
|
||
d.iter()
|
||
.any(|m| m.contains("Type mismatch for property 'TEXT'")),
|
||
"{d:?}"
|
||
);
|
||
let d = form_diags("SCREEN.Width = 80");
|
||
assert!(d.iter().any(|m| m.contains("read-only")), "{d:?}");
|
||
assert!(form_diags("Form1!Text1.Text = \"a\"").is_empty());
|
||
let d = form_diags("Text1 = \"x\"");
|
||
assert!(
|
||
d.iter().any(|m| m.contains("Text1") || m.contains("TEXT1")),
|
||
"{d:?}"
|
||
);
|
||
assert!(form_diags("Command1(3).Caption = \"drei\"").is_empty());
|
||
assert!(form_diags("zeichen% = SCREEN(1, 1)").is_empty());
|
||
assert!(form_diags("SHOW\nHIDE").is_empty());
|
||
assert!(form_diags("Caption = \"Titel\"\nx% = Width").is_empty());
|
||
assert!(
|
||
form_diags("CALL Aus(Text1)\nSUB Aus(c AS CONTROL)\nc.Visible = 0\nEND SUB").is_empty()
|
||
);
|
||
let a = analyze_source_with_forms("FORM1", "Text1.Text = \"a\"", &form_catalog());
|
||
let h = a.hir.unwrap();
|
||
assert!(matches!(
|
||
h.procs[0].body[0].kind,
|
||
HStmtKind::SetObjectProperty { object: 1, .. }
|
||
));
|
||
let d = form_diags("Text1.Move 1, 2");
|
||
assert!(d.is_empty(), "{d:?}");
|
||
for src in ["Form1.Hide 1", "Form1.Show 1, 2", "SCREEN.Show 1"] {
|
||
let d = form_diags(src);
|
||
assert!(
|
||
d.iter().any(|m| m.contains("Argument-count mismatch")),
|
||
"{src}: {d:?}"
|
||
);
|
||
}
|
||
let d = form_diags("Form1.Show \"modal\"");
|
||
assert!(d.iter().any(|m| m.contains("Type mismatch")), "{d:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn typeof_form_control_und_form_metabefehl() {
|
||
let d = form_diags("DIM Ziel AS CONTROL\nIF TYPEOF Ziel IS CommandButton THEN PRINT 1");
|
||
assert!(d.is_empty(), "{d:?}");
|
||
let a = analyze_source("MeinForm", "'$FORM\nMeinForm.Caption = \"x\"");
|
||
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
|
||
assert!(a
|
||
.hir
|
||
.unwrap()
|
||
.objects
|
||
.iter()
|
||
.any(|o| o.name == "MEINFORM" && o.class == ObjectClass::Form));
|
||
}
|
||
|
||
#[test]
|
||
fn dialoge_listindex_und_zeichenmessung_werden_abgesenkt() {
|
||
let source = "MSGBOX \"Hinweis\"\nantwort% = MSGBOX(\"Weiter?\", 4, \"Frage\")\nname$ = INPUTBOX$(\"Name\")\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT Picture1.TEXTWIDTH(\"abc\")";
|
||
let diagnostics = form_diags(source);
|
||
assert!(diagnostics.is_empty(), "{diagnostics:?}");
|
||
let arrays = form_diags("Command1(1).SETFOCUS\nCommand1(1).MOVE 1, 2, 3, 4");
|
||
assert!(arrays.is_empty(), "{arrays:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn ereignisprozedur_signatur_und_normale_unterstrich_sub() {
|
||
let ok = "SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nEND SUB";
|
||
assert!(form_diags(ok).is_empty(), "{:?}", form_diags(ok));
|
||
let bad = form_diags("SUB Form_MouseDown(X AS SINGLE)\nEND SUB");
|
||
assert!(
|
||
bad.iter().any(|d| d.contains("BUTTON AS INTEGER")),
|
||
"{bad:?}"
|
||
);
|
||
assert!(form_diags("SUB Zins_Berechnen(n AS INTEGER)\nEND SUB").is_empty());
|
||
let array_bad = form_diags("SUB Command1_Click()\nEND SUB");
|
||
assert!(
|
||
array_bad.iter().any(|d| d.contains("INDEX AS INTEGER")),
|
||
"{array_bad:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn dreizeiler_wird_namentlich_statt_falsch_abgewiesen() {
|
||
let d = diags("Text1.Text = \"hallo\"\nForm1.Show 1\nUNLOAD Form1");
|
||
assert_eq!(d.len(), 3, "{d:?}");
|
||
assert!(d[0].contains("TEXT1"));
|
||
assert!(d[1].contains("FORM1"));
|
||
assert!(d[2].contains("FORM1"));
|
||
}
|
||
|
||
// ---- HIR-Lowering ------------------------------------------------------
|
||
|
||
use crate::hir::{HExpr, HStmtKind, NumTy};
|
||
|
||
fn hir_of(src: &str) -> crate::hir::HirModule {
|
||
let a = analyze_source("TEST", src);
|
||
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
|
||
a.hir.expect("HIR erwartet")
|
||
}
|
||
|
||
#[test]
|
||
fn hir_konvertierung_materialisiert() {
|
||
// d# = i% + 1.5# → Conv(INTEGER→DOUBLE) unter der DOUBLE-Addition
|
||
let h = hir_of("i% = 2\nd# = i% + 1.5#");
|
||
let main = &h.procs[0];
|
||
let HStmtKind::Assign { place, value } = &main.body[1].kind else {
|
||
panic!("Assign erwartet: {:?}", main.body[1].kind);
|
||
};
|
||
assert_eq!(place.ty, crate::hir::HTy::Num(NumTy::Dbl));
|
||
let HExpr::Bin { ty, l, .. } = value else {
|
||
panic!("Bin erwartet: {value:?}");
|
||
};
|
||
assert_eq!(*ty, NumTy::Dbl);
|
||
assert!(
|
||
matches!(
|
||
**l,
|
||
HExpr::Conv {
|
||
from: NumTy::Int,
|
||
to: NumTy::Dbl,
|
||
..
|
||
}
|
||
),
|
||
"Conv INTEGER→DOUBLE erwartet: {l:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hir_slots_aufgeloest() {
|
||
let h = hir_of("a% = 1\nb% = a%");
|
||
assert_eq!(h.globals.len(), 2);
|
||
let HStmtKind::Assign { value, .. } = &h.procs[0].body[1].kind else {
|
||
panic!();
|
||
};
|
||
assert!(matches!(value, HExpr::Load(_)));
|
||
}
|
||
|
||
#[test]
|
||
fn hir_intdiv_rundet_operanden() {
|
||
// 7.5 \ 2 → beide Operanden Conv → LONG (SINGLE-Operand)
|
||
let h = hir_of("x& = 7.5 \\ 2");
|
||
let HStmtKind::Assign { value, .. } = &h.procs[0].body[0].kind else {
|
||
panic!();
|
||
};
|
||
let HExpr::Bin { op, ty, l, .. } = value else {
|
||
panic!("Bin erwartet: {value:?}");
|
||
};
|
||
assert_eq!(*op, crate::hir::HArith::IDiv);
|
||
assert_eq!(*ty, NumTy::Lng);
|
||
assert!(matches!(**l, HExpr::Conv { to: NumTy::Lng, .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn hir_select_wird_abgesenkt() {
|
||
let h = hir_of("SELECT CASE 2\nCASE 1\nPRINT \"a\"\nCASE ELSE\nPRINT \"b\"\nEND SELECT");
|
||
// Temp-Zuweisung + If-Kette
|
||
let main = &h.procs[0];
|
||
assert!(matches!(main.body[0].kind, HStmtKind::Assign { .. }));
|
||
assert!(matches!(main.body[1].kind, HStmtKind::If { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn hir_byref_und_byval() {
|
||
let h = hir_of("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
|
||
let main = &h.procs[0];
|
||
let calls: Vec<_> = main
|
||
.body
|
||
.iter()
|
||
.filter_map(|s| match &s.kind {
|
||
HStmtKind::CallSub { args, .. } => Some(args),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
assert_eq!(calls.len(), 2);
|
||
assert!(matches!(calls[0][0], crate::hir::HArg::ByRef(_)));
|
||
assert!(matches!(calls[1][0], crate::hir::HArg::ByVal(_)));
|
||
}
|
||
|
||
#[test]
|
||
fn byref_verlangt_exakten_typ() {
|
||
let d = diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo n%");
|
||
assert!(
|
||
d.contains(&"Parameter type mismatch".to_string()),
|
||
"BYREF mit abweichendem Typ muss abgelehnt werden: {d:?}"
|
||
);
|
||
// BYVAL (Klammern) konvertiert stattdessen
|
||
assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty());
|
||
}
|
||
|
||
// ---- ISAM (Change `phase-3-isam`) -----------------------------------
|
||
|
||
/// Satztyp und geöffnete Tabelle als Vorspann für die ISAM-Tests.
|
||
const ISAM_KOPF: &str = "TYPE KundeTyp\n Nummer AS LONG\n Name AS STRING * 20\n END TYPE\n DIM k AS KundeTyp\n OPEN \"db\" FOR ISAM KundeTyp \"Kunden\" AS #1\n";
|
||
|
||
/// Aufgabe 2.1: die Anweisung parst mit `#`-Dateinummer diagnose-frei.
|
||
#[test]
|
||
fn isam_anweisung_parst_ohne_diagnose() {
|
||
let d = diags(&format!("{ISAM_KOPF}SETINDEX #1, \"Name\""));
|
||
assert!(d.is_empty(), "SETINDEX soll diagnose-frei parsen: {d:?}");
|
||
// Auch ohne `#` (die Original-Hilfe schreibt es optional).
|
||
let d = diags(&format!("{ISAM_KOPF}SETINDEX 1, \"Name\""));
|
||
assert!(d.is_empty(), "{d:?}");
|
||
}
|
||
|
||
/// Aufgabe 2.2: falsche Argumentanzahl wird gemeldet und nennt das
|
||
/// Element (Guiding Principle).
|
||
#[test]
|
||
fn isam_argumentanzahl_nennt_das_element() {
|
||
let d = diags(&format!("{ISAM_KOPF}SEEKGT #1"));
|
||
assert!(
|
||
d.iter().any(|m| m.contains("SEEKGT")),
|
||
"Diagnose muss SEEKGT nennen: {d:?}"
|
||
);
|
||
assert!(
|
||
d.iter().any(|m| m.contains("Argument-count mismatch")),
|
||
"{d:?}"
|
||
);
|
||
}
|
||
|
||
/// Aufgabe 2.3: das Satzargument wird gegen den Tabellentyp geprüft.
|
||
#[test]
|
||
fn isam_satzargument_wird_typgeprueft() {
|
||
let d = diags(&format!("{ISAM_KOPF}DIM x AS STRING\nRETRIEVE #1, x$"));
|
||
assert!(
|
||
d.contains(&"Type mismatch".to_string()),
|
||
"String statt Satzvariable muss auffallen: {d:?}"
|
||
);
|
||
// Ein anderer benutzerdefinierter Typ passt ebenfalls nicht.
|
||
let quelle = format!(
|
||
"TYPE Anderer\n z AS INTEGER\nEND TYPE\n{ISAM_KOPF} DIM a AS Anderer\nRETRIEVE #1, a"
|
||
);
|
||
assert!(
|
||
diags(&quelle).contains(&"Type mismatch".to_string()),
|
||
"fremder Satztyp muss auffallen: {:?}",
|
||
diags(&quelle)
|
||
);
|
||
// Der richtige Typ ist diagnose-frei.
|
||
let d = diags(&format!("{ISAM_KOPF}RETRIEVE #1, k"));
|
||
assert!(d.is_empty(), "{d:?}");
|
||
}
|
||
|
||
/// Aufgabe 2.5: kein ISAM-Element endet als unbekannter Bezeichner —
|
||
/// ein Programm, das jedes Element genau einmal verwendet.
|
||
#[test]
|
||
fn kein_isam_element_ist_unbekannter_bezeichner() {
|
||
let quelle = format!(
|
||
"{ISAM_KOPF} CREATEINDEX #1, \"NachName\", 0, \"Name\"\n CREATEINDEX #1, \"Zwei\", 1, \"Name\", \"-Nummer\"\n SETINDEX #1, \"NachName\"\n i$ = GETINDEX$(1)\n INSERT #1, k\n MOVEFIRST #1\n MOVELAST #1\n MOVENEXT #1\n MOVEPREVIOUS #1\n SEEKEQ #1, \"a\"\n SEEKGT #1, \"a\"\n SEEKGE #1, \"a\"\n RETRIEVE #1, k\n UPDATE #1, k\n DELETE #1\n e% = EOF(1)\n b% = BOF(1)\n BEGINTRANS\n s% = SAVEPOINT\n ROLLBACK s%\n ROLLBACK\n ROLLBACK ALL\n COMMITTRANS\n m& = SETMEM(1024)\n DELETEINDEX #1, \"NachName\"\n CLOSE #1\n DELETETABLE \"db\", \"Kunden\"\n"
|
||
);
|
||
let d = diags(&quelle);
|
||
assert!(
|
||
!d.iter().any(|m| m.contains("not defined")),
|
||
"ISAM-Element wird als unbekannter Bezeichner abgewiesen: {d:?}"
|
||
);
|
||
assert!(d.is_empty(), "unerwartete Diagnosen: {d:?}");
|
||
}
|
||
|
||
/// `ROLLBACK ALL` senkt auf die Sentinel-Kennung ab und ist damit von
|
||
/// `ROLLBACK kennung` unterscheidbar.
|
||
#[test]
|
||
fn rollback_all_wird_als_sentinel_abgesenkt() {
|
||
let a = crate::analyze_source("TEST", "BEGINTRANS\nROLLBACK ALL");
|
||
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
|
||
// `ALL` darf keine Variable werden — sonst zaehlte OPTION EXPLICIT es an.
|
||
let d = diags("OPTION EXPLICIT\nBEGINTRANS\nROLLBACK ALL");
|
||
assert!(d.is_empty(), "{d:?}");
|
||
}
|
||
|
||
/// `CREATEINDEX` ist in der Spaltenzahl unbegrenzt — die Original-Hilfe
|
||
/// nennt keine Obergrenze, also darf die Signatur auch keine setzen.
|
||
#[test]
|
||
fn createindex_ohne_obergrenze_der_spaltenzahl() {
|
||
let felder: String = (1..=12).map(|i| format!(" F{i} AS LONG\n")).collect();
|
||
let spalten: String = (1..=12).map(|i| format!(", \"F{i}\"")).collect();
|
||
let quelle = format!(
|
||
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0{spalten}"
|
||
);
|
||
let d = diags(&quelle);
|
||
assert!(
|
||
d.is_empty(),
|
||
"zwölfspaltiger Index muss zulässig sein: {d:?}"
|
||
);
|
||
// Ein Spaltenname muss trotzdem ein String sein.
|
||
let quelle = format!(
|
||
"TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0, \"F1\", 42"
|
||
);
|
||
assert!(
|
||
diags(&quelle).contains(&"Type mismatch".to_string()),
|
||
"Zahl als Spaltenname muss auffallen: {:?}",
|
||
diags(&quelle)
|
||
);
|
||
}
|
||
}
|