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:
@@ -1,79 +1,117 @@
|
||||
//! `Host`-Abstraktion für Konsolen-E/A (Design-Entscheidung D3):
|
||||
//! Alle Konsolenwirkungen der VM laufen über dieses Trait. Host-Aufrufe
|
||||
//! dürfen blockieren; die Abbruchprüfung (Strg+Untbr) obliegt dem Host.
|
||||
//! `Host`-Abstraktion für den Textbildschirm.
|
||||
//!
|
||||
//! Der Host ist bewusst dumm: er zeigt den Zellenpuffer an und liefert
|
||||
//! Ereignisse. Alle Bildschirmwirkungen entstehen in der Laufzeitbibliothek
|
||||
//! auf [`TextScreen`] — ein Zeichenstrom könnte `LOCATE`, `COLOR`, `CSRLIN`
|
||||
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
|
||||
//! (Entscheidung 2026-09-03, D2).
|
||||
//!
|
||||
//! Host-Aufrufe dürfen blockieren; kooperative Zustellpunkte bringt erst die
|
||||
//! Ereignisschleife der Forms-Engine (Phase 4).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{BufRead, Write as _};
|
||||
|
||||
use crate::screen::TextScreen;
|
||||
|
||||
/// Was vom Terminal hereinkommt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ereignis {
|
||||
/// Tastendruck in `INKEY$`-Form: ein Zeichen für Zeichentasten,
|
||||
/// Nullzeichen + Kennung für Sondertasten.
|
||||
Taste(String),
|
||||
/// Die Darstellungsfläche hat ihre Größe geändert.
|
||||
Groesse { cols: usize, rows: usize },
|
||||
/// Abbruchwunsch (Strg+Untbr).
|
||||
Abbruch,
|
||||
/// Eingabeende — es kommt nichts mehr.
|
||||
Ende,
|
||||
}
|
||||
|
||||
/// Steuerzeichen in `INKEY$`-Form (Kodierung des Vorbilds).
|
||||
pub mod taste {
|
||||
pub const ENTER: &str = "\r";
|
||||
pub const BACKSPACE: &str = "\u{8}";
|
||||
pub const ESC: &str = "\u{1b}";
|
||||
pub const TAB: &str = "\t";
|
||||
|
||||
/// Sondertaste: Nullzeichen + Kennung (F1 = 59, F2 = 60 …).
|
||||
pub fn sonder(code: u8) -> String {
|
||||
format!("\0{}", code as char)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Host {
|
||||
/// Text ausgeben (ohne implizite Zeilenumbrüche).
|
||||
fn write(&mut self, s: &str);
|
||||
/// Eine Eingabezeile lesen (ohne Zeilenende); `None` = Eingabeende.
|
||||
fn read_line(&mut self) -> Option<String>;
|
||||
/// Abbruchwunsch (Strg+Untbr)? Wird an Anweisungsgrenzen geprüft.
|
||||
fn interrupted(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
/// `SLEEP` — blockierend im Host.
|
||||
fn sleep(&mut self, _secs: f64) {}
|
||||
/// Aktuellen Bildschirmzustand anzeigen.
|
||||
fn present(&mut self, screen: &TextScreen);
|
||||
|
||||
/// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`,
|
||||
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
|
||||
}
|
||||
|
||||
/// Konsolen-Host für `tbc run`: stdout/stdin.
|
||||
#[derive(Default)]
|
||||
pub struct ConsoleHost;
|
||||
|
||||
impl Host for ConsoleHost {
|
||||
fn write(&mut self, s: &str) {
|
||||
let mut out = std::io::stdout().lock();
|
||||
let _ = out.write_all(s.as_bytes());
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Option<String> {
|
||||
let mut line = String::new();
|
||||
match std::io::stdin().lock().read_line(&mut line) {
|
||||
Ok(0) => None,
|
||||
Ok(_) => {
|
||||
while line.ends_with('\n') || line.ends_with('\r') {
|
||||
line.pop();
|
||||
}
|
||||
Some(line)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep(&mut self, secs: f64) {
|
||||
if secs > 0.0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture-Host für Tests: zeichnet die Ausgabe byte-genau auf und
|
||||
/// liefert vorbereitete Eingabezeilen.
|
||||
/// Host ohne Terminal: hält den Bildschirmzustand fest und speist
|
||||
/// vorbereitete Ereignisse ein. Grundlage des Korpus-Harness.
|
||||
#[derive(Default)]
|
||||
pub struct CaptureHost {
|
||||
pub output: String,
|
||||
pub input: VecDeque<String>,
|
||||
/// Zuletzt angezeigter Bildschirm.
|
||||
pub screen: Option<TextScreen>,
|
||||
/// Wie oft angezeigt wurde (Nachweis der Zustellpunkte).
|
||||
pub presents: usize,
|
||||
ereignisse: VecDeque<Ereignis>,
|
||||
/// Ereignisse, die erst nach der n-ten Anzeige eingereiht werden —
|
||||
/// so lässt sich eine Größenänderung mitten im Lauf nachstellen.
|
||||
verzoegert: Vec<(usize, Ereignis)>,
|
||||
}
|
||||
|
||||
impl CaptureHost {
|
||||
/// Host mit vorbereiteten Eingabezeilen: jede Zeile wird zu ihren
|
||||
/// Zeichen plus Enter.
|
||||
pub fn with_input(lines: &[&str]) -> Self {
|
||||
CaptureHost {
|
||||
output: String::new(),
|
||||
input: lines.iter().map(|s| s.to_string()).collect(),
|
||||
let mut h = CaptureHost::default();
|
||||
for l in lines {
|
||||
h.tippe(l);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter.
|
||||
pub fn tippe(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
self.ereignisse.push_back(Ereignis::Taste(c.to_string()));
|
||||
}
|
||||
self.ereignisse
|
||||
.push_back(Ereignis::Taste(taste::ENTER.to_string()));
|
||||
}
|
||||
|
||||
/// Beliebiges Ereignis einreihen (Größenänderung, Abbruch, Sondertaste).
|
||||
pub fn ereignis(&mut self, e: Ereignis) {
|
||||
self.ereignisse.push_back(e);
|
||||
}
|
||||
|
||||
/// Ereignis erst nach der `nach`-ten Anzeige einreihen.
|
||||
pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) {
|
||||
self.verzoegert.push((nach, e));
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for CaptureHost {
|
||||
fn write(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
fn present(&mut self, screen: &TextScreen) {
|
||||
self.screen = Some(screen.clone());
|
||||
self.presents += 1;
|
||||
let faellig = self.presents;
|
||||
let mut i = 0;
|
||||
while i < self.verzoegert.len() {
|
||||
if self.verzoegert[i].0 <= faellig {
|
||||
let (_, e) = self.verzoegert.remove(i);
|
||||
self.ereignisse.push_back(e);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Option<String> {
|
||||
self.input.pop_front()
|
||||
fn next_event(&mut self, _blockierend: bool) -> Option<Ereignis> {
|
||||
self.ereignisse.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +120,55 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_host_zeichnet_bytegenau_auf() {
|
||||
fn capture_host_haelt_zeichen_und_attribut() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.write(" 1 2 ");
|
||||
h.write("\n");
|
||||
h.write("x");
|
||||
assert_eq!(h.output, " 1 2 \nx");
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(5, 10).unwrap();
|
||||
s.set_color(14, 1);
|
||||
s.print("x");
|
||||
h.present(&s);
|
||||
let gehalten = h.screen.as_ref().unwrap();
|
||||
let z = gehalten.cell(5, 10);
|
||||
assert_eq!(z.ch, 'x');
|
||||
assert_eq!((z.fg, z.bg), (14, 1));
|
||||
assert_eq!(h.presents, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verzoegertes_ereignis_kommt_nach_der_anzeige() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.ereignis_nach(2, Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
let s = TextScreen::new();
|
||||
h.present(&s);
|
||||
assert_eq!(h.next_event(false), None);
|
||||
h.present(&s);
|
||||
assert_eq!(
|
||||
h.next_event(false),
|
||||
Some(Ereignis::Groesse { cols: 120, rows: 40 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groessenaenderung_und_abbruch_als_ereignis() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
h.ereignis(Ereignis::Abbruch);
|
||||
assert_eq!(
|
||||
h.next_event(true),
|
||||
Some(Ereignis::Groesse { cols: 120, rows: 40 })
|
||||
);
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Abbruch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vorbereitete_zeile_wird_zu_tasten() {
|
||||
let mut h = CaptureHost::with_input(&["ab"]);
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into())));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into())));
|
||||
assert_eq!(
|
||||
h.next_event(true),
|
||||
Some(Ereignis::Taste(taste::ENTER.into()))
|
||||
);
|
||||
assert_eq!(h.next_event(false), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user