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:
2026-09-04 06:54:19 +02:00
parent a6f005d023
commit 52ccbb5848
94 changed files with 10421 additions and 792 deletions

View File

@@ -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");
}