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:
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