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

@@ -71,6 +71,8 @@ pub struct Vm {
resume_pc: usize,
// Steuerung
flags: u32,
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
tick_zaehler: u32,
breakpoints: HashSet<u32>,
data_ptr: usize,
}
@@ -99,6 +101,7 @@ impl Vm {
in_handler: false,
resume_pc: 0,
flags: 0,
tick_zaehler: 0,
breakpoints: HashSet::new(),
data_ptr: 0,
module,
@@ -508,6 +511,15 @@ impl Vm {
let f = self.frames.last_mut().unwrap();
f.line = line;
f.last_stmt_pc = pc;
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
// hat, und regelmäßig Ereignisse abholen. Das ist keine
// Debugger-Funktion — ohne sie sähe niemand die Ausgabe und
// Größenänderungen kämen nie an.
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
if self.rt.screen.ist_veraendert() || self.tick_zaehler % 1024 == 0 {
self.rt.tick(host);
self.rt.screen.veraenderung_quittieren();
}
if self.flags != 0 {
if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line }));
@@ -515,7 +527,7 @@ impl Vm {
if self.flags & F_BREAK != 0 && self.breakpoints.contains(&line) {
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
}
if self.flags & F_POLL != 0 && host.interrupted() {
if self.flags & F_POLL != 0 && self.rt.abbruch {
return Ok(Flow::Event(RunEvent::Interrupted { line }));
}
}
@@ -1204,6 +1216,30 @@ impl Vm {
self.do_input(argc, line_mode, prompt, question, host)?;
Ok(Flow::Normal)
}
I::InputFile(argc, line_mode) => {
self.do_input_file(argc, line_mode)?;
Ok(Flow::Normal)
}
I::GetPut(put, mit_nr, art, zusatz) => {
self.do_get_put(put, mit_nr, art, zusatz)?;
Ok(Flow::Normal)
}
I::SetErr => {
let n = self.pop_i32()?;
if !(0..=32767).contains(&n) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
self.err = n as u16;
Ok(Flow::Normal)
}
I::Field(anzahl) => {
self.do_field(anzahl)?;
Ok(Flow::Normal)
}
I::LsetRset(rset) => {
self.do_lset_rset(rset)?;
Ok(Flow::Normal)
}
}
}
@@ -1247,15 +1283,14 @@ impl Vm {
loop {
if prompt != 0xFFFF {
let p = self.module.strings[prompt as usize].clone();
self.rt.print.write(host, &p);
self.rt.screen.print(&p);
}
if question {
self.rt.print.write(host, "? ");
self.rt.screen.print("? ");
}
let Some(mut line) = host.read_line() else {
let Some(mut line) = self.rt.read_line(host) else {
return Err(RuntimeError(62)); // Input past end of file
};
self.rt.print.col = 0;
if line_mode {
let r = refs.first().ok_or(RuntimeError(51))?;
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?;
@@ -1264,7 +1299,7 @@ impl Vm {
// Felder trennen (Quotes respektieren).
let fields = split_input_fields(&line);
if fields.len() != refs.len() {
self.rt.print.write(host, "Redo from start\n");
self.rt.screen.print("Redo from start\n");
line.clear();
continue;
}
@@ -1282,7 +1317,7 @@ impl Vm {
}
}
if !ok {
self.rt.print.write(host, "Redo from start\n");
self.rt.screen.print("Redo from start\n");
continue;
}
for (r, v) in refs.iter().zip(vals) {
@@ -1292,6 +1327,230 @@ impl Vm {
}
}
/// `FIELD #n, laenge AS var$, …` — Recordpuffer aufteilen. Die
/// Feldvariablen werden gemerkt: `GET` frischt sie aus dem Puffer auf,
/// `LSET`/`RSET` schreiben zugleich in den Puffer.
fn do_field(&mut self, anzahl: u8) -> Result<(), RuntimeError> {
let mut paare = Vec::with_capacity(anzahl as usize);
for _ in 0..anzahl {
let r = match self.pop()? {
Value::Ref(r) => r,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let laenge = self.pop_i32()?;
paare.push((laenge, r));
}
paare.reverse();
let nummer = self.pop_i32()?;
let datei = self.rt.dateien.get(nummer)?;
datei.felder.clear();
let mut start = 0usize;
for (laenge, r) in paare {
if laenge < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let n = laenge as usize;
if start + n * tb_runtime::fileio::UTF32_BREITE > datei.puffer.len() {
return Err(RuntimeError(50)); // FIELD overflow
}
datei.felder.push(tb_runtime::fileio::Feld {
referenz: r,
start,
laenge: n,
});
start += n * tb_runtime::fileio::UTF32_BREITE;
}
// Feldvariablen sofort aus dem Puffer belegen.
self.felder_auffrischen(nummer)
}
/// Feldvariablen einer Datei aus dem Recordpuffer neu belegen.
fn felder_auffrischen(&mut self, nummer: i32) -> Result<(), RuntimeError> {
let datei = self.rt.dateien.get(nummer)?;
let werte: Vec<(VarRef, String)> = datei
.felder
.iter()
.map(|f| {
(
f.referenz.clone(),
tb_runtime::fileio::feld_lesen(&datei.puffer, f.start, f.laenge),
)
})
.collect();
for (r, t) in werte {
self.write_ref(&r, Value::Str(Rc::from(t.as_str())))?;
}
Ok(())
}
/// `LSET`/`RSET` — bündig zuweisen; bei einem `FIELD`-Feld zusätzlich in
/// den Recordpuffer schreiben.
fn do_lset_rset(&mut self, rset: bool) -> Result<(), RuntimeError> {
let wert = self.pop()?;
let ziel = match self.pop()? {
Value::Ref(r) => r,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let Value::Str(text) = &wert else {
// UDT-Zuweisung: unverändert durchreichen (Vorbild).
self.write_ref(&ziel, wert)?;
return Ok(());
};
// Gehört das Ziel zu einem FIELD-Puffer?
let treffer = self.rt.dateien.feld_finden(&ziel);
match treffer {
Some((nummer, start, laenge)) => {
let datei = self.rt.dateien.get(nummer)?;
tb_runtime::fileio::feld_setzen(
&mut datei.puffer,
start,
laenge,
text,
rset,
);
let neu = tb_runtime::fileio::feld_lesen(&datei.puffer, start, laenge);
self.write_ref(&ziel, Value::Str(Rc::from(neu.as_str())))?;
}
None => {
// Ohne Feldbindung wirkt LSET/RSET auf die Stringlänge des
// bisherigen Werts (Vorbild).
let alt = match self.read_ref(&ziel)? {
Value::Str(s) => s.chars().count(),
_ => text.chars().count(),
};
let mut puffer = vec![0u8; alt * tb_runtime::fileio::UTF32_BREITE];
tb_runtime::fileio::feld_setzen(&mut puffer, 0, alt, text, rset);
let neu = tb_runtime::fileio::feld_lesen(&puffer, 0, alt);
self.write_ref(&ziel, Value::Str(Rc::from(neu.as_str())))?;
}
}
Ok(())
}
/// `GET`/`PUT` — Datensatz (RANDOM) bzw. Bytes (BINARY) übertragen.
fn do_get_put(
&mut self,
put: bool,
mit_nr: bool,
art: u8,
zusatz: u16,
) -> Result<(), RuntimeError> {
use tb_runtime::fileio as fio;
let ziel = if art != 0 {
match self.pop()? {
Value::Ref(r) => Some(r),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}
} else {
None
};
let nr = if mit_nr { Some(self.pop_i32()?) } else { None };
let nummer = self.pop_i32()?;
let udts = self.module.udts.clone();
let datei = self.rt.dateien.get(nummer)?;
let binaer = datei.modus == fio::Modus::Binary;
// Ohne Nummer gilt die Position nach der letzten Operation.
let pos = match nr {
Some(n) if n >= 1 => n as u64,
Some(_) => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
None => datei.position,
};
// Typ der Zielvariablen bestimmt die übertragene Länge.
let typ = match art {
0 => None,
1 => Some(TypeInit::Int),
2 => Some(TypeInit::Lng),
3 => Some(TypeInit::Sng),
4 => Some(TypeInit::Dbl),
5 => Some(TypeInit::Cur),
6 => Some(TypeInit::FixedStr(zusatz as u32)),
7 => Some(TypeInit::Udt(zusatz)),
// Variabler String: aktuelle Zeichenzahl bestimmt die Länge.
_ => {
let laenge = match self.read_ref(ziel.as_ref().unwrap())? {
Value::Str(s) => s.chars().count() as u32,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
Some(TypeInit::FixedStr(laenge))
}
};
if binaer {
let Some(t) = typ else {
// Ohne Variable überträgt BINARY nichts.
return Ok(());
};
let n = fio::breite(&t, &udts).ok_or(RuntimeError::TYPE_MISMATCH)?;
if put {
let wert = self.read_ref(ziel.as_ref().unwrap())?;
let mut puffer = vec![0u8; n];
fio::wert_schreiben(&mut puffer, 0, &wert, &t, &udts)?;
self.rt.dateien.get(nummer)?.bytes_schreiben(pos, &puffer)?;
} else {
let puffer = self.rt.dateien.get(nummer)?.bytes_lesen(pos, n)?;
let (v, _) = fio::wert_lesen(&puffer, 0, &t, &udts)?;
self.write_ref(ziel.as_ref().unwrap(), v)?;
}
return Ok(());
}
// RANDOM: über den Recordpuffer.
if put {
if let (Some(r), Some(t)) = (&ziel, &typ) {
let wert = self.read_ref(r)?;
let datei = self.rt.dateien.get(nummer)?;
let mut puffer = std::mem::take(&mut datei.puffer);
puffer.iter_mut().for_each(|b| *b = b' ');
let ergebnis = fio::wert_schreiben(&mut puffer, 0, &wert, t, &udts);
self.rt.dateien.get(nummer)?.puffer = puffer;
ergebnis?;
}
self.rt.dateien.get(nummer)?.record_schreiben(pos)?;
} else {
self.rt.dateien.get(nummer)?.record_lesen(pos)?;
self.felder_auffrischen(nummer)?;
if let (Some(r), Some(t)) = (&ziel, &typ) {
let puffer = self.rt.dateien.get(nummer)?.puffer.clone();
let (v, _) = fio::wert_lesen(&puffer, 0, t, &udts)?;
self.write_ref(r, v)?;
}
}
Ok(())
}
/// `INPUT #n` / `LINE INPUT #n` — Werte aus einer sequenziellen Datei.
fn do_input_file(&mut self, argc: u8, line_mode: bool) -> Result<(), RuntimeError> {
let mut refs = Vec::with_capacity(argc as usize);
for _ in 0..argc {
match self.pop()? {
Value::Ref(r) => refs.push(r),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}
}
refs.reverse();
let nummer = self.pop_i32()?;
let Some(zeile) = self.rt.dateien.get(nummer)?.zeile_lesen()? else {
return Err(RuntimeError(62)); // Input past end of file
};
if line_mode {
let r = refs.first().ok_or(RuntimeError(51))?;
self.write_ref(r, Value::Str(Rc::from(zeile.as_str())))?;
return Ok(());
}
let felder = tb_runtime::fileio::felder_trennen(&zeile);
for (i, r) in refs.iter().enumerate() {
let text = felder.get(i).map(String::as_str).unwrap_or("");
let ziel = self.read_ref(r)?;
let wert = input_value(&ziel, text).ok_or(RuntimeError(64))?; // Bad file name
self.write_ref(r, wert)?;
}
Ok(())
}
// Typisierte Binäroperationen mit Überlaufprüfung.
fn bin_i16(&mut self, f: impl Fn(i16, i16) -> Option<i16>) -> Result<Flow, RuntimeError> {
let b = self.pop_i16()?;
@@ -1473,3 +1732,4 @@ fn strict_number(t: &str) -> Option<f64> {
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
cleaned.parse::<f64>().ok()
}