Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A
Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben). Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature. Vollstaendigkeits-Inventar - docs/inventar.md mit 285 Eintraegen aus den Themenlisten von bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle - crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten Bruchschritt (Puffer, Host, Korpus) - Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui behaelt Farbabbildung, Widget und den neuen Terminal-Host - Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus Ereignisse (Taste, Groessenaenderung, Abbruch) - Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht bestehenden Sollausgaben blieben dabei unveraendert Groessenunabhaengigkeit - 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor und VIEW PRINT geklemmt) - Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf Bibliothek - Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch) - Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS - Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON) - PRINT USING, LPRINT USING, FORMAT$, SetFormatCC - Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge) - Datum und Zeit mit Serienwerten ab 1899 - Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#, IRR#, MIRR#, SLN#, SYD#, DDB# - Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen), BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV - System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung Altlasten aus Phase 2 - ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar (prozeduruebergreifender Fixup im Codegenerator) - DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum) - Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest Vom Inventar aufgedeckte Fehler - Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die String*-Routinen, LINE und OPEN "COMn:") -- geschlossen - Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik, Record-Konvertierung, Rest der Dateisystemfunktionen - bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar bildet die Vereinigung mit qb45advr.hlp Neue Changes - phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb) - phase-3-ortszeit: zieht die UTC-Abweichung zurueck Neue Abhaengigkeit: unicode-width. Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei, docs/bibliothek.md neu, PLAN.md fortgeschrieben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,10 +235,97 @@ pub enum Builtin {
|
||||
PrintTab,
|
||||
PrintSpc,
|
||||
PrintNewline,
|
||||
PrintUsing,
|
||||
FormatS,
|
||||
SetFormatCc,
|
||||
// Bildschirm (Anweisungen)
|
||||
Cls,
|
||||
Color,
|
||||
Locate,
|
||||
Width,
|
||||
ViewPrint,
|
||||
ScreenStmt,
|
||||
KeyAssign,
|
||||
KeyList,
|
||||
KeyDisplay,
|
||||
// Bildschirm (Funktionen)
|
||||
Csrlin,
|
||||
PosFn,
|
||||
ScreenFn,
|
||||
// Tastatur
|
||||
InkeyS,
|
||||
InputS,
|
||||
// System
|
||||
EnvironS,
|
||||
EnvironSet,
|
||||
Fre,
|
||||
Clear,
|
||||
Tron,
|
||||
Troff,
|
||||
StackFn,
|
||||
StackStmt,
|
||||
Erdev,
|
||||
ErdevS,
|
||||
// Datei-E/A
|
||||
Open,
|
||||
Close,
|
||||
CloseAll,
|
||||
PrintZiel,
|
||||
WriteFile,
|
||||
EofF,
|
||||
LofF,
|
||||
LocF,
|
||||
SeekF,
|
||||
SeekStmt,
|
||||
Freefile,
|
||||
Fileattr,
|
||||
LockStmt,
|
||||
Kill,
|
||||
NameStmt,
|
||||
Files,
|
||||
Chdir,
|
||||
Chdrive,
|
||||
Mkdir,
|
||||
Rmdir,
|
||||
CurdirS,
|
||||
DirS,
|
||||
Lpos,
|
||||
ShellStmt,
|
||||
ShellFn,
|
||||
MkS,
|
||||
CvF,
|
||||
// Finanzmathematik
|
||||
Fv,
|
||||
Pv,
|
||||
Pmt,
|
||||
NPer,
|
||||
IPmt,
|
||||
PPmt,
|
||||
Rate,
|
||||
Npv,
|
||||
Irr,
|
||||
Mirr,
|
||||
Sln,
|
||||
Syd,
|
||||
Ddb,
|
||||
// Sonstiges
|
||||
Timer,
|
||||
DateS,
|
||||
TimeS,
|
||||
DateSet,
|
||||
TimeSet,
|
||||
Now,
|
||||
DateSerial,
|
||||
TimeSerial,
|
||||
DateValue,
|
||||
TimeValue,
|
||||
DayF,
|
||||
MonthF,
|
||||
YearF,
|
||||
WeekdayF,
|
||||
HourF,
|
||||
MinuteF,
|
||||
SecondF,
|
||||
CommandS,
|
||||
Doevents,
|
||||
Sleep,
|
||||
@@ -365,7 +452,29 @@ pub enum HStmtKind {
|
||||
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?
|
||||
trailing: bool,
|
||||
},
|
||||
/// `FIELD #n, laenge AS var$, …` — Recordpuffer in Felder aufteilen.
|
||||
Field {
|
||||
file: HExpr,
|
||||
fields: Vec<(HExpr, HPlace)>,
|
||||
},
|
||||
/// `LSET`/`RSET` — links- bzw. rechtsbündig zuweisen; ist das Ziel ein
|
||||
/// `FIELD`-Feld, wirkt die Zuweisung zugleich auf den Recordpuffer.
|
||||
LsetRset {
|
||||
rset: bool,
|
||||
target: HPlace,
|
||||
value: HExpr,
|
||||
},
|
||||
/// `GET`/`PUT` auf einer Datei: Datensatz- bzw. Bytenummer und Ziel-
|
||||
/// bzw. Quellvariable. Ohne Variable wirkt der Recordpuffer.
|
||||
GetPut {
|
||||
put: bool,
|
||||
file: HExpr,
|
||||
recnum: Option<HExpr>,
|
||||
var: Option<HPlace>,
|
||||
},
|
||||
Input {
|
||||
/// `INPUT #n` — Quelle ist eine Datei statt der Tastatur.
|
||||
file: Option<HExpr>,
|
||||
line_mode: bool,
|
||||
prompt: Option<String>,
|
||||
/// Fragezeichen nach dem Prompt (`;`-Form).
|
||||
@@ -425,6 +534,8 @@ pub enum HStmtKind {
|
||||
Resume(HResume),
|
||||
/// `ERROR n`.
|
||||
RaiseError(HExpr),
|
||||
/// `ERR = n` — Fehlercode setzen, ohne einen Fehler auszulösen.
|
||||
SetErr(HExpr),
|
||||
Read(Vec<HPlace>),
|
||||
/// Ziel als Index in `HirModule::data` (0 = Anfang).
|
||||
Restore(u32),
|
||||
|
||||
@@ -119,6 +119,10 @@ pub enum TokenKind {
|
||||
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
|
||||
MetaStatic,
|
||||
MetaDynamic,
|
||||
/// Rohtext einer `DATA`-Anweisung bis zum Anweisungsende. Er wird
|
||||
/// bewusst **nicht** zerlegt oder normalisiert: unquotierte Elemente
|
||||
/// behalten ihre Groß- und Kleinschreibung und ihren inneren Leerraum.
|
||||
DataRaw(String),
|
||||
/// Ende einer logischen Zeile.
|
||||
Eol,
|
||||
Eof,
|
||||
@@ -432,6 +436,27 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
break 'line;
|
||||
}
|
||||
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
|
||||
if kw == Kw::Data {
|
||||
// Der Rest der Anweisung ist Rohtext: bis zum
|
||||
// `:` außerhalb von Anführungszeichen oder bis
|
||||
// zum Zeilenende.
|
||||
let start = i;
|
||||
let mut in_quote = false;
|
||||
while i < chars.len() {
|
||||
match chars[i] {
|
||||
'"' => in_quote = !in_quote,
|
||||
':' if !in_quote => break,
|
||||
'\'' if !in_quote => break,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let roh: String = chars[start..i].iter().collect();
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::DataRaw(roh),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,9 +218,12 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::Kw(Kw::Line) => {
|
||||
self.advance();
|
||||
if self.expect_kw(Kw::Input, "INPUT") {
|
||||
if self.k() == TokenKind::Kw(Kw::Input) {
|
||||
self.advance();
|
||||
self.parse_input(true, pos)
|
||||
} else {
|
||||
// Grafikform LINE (x1,y1)-(x2,y2): deklariertes Non-Feature.
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
None
|
||||
}
|
||||
@@ -474,28 +477,40 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::Kw(Kw::Data) => {
|
||||
self.advance();
|
||||
// Der Lexer liefert den Rohtext; hier wird nur an Kommas
|
||||
// außerhalb von Anführungszeichen getrennt. Unquotierte
|
||||
// Elemente verlieren nur den Leerraum an den Rändern.
|
||||
let roh = match self.k() {
|
||||
TokenKind::DataRaw(t) => {
|
||||
self.advance();
|
||||
t
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
let mut items = Vec::new();
|
||||
let mut cur = String::new();
|
||||
while !self.at_stmt_end() {
|
||||
match self.k() {
|
||||
TokenKind::Comma => {
|
||||
items.push(cur.trim().to_string());
|
||||
cur = String::new();
|
||||
let mut in_quote = false;
|
||||
let mut quotiert = false;
|
||||
for c in roh.chars() {
|
||||
match c {
|
||||
'"' if !in_quote && cur.trim().is_empty() => {
|
||||
in_quote = true;
|
||||
quotiert = true;
|
||||
cur.clear();
|
||||
}
|
||||
TokenKind::Str(s) => cur.push_str(&s),
|
||||
TokenKind::Num(n) => cur.push_str(&num_text(n)),
|
||||
TokenKind::Ident { name, suffix } => {
|
||||
cur.push_str(&name);
|
||||
if let Some(sfx) = suffix {
|
||||
cur.push(sfx.as_char());
|
||||
}
|
||||
'"' if in_quote => in_quote = false,
|
||||
',' if !in_quote => {
|
||||
items.push(if quotiert {
|
||||
std::mem::take(&mut cur)
|
||||
} else {
|
||||
std::mem::take(&mut cur).trim().to_string()
|
||||
});
|
||||
quotiert = false;
|
||||
}
|
||||
TokenKind::Minus => cur.push('-'),
|
||||
_ => {}
|
||||
_ => cur.push(c),
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
items.push(cur.trim().to_string());
|
||||
items.push(if quotiert { cur } else { cur.trim().to_string() });
|
||||
Some(Stmt::Data { items, pos })
|
||||
}
|
||||
TokenKind::Kw(Kw::Read) => {
|
||||
@@ -1401,6 +1416,14 @@ impl<'a> P<'a> {
|
||||
|
||||
fn parse_def_fn(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
self.advance(); // DEF
|
||||
// `DEF SEG` ist Segmentadressierung und damit deklariertes
|
||||
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
||||
// `DEF FNname` einleitet.
|
||||
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
return None;
|
||||
}
|
||||
let (name, suffix) = match self.k() {
|
||||
TokenKind::Ident { name, suffix } if name.starts_with("FN") => {
|
||||
self.advance();
|
||||
@@ -1710,6 +1733,23 @@ impl<'a> P<'a> {
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Option<Expr> {
|
||||
// `SEEK(n)` ist zugleich Funktion und Anweisung; in Ausdrucksposition
|
||||
// gilt die Funktionsform.
|
||||
if self.k() == TokenKind::Kw(Kw::Seek) && self.k_at(1) == TokenKind::LParen {
|
||||
let pos = self.pos();
|
||||
self.advance(); // SEEK
|
||||
self.advance(); // (
|
||||
let arg = self.parse_expr()?;
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
self.err("Expected: )");
|
||||
}
|
||||
return Some(Expr::Name {
|
||||
name: "SEEK".into(),
|
||||
suffix: None,
|
||||
args: Some(vec![arg]),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
match self.k() {
|
||||
TokenKind::Num(n) => {
|
||||
self.advance();
|
||||
@@ -1743,16 +1783,6 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn num_text(n: NumValue) -> String {
|
||||
match n {
|
||||
NumValue::Int(v) => v.to_string(),
|
||||
NumValue::Long(v) => v.to_string(),
|
||||
NumValue::Single(v) => v.to_string(),
|
||||
NumValue::Double(v) => v.to_string(),
|
||||
NumValue::Currency(v) => format!("{}", v as f64 / 10_000.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -247,6 +247,33 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
||||
"DOEVENTS" => (0, 0, &[], I),
|
||||
"MSGBOX" => (1, 3, &[S, N, S], I),
|
||||
"INPUTBOX$" => (1, 5, &[S, S, S, N, N], St),
|
||||
// Finanzmathematik (Original-Hilfe schreibt sie mit `#`; beide
|
||||
// Schreibweisen werden angenommen, gerechnet wird in DOUBLE).
|
||||
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
|
||||
(5, 5, &[N, N, N, N, N], Db)
|
||||
}
|
||||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
|
||||
(6, 6, &[N, N, N, N, N, N], Db)
|
||||
}
|
||||
"NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db),
|
||||
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
|
||||
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
|
||||
"SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, N, N, N], Db),
|
||||
// Record-Konvertierung (Zahl ↔ Bytedarstellung im Feldpuffer)
|
||||
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" => (1, 1, &[N], St),
|
||||
"MKSMBF$" | "MKDMBF$" => (1, 1, &[N], St),
|
||||
"CVI" => (1, 1, &[S], I),
|
||||
"CVL" => (1, 1, &[S], L),
|
||||
"CVS" | "CVSMBF" => (1, 1, &[S], Sg),
|
||||
"CVD" | "CVDMBF" => (1, 1, &[S], Db),
|
||||
"CVC" => (1, 1, &[S], Cu),
|
||||
// Dateisystem und System
|
||||
"CURDIR$" => (0, 1, &[S], St),
|
||||
"DIR$" => (0, 1, &[S], St),
|
||||
"LPOS" => (1, 1, &[N], I),
|
||||
"STACK" => (0, 0, &[], L),
|
||||
"ERDEV" => (0, 0, &[], I),
|
||||
"ERDEV$" => (0, 0, &[], St),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -261,16 +288,37 @@ fn banned_feature(name: &str) -> bool {
|
||||
"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" | "PAINT" | "DRAW" | "PALETTE"
|
||||
| "PCOPY" | "PMAP" | "WINDOW"
|
||||
| "PCOPY" | "PMAP" | "WINDOW" | "POINT" | "VIEW"
|
||||
// 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) und `LINE` ohne `INPUT`
|
||||
/// im Parser, `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)", "LINE", "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::*;
|
||||
@@ -293,6 +341,10 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
|
||||
"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]),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -318,6 +370,8 @@ pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
|
||||
udt_defs: Vec::new(),
|
||||
consts: HashMap::new(),
|
||||
module_vars: HashMap::new(),
|
||||
module_labels: HashMap::new(),
|
||||
module_line_labels: HashMap::new(),
|
||||
globals: Vec::new(),
|
||||
data: Vec::new(),
|
||||
data_marks_name: HashMap::new(),
|
||||
@@ -344,6 +398,10 @@ struct Sema {
|
||||
consts: HashMap<String, (Ty, Option<ConstVal>)>,
|
||||
/// Modulvariablen (Schlüssel wie `var_key`).
|
||||
module_vars: HashMap<String, VarInfo>,
|
||||
/// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in
|
||||
/// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds).
|
||||
module_labels: HashMap<String, LabelId>,
|
||||
module_line_labels: HashMap<u32, LabelId>,
|
||||
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
||||
globals: Vec<hir::HVar>,
|
||||
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
||||
@@ -407,6 +465,8 @@ impl Sema {
|
||||
};
|
||||
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 {
|
||||
@@ -980,6 +1040,34 @@ impl Sema {
|
||||
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);
|
||||
@@ -1028,39 +1116,76 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
}
|
||||
if *printer {
|
||||
self.push(out, scope, HStmtKind::Unsupported("LPRINT"));
|
||||
} else if file.is_some() {
|
||||
self.push(out, scope, HStmtKind::Unsupported("PRINT #"));
|
||||
} else if using.is_some() {
|
||||
self.push(out, scope, HStmtKind::Unsupported("PRINT USING"));
|
||||
// `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, .. } => {
|
||||
if let Some(f) = file {
|
||||
self.want_num(f, scope);
|
||||
for v in vars {
|
||||
self.lower_place(v, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("INPUT #"));
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
let (ptext, question) = match prompt {
|
||||
Some((t, q)) => (Some(t.clone()), *q),
|
||||
None => (None, true),
|
||||
// `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 { line_mode: *line, prompt: ptext, question, targets },
|
||||
HStmtKind::Input {
|
||||
file: datei,
|
||||
line_mode: *line,
|
||||
prompt: ptext,
|
||||
question,
|
||||
targets,
|
||||
},
|
||||
);
|
||||
}
|
||||
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
|
||||
@@ -1193,15 +1318,19 @@ impl Sema {
|
||||
self.push(out, scope, HStmtKind::ReturnGosub(id));
|
||||
}
|
||||
Stmt::OnError { local, action, pos } => {
|
||||
// Nicht-lokales ON ERROR in einer Prozedur: das Sprungziel
|
||||
// liegt zwingend im Prozedurrumpf (Label-Scoping) — wird wie
|
||||
// ON LOCAL ERROR behandelt. Die Scoping-Regel des Vorbilds
|
||||
// (Handler-Label auf Modulebene) ist als Aufgabe in PLAN.md
|
||||
// Phase 3 eingeplant.
|
||||
let local = *local || !scope.is_module;
|
||||
// `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) => {
|
||||
if let Some(id) = self.label_id(t, scope, *pos) {
|
||||
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) });
|
||||
}
|
||||
}
|
||||
@@ -1370,54 +1499,114 @@ impl Sema {
|
||||
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
|
||||
}
|
||||
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
|
||||
Stmt::Open { file, isam, number, len, pos, .. } => {
|
||||
self.want_str(file, scope);
|
||||
self.want_num(number, scope);
|
||||
if let Some(l) = len {
|
||||
self.want_num(l, scope);
|
||||
}
|
||||
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 {
|
||||
if !ty_name.is_empty() && !self.udt_ids.contains_key(ty_name) {
|
||||
self.err(*pos, "Type not defined");
|
||||
}
|
||||
// ISAM kommt mit dem Change `phase-3-isam`.
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN FOR ISAM"));
|
||||
return;
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN"));
|
||||
// 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, .. } => {
|
||||
self.want_str(mode, scope);
|
||||
self.want_num(number, scope);
|
||||
self.want_str(file, scope);
|
||||
if let Some(l) = len {
|
||||
self.want_num(l, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN"));
|
||||
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, .. } => {
|
||||
for f in files {
|
||||
self.want_num(f, scope);
|
||||
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] },
|
||||
);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("CLOSE"));
|
||||
}
|
||||
Stmt::FieldStmt { file, fields, .. } => {
|
||||
self.want_num(file, scope);
|
||||
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 {
|
||||
self.want_num(width, scope);
|
||||
let (_, t) = self.lower_place(var, scope);
|
||||
if !is_str(&t) {
|
||||
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::Unsupported("FIELD"));
|
||||
self.push(out, scope, HStmtKind::Field { file: fe, fields: hf });
|
||||
}
|
||||
Stmt::GetPut { put, file, recnum, var, .. } => {
|
||||
self.want_num(file, scope);
|
||||
if let Some(r) = recnum {
|
||||
self.want_num(r, scope);
|
||||
}
|
||||
if let Some(v) = var {
|
||||
self.lower_place(v, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported(if *put { "PUT" } else { "GET" }));
|
||||
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);
|
||||
@@ -1429,7 +1618,15 @@ impl Sema {
|
||||
if !ok {
|
||||
self.err(*pos, "Type mismatch");
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported(if *rset { "RSET" } else { "LSET" }));
|
||||
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 {
|
||||
@@ -1438,14 +1635,42 @@ impl Sema {
|
||||
for e in items {
|
||||
self.lower_expr(e, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("WRITE"));
|
||||
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);
|
||||
self.push(out, scope, HStmtKind::Unsupported("SEEK"));
|
||||
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 { unlock, file, from, to, .. } => {
|
||||
Stmt::LockStmt { file, from, to, .. } => {
|
||||
self.want_num(file, scope);
|
||||
if let Some(f) = from {
|
||||
self.want_num(f, scope);
|
||||
@@ -1453,16 +1678,24 @@ impl Sema {
|
||||
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::Unsupported(if *unlock { "UNLOCK" } else { "LOCK" }),
|
||||
HStmtKind::BuiltinStmt { b: Builtin::LockStmt, args: vec![fe] },
|
||||
);
|
||||
}
|
||||
Stmt::NameStmt { old, new, .. } => {
|
||||
self.want_str(old, scope);
|
||||
self.want_str(new, scope);
|
||||
self.push(out, scope, HStmtKind::Unsupported("NAME"));
|
||||
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, .. } => {
|
||||
@@ -1472,9 +1705,24 @@ impl Sema {
|
||||
if let Some(b) = bottom {
|
||||
self.want_num(b, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("VIEW PRINT"));
|
||||
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::EventControl { device, index, pos, .. } => {
|
||||
Stmt::EventControl { device, index, action, pos } => {
|
||||
match device.as_str() {
|
||||
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {}
|
||||
_ => self.err(*pos, "Feature unavailable"),
|
||||
@@ -1482,6 +1730,21 @@ impl Sema {
|
||||
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;
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("Ereignissteuerung"));
|
||||
}
|
||||
}
|
||||
@@ -1815,6 +2078,19 @@ impl Sema {
|
||||
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(
|
||||
@@ -2068,26 +2344,98 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bildschirmanweisungen: ausgelassene Argumente kommen als -1
|
||||
// durch, damit die Laufzeit „weglassen" von „null" unterscheidet.
|
||||
"ENVIRON" | "CLEAR" | "TRON" | "TROFF" | "STACK" => {
|
||||
let bt = match name {
|
||||
"ENVIRON" => Builtin::EnvironSet,
|
||||
"CLEAR" => Builtin::Clear,
|
||||
"TRON" => Builtin::Tron,
|
||||
"TROFF" => Builtin::Troff,
|
||||
_ => 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" => {
|
||||
// `SCREEN n` mit literalem n > 0 ist ein Grafikmodus und
|
||||
// damit schon zur Compile-Zeit abweisbar.
|
||||
if name == "SCREEN" {
|
||||
if let Some(Expr::IntLit(n)) = args.first() {
|
||||
if *n != 0 {
|
||||
self.err(pos, "Feature unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
|
||||
_ => self.push(out, scope, HStmtKind::Unsupported(match name {
|
||||
"CLS" => "CLS",
|
||||
"COLOR" => "COLOR",
|
||||
"LOCATE" => "LOCATE",
|
||||
"WIDTH" => "WIDTH",
|
||||
"SCREEN" => "SCREEN",
|
||||
"KEY" => "KEY",
|
||||
"CLEAR" => "CLEAR",
|
||||
"FILES" => "FILES",
|
||||
"KILL" => "KILL",
|
||||
"CHDIR" => "CHDIR",
|
||||
"MKDIR" => "MKDIR",
|
||||
"RMDIR" => "RMDIR",
|
||||
"SHELL" => "SHELL",
|
||||
"RUN" => "RUN",
|
||||
"ENVIRON" => "ENVIRON",
|
||||
"RESET" => "RESET",
|
||||
"TRON" => "TRON",
|
||||
"TROFF" => "TROFF",
|
||||
"MSGBOX" => "MSGBOX",
|
||||
"SETUEVENT" => "SETUEVENT",
|
||||
_ => "Anweisung",
|
||||
@@ -2095,6 +2443,37 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
@@ -2110,7 +2489,11 @@ impl Sema {
|
||||
let real: Vec<&Expr> =
|
||||
args.iter().filter(|a| !matches!(a, Expr::Missing)).collect();
|
||||
if real.len() < min as usize || args.len() > max as usize {
|
||||
self.err(pos, "Argument-count mismatch");
|
||||
// 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;
|
||||
@@ -2153,6 +2536,17 @@ impl Sema {
|
||||
}
|
||||
(he, t)
|
||||
}
|
||||
/// `OPEN "COM1:" …` — serielle Schnittstelle ist deklariertes Non-Feature.
|
||||
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
||||
/// bleibt der Datei-E/A überlassen.
|
||||
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
||||
if let Expr::StrLit(s) = file {
|
||||
if ist_com_geraet(s) {
|
||||
self.err(pos, "Feature unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||||
let (he, t) = self.lower_expr(e, scope);
|
||||
if !is_str(&t) {
|
||||
@@ -2960,6 +3354,180 @@ impl Sema {
|
||||
};
|
||||
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),
|
||||
"ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||||
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
|
||||
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
|
||||
"ERDEV" => b(Builtin::Erdev, vec![], Ty::Int, self),
|
||||
"ERDEV$" => b(Builtin::ErdevS, vec![], Ty::Str, self),
|
||||
"CURDIR$" => b(Builtin::CurdirS, vec![], Ty::Str, self),
|
||||
"DIR$" => {
|
||||
let m = if lowered.is_empty() {
|
||||
HExpr::Str(String::new())
|
||||
} else {
|
||||
take(&lowered, 0).0
|
||||
};
|
||||
b(Builtin::DirS, vec![m], Ty::Str, self)
|
||||
}
|
||||
"LPOS" => b(Builtin::Lpos, vec![], Ty::Int, self),
|
||||
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" | "MKSMBF$" | "MKDMBF$" => {
|
||||
// Zweites Argument wählt Breite und Typ.
|
||||
let art = match full_name {
|
||||
"MKI$" => 0,
|
||||
"MKL$" => 1,
|
||||
"MKS$" | "MKSMBF$" => 2,
|
||||
"MKD$" | "MKDMBF$" => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let wert = take(&lowered, 0).0;
|
||||
b(Builtin::MkS, vec![wert, HExpr::Lng(art)], Ty::Str, self)
|
||||
}
|
||||
"CVI" | "CVL" | "CVS" | "CVD" | "CVC" | "CVSMBF" | "CVDMBF" => {
|
||||
let art = match full_name {
|
||||
"CVI" => 0,
|
||||
"CVL" => 1,
|
||||
"CVS" | "CVSMBF" => 2,
|
||||
"CVD" | "CVDMBF" => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let ret = match art {
|
||||
0 => Ty::Int,
|
||||
1 => Ty::Lng,
|
||||
2 => Ty::Sng,
|
||||
3 => Ty::Dbl,
|
||||
_ => Ty::Cur,
|
||||
};
|
||||
let s = take(&lowered, 0).0;
|
||||
b(Builtin::CvF, vec![s, HExpr::Lng(art)], ret, self)
|
||||
}
|
||||
"SHELL" => {
|
||||
let c = take(&lowered, 0).0;
|
||||
b(Builtin::ShellFn, vec![c], Ty::Lng, self)
|
||||
}
|
||||
// --- Finanzmathematik ---
|
||||
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
|
||||
let a: Vec<HExpr> = (0..5).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = match full_name.trim_end_matches('#') {
|
||||
"FV" => Builtin::Fv,
|
||||
"PV" => Builtin::Pv,
|
||||
"PMT" => Builtin::Pmt,
|
||||
_ => Builtin::NPer,
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
|
||||
let a: Vec<HExpr> = (0..6).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = match full_name.trim_end_matches('#') {
|
||||
"IPMT" => Builtin::IPmt,
|
||||
"PPMT" => Builtin::PPmt,
|
||||
_ => Builtin::Rate,
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"NPV" | "NPV#" => {
|
||||
let zins = conv_arg!(0, NumTy::Dbl);
|
||||
let reihe = take(&lowered, 1).0;
|
||||
b(Builtin::Npv, vec![zins, reihe], Ty::Dbl, self)
|
||||
}
|
||||
"IRR" | "IRR#" => {
|
||||
let reihe = take(&lowered, 0).0;
|
||||
let schaetzung = conv_arg!(1, NumTy::Dbl);
|
||||
b(Builtin::Irr, vec![reihe, schaetzung], Ty::Dbl, self)
|
||||
}
|
||||
"MIRR" | "MIRR#" => {
|
||||
let reihe = take(&lowered, 0).0;
|
||||
let f = conv_arg!(1, NumTy::Dbl);
|
||||
let w = conv_arg!(2, NumTy::Dbl);
|
||||
b(Builtin::Mirr, vec![reihe, f, w], Ty::Dbl, self)
|
||||
}
|
||||
"SLN" | "SLN#" => {
|
||||
let a: Vec<HExpr> = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
b(Builtin::Sln, a, Ty::Dbl, self)
|
||||
}
|
||||
"SYD" | "SYD#" | "DDB" | "DDB#" => {
|
||||
let a: Vec<HExpr> = (0..4).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = if full_name.starts_with("SYD") {
|
||||
Builtin::Syd
|
||||
} else {
|
||||
Builtin::Ddb
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
|
||||
"DATESERIAL" | "TIMESERIAL" => {
|
||||
let x = conv_arg!(0, NumTy::Lng);
|
||||
let y = conv_arg!(1, NumTy::Lng);
|
||||
let z = conv_arg!(2, NumTy::Lng);
|
||||
let bt = if full_name == "DATESERIAL" {
|
||||
Builtin::DateSerial
|
||||
} else {
|
||||
Builtin::TimeSerial
|
||||
};
|
||||
b(bt, vec![x, y, z], Ty::Dbl, self)
|
||||
}
|
||||
"DATEVALUE" => b(Builtin::DateValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
|
||||
"TIMEVALUE" => b(Builtin::TimeValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
|
||||
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => {
|
||||
let e = conv_arg!(0, NumTy::Dbl);
|
||||
let bt = match full_name {
|
||||
"DAY" => Builtin::DayF,
|
||||
"MONTH" => Builtin::MonthF,
|
||||
"YEAR" => Builtin::YearF,
|
||||
"WEEKDAY" => Builtin::WeekdayF,
|
||||
"HOUR" => Builtin::HourF,
|
||||
"MINUTE" => Builtin::MinuteF,
|
||||
_ => Builtin::SecondF,
|
||||
};
|
||||
b(bt, vec![e], Ty::Int, self)
|
||||
}
|
||||
"FORMAT$" => {
|
||||
let mut a = vec![take(&lowered, 0).0];
|
||||
if get(1).is_some() {
|
||||
a.push(take(&lowered, 1).0);
|
||||
}
|
||||
b(Builtin::FormatS, a, Ty::Str, self)
|
||||
}
|
||||
// --- Bildschirm ---
|
||||
"TAB" | "SPC" => {
|
||||
// In einer PRINT-Liste werden sie eigens abgesenkt; hier
|
||||
// stehen sie außerhalb und sind dort nicht zulässig.
|
||||
self.err(pos, &format!("Illegal function call: {full_name}"));
|
||||
(HExpr::Str(String::new()), Ty::Str)
|
||||
}
|
||||
"CSRLIN" => b(Builtin::Csrlin, vec![], Ty::Int, self),
|
||||
"POS" => {
|
||||
// `POS(0)` — das Argument ist im Vorbild ein Dummy.
|
||||
b(Builtin::PosFn, vec![], Ty::Int, self)
|
||||
}
|
||||
"SCREEN" => {
|
||||
let zeile = conv_arg!(0, NumTy::Lng);
|
||||
let spalte = conv_arg!(1, NumTy::Lng);
|
||||
let farbe = if get(2).is_some() {
|
||||
conv_arg!(2, NumTy::Lng)
|
||||
} else {
|
||||
HExpr::Lng(0)
|
||||
};
|
||||
b(Builtin::ScreenFn, vec![zeile, spalte, farbe], Ty::Int, self)
|
||||
}
|
||||
"INKEY$" => b(Builtin::InkeyS, vec![], Ty::Str, self),
|
||||
"INPUT$" => {
|
||||
let n = conv_arg!(0, NumTy::Lng);
|
||||
b(Builtin::InputS, vec![n], Ty::Str, self)
|
||||
}
|
||||
// --- Fehlerstatus ---
|
||||
"ERR" => (HExpr::Err, Ty::Lng),
|
||||
"ERL" => (HExpr::Erl, Ty::Lng),
|
||||
@@ -2972,34 +3540,6 @@ impl Sema {
|
||||
// --- Spätere Phasen: dokumentiert, aber noch nicht verfügbar ---
|
||||
_ => {
|
||||
let name: &'static str = match full_name {
|
||||
"INKEY$" => "INKEY$",
|
||||
"INPUT$" => "INPUT$",
|
||||
"CSRLIN" => "CSRLIN",
|
||||
"POS" => "POS",
|
||||
"SCREEN" => "SCREEN",
|
||||
"TAB" => "TAB",
|
||||
"SPC" => "SPC",
|
||||
"NOW" => "NOW",
|
||||
"DATESERIAL" => "DATESERIAL",
|
||||
"TIMESERIAL" => "TIMESERIAL",
|
||||
"DATEVALUE" => "DATEVALUE",
|
||||
"TIMEVALUE" => "TIMEVALUE",
|
||||
"DAY" => "DAY",
|
||||
"MONTH" => "MONTH",
|
||||
"YEAR" => "YEAR",
|
||||
"WEEKDAY" => "WEEKDAY",
|
||||
"HOUR" => "HOUR",
|
||||
"MINUTE" => "MINUTE",
|
||||
"SECOND" => "SECOND",
|
||||
"FORMAT$" => "FORMAT$",
|
||||
"FRE" => "FRE",
|
||||
"EOF" => "EOF",
|
||||
"LOF" => "LOF",
|
||||
"LOC" => "LOC",
|
||||
"SEEK" => "SEEK",
|
||||
"FREEFILE" => "FREEFILE",
|
||||
"FILEATTR" => "FILEATTR",
|
||||
"ENVIRON$" => "ENVIRON$",
|
||||
"MSGBOX" => "MSGBOX",
|
||||
"INPUTBOX$" => "INPUTBOX$",
|
||||
_ => "Funktion",
|
||||
@@ -3110,7 +3650,7 @@ mod tests {
|
||||
#[test]
|
||||
fn argumentanzahl() {
|
||||
assert!(diags("PRINT LEFT$(\"a\")")
|
||||
.contains(&"Argument-count mismatch".to_string()));
|
||||
.contains(&"Argument-count mismatch: LEFT$".to_string()));
|
||||
assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1")
|
||||
.contains(&"Argument-count mismatch".to_string()));
|
||||
}
|
||||
|
||||
498
crates/tb-frontend/tests/inventar.rs
Normal file
498
crates/tb-frontend/tests/inventar.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
//! Abgleich `docs/inventar.md` ↔ Implementierungsstand.
|
||||
//!
|
||||
//! Das Inventar ist die Sollseite der Leitplanke „Vollständigkeit ist das
|
||||
//! Soll"; dieser Test hält es gegen den Code, damit der Status nicht
|
||||
//! verrottet. Prüfregeln siehe `pruefe_status`.
|
||||
//!
|
||||
//! ponytail: Die Tabellen `builtin_fn`/`builtin_stmt`/`banned_feature` sind
|
||||
//! private `match`-Ausdrücke ohne Namensliste. Statt sie öffentlich zu machen
|
||||
//! oder die Namen ein zweites Mal zu pflegen, liest der Test die Stringliterale
|
||||
//! aus dem Quelltext. Ceiling: Wird `sema.rs` umstrukturiert, schlägt
|
||||
//! `abschnitt` fehl und nennt den fehlenden Anker — dann hier nachziehen oder
|
||||
//! die Tabellen als `pub const`-Listen exportieren.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const IMPLEMENTIERT: &str = "implementiert";
|
||||
const OFFEN: &str = "offen";
|
||||
const NON_FEATURE: &str = "Non-Feature";
|
||||
|
||||
fn wurzel() -> PathBuf {
|
||||
// crates/tb-frontend/ → Projektwurzel
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
|
||||
}
|
||||
|
||||
// ---- Inventar lesen --------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Eintrag {
|
||||
name: String,
|
||||
art: String,
|
||||
status: String,
|
||||
fundstelle: String,
|
||||
zeile: usize,
|
||||
}
|
||||
|
||||
fn inventar() -> Vec<Eintrag> {
|
||||
let pfad = wurzel().join("docs/inventar.md");
|
||||
let text = std::fs::read_to_string(&pfad)
|
||||
.unwrap_or_else(|e| panic!("docs/inventar.md nicht lesbar: {e}"));
|
||||
let mut out = Vec::new();
|
||||
for (i, zeile) in text.lines().enumerate() {
|
||||
let zeile = zeile.trim();
|
||||
if !zeile.starts_with("| `") {
|
||||
continue;
|
||||
}
|
||||
let spalten: Vec<&str> = zeile
|
||||
.trim_matches('|')
|
||||
.split('|')
|
||||
.map(|s| s.trim())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
spalten.len(),
|
||||
6,
|
||||
"docs/inventar.md Zeile {}: {} Spalten statt 6 — {zeile}",
|
||||
i + 1,
|
||||
spalten.len()
|
||||
);
|
||||
out.push(Eintrag {
|
||||
name: spalten[0].trim_matches('`').to_string(),
|
||||
art: spalten[1].to_string(),
|
||||
status: spalten[3].to_string(),
|
||||
fundstelle: spalten[4].to_string(),
|
||||
zeile: i + 1,
|
||||
});
|
||||
}
|
||||
assert!(!out.is_empty(), "docs/inventar.md enthält keine Einträge");
|
||||
out
|
||||
}
|
||||
|
||||
// ---- Implementierungsstand aus sema.rs -------------------------------------
|
||||
|
||||
fn sema_quelle() -> String {
|
||||
let pfad = wurzel().join("crates/tb-frontend/src/sema.rs");
|
||||
std::fs::read_to_string(&pfad).unwrap_or_else(|e| panic!("sema.rs nicht lesbar: {e}"))
|
||||
}
|
||||
|
||||
/// Quelltextabschnitt zwischen zwei Ankern (exklusive Endanker).
|
||||
fn abschnitt<'a>(quelle: &'a str, von: &str, bis: &str) -> &'a str {
|
||||
let a = quelle
|
||||
.find(von)
|
||||
.unwrap_or_else(|| panic!("Anker nicht gefunden in sema.rs: {von}"));
|
||||
let rest = &quelle[a..];
|
||||
let b = rest
|
||||
.find(bis)
|
||||
.unwrap_or_else(|| panic!("Endanker nicht gefunden in sema.rs: {bis}"));
|
||||
&rest[..b]
|
||||
}
|
||||
|
||||
/// Alle Stringliterale eines Abschnitts.
|
||||
fn literale(abschnitt: &str) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
let bytes: Vec<char> = abschnitt.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == '"' {
|
||||
let mut s = String::new();
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i] != '"' {
|
||||
s.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
if !s.is_empty() {
|
||||
out.insert(s);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
struct Stand {
|
||||
signaturen: BTreeSet<String>,
|
||||
verboten: BTreeSet<String>,
|
||||
nicht_verfuegbar: BTreeSet<String>,
|
||||
/// Non-Features, die an ihrer Syntax statt an ihrem Namen erkannt werden.
|
||||
syntaktisch: BTreeSet<String>,
|
||||
}
|
||||
|
||||
fn stand() -> Stand {
|
||||
let q = sema_quelle();
|
||||
let fn_tab = abschnitt(&q, "fn builtin_fn(", "/// Deklarierte Non-Features");
|
||||
let ban = abschnitt(
|
||||
&q,
|
||||
"fn banned_feature(",
|
||||
"/// Non-Features, die nicht über ihren Namen",
|
||||
);
|
||||
let stmt_tab = abschnitt(&q, "fn builtin_stmt(", "// ---- Einstiegspunkte");
|
||||
let unsup_fn = abschnitt(
|
||||
&q,
|
||||
"// --- Spätere Phasen: dokumentiert",
|
||||
"(HExpr::Unsupported(name)",
|
||||
);
|
||||
let unsup_stmt = abschnitt(
|
||||
&q,
|
||||
"// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.",
|
||||
r#"_ => "Anweisung""#,
|
||||
);
|
||||
|
||||
let mut signaturen = literale(fn_tab);
|
||||
signaturen.extend(literale(stmt_tab));
|
||||
let mut nicht_verfuegbar = literale(unsup_fn);
|
||||
nicht_verfuegbar.extend(literale(unsup_stmt));
|
||||
// Platzhalter der Sammelzweige sind keine Elementnamen.
|
||||
nicht_verfuegbar.remove("Funktion");
|
||||
nicht_verfuegbar.remove("Anweisung");
|
||||
|
||||
let syn = abschnitt(
|
||||
&q,
|
||||
"pub const SYNTAKTISCH_ABGEWIESEN",
|
||||
"/// Ist `s` ein serieller Gerätename",
|
||||
);
|
||||
|
||||
Stand {
|
||||
signaturen,
|
||||
verboten: literale(ban),
|
||||
nicht_verfuegbar,
|
||||
syntaktisch: literale(syn).into_iter().map(|s| s.to_uppercase()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Regeln ----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn statusvokabular_ist_beschraenkt() {
|
||||
for e in inventar() {
|
||||
assert!(
|
||||
matches!(
|
||||
e.art.as_str(),
|
||||
"Anweisung" | "Funktion" | "Metabefehl" | "Routine"
|
||||
),
|
||||
"Zeile {}: `{}` trägt unzulässige Art `{}`",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.art
|
||||
);
|
||||
assert!(
|
||||
matches!(e.status.as_str(), IMPLEMENTIERT | OFFEN | NON_FEATURE),
|
||||
"Zeile {}: `{}` trägt unzulässigen Status `{}` \
|
||||
(erlaubt: {IMPLEMENTIERT}/{OFFEN}/{NON_FEATURE})",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_features_nennen_eine_fundstelle_in_der_sprachreferenz() {
|
||||
for e in inventar() {
|
||||
if e.status == NON_FEATURE {
|
||||
assert!(
|
||||
e.fundstelle.contains("sprachreferenz.md"),
|
||||
"Zeile {}: `{}` ist Non-Feature, nennt aber keine Fundstelle \
|
||||
in docs/sprachreferenz.md (gefunden: `{}`)",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.fundstelle
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offene_elemente_haben_keine_fundstelle() {
|
||||
for e in inventar() {
|
||||
if e.status == OFFEN {
|
||||
assert_eq!(
|
||||
e.fundstelle, "-",
|
||||
"Zeile {}: `{}` ist offen, trägt aber die Fundstelle `{}`",
|
||||
e.zeile, e.name, e.fundstelle
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernregel: Inventarstatus und Code dürfen nicht auseinanderlaufen.
|
||||
///
|
||||
/// Geprüft wird nur, was mechanisch belegbar ist — die Bibliothekselemente aus
|
||||
/// `builtin_fn`/`builtin_stmt`, die Non-Feature-Liste und die
|
||||
/// „nicht verfügbar"-Marker. Anweisungen, die der Parser direkt kennt
|
||||
/// (`PRINT`, `IF`, …), und Metabefehle tauchen in keiner dieser Tabellen auf;
|
||||
/// für sie gilt nur die schwächere Regel `parserelemente_sind_keine_non_features`.
|
||||
#[test]
|
||||
fn inventar_stimmt_mit_code_ueberein() {
|
||||
let inv = inventar();
|
||||
let st = stand();
|
||||
let mut fehler: Vec<String> = Vec::new();
|
||||
|
||||
// Ein Name kann zweimal vorkommen (Funktion und Anweisung, z. B. `SCREEN`).
|
||||
// Schlüssel ist der Name in Großschreibung — der Dialekt ist
|
||||
// case-insensitiv, der Code führt die Namen groß.
|
||||
//
|
||||
// ponytail: Einträge mit klammerndem Zusatz (`GET (Grafik)`) sind
|
||||
// syntaktische Varianten eines Tokens, das auch eine unterstützte Form hat.
|
||||
// Sie sind über den Namen nicht prüfbar und bleiben hier außen vor; ihre
|
||||
// Ablehnung liegt im Parser. Ceiling: sobald der Parser eine Liste seiner
|
||||
// abgewiesenen Formen exportiert, hier gegenprüfen.
|
||||
let mut je_name: BTreeMap<String, Vec<&Eintrag>> = BTreeMap::new();
|
||||
for e in &inv {
|
||||
// Syntaktisch erkannte Formen tragen einen klammernden Zusatz oder
|
||||
// stehen namentlich in SYNTAKTISCH_ABGEWIESEN — sie werden unten
|
||||
// gegen diese Liste geprüft, nicht gegen `banned_feature`.
|
||||
if st.syntaktisch.contains(&e.name.to_uppercase()) {
|
||||
assert_eq!(
|
||||
e.status, NON_FEATURE,
|
||||
"Zeile {}: `{}` steht in SYNTAKTISCH_ABGEWIESEN, das Inventar sagt aber `{}`",
|
||||
e.zeile, e.name, e.status
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Ein klammernder Zusatz benennt die Form (`KEY (Ereignis)`); für den
|
||||
// Codeabgleich zählt das Grundtoken davor.
|
||||
let schluessel = e
|
||||
.name
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or(&e.name)
|
||||
.trim()
|
||||
.to_uppercase();
|
||||
je_name.entry(schluessel).or_default().push(e);
|
||||
}
|
||||
|
||||
for (name, eintraege) in &je_name {
|
||||
let stati: BTreeSet<&str> = eintraege.iter().map(|e| e.status.as_str()).collect();
|
||||
let _ = &name;
|
||||
let zeilen: Vec<String> = eintraege.iter().map(|e| e.zeile.to_string()).collect();
|
||||
let wo = format!("Zeile(n) {}", zeilen.join(", "));
|
||||
|
||||
let hat_signatur = st.signaturen.contains(name.as_str());
|
||||
// `ON COM(1) GOSUB …` scheitert am Gerätenamen: für die ON-Formen
|
||||
// zählt das zweite Token. Andere Mehrwortnamen (`LINE INPUT`,
|
||||
// `VIEW PRINT`) sind eigenständige unterstützte Anweisungen und
|
||||
// erben nichts vom Grundtoken.
|
||||
let ist_verboten = match name.strip_prefix("ON ") {
|
||||
Some(geraet) => st.verboten.contains(geraet),
|
||||
None => st.verboten.contains(name.as_str()),
|
||||
};
|
||||
let ist_marker = st.nicht_verfuegbar.contains(name.as_str());
|
||||
|
||||
if ist_verboten && !stati.contains(NON_FEATURE) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Code weist es als Non-Feature ab, Inventar sagt {stati:?}"
|
||||
));
|
||||
}
|
||||
if !ist_verboten && stati.contains(NON_FEATURE) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt Non-Feature, Code weist es nicht ab \
|
||||
(fehlt in banned_feature)"
|
||||
));
|
||||
}
|
||||
if ist_marker && !stati.contains(OFFEN) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): senkt auf den „nicht verfügbar\"-Marker ab, \
|
||||
Inventar sagt {stati:?}"
|
||||
));
|
||||
}
|
||||
if stati == BTreeSet::from([IMPLEMENTIERT]) && ist_marker {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt implementiert, senkt aber weiterhin \
|
||||
auf den „nicht verfügbar\"-Marker ab"
|
||||
));
|
||||
}
|
||||
// Nur wenn ALLE Formen dieses Namens offen sind — sonst erklärt die
|
||||
// implementierte Form (etwa `DATE$` als Funktion) die Signatur.
|
||||
if stati == BTreeSet::from([OFFEN]) && hat_signatur && !ist_marker && !ist_verboten {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt offen, aber der Code hat eine Signatur \
|
||||
ohne „nicht verfügbar\"-Marker — entweder implementiert oder Marker fehlt"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Gegenrichtung: kennt der Code Elemente, die das Inventar nicht führt?
|
||||
// Ein Element darf im Code unter mehreren Schreibweisen stehen: die
|
||||
// Original-Hilfe schreibt die Finanzfunktionen mit Typsuffix (`FV#`), der
|
||||
// Code nimmt beide an. Für den Abgleich zählt das Inventar als getroffen,
|
||||
// wenn eine der Schreibweisen passt.
|
||||
let bekannt = |n: &str| {
|
||||
let up = n.to_uppercase();
|
||||
je_name.contains_key(&up)
|
||||
|| ['#', '!', '&', '%', '@', '$']
|
||||
.iter()
|
||||
.any(|s| je_name.contains_key(&format!("{up}{s}")))
|
||||
};
|
||||
for name in st.signaturen.iter().chain(st.verboten.iter()) {
|
||||
if !bekannt(name) {
|
||||
fehler.push(format!(
|
||||
"`{name}`: im Code bekannt, fehlt aber in docs/inventar.md"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
fehler.is_empty(),
|
||||
"Inventar und Code laufen auseinander ({} Abweichungen):\n {}",
|
||||
fehler.len(),
|
||||
fehler.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abdeckungsstand_wird_ausgewiesen() {
|
||||
let inv = inventar();
|
||||
let zaehle = |s: &str| inv.iter().filter(|e| e.status == s).count();
|
||||
let (i, o, n) = (
|
||||
zaehle(IMPLEMENTIERT),
|
||||
zaehle(OFFEN),
|
||||
zaehle(NON_FEATURE),
|
||||
);
|
||||
println!(
|
||||
"Abdeckung: implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
|
||||
inv.len()
|
||||
);
|
||||
|
||||
// Der Kopf der Tabelle nennt dieselben Zahlen — sonst ist die Doku stale.
|
||||
let text = std::fs::read_to_string(wurzel().join("docs/inventar.md")).unwrap();
|
||||
let erwartet = format!(
|
||||
"implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
|
||||
inv.len()
|
||||
);
|
||||
assert!(
|
||||
text.contains(&erwartet),
|
||||
"Abdeckungszeile in docs/inventar.md stimmt nicht; erwartet: {erwartet}"
|
||||
);
|
||||
assert_eq!(i + o + n, inv.len(), "Statussumme ≠ Einträge");
|
||||
}
|
||||
|
||||
// ---- Namentliche Ablehnung -------------------------------------------------
|
||||
|
||||
/// Ein im Inventar geführtes Element darf nie als unbekannter Bezeichner oder
|
||||
/// mit einem generischen Syntaxfehler enden — die Ablehnung MUSS es nennen
|
||||
/// (Guiding Principle, Capability `sprachinventar`).
|
||||
///
|
||||
/// Geprüft wird stichprobenartig je Ablehnungsweg, nicht über alle 285
|
||||
/// Einträge: die vollständige Zuordnung leistet `inventar_stimmt_mit_code_ueberein`.
|
||||
#[test]
|
||||
fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
|
||||
// (Quelltext, erwartetes Element, erwarteter Meldungsbestandteil)
|
||||
let faelle: &[(&str, &str, &str)] = &[
|
||||
// Non-Feature über die Namensliste
|
||||
("X = PEEK(100)", "PEEK", "Feature unavailable"),
|
||||
("Y = POINT(1, 2)", "POINT", "Feature unavailable"),
|
||||
("CALLS Foo", "CALLS", "Feature unavailable"),
|
||||
// Non-Feature über die Syntax (teilt das Schlüsselwort)
|
||||
("LINE (1, 1)-(2, 2)", "LINE", "Feature unavailable"),
|
||||
("GET (1, 1)-(2, 2), A", "GET (Grafik)", "Feature unavailable"),
|
||||
(r#"OPEN "COM1:9600,N,8,1" AS #1"#, "OPEN COM", "Feature unavailable"),
|
||||
// Ereignisgerät als Funktionsform
|
||||
("ON PEN GOSUB Ziel\nZiel:\nRETURN", "ON PEN", "Feature unavailable"),
|
||||
];
|
||||
|
||||
for (quelle, element, erwartet) in faelle {
|
||||
let a = tb_frontend::analyze_source("probe.bas", quelle);
|
||||
let texte: Vec<String> = a.diagnostics.iter().map(|d| d.message.clone()).collect();
|
||||
assert!(
|
||||
texte.iter().any(|m| m.contains(erwartet)),
|
||||
"`{element}`: erwartete Meldung `{erwartet}`, bekam {texte:?} \
|
||||
(Quelle: {quelle:?})"
|
||||
);
|
||||
assert!(
|
||||
!texte.iter().any(|m| m.contains("not defined")),
|
||||
"`{element}`: wird als unbekannter Bezeichner abgewiesen statt \
|
||||
namentlich — {texte:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gegenprobe: ein Element mit „nicht verfügbar"-Marker kompiliert zwar, endet
|
||||
/// aber zur Laufzeit in einem Fehler, der es benennt — nicht in einem
|
||||
/// generischen Syntaxfehler.
|
||||
#[test]
|
||||
fn offene_elemente_erzeugen_keinen_syntaxfehler() {
|
||||
for quelle in ["CLS", "LOCATE 5, 10", "COLOR 14, 1", r#"OPEN "d.txt" FOR OUTPUT AS #1"#] {
|
||||
let a = tb_frontend::analyze_source("probe.bas", quelle);
|
||||
assert!(
|
||||
a.diagnostics.is_empty(),
|
||||
"{quelle:?}: offenes Element soll übersetzen und erst zur Laufzeit \
|
||||
namentlich abgewiesen werden, bekam {:?}",
|
||||
a.diagnostics
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Jedes Inventarelement besitzt eine Signatur — auch die, deren
|
||||
/// Laufzeitverhalten noch aussteht. Sie dürfen nie als unbekannter Bezeichner
|
||||
/// enden (Capability `sprach-frontend`, Anforderung „Jedes dokumentierte
|
||||
/// Element besitzt eine Signatur").
|
||||
#[test]
|
||||
fn bislang_signaturlose_elemente_haben_signaturen() {
|
||||
let quelle = r#"
|
||||
DIM Zahlungen#(1 TO 3)
|
||||
DIM D#, S$, I%, L&, C@, G!
|
||||
D# = FV#(0.05, 10, -100, 0, 0)
|
||||
D# = PV#(0.05, 10, -100, 0, 0)
|
||||
D# = PMT#(0.05, 10, 1000, 0, 0)
|
||||
D# = NPER#(0.05, -100, 1000, 0, 0)
|
||||
D# = IPMT#(0.05, 1, 10, 1000, 0, 0)
|
||||
D# = PPMT#(0.05, 1, 10, 1000, 0, 0)
|
||||
D# = RATE#(10, -100, 1000, 0, 0, 0.1)
|
||||
D# = NPV#(0.05, Zahlungen#())
|
||||
D# = IRR#(Zahlungen#(), 0.1)
|
||||
D# = MIRR#(Zahlungen#(), 0.05, 0.06)
|
||||
D# = SLN#(1000, 100, 10)
|
||||
D# = SYD#(1000, 100, 10, 1)
|
||||
D# = DDB#(1000, 100, 10, 1)
|
||||
S$ = MKI$(1)
|
||||
S$ = MKL$(1)
|
||||
S$ = MKS$(1)
|
||||
S$ = MKD$(1)
|
||||
S$ = MKC$(1)
|
||||
S$ = MKSMBF$(1)
|
||||
S$ = MKDMBF$(1)
|
||||
I% = CVI("ab")
|
||||
L& = CVL("abcd")
|
||||
G! = CVS("abcd")
|
||||
D# = CVD("abcdefgh")
|
||||
C@ = CVC("abcdefgh")
|
||||
G! = CVSMBF("abcd")
|
||||
D# = CVDMBF("abcdefgh")
|
||||
S$ = CURDIR$
|
||||
S$ = DIR$("*.txt")
|
||||
I% = LPOS(0)
|
||||
L& = STACK
|
||||
I% = ERDEV
|
||||
S$ = ERDEV$
|
||||
CHDRIVE "C"
|
||||
STACK 2048
|
||||
SetFormatCC 49
|
||||
"#;
|
||||
let a = tb_frontend::analyze_source("signaturen.bas", quelle);
|
||||
let unbekannt: Vec<&tb_frontend::Diagnostic> = a
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|d| d.message.contains("not defined"))
|
||||
.collect();
|
||||
assert!(
|
||||
unbekannt.is_empty(),
|
||||
"Elemente werden als unbekannter Bezeichner abgewiesen: {unbekannt:?}"
|
||||
);
|
||||
assert!(
|
||||
a.diagnostics.is_empty(),
|
||||
"unerwartete Diagnosen: {:?}",
|
||||
a.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
/// Die Signatur wirkt auch dort, wo das Laufzeitverhalten noch aussteht:
|
||||
/// eine falsche Argumentanzahl wird gemeldet und nennt das Element.
|
||||
#[test]
|
||||
fn signatur_greift_ohne_laufzeitverhalten() {
|
||||
let a = tb_frontend::analyze_source("argzahl.bas", "S$ = MKL$(1, 2)");
|
||||
let texte: Vec<String> = a.diagnostics.iter().map(|d| d.message.clone()).collect();
|
||||
assert!(
|
||||
texte.iter().any(|m| m.contains("MKL$")),
|
||||
"Diagnose nennt `MKL$` nicht: {texte:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user