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:
@@ -16,7 +16,9 @@ fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) {
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::with_input(input);
|
||||
let ev = vm.run(&mut host);
|
||||
(ev, host.output)
|
||||
// Die Tests vergleichen Textausgabe; der Bildschirm-Snapshot ist der
|
||||
// Nachfolger des früheren Byte-Stroms (Entscheidung 2026-09-03, D3).
|
||||
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||
}
|
||||
|
||||
fn out(src: &str) -> String {
|
||||
@@ -366,7 +368,7 @@ fn breakpoint_haelt_an_und_setzt_fort() {
|
||||
// Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut).
|
||||
vm.remove_breakpoint(3);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.output, " 6 \n");
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -399,7 +401,7 @@ fn stop_liefert_zeile_und_ist_fortsetzbar() {
|
||||
}
|
||||
// IDE-Semantik: CONT = weiterlaufen.
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.output, "a\nb\n");
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a\nb\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -444,8 +446,9 @@ fn print_zonen_und_tab() {
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber Phase 3: Datei-E/A → Laufzeitfehler 73.
|
||||
let (ev, _) = run("OPEN \"x.txt\" FOR INPUT AS #1");
|
||||
// Dokumentiert, aber noch offen: ISAM kommt mit `phase-3-isam`,
|
||||
// die Ereignissteuerung mit Phase 4 → Laufzeitfehler 73.
|
||||
let (ev, _) = run("TYPE T\n f AS INTEGER\nEND TYPE\nOPEN \"db\" FOR ISAM T \"tab\" AS #1");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
@@ -455,3 +458,205 @@ fn unsupported_feature_fehler_73() {
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Zustellpunkte des Hosts (Spec konsolen-basisbibliothek) ---------------
|
||||
|
||||
/// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde
|
||||
/// die Eingabeaufforderung unsichtbar da.
|
||||
#[test]
|
||||
fn ausgabe_erscheint_vor_der_eingabe() {
|
||||
use tb_runtime::host::{CaptureHost, Host};
|
||||
let module =
|
||||
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::with_input(&["7"]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
|
||||
// Der Host hat den Bildschirm mehrfach gesehen, und schon beim ersten
|
||||
// Mal stand der Text da.
|
||||
assert!(host.presents > 0, "Host wurde nie zur Anzeige aufgefordert");
|
||||
let text = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
assert!(text.starts_with("Frage:\n"), "{text:?}");
|
||||
assert!(text.contains(" 7 "), "{text:?}");
|
||||
}
|
||||
|
||||
/// Eingegangene Zeichen werden auf dem Bildschirm wiederholt (Echo), und
|
||||
/// Rückschritt löscht das zuletzt getippte Zeichen.
|
||||
#[test]
|
||||
fn eingabe_wird_geechot_und_rueckschritt_loescht() {
|
||||
use tb_runtime::host::{taste, CaptureHost, Ereignis};
|
||||
let module = tb_vm::compile_source("TEST", "INPUT a$\nPRINT a$\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['a', 'b', 'x'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
}
|
||||
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string()));
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.to_string()));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let text = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// Erste Zeile: das Echo ohne das gelöschte Zeichen.
|
||||
assert_eq!(text.lines().next(), Some("? ab"));
|
||||
assert!(text.contains("ab\n"), "{text:?}");
|
||||
}
|
||||
|
||||
/// Größenänderung während des Laufs wirkt auf den Bildschirm.
|
||||
#[test]
|
||||
fn groessenaenderung_wirkt_zur_laufzeit() {
|
||||
use tb_runtime::host::{CaptureHost, Ereignis};
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
|
||||
// Inhalt bleibt oben links erhalten.
|
||||
assert_eq!(vm.rt.screen.cell(1, 1).ch, 'a');
|
||||
}
|
||||
|
||||
// ---- Bildschirm und Tastatur (Spec textbildschirm) -------------------------
|
||||
|
||||
#[test]
|
||||
fn locate_ausserhalb_loest_fehler_5_aus() {
|
||||
assert_eq!(err_code("LOCATE 26, 1"), 5);
|
||||
assert_eq!(err_code("LOCATE 1, 81"), 5);
|
||||
assert_eq!(err_code("LOCATE 0, 1"), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_funktion_ausserhalb_loest_fehler_5_aus() {
|
||||
assert_eq!(err_code("x% = SCREEN(30, 1)"), 5);
|
||||
assert_eq!(err_code("x% = SCREEN(1, 200)"), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_prueft_wertebereich() {
|
||||
assert_eq!(err_code("COLOR 32, 0"), 5);
|
||||
assert_eq!(err_code("COLOR 7, 8"), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_anweisung_nur_textmodus() {
|
||||
// Literaler Grafikmodus schon zur Compile-Zeit.
|
||||
let d = tb_vm::compile_source("T", "SCREEN 1").unwrap_err();
|
||||
assert!(
|
||||
d.iter().any(|x| x.message.contains("Feature unavailable")),
|
||||
"{d:?}"
|
||||
);
|
||||
// Textmodus ist folgenlos zulässig.
|
||||
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
|
||||
// Berechneter Modus erst zur Laufzeit.
|
||||
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inkey_ohne_taste_ist_leer() {
|
||||
assert_eq!(out("a$ = INKEY$\nPRINT \"[\"; a$; \"]\""), "[]\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inkey_liefert_zeichen_und_sondertaste() {
|
||||
use tb_runtime::host::{taste, CaptureHost, Ereignis};
|
||||
let module = tb_vm::compile_source(
|
||||
"T",
|
||||
"a$ = INKEY$\nb$ = INKEY$\nPRINT a$; LEN(b$); ASC(b$)\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("q".into()));
|
||||
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let t = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// "q", dann Länge 2 und führendes Nullzeichen.
|
||||
assert_eq!(t, "q 2 0 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_s_liest_genau_n_zeichen_ohne_echo() {
|
||||
use tb_runtime::host::{CaptureHost, Ereignis};
|
||||
let module = tb_vm::compile_source("T", "a$ = INPUT$(3)\nPRINT a$\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['x', 'y', 'z', 'w'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
// Nur das PRINT erscheint — INPUT$ echot nicht.
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cls_setzt_cursor_zurueck() {
|
||||
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
|
||||
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT CSRLIN; POS(0)"), " 1 4 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width_hebt_die_groesse_an() {
|
||||
assert_eq!(
|
||||
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
|
||||
" 32 \n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
|
||||
|
||||
/// Ein modulweiter Handler ist auch aus einer Prozedur heraus ansprechbar;
|
||||
/// `RESUME NEXT` setzt dann hinter dem Prozeduraufruf fort.
|
||||
#[test]
|
||||
fn modulweites_on_error_aus_prozedur() {
|
||||
let src = "DECLARE SUB T ()\n\
|
||||
ON ERROR GOTO Handler\n\
|
||||
T\n\
|
||||
PRINT \"zurueck\"\n\
|
||||
END\n\
|
||||
Handler:\n\
|
||||
PRINT \"Handler\"; ERR\n\
|
||||
RESUME NEXT\n\
|
||||
SUB T\n\
|
||||
ON ERROR GOTO Handler\n\
|
||||
x = 1 / 0\n\
|
||||
END SUB\n";
|
||||
assert_eq!(out(src), "Handler 11 \nzurueck\n");
|
||||
}
|
||||
|
||||
/// `ON LOCAL ERROR` verlangt weiterhin ein Label im eigenen Rumpf.
|
||||
#[test]
|
||||
fn on_local_error_bleibt_prozedurlokal() {
|
||||
let src = "DECLARE SUB T ()\n\
|
||||
ON ERROR GOTO Handler\n\
|
||||
T\n\
|
||||
END\n\
|
||||
Handler:\n\
|
||||
RESUME NEXT\n\
|
||||
SUB T\n\
|
||||
ON LOCAL ERROR GOTO Handler\n\
|
||||
END SUB\n";
|
||||
let d = tb_vm::compile_source("T", src).unwrap_err();
|
||||
assert!(
|
||||
d.iter().any(|x| x.message.contains("Label not defined")),
|
||||
"{d:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Ein prozedurlokaler Handler fängt vor dem modulweiten.
|
||||
#[test]
|
||||
fn lokaler_handler_hat_vorrang() {
|
||||
let src = "DECLARE SUB T ()\n\
|
||||
ON ERROR GOTO Modul\n\
|
||||
T\n\
|
||||
END\n\
|
||||
Modul:\n\
|
||||
PRINT \"Modul\"\n\
|
||||
RESUME NEXT\n\
|
||||
SUB T\n\
|
||||
ON LOCAL ERROR GOTO Lokal\n\
|
||||
x = 1 / 0\n\
|
||||
EXIT SUB\n\
|
||||
Lokal:\n\
|
||||
PRINT \"Lokal\"\n\
|
||||
RESUME NEXT\n\
|
||||
END SUB\n";
|
||||
assert_eq!(out(src), "Lokal\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user