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

@@ -316,11 +316,18 @@ instrs! {
0xC8 RaiseError; // Code vom Stack (ERROR n)
0xC9 LoadErr;
0xCA LoadErl;
0xCB SetErr; // ERR = n (setzt den Code, löst nichts aus)
// 0xD0 — DATA und Eingabe
0xD0 ReadData(a: u8); // nächstes DATA-Element; 0 = String, 1 = Zahl (DOUBLE)
0xD1 Restore(a: u32);
0xD2 Input(a: u8, b: bool, c: u16, d: bool); // argc, line_mode, prompt (0xFFFF=ohne), '?'
0xD3 InputFile(a: u8, b: bool); // argc, line_mode — Dateinummer liegt unter den Referenzen
// put?, mit Recordnummer?, Feldart (0 = ohne Variable, s. `Feldart`),
// Zusatz (UDT-Index bzw. Länge fester Strings)
0xD4 GetPut(a: bool, b: bool, c: u8, d: u16);
0xD5 Field(a: u8); // Feldzahl; Stack: Dateinummer, dann (Länge, Referenz)*
0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
}
// ---- Modulstruktur ------------------------------------------------------------

View File

@@ -19,6 +19,7 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
strings: Vec::new(),
string_ids: HashMap::new(),
jump_tables: Vec::new(),
modul_label_pc: Vec::new(),
};
let mut procs = Vec::new();
for proc in &hir.procs {
@@ -74,6 +75,10 @@ struct Codegen {
strings: Vec<Rc<str>>,
string_ids: HashMap<String, u16>,
jump_tables: Vec<Vec<u32>>,
/// Sprungziele des Modulrumpfs (Prozedur 0). Ein modulweites
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
modul_label_pc: Vec<Option<u32>>,
}
struct ProcCtx {
@@ -137,9 +142,26 @@ impl Codegen {
// Rumpfende
self.emit_proc_exit(&mut ctx, proc);
// Prozedur 0 (Modulrumpf) gibt ihre Sprungziele weiter.
if proc.kind == hir::HProcKind::Main {
self.modul_label_pc = ctx.label_pc.clone();
}
// Fixups patchen
for (idx, label) in std::mem::take(&mut ctx.fixups) {
let pc = ctx.label_pc[label as usize].expect("Label ohne Position");
// Modulweites `ON ERROR GOTO` in einer Prozedur: das Label lebt
// im Modulrumpf, nicht im eigenen.
let modulweit = proc.kind != hir::HProcKind::Main
&& matches!(ctx.code[idx], Instr::OnErrorGoto(_));
let pc = if modulweit {
self.modul_label_pc
.get(label as usize)
.copied()
.flatten()
.expect("Modul-Label ohne Position")
} else {
ctx.label_pc[label as usize].expect("Label ohne Position")
};
match &mut ctx.code[idx] {
Instr::Jump(t)
| Instr::JumpIfFalse(t)
@@ -224,7 +246,57 @@ impl Codegen {
ctx.emit(Instr::CallBuiltin(ids::PRINT_NEWLINE, 0));
}
}
HStmtKind::Input { line_mode, prompt, question, targets } => {
HStmtKind::SetErr(e) => {
self.expr(ctx, e);
ctx.emit(Instr::SetErr);
}
HStmtKind::Field { file, fields } => {
self.expr(ctx, file);
for (len, place) in fields {
self.expr(ctx, len);
self.make_ref(ctx, place);
}
ctx.emit(Instr::Field(fields.len() as u8));
}
HStmtKind::LsetRset { rset, target, value } => {
self.make_ref(ctx, target);
self.expr(ctx, value);
ctx.emit(Instr::LsetRset(*rset));
}
HStmtKind::GetPut { put, file, recnum, var } => {
self.expr(ctx, file);
if let Some(r) = recnum {
self.expr(ctx, r);
}
let (art, zusatz) = match var {
None => (0u8, 0u16),
Some(v) => {
self.make_ref(ctx, v);
match &v.ty {
HTy::Num(NumTy::Int) => (1, 0),
HTy::Num(NumTy::Lng) => (2, 0),
HTy::Num(NumTy::Sng) => (3, 0),
HTy::Num(NumTy::Dbl) => (4, 0),
HTy::Num(NumTy::Cur) => (5, 0),
HTy::FixedStr(n) => (6, *n as u16),
HTy::Udt(i) => (7, *i),
// Variable Strings: Länge erst zur Laufzeit.
HTy::Str => (8, 0),
}
}
};
ctx.emit(Instr::GetPut(*put, recnum.is_some(), art, zusatz));
}
HStmtKind::Input { file, line_mode, prompt, question, targets } => {
if let Some(f) = file {
// Dateinummer zuerst, dann die Referenzen darüber.
self.expr(ctx, f);
for t in targets {
self.make_ref(ctx, t);
}
ctx.emit(Instr::InputFile(targets.len() as u8, *line_mode));
return;
}
for t in targets {
self.make_ref(ctx, t);
}
@@ -901,9 +973,90 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::PrintTab => ids::PRINT_TAB,
Builtin::PrintSpc => ids::PRINT_SPC,
Builtin::PrintNewline => ids::PRINT_NEWLINE,
Builtin::PrintUsing => ids::PRINT_USING,
Builtin::FormatS => ids::FORMAT_S,
Builtin::SetFormatCc => ids::SET_FORMAT_CC,
Builtin::Cls => ids::CLS,
Builtin::Color => ids::COLOR,
Builtin::Locate => ids::LOCATE,
Builtin::Width => ids::WIDTH,
Builtin::ViewPrint => ids::VIEW_PRINT,
Builtin::ScreenStmt => ids::SCREEN_STMT,
Builtin::KeyAssign => ids::KEY_ASSIGN,
Builtin::KeyList => ids::KEY_LIST,
Builtin::KeyDisplay => ids::KEY_DISPLAY,
Builtin::Csrlin => ids::CSRLIN,
Builtin::PosFn => ids::POS_FN,
Builtin::ScreenFn => ids::SCREEN_FN,
Builtin::InkeyS => ids::INKEY_S,
Builtin::InputS => ids::INPUT_S,
Builtin::EnvironS => ids::ENVIRON_S,
Builtin::EnvironSet => ids::ENVIRON_SET,
Builtin::Fre => ids::FRE,
Builtin::Clear => ids::CLEAR,
Builtin::Tron => ids::TRON,
Builtin::Troff => ids::TROFF,
Builtin::StackFn => ids::STACK_FN,
Builtin::StackStmt => ids::STACK_STMT,
Builtin::Erdev => ids::ERDEV,
Builtin::ErdevS => ids::ERDEV_S,
Builtin::Open => ids::OPEN,
Builtin::Close => ids::CLOSE,
Builtin::CloseAll => ids::CLOSE_ALL,
Builtin::PrintZiel => ids::PRINT_ZIEL,
Builtin::WriteFile => ids::WRITE_FILE,
Builtin::EofF => ids::EOF_F,
Builtin::LofF => ids::LOF_F,
Builtin::LocF => ids::LOC_F,
Builtin::SeekF => ids::SEEK_F,
Builtin::SeekStmt => ids::SEEK_STMT,
Builtin::Freefile => ids::FREEFILE,
Builtin::Fileattr => ids::FILEATTR,
Builtin::LockStmt => ids::LOCK_STMT,
Builtin::Kill => ids::KILL,
Builtin::NameStmt => ids::NAME_STMT,
Builtin::Files => ids::FILES,
Builtin::Chdir => ids::CHDIR,
Builtin::Chdrive => ids::CHDRIVE,
Builtin::Mkdir => ids::MKDIR,
Builtin::Rmdir => ids::RMDIR,
Builtin::CurdirS => ids::CURDIR_S,
Builtin::DirS => ids::DIR_S,
Builtin::Lpos => ids::LPOS,
Builtin::ShellStmt => ids::SHELL_STMT,
Builtin::ShellFn => ids::SHELL_FN,
Builtin::MkS => ids::MK_S,
Builtin::CvF => ids::CV_F,
Builtin::Fv => ids::FV,
Builtin::Pv => ids::PV,
Builtin::Pmt => ids::PMT,
Builtin::NPer => ids::NPER,
Builtin::IPmt => ids::IPMT,
Builtin::PPmt => ids::PPMT,
Builtin::Rate => ids::RATE,
Builtin::Npv => ids::NPV,
Builtin::Irr => ids::IRR,
Builtin::Mirr => ids::MIRR,
Builtin::Sln => ids::SLN,
Builtin::Syd => ids::SYD,
Builtin::Ddb => ids::DDB,
Builtin::Timer => ids::TIMER,
Builtin::DateS => ids::DATE_S,
Builtin::TimeS => ids::TIME_S,
Builtin::DateSet => ids::DATE_SET,
Builtin::TimeSet => ids::TIME_SET,
Builtin::Now => ids::NOW,
Builtin::DateSerial => ids::DATE_SERIAL,
Builtin::TimeSerial => ids::TIME_SERIAL,
Builtin::DateValue => ids::DATE_VALUE,
Builtin::TimeValue => ids::TIME_VALUE,
Builtin::DayF => ids::DAY_F,
Builtin::MonthF => ids::MONTH_F,
Builtin::YearF => ids::YEAR_F,
Builtin::WeekdayF => ids::WEEKDAY_F,
Builtin::HourF => ids::HOUR_F,
Builtin::MinuteF => ids::MINUTE_F,
Builtin::SecondF => ids::SECOND_F,
Builtin::CommandS => ids::COMMAND_S,
Builtin::Doevents => ids::DOEVENTS,
Builtin::Sleep => ids::SLEEP,

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()
}