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:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -489,6 +489,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"thiserror",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -499,6 +500,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"log",
|
||||
"ratatui",
|
||||
"tb-runtime",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ authors = ["Georg Bauer"]
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
|
||||
# Text
|
||||
unicode-width = "0.2"
|
||||
|
||||
# Allgemein
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
|
||||
161
PLAN.md
161
PLAN.md
@@ -245,54 +245,115 @@ Vorgezogen aus Phase 3 (Meilenstein-Voraussetzung): `Host`-Trait
|
||||
(Konsole/Capture), Builtin-Dispatch-Tabelle, PRINT-Formatierung mit
|
||||
Druckzonen sowie die Korpus-Stringfunktionen (`tb-runtime`).
|
||||
|
||||
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`)
|
||||
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm
|
||||
|
||||
**Status: Kern abgeschlossen (2026-09-04), ISAM offen.** Umgesetzt im
|
||||
OpenSpec-Change `phase-3-runtime-bildschirm`; der Rest steckt im Change
|
||||
`phase-3-isam`, mit dem Phase 3 endet.
|
||||
|
||||
Ziel ist die **vollständige** Standardbibliothek des Vorbilds (siehe
|
||||
Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
|
||||
Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
|
||||
- [ ] Vollständigkeits-Inventar erstellen: maschinenlesbare Liste aller
|
||||
Anweisungen/Funktionen des Vorbilds aus der Original-Hilfe
|
||||
(dos-help.soulsphere.org, Topic-Listen) mit Status
|
||||
implementiert/offen/Non-Feature → `docs/inventar.md`; ab dann
|
||||
Abdeckung je Phase fortschreiben
|
||||
- [ ] Strings: `LEFT$`, `MID$` (auch als Anweisung), `INSTR`, `STR$`/`VAL`,
|
||||
`SPACE$`, `STRING$`, `LTRIM$`/`RTRIM$`, `UCASE$`/`LCASE$` …
|
||||
- [ ] Zahlenformatierung: `PRINT`-Zonen, `PRINT USING` (vollständig;
|
||||
dabei offene Detailfrage klären: `%`-Präfix bei Feldüberlauf),
|
||||
Banker's Rounding, `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR`
|
||||
- [ ] Mathematik: `RND`/`RANDOMIZE` (kompatibler PRNG!), trigonometrische
|
||||
Funktionen, Integer-Überlaufverhalten (Fehler 6)
|
||||
- [ ] Datei-E/A: `OPEN` (sequenziell/random/binär), `INPUT#`/`LINE INPUT#`,
|
||||
`PRINT#`/`WRITE#`, `GET`/`PUT` mit Record-Typen, `EOF`/`LOF`/`SEEK`,
|
||||
Pfadsemantik plattformübergreifend. Record-Layout: feste Strings als
|
||||
**UTF-32** (Entscheidung 2026-09-02 — 4 Bytes/Zeichen, feste
|
||||
Record-Länge; bewusst inkompatibel zu Vorbild-Dateien)
|
||||
- [ ] ISAM-Dateiunterstützung (Entscheidung 2026-09-02: wird implementiert,
|
||||
nicht Non-Feature): Anweisungen/Funktionen der Professional Edition
|
||||
(`OPEN … FOR ISAM`, Tabellen/Indizes, `SEEKGT`-Familie …) — Umfang
|
||||
aus der Original-Hilfe inventarisieren, dann implementieren
|
||||
- [ ] Breite Unicode-Zeichen (Emoji, CJK): belegen **zwei Zellen**
|
||||
(Entscheidung 2026-09-02) — Zellenmodell und `LOCATE`/`POS`-Semantik
|
||||
entsprechend umsetzen (unicode-width), Verhalten dokumentieren
|
||||
- [ ] Bildschirm: `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT`, `INKEY$`,
|
||||
`CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer
|
||||
- [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros
|
||||
(`KEY LIST`/`ON`/`OFF`) und in docs/sprachreferenz.md festhalten
|
||||
- [ ] Aus Phase 2 verlagert — `ON ERROR GOTO`-Scoping des Vorbilds im
|
||||
Frontend abbilden: Handler-Label auf Modulebene ist auch aus
|
||||
Prozeduren heraus ansprechbar (derzeit wird nicht-lokales
|
||||
`ON ERROR` in einer Prozedur wie `ON LOCAL ERROR` behandelt,
|
||||
siehe sema.rs, lower_stmt/OnError)
|
||||
- [ ] Aus Phase 2 verlagert — DATA-Rohtext erhalten: unquotierte
|
||||
DATA-Elemente verlieren derzeit die Groß-/Kleinschreibung, weil
|
||||
der Lexer Bezeichner normalisiert; Rohtext-Erfassung im Lexer,
|
||||
Korpustest ergänzen
|
||||
- [ ] Aus Phase 2 verlagert — `TODO verify`-Zellen der
|
||||
Konvertierungsmatrix (docs/tbvm-design.md) gegen die
|
||||
Original-Hilfe auflösen: CURRENCY-Promotion mit Gleitkomma,
|
||||
`^`-Ergebnistyp, Exponentialform-Schwellen der Textdarstellung,
|
||||
CURRENCY-Ausgabeformat; zugehörige Korpustests nachschärfen
|
||||
- [ ] Meilenstein: klassische Konsolenprogramme laufen unverändert
|
||||
- [x] Vollständigkeits-Inventar → [docs/inventar.md](docs/inventar.md):
|
||||
285 Einträge aus den Themenlisten von `bas7advr.hlp` und
|
||||
`qb45advr.hlp` mit Status und Fundstelle. Ein Test
|
||||
(`crates/tb-frontend/tests/inventar.rs`) hält die Tabelle in beide
|
||||
Richtungen gegen den Code — der Abdeckungsstand kann nicht mehr
|
||||
veralten. **Stand: 195 implementiert · 37 offen · 53 Non-Feature.**
|
||||
Die 37 offenen sind 22 ISAM (`phase-3-isam`), 13 Forms und
|
||||
Ereignissteuerung (Phase 4), `RUN` und `$INCLUDE` (Phase 5).
|
||||
- [x] **Bildschirmpuffer in die Laufzeit** (Entscheidung 2026-09-03):
|
||||
`Cell`/`TextScreen` liegen ratatui-frei in `tb-runtime::screen`;
|
||||
`tb-ui` behält Farbabbildung, Widget und den Terminal-Host. Damit
|
||||
erreicht die Bibliothek den Puffer, und Bildschirmverhalten ist ohne
|
||||
Terminal prüfbar.
|
||||
- [x] **`Host` neu geschnitten** (BREAKING): statt Zeichenstrom nun
|
||||
Anzeige des Zellenpuffers plus Ereignisse (Taste, Größenänderung,
|
||||
Abbruch). `write`/`read_line` konnten `LOCATE`, `COLOR`, `CSRLIN`
|
||||
und das Zurücklesen per `SCREEN(z, s)` nicht transportieren.
|
||||
- [x] **Korpusvergleich auf Bildschirm-Snapshot** (BREAKING): getrimmt auf
|
||||
die belegte Fläche, Attributebene nur bei gesetzten Farben. Die acht
|
||||
bestehenden Sollausgaben blieben dabei unverändert.
|
||||
- [x] **80×25 ist nirgends eine feste Grenze** (Vorgabe 2026-09-03):
|
||||
alle Bildschirmpfade rechnen mit der aktuellen Größe;
|
||||
Größenänderungen während der Ausführung werden zugestellt (Inhalt
|
||||
oben links erhalten, Cursor und `VIEW PRINT` geklemmt). Der Korpus
|
||||
weist es nach: dasselbe Programm bei 80×25 und 120×40 mit je eigener
|
||||
Sollausgabe, plus ein Programm mit Größenwechsel mitten im Lauf.
|
||||
- [x] Breite Unicode-Zeichen belegen **zwei Zellen** (Emoji, CJK):
|
||||
Cursorfortschritt, `POS`, Umbruch am rechten Rand und das Leeren der
|
||||
Gegenhälfte beim Überschreiben.
|
||||
- [x] Bildschirm: `CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT`,
|
||||
`SCREEN` (Anweisung und Funktion), `CSRLIN`, `POS`, `TAB`, `SPC`
|
||||
- [x] Tastatur: `INKEY$` (nicht blockierend, Sondertasten als
|
||||
`CHR$(0)+code`), `INPUT$`, Funktionstasten-Makros `KEY n, text$` /
|
||||
`KEY LIST` / `KEY ON|OFF` (Umfang aus der Original-Hilfe geklärt)
|
||||
- [x] Strings vollständig; Randfälle (`VAL`-Präfix, `STR$`-Vorzeichenspalte,
|
||||
Fehler 5 bei Startposition < 1) im Korpus verankert
|
||||
- [x] Zahlenformatierung: Druckzonen, `PRINT USING`/`LPRINT USING`/
|
||||
`FORMAT$` vollständig inklusive `%`-Präfix bei Feldüberlauf und
|
||||
`SetFormatCC`; Banker's Rounding; `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR`
|
||||
- [x] Mathematik inklusive **kompatiblem PRNG**: gleiche Saat ergibt
|
||||
dieselbe Folge, `RND(0)` wiederholt, `RANDOMIZE` ohne Argument
|
||||
erfragt den Startwert; Algorithmus dokumentiert
|
||||
- [x] **Finanzmathematik** (Befund des Inventars — im ursprünglichen
|
||||
Umfang übersehen): `FV#`, `PV#`, `Pmt#`, `IPmt#`, `PPmt#`, `NPer#`,
|
||||
`Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`, `SYD#`, `DDB#`
|
||||
- [x] Datum und Zeit: Serienwerte, `DATESERIAL`/`TIMESERIAL`,
|
||||
`DATEVALUE`/`TIMEVALUE`, `DAY`…`SECOND`, `NOW`, `DATE$`/`TIME$` als
|
||||
Funktion und Anweisung
|
||||
- [x] Datei-E/A: `OPEN` in beiden Syntaxen (sequenziell, `RANDOM`,
|
||||
`BINARY`), `PRINT #`/`WRITE #`/`INPUT #`/`LINE INPUT #`,
|
||||
`GET`/`PUT` mit Recordpuffer **und** mit UDT-Variablen,
|
||||
`FIELD`/`LSET`/`RSET`, `EOF`/`LOF`/`LOC`/`SEEK`/`FREEFILE`/
|
||||
`FILEATTR`, `LOCK`/`UNLOCK`, `LPRINT`/`LPOS`, die
|
||||
Record-Konvertierungsfunktionen (`MK…$`/`CV…`) und die
|
||||
Dateisystem-Anweisungen. Feste Strings als **UTF-32**.
|
||||
- [x] System: `ENVIRON`/`ENVIRON$`, `COMMAND$`, `FRE`, `CLEAR`,
|
||||
`TRON`/`TROFF`, `STACK`, `ERDEV`/`ERDEV$`, `ERR`-Anweisung
|
||||
- [x] Aus Phase 2 verlagert — `ON ERROR GOTO`-Scoping: ein modulweiter
|
||||
Handler ist jetzt aus Prozeduren heraus ansprechbar (Sprungziel im
|
||||
Modulrumpf, prozedurübergreifender Fixup im Codegenerator); nur
|
||||
`ON LOCAL ERROR` bleibt prozedurlokal
|
||||
- [x] Aus Phase 2 verlagert — DATA-Rohtext: der Lexer liefert den
|
||||
Anweisungstext unverändert, unquotierte Elemente behalten Groß-/
|
||||
Kleinschreibung und inneren Leerraum
|
||||
- [x] Aus Phase 2 verlagert — die vier `TODO verify`-Zellen der
|
||||
Konvertierungsmatrix sind aufgelöst. Die Original-Hilfe schweigt zu
|
||||
allen vieren; sie sind daher als datierte **Projektentscheidung**
|
||||
begründet dokumentiert und in `konvertierung.bas` verankert.
|
||||
- [x] **Meilenstein: klassische Konsolenprogramme laufen unverändert** —
|
||||
alle 22 Korpusprogramme laufen ohne Laufzeitfehler 73; der Fehler
|
||||
tritt nur noch bei bewusst offenen Elementen auf (ISAM, Forms,
|
||||
Ereignissteuerung).
|
||||
- [ ] **ISAM** (Change `phase-3-isam`): `OPEN … FOR ISAM`, Indizes,
|
||||
Cursor, Satzoperationen, Transaktionen. Speicherschicht `redb`
|
||||
(Entscheidung 2026-09-03). Damit endet Phase 3.
|
||||
|
||||
**Befunde dieser Phase, die den Plan korrigiert haben**
|
||||
|
||||
- Das Inventar deckte am ersten Tag zwölf Non-Features auf, die der
|
||||
Compiler entgegen der Phase-1-Spec **nicht** abwies (`CALLS`, `SSEG`,
|
||||
`POINT`, `VIEW`, `COM`, `PEN`, `STRIG`, `STICK`, die
|
||||
`String*`-Routinen sowie `LINE` und `OPEN "COMn:"` an ihrer Syntax).
|
||||
Alle geschlossen.
|
||||
- Drei ganze Gruppen fehlten im ursprünglichen Umfang: Finanzmathematik,
|
||||
Record-Konvertierung und ein Rest an Dateisystemfunktionen.
|
||||
- `bas7advr.hlp` allein ist **keine** vollständige Quelle: `SGN`, `SQR`,
|
||||
`SPACE$`, `STRING$`, `CSRLIN`, `BEEP` und `DATE$` als Funktion fehlen
|
||||
dort. Das Inventar bildet deshalb die Vereinigung mit `qb45advr.hlp`.
|
||||
|
||||
**Offene Entscheidungen, die in spätere Phasen zeigen**
|
||||
|
||||
- Die Uhr lief zunächst in **UTC**, weil Ortszeit eine
|
||||
Zeitzonenbibliothek gebraucht hätte. Entscheidung 2026-09-04: die
|
||||
Bibliothek kommt, die Abweichung wird zurückgezogen → Change
|
||||
`phase-3-ortszeit`.
|
||||
- `LPRINT` schreibt am Programmende nach `LPT1.TXT` im Arbeitsverzeichnis;
|
||||
einen plattformübergreifenden Druckerkanal gibt es nicht.
|
||||
- Neue Abhängigkeit `unicode-width` (Doppelzellen). Für ISAM kommt `redb`
|
||||
hinzu.
|
||||
|
||||
## Phase 4 — Forms-Engine (`tb-ui::forms`)
|
||||
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-
|
||||
@@ -370,10 +431,14 @@ bleibt gültig. Noch nichts davon ist beschlossen; Sammlung wächst:
|
||||
mit Record-Buffer-Datentypen, auf denen `SELECT`/`UPDATE`/`INSERT`/
|
||||
`DELETE` arbeiten (natürliche Weiterentwicklung der ISAM-Idee auf
|
||||
SQLite als Speicher)
|
||||
- **Runtime-Bibliotheken als BASIC-Bibliotheken** (2026-09-02): in der
|
||||
Runtime integrierte Rust-Bibliotheken (z. B. crossterm für direkte
|
||||
Terminalsteuerung) als eigene, importierbare Bibliotheken für
|
||||
BASIC-Programme bereitstellen
|
||||
- **Runtime-Bibliotheken als BASIC-Bibliotheken** (2026-09-02, erweitert
|
||||
2026-09-04): in der Runtime integrierte Rust-Bibliotheken als eigene,
|
||||
importierbare Bibliotheken für BASIC-Programme bereitstellen —
|
||||
- **crossterm** für direkte Terminalsteuerung
|
||||
- **die Zeitzonenbibliothek** (ab dem Change `phase-3-ortszeit` in der
|
||||
Laufzeit): Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung
|
||||
zwischen Zonen. Der Kernstandard braucht davon nur den Versatz für
|
||||
die Ortszeit; alles Weitere wäre eine Erweiterung.
|
||||
- Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten
|
||||
|
||||
---
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::ConsoleHost;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_ui::host::TerminalHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -90,8 +91,36 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
};
|
||||
let mut vm = Vm::new(module);
|
||||
vm.rt.command = args[1..].join(" ");
|
||||
let mut host = ConsoleHost;
|
||||
match vm.run(&mut host) {
|
||||
// Im Rohmodus ist Strg+C kein Signal mehr — der Abbruch kommt als
|
||||
// Ereignis und muss den Lauf beenden können.
|
||||
vm.set_poll_interrupt(true);
|
||||
|
||||
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
|
||||
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
|
||||
let ereignis = match TerminalHost::new() {
|
||||
Ok(mut host) => {
|
||||
// Der Bildschirm folgt der Terminalgröße von Beginn an (80×25 ist
|
||||
// stets nur die untere Schranke, nie eine feste Größe).
|
||||
if let Ok((cols, rows)) = host.groesse() {
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
}
|
||||
let e = vm.run(&mut host);
|
||||
drop(host); // Alternativschirm verlassen, bevor gedruckt wird
|
||||
e
|
||||
}
|
||||
Err(_) => vm.run(&mut PipeHost::new()),
|
||||
};
|
||||
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
|
||||
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
|
||||
// Datei LPT1.TXT im aktuellen Verzeichnis (dokumentierte Abweichung —
|
||||
// einen Druckerkanal gibt es plattformübergreifend nicht).
|
||||
if !vm.rt.print.drucker.is_empty() {
|
||||
if let Err(e) = std::fs::write("LPT1.TXT", &vm.rt.print.drucker) {
|
||||
eprintln!("Druckerausgabe nicht schreibbar: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match ereignis {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
@@ -103,8 +132,61 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } | RunEvent::Interrupted { .. } => {
|
||||
ExitCode::from(2)
|
||||
RunEvent::Interrupted { line } => {
|
||||
eprintln!("Abgebrochen in Zeile {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Host ohne Terminal: für Pipes und Skripte (`tbc run x.bas < eingabe.txt`).
|
||||
/// Zeigt während des Laufs nichts an; die Ausgabe entsteht am Ende aus dem
|
||||
/// Bildschirm-Snapshot. Tastendrücke kommen zeilenweise von stdin.
|
||||
struct PipeHost {
|
||||
puffer: std::collections::VecDeque<Ereignis>,
|
||||
eof: bool,
|
||||
}
|
||||
|
||||
impl PipeHost {
|
||||
fn new() -> Self {
|
||||
PipeHost { puffer: std::collections::VecDeque::new(), eof: false }
|
||||
}
|
||||
|
||||
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
||||
fn nachfuellen(&mut self) {
|
||||
use std::io::BufRead;
|
||||
if self.eof {
|
||||
return;
|
||||
}
|
||||
let mut zeile = String::new();
|
||||
match std::io::stdin().lock().read_line(&mut zeile) {
|
||||
Ok(0) | Err(_) => {
|
||||
self.eof = true;
|
||||
self.puffer.push_back(Ereignis::Ende);
|
||||
}
|
||||
Ok(_) => {
|
||||
while zeile.ends_with('\n') || zeile.ends_with('\r') {
|
||||
zeile.pop();
|
||||
}
|
||||
for c in zeile.chars() {
|
||||
self.puffer.push_back(Ereignis::Taste(c.to_string()));
|
||||
}
|
||||
self.puffer
|
||||
.push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for PipeHost {
|
||||
fn present(&mut self, _screen: &tb_runtime::screen::TextScreen) {}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
if self.puffer.is_empty() && blockierend {
|
||||
self.nachfuellen();
|
||||
}
|
||||
self.puffer.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
//! Kompatibilitäts-Harness (Phase-2-Meilenstein): jede Korpusdatei
|
||||
//! `tests/compat/*.bas` wird kompiliert, im Capture-Host ausgeführt und
|
||||
//! byte-genau gegen ihre `.out` verglichen. Bei Abweichung nennt der
|
||||
//! Test Datei, erste abweichende Zeile sowie Soll und Ist.
|
||||
//! Kompatibilitäts-Harness: jede Korpusdatei `tests/compat/*.bas` wird
|
||||
//! kompiliert, ohne Terminal ausgeführt und ihr Bildschirmzustand gegen die
|
||||
//! `.out` verglichen — getrimmt auf die belegte Fläche, mit Attributebene nur
|
||||
//! bei gesetzten Farben (Entscheidung 2026-09-03, D3).
|
||||
//!
|
||||
//! Jede Korpusdatei nennt ihre Bildschirmgröße in der ersten Zeile:
|
||||
//!
|
||||
//! ```text
|
||||
//! ' tb-screen: 80x25
|
||||
//! ```
|
||||
//!
|
||||
//! Es gibt bewusst keinen stillschweigenden Vorgabewert — 80×25 ist überall
|
||||
//! nur die untere Schranke, nie eine feste Größe, und der Korpus muss das
|
||||
//! nachweisen können.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -12,43 +22,185 @@ fn compat_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat")
|
||||
}
|
||||
|
||||
fn run_corpus_file(path: &Path) -> String {
|
||||
/// `<cols>x<rows>` lesen.
|
||||
fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) {
|
||||
let (c, r) = wert
|
||||
.trim()
|
||||
.split_once(['x', 'X'])
|
||||
.unwrap_or_else(|| panic!("{name}: Größe {wert:?} nicht als <spalten>x<zeilen> lesbar"));
|
||||
let parse = |s: &str, was: &str| {
|
||||
s.trim()
|
||||
.parse::<usize>()
|
||||
.unwrap_or_else(|_| panic!("{name}: {was} {s:?} ist keine Zahl"))
|
||||
};
|
||||
(parse(c, "Spaltenzahl"), parse(r, "Zeilenzahl"))
|
||||
}
|
||||
|
||||
/// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen.
|
||||
fn direktive(src: &str, marke: &str) -> Option<String> {
|
||||
src.lines()
|
||||
.take(4)
|
||||
.find(|l| l.contains(marke))
|
||||
.map(|l| l.split(marke).nth(1).unwrap().trim().to_string())
|
||||
}
|
||||
|
||||
/// Bildschirmgrößen aus der Kopfzeile `' tb-screen: <cols>x<rows>[, …]`.
|
||||
/// Fehlt sie, schlägt der Test fehl — ohne Vorgabewert. Mehrere Größen
|
||||
/// bedeuten: das Programm läuft je Größe einmal, mit eigener Sollausgabe.
|
||||
fn screen_groessen(name: &str, src: &str) -> Vec<(usize, usize)> {
|
||||
let wert = direktive(src, "tb-screen:").unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{name}: keine Bildschirmgröße deklariert — erste Zeile braucht \
|
||||
`\' tb-screen: <spalten>x<zeilen>`"
|
||||
)
|
||||
});
|
||||
wert.split(',').map(|w| groesse_lesen(name, w)).collect()
|
||||
}
|
||||
|
||||
/// Optionale Größenänderung mitten im Lauf:
|
||||
/// `' tb-resize: nach <n> auf <cols>x<rows>`.
|
||||
fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
|
||||
let wert = direktive(src, "tb-resize:")?;
|
||||
let teile: Vec<&str> = wert.split_whitespace().collect();
|
||||
match teile.as_slice() {
|
||||
["nach", n, "auf", g] => Some((
|
||||
n.parse()
|
||||
.unwrap_or_else(|_| panic!("{name}: {n:?} ist keine Zahl")),
|
||||
groesse_lesen(name, g),
|
||||
)),
|
||||
_ => panic!("{name}: tb-resize erwartet `nach <n> auf <spalten>x<zeilen>`, fand {wert:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit
|
||||
/// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen
|
||||
/// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt.
|
||||
struct TempVerzeichnis {
|
||||
vorher: PathBuf,
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl TempVerzeichnis {
|
||||
fn neu(name: &str) -> TempVerzeichnis {
|
||||
let vorher = std::env::current_dir().unwrap();
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"tb_korpus_{}_{}",
|
||||
name.replace('.', "_"),
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
TempVerzeichnis { vorher, dir }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempVerzeichnis {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::env::set_current_dir(&self.vorher);
|
||||
let _ = std::fs::remove_dir_all(&self.dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// Korpusdatei ausführen und den Bildschirm-Snapshot liefern.
|
||||
fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
|
||||
let src = std::fs::read_to_string(path).unwrap();
|
||||
let name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
|
||||
let module = tb_vm::compile_source(&name, &src)
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let modul_name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
|
||||
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, &src)[0]);
|
||||
let module = tb_vm::compile_source(&modul_name, &src)
|
||||
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
|
||||
let mut vm = Vm::new(module);
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
// `' tb-tempdir` — das Programm arbeitet mit Dateien.
|
||||
let _temp = direktive(&src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
|
||||
let mut host = CaptureHost::default();
|
||||
if let Some((nach, (c, r))) = resize_direktive(&name, &src) {
|
||||
host.ereignis_nach(
|
||||
nach,
|
||||
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
|
||||
);
|
||||
}
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Ended => host.output,
|
||||
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
||||
other => panic!(
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nAusgabe bisher:\n{}",
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
|
||||
path.display(),
|
||||
host.output
|
||||
tb_runtime::snapshot::snapshot(&vm.rt.screen)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Erste abweichende Zeile melden (byte-genau, inkl. Leerzeichen am Ende).
|
||||
/// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
|
||||
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
|
||||
fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
if want == got {
|
||||
return;
|
||||
}
|
||||
let want_lines: Vec<&str> = want.split('\n').collect();
|
||||
let got_lines: Vec<&str> = got.split('\n').collect();
|
||||
for (i, (w, g)) in want_lines.iter().zip(got_lines.iter()).enumerate() {
|
||||
if w != g {
|
||||
let marke = tb_runtime::snapshot::ATTRIBUT_MARKE;
|
||||
let teile = |s: &str| match s.split_once(&format!("{marke}\n")) {
|
||||
Some((t, a)) => (t.to_string(), Some(a.to_string())),
|
||||
None => (s.to_string(), None),
|
||||
};
|
||||
let (want_text, want_attr) = teile(want);
|
||||
let (got_text, got_attr) = teile(got);
|
||||
|
||||
if want_text != got_text {
|
||||
// `lines()` statt `split('\n')`: Snapshots enden je Zeile mit `\n`,
|
||||
// sonst zählt der leere Rest als zusätzliche Zeile und die
|
||||
// Zeilenanzahl-Meldung wäre nie erreichbar.
|
||||
let w: Vec<&str> = want_text.lines().collect();
|
||||
let g: Vec<&str> = got_text.lines().collect();
|
||||
for (i, (a, b)) in w.iter().zip(g.iter()).enumerate() {
|
||||
if a != b {
|
||||
panic!(
|
||||
"{file}: Abweichung in Zeile {}:\n Soll: {w:?}\n Ist: {g:?}",
|
||||
"{file}: Abweichung in Zeile {}:\n Soll: {a:?}\n Ist: {b:?}",
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
panic!(
|
||||
"{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want}\nIst:\n{got}",
|
||||
want_lines.len(),
|
||||
got_lines.len()
|
||||
"{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want_text}\nIst:\n{got_text}",
|
||||
w.len(),
|
||||
g.len()
|
||||
);
|
||||
}
|
||||
|
||||
match (want_attr, got_attr) {
|
||||
(Some(w), None) => panic!("{file}: Attributebene fehlt in der Ausgabe.\nSoll:\n{w}"),
|
||||
(None, Some(g)) => panic!(
|
||||
"{file}: unerwartete Attributebene — das Programm setzt Farben, \
|
||||
die Sollausgabe kennt keine.\nIst:\n{g}"
|
||||
),
|
||||
(Some(w), Some(g)) => {
|
||||
for (z, (wz, gz)) in w.lines().zip(g.lines()).enumerate() {
|
||||
if wz != gz {
|
||||
// Je Zelle zwei Hexziffern → Spalte aus dem Byteoffset.
|
||||
let spalte = wz
|
||||
.chars()
|
||||
.zip(gz.chars())
|
||||
.position(|(a, b)| a != b)
|
||||
.map(|i| i / 2 + 1)
|
||||
.unwrap_or(0);
|
||||
let paar = |s: &str| {
|
||||
s.chars()
|
||||
.skip((spalte - 1) * 2)
|
||||
.take(2)
|
||||
.collect::<String>()
|
||||
};
|
||||
panic!(
|
||||
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
|
||||
Soll: {}\n Ist: {}",
|
||||
z + 1,
|
||||
paar(wz),
|
||||
paar(gz)
|
||||
);
|
||||
}
|
||||
}
|
||||
panic!("{file}: Attributebenen unterschiedlich lang.\nSoll:\n{w}\nIst:\n{g}");
|
||||
}
|
||||
(None, None) => unreachable!("Snapshots gleich, aber Vergleich schlug fehl"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -63,16 +215,26 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let out_path = path.with_extension("out");
|
||||
let src = std::fs::read_to_string(&path).unwrap();
|
||||
let groessen = screen_groessen(&name, &src);
|
||||
for (cols, rows) in &groessen {
|
||||
// Bei mehreren Größen trägt jede ihre eigene Sollausgabe.
|
||||
let out_path = if groessen.len() == 1 {
|
||||
path.with_extension("out")
|
||||
} else {
|
||||
path.with_extension(format!("{cols}x{rows}.out"))
|
||||
};
|
||||
let want = std::fs::read_to_string(&out_path)
|
||||
.unwrap_or_else(|_| panic!("{name}: Sollausgabe {} fehlt", out_path.display()));
|
||||
// .out-Dateien sind LF-normiert (.gitattributes); zur Sicherheit
|
||||
// CRLF des Checkouts entfernen.
|
||||
let want = want.replace("\r\n", "\n");
|
||||
let got = run_corpus_file(&path);
|
||||
assert_output_matches(&name, &want, &got);
|
||||
let got = run_corpus_file(&path, Some((*cols, *rows)));
|
||||
let etikett = format!("{name} @ {cols}x{rows}");
|
||||
assert_output_matches(&etikett, &want, &got);
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
|
||||
}
|
||||
|
||||
@@ -130,3 +292,96 @@ fn tbc_build_erzeugt_tbc() {
|
||||
let _ = std::fs::remove_file(&f);
|
||||
let _ = std::fs::remove_file(&tbc);
|
||||
}
|
||||
|
||||
// ---- Harness meldet Abweichungen benannt (Spec kompat-testkorpus) ----------
|
||||
|
||||
fn meldung(want: &str, got: &str) -> String {
|
||||
let r = std::panic::catch_unwind(|| assert_output_matches("probe.bas", want, got));
|
||||
let e = r.expect_err("erwartete eine Abweichung");
|
||||
e.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn textabweichung_nennt_zeile_soll_und_ist() {
|
||||
let m = meldung("a\nb\n", "a\nX\n");
|
||||
assert!(m.contains("Zeile 2"), "{m}");
|
||||
assert!(m.contains("\"b\""), "{m}");
|
||||
assert!(m.contains("\"X\""), "{m}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeilenanzahl_wird_benannt() {
|
||||
let m = meldung("a\nb\n", "a\n");
|
||||
assert!(m.contains("Zeilenanzahl"), "{m}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn farbabweichung_nennt_zeile_spalte_soll_und_ist() {
|
||||
let marke = tb_runtime::snapshot::ATTRIBUT_MARKE;
|
||||
let want = format!("ab\n{marke}\n7070\n");
|
||||
let got = format!("ab\n{marke}\n70e1\n");
|
||||
let m = meldung(&want, &got);
|
||||
assert!(m.contains("Farbabweichung"), "{m}");
|
||||
assert!(m.contains("Zeile 1"), "{m}");
|
||||
assert!(m.contains("Spalte 2"), "{m}");
|
||||
assert!(m.contains("70") && m.contains("e1"), "{m}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlende_und_unerwartete_attributebene_werden_benannt() {
|
||||
let marke = tb_runtime::snapshot::ATTRIBUT_MARKE;
|
||||
let mit = format!("ab\n{marke}\n7070\n");
|
||||
assert!(meldung(&mit, "ab\n").contains("Attributebene fehlt"));
|
||||
assert!(meldung("ab\n", &mit).contains("unerwartete Attributebene"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlende_groessenangabe_wird_abgewiesen() {
|
||||
let dir = std::env::temp_dir().join("tb_korpus_ohne_groesse");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let f = dir.join("ohne.bas");
|
||||
std::fs::write(&f, "PRINT 1\n").unwrap();
|
||||
let r = std::panic::catch_unwind(|| run_corpus_file(&f, None));
|
||||
let e = r.expect_err("Datei ohne Größenangabe muss abgewiesen werden");
|
||||
let m = e
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
assert!(m.contains("keine Bildschirmgröße deklariert"), "{m}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Sollausgaben neu erzeugen (nur auf Anforderung):
|
||||
/// `cargo test -p tb-cli --test compat -- --ignored erzeuge_sollausgaben`
|
||||
///
|
||||
/// Die erzeugten Dateien sind zu prüfen, bevor sie eingecheckt werden — ein
|
||||
/// unerwarteter Diff ist ein Befund, kein Anpassungsgrund.
|
||||
#[test]
|
||||
#[ignore = "erzeugt Golden Files; nur bewusst aufrufen"]
|
||||
fn erzeuge_sollausgaben() {
|
||||
let dir = compat_dir();
|
||||
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/compat fehlt")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let src = std::fs::read_to_string(&path).unwrap();
|
||||
let groessen = screen_groessen(&name, &src);
|
||||
for (cols, rows) in &groessen {
|
||||
let out_path = if groessen.len() == 1 {
|
||||
path.with_extension("out")
|
||||
} else {
|
||||
path.with_extension(format!("{cols}x{rows}.out"))
|
||||
};
|
||||
let got = run_corpus_file(&path, Some((*cols, *rows)));
|
||||
std::fs::write(&out_path, &got).unwrap();
|
||||
println!("geschrieben: {}", out_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,10 +235,97 @@ pub enum Builtin {
|
||||
PrintTab,
|
||||
PrintSpc,
|
||||
PrintNewline,
|
||||
PrintUsing,
|
||||
FormatS,
|
||||
SetFormatCc,
|
||||
// Bildschirm (Anweisungen)
|
||||
Cls,
|
||||
Color,
|
||||
Locate,
|
||||
Width,
|
||||
ViewPrint,
|
||||
ScreenStmt,
|
||||
KeyAssign,
|
||||
KeyList,
|
||||
KeyDisplay,
|
||||
// Bildschirm (Funktionen)
|
||||
Csrlin,
|
||||
PosFn,
|
||||
ScreenFn,
|
||||
// Tastatur
|
||||
InkeyS,
|
||||
InputS,
|
||||
// System
|
||||
EnvironS,
|
||||
EnvironSet,
|
||||
Fre,
|
||||
Clear,
|
||||
Tron,
|
||||
Troff,
|
||||
StackFn,
|
||||
StackStmt,
|
||||
Erdev,
|
||||
ErdevS,
|
||||
// Datei-E/A
|
||||
Open,
|
||||
Close,
|
||||
CloseAll,
|
||||
PrintZiel,
|
||||
WriteFile,
|
||||
EofF,
|
||||
LofF,
|
||||
LocF,
|
||||
SeekF,
|
||||
SeekStmt,
|
||||
Freefile,
|
||||
Fileattr,
|
||||
LockStmt,
|
||||
Kill,
|
||||
NameStmt,
|
||||
Files,
|
||||
Chdir,
|
||||
Chdrive,
|
||||
Mkdir,
|
||||
Rmdir,
|
||||
CurdirS,
|
||||
DirS,
|
||||
Lpos,
|
||||
ShellStmt,
|
||||
ShellFn,
|
||||
MkS,
|
||||
CvF,
|
||||
// Finanzmathematik
|
||||
Fv,
|
||||
Pv,
|
||||
Pmt,
|
||||
NPer,
|
||||
IPmt,
|
||||
PPmt,
|
||||
Rate,
|
||||
Npv,
|
||||
Irr,
|
||||
Mirr,
|
||||
Sln,
|
||||
Syd,
|
||||
Ddb,
|
||||
// Sonstiges
|
||||
Timer,
|
||||
DateS,
|
||||
TimeS,
|
||||
DateSet,
|
||||
TimeSet,
|
||||
Now,
|
||||
DateSerial,
|
||||
TimeSerial,
|
||||
DateValue,
|
||||
TimeValue,
|
||||
DayF,
|
||||
MonthF,
|
||||
YearF,
|
||||
WeekdayF,
|
||||
HourF,
|
||||
MinuteF,
|
||||
SecondF,
|
||||
CommandS,
|
||||
Doevents,
|
||||
Sleep,
|
||||
@@ -365,7 +452,29 @@ pub enum HStmtKind {
|
||||
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?
|
||||
trailing: bool,
|
||||
},
|
||||
/// `FIELD #n, laenge AS var$, …` — Recordpuffer in Felder aufteilen.
|
||||
Field {
|
||||
file: HExpr,
|
||||
fields: Vec<(HExpr, HPlace)>,
|
||||
},
|
||||
/// `LSET`/`RSET` — links- bzw. rechtsbündig zuweisen; ist das Ziel ein
|
||||
/// `FIELD`-Feld, wirkt die Zuweisung zugleich auf den Recordpuffer.
|
||||
LsetRset {
|
||||
rset: bool,
|
||||
target: HPlace,
|
||||
value: HExpr,
|
||||
},
|
||||
/// `GET`/`PUT` auf einer Datei: Datensatz- bzw. Bytenummer und Ziel-
|
||||
/// bzw. Quellvariable. Ohne Variable wirkt der Recordpuffer.
|
||||
GetPut {
|
||||
put: bool,
|
||||
file: HExpr,
|
||||
recnum: Option<HExpr>,
|
||||
var: Option<HPlace>,
|
||||
},
|
||||
Input {
|
||||
/// `INPUT #n` — Quelle ist eine Datei statt der Tastatur.
|
||||
file: Option<HExpr>,
|
||||
line_mode: bool,
|
||||
prompt: Option<String>,
|
||||
/// Fragezeichen nach dem Prompt (`;`-Form).
|
||||
@@ -425,6 +534,8 @@ pub enum HStmtKind {
|
||||
Resume(HResume),
|
||||
/// `ERROR n`.
|
||||
RaiseError(HExpr),
|
||||
/// `ERR = n` — Fehlercode setzen, ohne einen Fehler auszulösen.
|
||||
SetErr(HExpr),
|
||||
Read(Vec<HPlace>),
|
||||
/// Ziel als Index in `HirModule::data` (0 = Anfang).
|
||||
Restore(u32),
|
||||
|
||||
@@ -119,6 +119,10 @@ pub enum TokenKind {
|
||||
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
|
||||
MetaStatic,
|
||||
MetaDynamic,
|
||||
/// Rohtext einer `DATA`-Anweisung bis zum Anweisungsende. Er wird
|
||||
/// bewusst **nicht** zerlegt oder normalisiert: unquotierte Elemente
|
||||
/// behalten ihre Groß- und Kleinschreibung und ihren inneren Leerraum.
|
||||
DataRaw(String),
|
||||
/// Ende einer logischen Zeile.
|
||||
Eol,
|
||||
Eof,
|
||||
@@ -432,6 +436,27 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
break 'line;
|
||||
}
|
||||
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
|
||||
if kw == Kw::Data {
|
||||
// Der Rest der Anweisung ist Rohtext: bis zum
|
||||
// `:` außerhalb von Anführungszeichen oder bis
|
||||
// zum Zeilenende.
|
||||
let start = i;
|
||||
let mut in_quote = false;
|
||||
while i < chars.len() {
|
||||
match chars[i] {
|
||||
'"' => in_quote = !in_quote,
|
||||
':' if !in_quote => break,
|
||||
'\'' if !in_quote => break,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let roh: String = chars[start..i].iter().collect();
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::DataRaw(roh),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,9 +218,12 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::Kw(Kw::Line) => {
|
||||
self.advance();
|
||||
if self.expect_kw(Kw::Input, "INPUT") {
|
||||
if self.k() == TokenKind::Kw(Kw::Input) {
|
||||
self.advance();
|
||||
self.parse_input(true, pos)
|
||||
} else {
|
||||
// Grafikform LINE (x1,y1)-(x2,y2): deklariertes Non-Feature.
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
None
|
||||
}
|
||||
@@ -474,28 +477,40 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::Kw(Kw::Data) => {
|
||||
self.advance();
|
||||
// Der Lexer liefert den Rohtext; hier wird nur an Kommas
|
||||
// außerhalb von Anführungszeichen getrennt. Unquotierte
|
||||
// Elemente verlieren nur den Leerraum an den Rändern.
|
||||
let roh = match self.k() {
|
||||
TokenKind::DataRaw(t) => {
|
||||
self.advance();
|
||||
t
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
let mut items = Vec::new();
|
||||
let mut cur = String::new();
|
||||
while !self.at_stmt_end() {
|
||||
match self.k() {
|
||||
TokenKind::Comma => {
|
||||
items.push(cur.trim().to_string());
|
||||
cur = String::new();
|
||||
let mut in_quote = false;
|
||||
let mut quotiert = false;
|
||||
for c in roh.chars() {
|
||||
match c {
|
||||
'"' if !in_quote && cur.trim().is_empty() => {
|
||||
in_quote = true;
|
||||
quotiert = true;
|
||||
cur.clear();
|
||||
}
|
||||
TokenKind::Str(s) => cur.push_str(&s),
|
||||
TokenKind::Num(n) => cur.push_str(&num_text(n)),
|
||||
TokenKind::Ident { name, suffix } => {
|
||||
cur.push_str(&name);
|
||||
if let Some(sfx) = suffix {
|
||||
cur.push(sfx.as_char());
|
||||
'"' if in_quote => in_quote = false,
|
||||
',' if !in_quote => {
|
||||
items.push(if quotiert {
|
||||
std::mem::take(&mut cur)
|
||||
} else {
|
||||
std::mem::take(&mut cur).trim().to_string()
|
||||
});
|
||||
quotiert = false;
|
||||
}
|
||||
_ => cur.push(c),
|
||||
}
|
||||
}
|
||||
TokenKind::Minus => cur.push('-'),
|
||||
_ => {}
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
items.push(cur.trim().to_string());
|
||||
items.push(if quotiert { cur } else { cur.trim().to_string() });
|
||||
Some(Stmt::Data { items, pos })
|
||||
}
|
||||
TokenKind::Kw(Kw::Read) => {
|
||||
@@ -1401,6 +1416,14 @@ impl<'a> P<'a> {
|
||||
|
||||
fn parse_def_fn(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
self.advance(); // DEF
|
||||
// `DEF SEG` ist Segmentadressierung und damit deklariertes
|
||||
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
||||
// `DEF FNname` einleitet.
|
||||
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
return None;
|
||||
}
|
||||
let (name, suffix) = match self.k() {
|
||||
TokenKind::Ident { name, suffix } if name.starts_with("FN") => {
|
||||
self.advance();
|
||||
@@ -1710,6 +1733,23 @@ impl<'a> P<'a> {
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Option<Expr> {
|
||||
// `SEEK(n)` ist zugleich Funktion und Anweisung; in Ausdrucksposition
|
||||
// gilt die Funktionsform.
|
||||
if self.k() == TokenKind::Kw(Kw::Seek) && self.k_at(1) == TokenKind::LParen {
|
||||
let pos = self.pos();
|
||||
self.advance(); // SEEK
|
||||
self.advance(); // (
|
||||
let arg = self.parse_expr()?;
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
self.err("Expected: )");
|
||||
}
|
||||
return Some(Expr::Name {
|
||||
name: "SEEK".into(),
|
||||
suffix: None,
|
||||
args: Some(vec![arg]),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
match self.k() {
|
||||
TokenKind::Num(n) => {
|
||||
self.advance();
|
||||
@@ -1743,16 +1783,6 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn num_text(n: NumValue) -> String {
|
||||
match n {
|
||||
NumValue::Int(v) => v.to_string(),
|
||||
NumValue::Long(v) => v.to_string(),
|
||||
NumValue::Single(v) => v.to_string(),
|
||||
NumValue::Double(v) => v.to_string(),
|
||||
NumValue::Currency(v) => format!("{}", v as f64 / 10_000.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -247,6 +247,33 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
||||
"DOEVENTS" => (0, 0, &[], I),
|
||||
"MSGBOX" => (1, 3, &[S, N, S], I),
|
||||
"INPUTBOX$" => (1, 5, &[S, S, S, N, N], St),
|
||||
// Finanzmathematik (Original-Hilfe schreibt sie mit `#`; beide
|
||||
// Schreibweisen werden angenommen, gerechnet wird in DOUBLE).
|
||||
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
|
||||
(5, 5, &[N, N, N, N, N], Db)
|
||||
}
|
||||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
|
||||
(6, 6, &[N, N, N, N, N, N], Db)
|
||||
}
|
||||
"NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db),
|
||||
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
|
||||
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
|
||||
"SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, N, N, N], Db),
|
||||
// Record-Konvertierung (Zahl ↔ Bytedarstellung im Feldpuffer)
|
||||
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" => (1, 1, &[N], St),
|
||||
"MKSMBF$" | "MKDMBF$" => (1, 1, &[N], St),
|
||||
"CVI" => (1, 1, &[S], I),
|
||||
"CVL" => (1, 1, &[S], L),
|
||||
"CVS" | "CVSMBF" => (1, 1, &[S], Sg),
|
||||
"CVD" | "CVDMBF" => (1, 1, &[S], Db),
|
||||
"CVC" => (1, 1, &[S], Cu),
|
||||
// Dateisystem und System
|
||||
"CURDIR$" => (0, 1, &[S], St),
|
||||
"DIR$" => (0, 1, &[S], St),
|
||||
"LPOS" => (1, 1, &[N], I),
|
||||
"STACK" => (0, 0, &[], L),
|
||||
"ERDEV" => (0, 0, &[], I),
|
||||
"ERDEV$" => (0, 0, &[], St),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -261,16 +288,37 @@ fn banned_feature(name: &str) -> bool {
|
||||
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
|
||||
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
|
||||
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
|
||||
// Interlanguage-Schnittstelle
|
||||
| "CALLS" | "SSEG" | "SSEGADD"
|
||||
| "STRINGADDRESS" | "STRINGASSIGN" | "STRINGLENGTH" | "STRINGRELEASE"
|
||||
// Overlay-Mechanismus
|
||||
| "CHAIN"
|
||||
// Grafik
|
||||
| "PSET" | "PRESET" | "CIRCLE" | "PAINT" | "DRAW" | "PALETTE"
|
||||
| "PCOPY" | "PMAP" | "WINDOW"
|
||||
| "PCOPY" | "PMAP" | "WINDOW" | "POINT" | "VIEW"
|
||||
// Event-Geräte (auch als Funktion in `ON COM(1) GOSUB …`)
|
||||
| "COM" | "PEN" | "STRIG" | "STICK"
|
||||
// Klang (außer BEEP)
|
||||
| "SOUND" | "PLAY"
|
||||
)
|
||||
}
|
||||
|
||||
/// Non-Features, die nicht über ihren Namen abgewiesen werden können, weil
|
||||
/// sie ihr Schlüsselwort mit einer unterstützten Form teilen. Sie werden an
|
||||
/// der Syntax erkannt: `GET`/`PUT` mit `(` (Grafik) und `LINE` ohne `INPUT`
|
||||
/// im Parser, `OPEN` mit einem `COMn:`-Gerätenamen hier in der Semantik.
|
||||
/// Der Inventar-Abgleich liest diese Liste.
|
||||
pub const SYNTAKTISCH_ABGEWIESEN: &[&str] =
|
||||
&["GET (Grafik)", "PUT (Grafik)", "LINE", "OPEN COM", "DEF SEG"];
|
||||
|
||||
/// Ist `s` ein serieller Gerätename (`COM1:` …)? Nur für Stringliterale.
|
||||
fn ist_com_geraet(s: &str) -> bool {
|
||||
let up = s.to_ascii_uppercase();
|
||||
up.strip_prefix("COM")
|
||||
.and_then(|r| r.chars().next())
|
||||
.is_some_and(|c| c.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// Builtin-Anweisungen (Bibliothek, keine Keywords).
|
||||
fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
|
||||
use ArgK::*;
|
||||
@@ -293,6 +341,10 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
|
||||
"ENVIRON" => (1, 1, &[S]),
|
||||
"MSGBOX" => (1, 3, &[S, N, S]),
|
||||
"SETUEVENT" => (0, 0, &[]),
|
||||
"CHDRIVE" => (1, 1, &[S]),
|
||||
"STACK" => (0, 1, &[N]),
|
||||
// `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
|
||||
"SETFORMATCC" => (1, 1, &[N]),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -318,6 +370,8 @@ pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
|
||||
udt_defs: Vec::new(),
|
||||
consts: HashMap::new(),
|
||||
module_vars: HashMap::new(),
|
||||
module_labels: HashMap::new(),
|
||||
module_line_labels: HashMap::new(),
|
||||
globals: Vec::new(),
|
||||
data: Vec::new(),
|
||||
data_marks_name: HashMap::new(),
|
||||
@@ -344,6 +398,10 @@ struct Sema {
|
||||
consts: HashMap<String, (Ty, Option<ConstVal>)>,
|
||||
/// Modulvariablen (Schlüssel wie `var_key`).
|
||||
module_vars: HashMap<String, VarInfo>,
|
||||
/// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in
|
||||
/// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds).
|
||||
module_labels: HashMap<String, LabelId>,
|
||||
module_line_labels: HashMap<u32, LabelId>,
|
||||
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
||||
globals: Vec<hir::HVar>,
|
||||
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
||||
@@ -407,6 +465,8 @@ impl Sema {
|
||||
};
|
||||
self.hir_procs.resize_with(self.next_proc_id as usize, || None);
|
||||
self.hir_procs[0] = Some(main);
|
||||
self.module_labels = scope.labels.clone();
|
||||
self.module_line_labels = scope.line_labels.clone();
|
||||
|
||||
// Pass 3: Prozedurrümpfe.
|
||||
for proc in &module.procs {
|
||||
@@ -980,6 +1040,34 @@ impl Sema {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// `DATE$ = "..."` / `TIME$ = "..."` sind Anweisungen, keine
|
||||
// Zuweisungen an eine Variable.
|
||||
// `ERR = n` setzt den Fehlercode, ohne einen Fehler
|
||||
// auszulösen (Anweisungsform von ERR).
|
||||
if let Expr::Name { name, suffix: None, args: None, .. } = target {
|
||||
if name == "ERR" {
|
||||
let (e, t) = self.want_num(value, scope);
|
||||
let e = self.conv_num(e, &t, NumTy::Lng);
|
||||
self.push(out, scope, HStmtKind::SetErr(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Expr::Name { name, suffix: Some(Suffix::Str), args: None, .. } = target {
|
||||
if name == "DATE" || name == "TIME" {
|
||||
let (ve, _) = self.want_str(value, scope);
|
||||
let bt = if name == "DATE" {
|
||||
Builtin::DateSet
|
||||
} else {
|
||||
Builtin::TimeSet
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: bt, args: vec![ve] },
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (place, tt) = self.lower_place(target, scope);
|
||||
let (ve, vt) = self.lower_expr(value, scope);
|
||||
self.check_assign(&tt, &vt, *pos);
|
||||
@@ -1028,39 +1116,76 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
}
|
||||
if *printer {
|
||||
self.push(out, scope, HStmtKind::Unsupported("LPRINT"));
|
||||
} else if file.is_some() {
|
||||
self.push(out, scope, HStmtKind::Unsupported("PRINT #"));
|
||||
} else if using.is_some() {
|
||||
self.push(out, scope, HStmtKind::Unsupported("PRINT USING"));
|
||||
// `PRINT #n` und `LPRINT` schreiben dieselben Elemente an ein
|
||||
// anderes Ziel; danach steht der Bildschirm wieder.
|
||||
let ziel = if *printer {
|
||||
Some(HExpr::Lng(-2))
|
||||
} else if let Some(f) = file {
|
||||
let (e, t) = self.want_num(f, scope);
|
||||
Some(self.conv_num(e, &t, NumTy::Lng))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(z) = ziel.clone() {
|
||||
self.push_ziel(out, scope, z);
|
||||
}
|
||||
if let Some(u) = using {
|
||||
// `PRINT USING fmt$; a; b` — die Semikolons trennen nur
|
||||
// die Werte; Druckzonen gibt es hier nicht.
|
||||
let (fe, _) = self.want_str(u, scope);
|
||||
let mut args = vec![fe];
|
||||
for it in &hitems {
|
||||
match it {
|
||||
HPrintItem::Val(e) => args.push(e.clone()),
|
||||
_ => self.err(pos, "Illegal function call: PRINT USING"),
|
||||
}
|
||||
}
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::PrintUsing, args },
|
||||
);
|
||||
if !trailing {
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::PrintNewline, args: vec![] },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.push(out, scope, HStmtKind::Print { items: hitems, trailing });
|
||||
}
|
||||
if ziel.is_some() {
|
||||
self.push_ziel(out, scope, HExpr::Lng(-1));
|
||||
}
|
||||
}
|
||||
Stmt::Input { line, file, keep_cursor: _, prompt, vars, .. } => {
|
||||
if let Some(f) = file {
|
||||
self.want_num(f, scope);
|
||||
for v in vars {
|
||||
self.lower_place(v, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("INPUT #"));
|
||||
return;
|
||||
}
|
||||
let datei = file.as_ref().map(|f| {
|
||||
let (e, t) = self.want_num(f, scope);
|
||||
self.conv_num(e, &t, NumTy::Lng)
|
||||
});
|
||||
let mut targets = Vec::new();
|
||||
for v in vars {
|
||||
if let (Some(p), _) = self.lower_place(v, scope) {
|
||||
targets.push(p);
|
||||
}
|
||||
}
|
||||
let (ptext, question) = match prompt {
|
||||
Some((t, q)) => (Some(t.clone()), *q),
|
||||
None => (None, true),
|
||||
// `INPUT #n` kennt weder Eingabeaufforderung noch Fragezeichen.
|
||||
let (ptext, question) = match (&datei, prompt) {
|
||||
(Some(_), _) => (None, false),
|
||||
(None, Some((t, q))) => (Some(t.clone()), *q),
|
||||
(None, None) => (None, true),
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::Input { line_mode: *line, prompt: ptext, question, targets },
|
||||
HStmtKind::Input {
|
||||
file: datei,
|
||||
line_mode: *line,
|
||||
prompt: ptext,
|
||||
question,
|
||||
targets,
|
||||
},
|
||||
);
|
||||
}
|
||||
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
|
||||
@@ -1193,15 +1318,19 @@ impl Sema {
|
||||
self.push(out, scope, HStmtKind::ReturnGosub(id));
|
||||
}
|
||||
Stmt::OnError { local, action, pos } => {
|
||||
// Nicht-lokales ON ERROR in einer Prozedur: das Sprungziel
|
||||
// liegt zwingend im Prozedurrumpf (Label-Scoping) — wird wie
|
||||
// ON LOCAL ERROR behandelt. Die Scoping-Regel des Vorbilds
|
||||
// (Handler-Label auf Modulebene) ist als Aufgabe in PLAN.md
|
||||
// Phase 3 eingeplant.
|
||||
let local = *local || !scope.is_module;
|
||||
// `ON ERROR GOTO label` ohne `LOCAL` setzt den modulweiten
|
||||
// Handler; sein Sprungziel liegt im Modulrumpf, auch wenn die
|
||||
// Anweisung in einer Prozedur steht. Nur `ON LOCAL ERROR`
|
||||
// verlangt ein Label im eigenen Rumpf.
|
||||
let local = *local;
|
||||
match action {
|
||||
OnErrorAction::Goto(t) => {
|
||||
if let Some(id) = self.label_id(t, scope, *pos) {
|
||||
let id = if local || scope.is_module {
|
||||
self.label_id(t, scope, *pos)
|
||||
} else {
|
||||
self.module_label_id(t, *pos)
|
||||
};
|
||||
if let Some(id) = id {
|
||||
self.push(out, scope, HStmtKind::OnError { local, target: Some(id) });
|
||||
}
|
||||
}
|
||||
@@ -1370,54 +1499,114 @@ impl Sema {
|
||||
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
|
||||
}
|
||||
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
|
||||
Stmt::Open { file, isam, number, len, pos, .. } => {
|
||||
self.want_str(file, scope);
|
||||
self.want_num(number, scope);
|
||||
if let Some(l) = len {
|
||||
self.want_num(l, scope);
|
||||
Stmt::Open { file, mode, isam, number, len, pos, .. } => {
|
||||
let (fe, _) = self.want_str(file, scope);
|
||||
self.reject_com_device(file, *pos);
|
||||
let (ne, nt) = self.want_num(number, scope);
|
||||
let ne = self.conv_num(ne, &nt, NumTy::Lng);
|
||||
let le = match len {
|
||||
Some(l) => {
|
||||
let (e, t) = self.want_num(l, scope);
|
||||
self.conv_num(e, &t, NumTy::Lng)
|
||||
}
|
||||
// Vorgabe-Recordlänge des Vorbilds.
|
||||
None => HExpr::Lng(128),
|
||||
};
|
||||
if let Some((ty_name, _table)) = isam {
|
||||
if !ty_name.is_empty() && !self.udt_ids.contains_key(ty_name) {
|
||||
self.err(*pos, "Type not defined");
|
||||
}
|
||||
// ISAM kommt mit dem Change `phase-3-isam`.
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN FOR ISAM"));
|
||||
return;
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN"));
|
||||
// Ohne `FOR`-Klausel gilt RANDOM (Vorbild).
|
||||
let m = match mode {
|
||||
Some(OpenMode::Input) => "I",
|
||||
Some(OpenMode::Output) => "O",
|
||||
Some(OpenMode::Append) => "A",
|
||||
Some(OpenMode::Binary) => "B",
|
||||
_ => "R",
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt {
|
||||
b: Builtin::Open,
|
||||
args: vec![fe, ne, HExpr::Str(m.to_string()), le],
|
||||
},
|
||||
);
|
||||
}
|
||||
Stmt::OpenLegacy { mode, number, file, len, .. } => {
|
||||
self.want_str(mode, scope);
|
||||
self.want_num(number, scope);
|
||||
self.want_str(file, scope);
|
||||
if let Some(l) = len {
|
||||
self.want_num(l, scope);
|
||||
Stmt::OpenLegacy { mode, number, file, len, pos, .. } => {
|
||||
let (me, _) = self.want_str(mode, scope);
|
||||
let (ne, nt) = self.want_num(number, scope);
|
||||
let ne = self.conv_num(ne, &nt, NumTy::Lng);
|
||||
let (fe, _) = self.want_str(file, scope);
|
||||
self.reject_com_device(file, *pos);
|
||||
let le = match len {
|
||||
Some(l) => {
|
||||
let (e, t) = self.want_num(l, scope);
|
||||
self.conv_num(e, &t, NumTy::Lng)
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("OPEN"));
|
||||
None => HExpr::Lng(128),
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt {
|
||||
b: Builtin::Open,
|
||||
args: vec![fe, ne, me, le],
|
||||
},
|
||||
);
|
||||
}
|
||||
Stmt::CloseStmt { files, .. } => {
|
||||
for f in files {
|
||||
self.want_num(f, scope);
|
||||
if files.is_empty() {
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::CloseAll, args: vec![] },
|
||||
);
|
||||
}
|
||||
for f in files {
|
||||
let (e, t) = self.want_num(f, scope);
|
||||
let e = self.conv_num(e, &t, NumTy::Lng);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::Close, args: vec![e] },
|
||||
);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("CLOSE"));
|
||||
}
|
||||
Stmt::FieldStmt { file, fields, .. } => {
|
||||
self.want_num(file, scope);
|
||||
let (fe, ft) = self.want_num(file, scope);
|
||||
let fe = self.conv_num(fe, &ft, NumTy::Lng);
|
||||
let mut hf = Vec::new();
|
||||
for (width, var) in fields {
|
||||
self.want_num(width, scope);
|
||||
let (_, t) = self.lower_place(var, scope);
|
||||
if !is_str(&t) {
|
||||
let (we, wt) = self.want_num(width, scope);
|
||||
let we = self.conv_num(we, &wt, NumTy::Lng);
|
||||
let (place, t) = self.lower_place(var, scope);
|
||||
if !is_str(&t) && t != Ty::Unknown {
|
||||
self.err(var.pos(), "Type mismatch");
|
||||
}
|
||||
if let Some(p) = place {
|
||||
hf.push((we, p));
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("FIELD"));
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Field { file: fe, fields: hf });
|
||||
}
|
||||
Stmt::GetPut { put, file, recnum, var, .. } => {
|
||||
self.want_num(file, scope);
|
||||
if let Some(r) = recnum {
|
||||
self.want_num(r, scope);
|
||||
}
|
||||
if let Some(v) = var {
|
||||
self.lower_place(v, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported(if *put { "PUT" } else { "GET" }));
|
||||
let (fe, ft) = self.want_num(file, scope);
|
||||
let fe = self.conv_num(fe, &ft, NumTy::Lng);
|
||||
let re = recnum.as_ref().map(|r| {
|
||||
let (e, t) = self.want_num(r, scope);
|
||||
self.conv_num(e, &t, NumTy::Lng)
|
||||
});
|
||||
let ve = var.as_ref().and_then(|v| self.lower_place(v, scope).0);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::GetPut { put: *put, file: fe, recnum: re, var: ve },
|
||||
);
|
||||
}
|
||||
Stmt::LsetRset { rset, target, value, pos } => {
|
||||
let (_, tt) = self.lower_place(target, scope);
|
||||
@@ -1429,7 +1618,15 @@ impl Sema {
|
||||
if !ok {
|
||||
self.err(*pos, "Type mismatch");
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported(if *rset { "RSET" } else { "LSET" }));
|
||||
let (place, _) = self.lower_place(target, scope);
|
||||
let (ve, _) = self.lower_expr(value, scope);
|
||||
if let Some(place) = place {
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::LsetRset { rset: *rset, target: place, value: ve },
|
||||
);
|
||||
}
|
||||
}
|
||||
Stmt::WriteStmt { file, items, .. } => {
|
||||
if let Some(f) = file {
|
||||
@@ -1438,14 +1635,42 @@ impl Sema {
|
||||
for e in items {
|
||||
self.lower_expr(e, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("WRITE"));
|
||||
let mut args = Vec::new();
|
||||
for it in items {
|
||||
args.push(self.lower_expr(it, scope).0);
|
||||
}
|
||||
if let Some(f) = file {
|
||||
let (e, t) = self.want_num(f, scope);
|
||||
let e = self.conv_num(e, &t, NumTy::Lng);
|
||||
self.push_ziel(out, scope, e);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::WriteFile, args },
|
||||
);
|
||||
self.push_ziel(out, scope, HExpr::Lng(-1));
|
||||
} else {
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::WriteFile, args },
|
||||
);
|
||||
}
|
||||
}
|
||||
Stmt::SeekStmt { file, position, .. } => {
|
||||
self.want_num(file, scope);
|
||||
self.want_num(position, scope);
|
||||
self.push(out, scope, HStmtKind::Unsupported("SEEK"));
|
||||
let (fe, ft) = self.want_num(file, scope);
|
||||
let fe = self.conv_num(fe, &ft, NumTy::Lng);
|
||||
let (pe, pt) = self.want_num(position, scope);
|
||||
let pe = self.conv_num(pe, &pt, NumTy::Lng);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::SeekStmt, args: vec![fe, pe] },
|
||||
);
|
||||
}
|
||||
Stmt::LockStmt { unlock, file, from, to, .. } => {
|
||||
Stmt::LockStmt { file, from, to, .. } => {
|
||||
self.want_num(file, scope);
|
||||
if let Some(f) = from {
|
||||
self.want_num(f, scope);
|
||||
@@ -1453,16 +1678,24 @@ impl Sema {
|
||||
if let Some(t) = to {
|
||||
self.want_num(t, scope);
|
||||
}
|
||||
let (fe, ft) = self.want_num(file, scope);
|
||||
let fe = self.conv_num(fe, &ft, NumTy::Lng);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::Unsupported(if *unlock { "UNLOCK" } else { "LOCK" }),
|
||||
HStmtKind::BuiltinStmt { b: Builtin::LockStmt, args: vec![fe] },
|
||||
);
|
||||
}
|
||||
Stmt::NameStmt { old, new, .. } => {
|
||||
self.want_str(old, scope);
|
||||
self.want_str(new, scope);
|
||||
self.push(out, scope, HStmtKind::Unsupported("NAME"));
|
||||
let (a, _) = self.want_str(old, scope);
|
||||
let (b2, _) = self.want_str(new, scope);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::NameStmt, args: vec![a, b2] },
|
||||
);
|
||||
}
|
||||
// ---- Bildschirm/Ereignisse ----
|
||||
Stmt::ViewPrint { top, bottom, .. } => {
|
||||
@@ -1472,9 +1705,24 @@ impl Sema {
|
||||
if let Some(b) = bottom {
|
||||
self.want_num(b, scope);
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("VIEW PRINT"));
|
||||
let a = match (top, bottom) {
|
||||
(Some(t), Some(b)) => {
|
||||
let t = self.want_num(t, scope);
|
||||
let b = self.want_num(b, scope);
|
||||
vec![
|
||||
self.conv_num(t.0, &t.1, NumTy::Lng),
|
||||
self.conv_num(b.0, &b.1, NumTy::Lng),
|
||||
]
|
||||
}
|
||||
Stmt::EventControl { device, index, pos, .. } => {
|
||||
_ => vec![HExpr::Lng(-1), HExpr::Lng(-1)],
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::ViewPrint, args: a },
|
||||
);
|
||||
}
|
||||
Stmt::EventControl { device, index, action, pos } => {
|
||||
match device.as_str() {
|
||||
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {}
|
||||
_ => self.err(*pos, "Feature unavailable"),
|
||||
@@ -1482,6 +1730,21 @@ impl Sema {
|
||||
if let Some(i) = index {
|
||||
self.want_num(i, scope);
|
||||
}
|
||||
// `KEY ON`/`KEY OFF` ohne Index blendet die Softkey-Zeile
|
||||
// ein bzw. aus — das ist keine Ereignissteuerung
|
||||
// (die hieße `KEY(n) ON`).
|
||||
if device == "KEY" && index.is_none() && *action != EventAction::Stop {
|
||||
let ein = i32::from(*action == EventAction::On);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt {
|
||||
b: Builtin::KeyDisplay,
|
||||
args: vec![HExpr::Lng(ein)],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.push(out, scope, HStmtKind::Unsupported("Ereignissteuerung"));
|
||||
}
|
||||
}
|
||||
@@ -1815,6 +2078,19 @@ impl Sema {
|
||||
id
|
||||
}
|
||||
|
||||
/// Sprungziel im Modulrumpf auflösen (für `ON ERROR GOTO` aus einer
|
||||
/// Prozedur heraus).
|
||||
fn module_label_id(&mut self, target: &LabelRef, pos: SourcePos) -> Option<LabelId> {
|
||||
let id = match target {
|
||||
LabelRef::Name(n) => self.module_labels.get(n).copied(),
|
||||
LabelRef::Line(n) => self.module_line_labels.get(n).copied(),
|
||||
};
|
||||
if id.is_none() {
|
||||
self.err(pos, "Label not defined");
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
// ---- Aufrufe -----------------------------------------------------------
|
||||
|
||||
fn lower_call_stmt(
|
||||
@@ -2068,26 +2344,98 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bildschirmanweisungen: ausgelassene Argumente kommen als -1
|
||||
// durch, damit die Laufzeit „weglassen" von „null" unterscheidet.
|
||||
"ENVIRON" | "CLEAR" | "TRON" | "TROFF" | "STACK" => {
|
||||
let bt = match name {
|
||||
"ENVIRON" => Builtin::EnvironSet,
|
||||
"CLEAR" => Builtin::Clear,
|
||||
"TRON" => Builtin::Tron,
|
||||
"TROFF" => Builtin::Troff,
|
||||
_ => Builtin::StackStmt,
|
||||
};
|
||||
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
|
||||
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
|
||||
}
|
||||
"KILL" | "CHDIR" | "CHDRIVE" | "MKDIR" | "RMDIR" | "FILES" | "SHELL" => {
|
||||
let bt = match name {
|
||||
"KILL" => Builtin::Kill,
|
||||
"CHDIR" => Builtin::Chdir,
|
||||
"CHDRIVE" => Builtin::Chdrive,
|
||||
"MKDIR" => Builtin::Mkdir,
|
||||
"RMDIR" => Builtin::Rmdir,
|
||||
"FILES" => Builtin::Files,
|
||||
_ => Builtin::ShellStmt,
|
||||
};
|
||||
let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
|
||||
self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
|
||||
}
|
||||
"RESET" => self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::CloseAll, args: vec![] },
|
||||
),
|
||||
"SETFORMATCC" => {
|
||||
let (e, t) = lowered[0].clone();
|
||||
let n = self.conv_num(e, &t, NumTy::Lng);
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::SetFormatCc, args: vec![n] },
|
||||
);
|
||||
}
|
||||
"KEY" => {
|
||||
// `KEY LIST` (Makros auflisten) vs. `KEY n, text$` (zuweisen).
|
||||
let ist_list = matches!(
|
||||
args.first(),
|
||||
Some(Expr::Name { name, args: None, suffix: None, .. }) if name == "LIST"
|
||||
);
|
||||
if ist_list {
|
||||
if args.len() > 1 {
|
||||
self.err(pos, "Argument-count mismatch: KEY");
|
||||
}
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::KeyList, args: vec![] },
|
||||
);
|
||||
} else {
|
||||
if lowered.len() != 2 {
|
||||
self.err(pos, "Argument-count mismatch: KEY");
|
||||
}
|
||||
let (ne, nt) = lowered[0].clone();
|
||||
let n = self.conv_num(ne, &nt, NumTy::Lng);
|
||||
let t = lowered[1].0.clone();
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::KeyAssign, args: vec![n, t] },
|
||||
);
|
||||
}
|
||||
}
|
||||
"CLS" | "COLOR" | "LOCATE" | "WIDTH" | "SCREEN" => {
|
||||
// `SCREEN n` mit literalem n > 0 ist ein Grafikmodus und
|
||||
// damit schon zur Compile-Zeit abweisbar.
|
||||
if name == "SCREEN" {
|
||||
if let Some(Expr::IntLit(n)) = args.first() {
|
||||
if *n != 0 {
|
||||
self.err(pos, "Feature unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
let b = match name {
|
||||
"CLS" => Builtin::Cls,
|
||||
"COLOR" => Builtin::Color,
|
||||
"LOCATE" => Builtin::Locate,
|
||||
"SCREEN" => Builtin::ScreenStmt,
|
||||
_ => Builtin::Width,
|
||||
};
|
||||
let a = self.lower_opt_args(args, &lowered);
|
||||
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
|
||||
}
|
||||
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
|
||||
_ => self.push(out, scope, HStmtKind::Unsupported(match name {
|
||||
"CLS" => "CLS",
|
||||
"COLOR" => "COLOR",
|
||||
"LOCATE" => "LOCATE",
|
||||
"WIDTH" => "WIDTH",
|
||||
"SCREEN" => "SCREEN",
|
||||
"KEY" => "KEY",
|
||||
"CLEAR" => "CLEAR",
|
||||
"FILES" => "FILES",
|
||||
"KILL" => "KILL",
|
||||
"CHDIR" => "CHDIR",
|
||||
"MKDIR" => "MKDIR",
|
||||
"RMDIR" => "RMDIR",
|
||||
"SHELL" => "SHELL",
|
||||
"RUN" => "RUN",
|
||||
"ENVIRON" => "ENVIRON",
|
||||
"RESET" => "RESET",
|
||||
"TRON" => "TRON",
|
||||
"TROFF" => "TROFF",
|
||||
"MSGBOX" => "MSGBOX",
|
||||
"SETUEVENT" => "SETUEVENT",
|
||||
_ => "Anweisung",
|
||||
@@ -2095,6 +2443,37 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ausgabeziel von `PRINT` umschalten (−1 Bildschirm, −2 Drucker,
|
||||
/// sonst Dateinummer).
|
||||
fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) {
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::BuiltinStmt { b: Builtin::PrintZiel, args: vec![n] },
|
||||
);
|
||||
}
|
||||
|
||||
/// Argumente einer Bildschirmanweisung: ausgelassene (`LOCATE , 5`)
|
||||
/// werden zu -1, damit die Laufzeit „weglassen" erkennt (0 ist bei
|
||||
/// `COLOR` eine gültige Farbe).
|
||||
fn lower_opt_args(&mut self, args: &[Expr], lowered: &[(HExpr, Ty)]) -> Vec<HExpr> {
|
||||
args.iter()
|
||||
.enumerate()
|
||||
.map(|(i, a)| {
|
||||
if matches!(a, Expr::Missing) {
|
||||
HExpr::Lng(-1)
|
||||
} else {
|
||||
let (e, t) = lowered[i].clone();
|
||||
if t == Ty::Unknown {
|
||||
e
|
||||
} else {
|
||||
self.conv_num(e, &t, NumTy::Lng)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Zählt/prüft Builtin-Argumente und liefert die abgesenkten Ausdrücke.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn check_and_lower_builtin_args(
|
||||
@@ -2110,7 +2489,11 @@ impl Sema {
|
||||
let real: Vec<&Expr> =
|
||||
args.iter().filter(|a| !matches!(a, Expr::Missing)).collect();
|
||||
if real.len() < min as usize || args.len() > max as usize {
|
||||
self.err(pos, "Argument-count mismatch");
|
||||
// Katalogtext des Vorbilds, ergänzt um das Element: eine Diagnose
|
||||
// zu einem dokumentierten Element muss es benennen (Guiding
|
||||
// Principle) — sonst ist bei mehreren Aufrufen in einer Zeile
|
||||
// nicht erkennbar, welcher gemeint ist.
|
||||
self.err(pos, &format!("Argument-count mismatch: {name}"));
|
||||
}
|
||||
// Sonderfall INSTR([start%,] s$, such$)
|
||||
let instr_with_start = name == "INSTR" && args.len() == 3;
|
||||
@@ -2153,6 +2536,17 @@ impl Sema {
|
||||
}
|
||||
(he, t)
|
||||
}
|
||||
/// `OPEN "COM1:" …` — serielle Schnittstelle ist deklariertes Non-Feature.
|
||||
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
||||
/// bleibt der Datei-E/A überlassen.
|
||||
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
||||
if let Expr::StrLit(s) = file {
|
||||
if ist_com_geraet(s) {
|
||||
self.err(pos, "Feature unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||||
let (he, t) = self.lower_expr(e, scope);
|
||||
if !is_str(&t) {
|
||||
@@ -2960,6 +3354,180 @@ impl Sema {
|
||||
};
|
||||
b(Builtin::Rnd, a, Ty::Sng, self)
|
||||
}
|
||||
// --- Datei-E/A ---
|
||||
"EOF" | "LOF" | "LOC" | "SEEK" | "FILEATTR" => {
|
||||
let f = conv_arg!(0, NumTy::Lng);
|
||||
match full_name {
|
||||
"EOF" => b(Builtin::EofF, vec![f], Ty::Int, self),
|
||||
"LOF" => b(Builtin::LofF, vec![f], Ty::Lng, self),
|
||||
"LOC" => b(Builtin::LocF, vec![f], Ty::Lng, self),
|
||||
"SEEK" => b(Builtin::SeekF, vec![f], Ty::Lng, self),
|
||||
_ => {
|
||||
let art = conv_arg!(1, NumTy::Lng);
|
||||
b(Builtin::Fileattr, vec![f, art], Ty::Lng, self)
|
||||
}
|
||||
}
|
||||
}
|
||||
"FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self),
|
||||
"ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
|
||||
"FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
|
||||
"STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
|
||||
"ERDEV" => b(Builtin::Erdev, vec![], Ty::Int, self),
|
||||
"ERDEV$" => b(Builtin::ErdevS, vec![], Ty::Str, self),
|
||||
"CURDIR$" => b(Builtin::CurdirS, vec![], Ty::Str, self),
|
||||
"DIR$" => {
|
||||
let m = if lowered.is_empty() {
|
||||
HExpr::Str(String::new())
|
||||
} else {
|
||||
take(&lowered, 0).0
|
||||
};
|
||||
b(Builtin::DirS, vec![m], Ty::Str, self)
|
||||
}
|
||||
"LPOS" => b(Builtin::Lpos, vec![], Ty::Int, self),
|
||||
"MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" | "MKSMBF$" | "MKDMBF$" => {
|
||||
// Zweites Argument wählt Breite und Typ.
|
||||
let art = match full_name {
|
||||
"MKI$" => 0,
|
||||
"MKL$" => 1,
|
||||
"MKS$" | "MKSMBF$" => 2,
|
||||
"MKD$" | "MKDMBF$" => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let wert = take(&lowered, 0).0;
|
||||
b(Builtin::MkS, vec![wert, HExpr::Lng(art)], Ty::Str, self)
|
||||
}
|
||||
"CVI" | "CVL" | "CVS" | "CVD" | "CVC" | "CVSMBF" | "CVDMBF" => {
|
||||
let art = match full_name {
|
||||
"CVI" => 0,
|
||||
"CVL" => 1,
|
||||
"CVS" | "CVSMBF" => 2,
|
||||
"CVD" | "CVDMBF" => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let ret = match art {
|
||||
0 => Ty::Int,
|
||||
1 => Ty::Lng,
|
||||
2 => Ty::Sng,
|
||||
3 => Ty::Dbl,
|
||||
_ => Ty::Cur,
|
||||
};
|
||||
let s = take(&lowered, 0).0;
|
||||
b(Builtin::CvF, vec![s, HExpr::Lng(art)], ret, self)
|
||||
}
|
||||
"SHELL" => {
|
||||
let c = take(&lowered, 0).0;
|
||||
b(Builtin::ShellFn, vec![c], Ty::Lng, self)
|
||||
}
|
||||
// --- Finanzmathematik ---
|
||||
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
|
||||
let a: Vec<HExpr> = (0..5).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = match full_name.trim_end_matches('#') {
|
||||
"FV" => Builtin::Fv,
|
||||
"PV" => Builtin::Pv,
|
||||
"PMT" => Builtin::Pmt,
|
||||
_ => Builtin::NPer,
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
|
||||
let a: Vec<HExpr> = (0..6).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = match full_name.trim_end_matches('#') {
|
||||
"IPMT" => Builtin::IPmt,
|
||||
"PPMT" => Builtin::PPmt,
|
||||
_ => Builtin::Rate,
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"NPV" | "NPV#" => {
|
||||
let zins = conv_arg!(0, NumTy::Dbl);
|
||||
let reihe = take(&lowered, 1).0;
|
||||
b(Builtin::Npv, vec![zins, reihe], Ty::Dbl, self)
|
||||
}
|
||||
"IRR" | "IRR#" => {
|
||||
let reihe = take(&lowered, 0).0;
|
||||
let schaetzung = conv_arg!(1, NumTy::Dbl);
|
||||
b(Builtin::Irr, vec![reihe, schaetzung], Ty::Dbl, self)
|
||||
}
|
||||
"MIRR" | "MIRR#" => {
|
||||
let reihe = take(&lowered, 0).0;
|
||||
let f = conv_arg!(1, NumTy::Dbl);
|
||||
let w = conv_arg!(2, NumTy::Dbl);
|
||||
b(Builtin::Mirr, vec![reihe, f, w], Ty::Dbl, self)
|
||||
}
|
||||
"SLN" | "SLN#" => {
|
||||
let a: Vec<HExpr> = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
b(Builtin::Sln, a, Ty::Dbl, self)
|
||||
}
|
||||
"SYD" | "SYD#" | "DDB" | "DDB#" => {
|
||||
let a: Vec<HExpr> = (0..4).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
|
||||
let bt = if full_name.starts_with("SYD") {
|
||||
Builtin::Syd
|
||||
} else {
|
||||
Builtin::Ddb
|
||||
};
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
|
||||
"DATESERIAL" | "TIMESERIAL" => {
|
||||
let x = conv_arg!(0, NumTy::Lng);
|
||||
let y = conv_arg!(1, NumTy::Lng);
|
||||
let z = conv_arg!(2, NumTy::Lng);
|
||||
let bt = if full_name == "DATESERIAL" {
|
||||
Builtin::DateSerial
|
||||
} else {
|
||||
Builtin::TimeSerial
|
||||
};
|
||||
b(bt, vec![x, y, z], Ty::Dbl, self)
|
||||
}
|
||||
"DATEVALUE" => b(Builtin::DateValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
|
||||
"TIMEVALUE" => b(Builtin::TimeValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
|
||||
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => {
|
||||
let e = conv_arg!(0, NumTy::Dbl);
|
||||
let bt = match full_name {
|
||||
"DAY" => Builtin::DayF,
|
||||
"MONTH" => Builtin::MonthF,
|
||||
"YEAR" => Builtin::YearF,
|
||||
"WEEKDAY" => Builtin::WeekdayF,
|
||||
"HOUR" => Builtin::HourF,
|
||||
"MINUTE" => Builtin::MinuteF,
|
||||
_ => Builtin::SecondF,
|
||||
};
|
||||
b(bt, vec![e], Ty::Int, self)
|
||||
}
|
||||
"FORMAT$" => {
|
||||
let mut a = vec![take(&lowered, 0).0];
|
||||
if get(1).is_some() {
|
||||
a.push(take(&lowered, 1).0);
|
||||
}
|
||||
b(Builtin::FormatS, a, Ty::Str, self)
|
||||
}
|
||||
// --- Bildschirm ---
|
||||
"TAB" | "SPC" => {
|
||||
// In einer PRINT-Liste werden sie eigens abgesenkt; hier
|
||||
// stehen sie außerhalb und sind dort nicht zulässig.
|
||||
self.err(pos, &format!("Illegal function call: {full_name}"));
|
||||
(HExpr::Str(String::new()), Ty::Str)
|
||||
}
|
||||
"CSRLIN" => b(Builtin::Csrlin, vec![], Ty::Int, self),
|
||||
"POS" => {
|
||||
// `POS(0)` — das Argument ist im Vorbild ein Dummy.
|
||||
b(Builtin::PosFn, vec![], Ty::Int, self)
|
||||
}
|
||||
"SCREEN" => {
|
||||
let zeile = conv_arg!(0, NumTy::Lng);
|
||||
let spalte = conv_arg!(1, NumTy::Lng);
|
||||
let farbe = if get(2).is_some() {
|
||||
conv_arg!(2, NumTy::Lng)
|
||||
} else {
|
||||
HExpr::Lng(0)
|
||||
};
|
||||
b(Builtin::ScreenFn, vec![zeile, spalte, farbe], Ty::Int, self)
|
||||
}
|
||||
"INKEY$" => b(Builtin::InkeyS, vec![], Ty::Str, self),
|
||||
"INPUT$" => {
|
||||
let n = conv_arg!(0, NumTy::Lng);
|
||||
b(Builtin::InputS, vec![n], Ty::Str, self)
|
||||
}
|
||||
// --- Fehlerstatus ---
|
||||
"ERR" => (HExpr::Err, Ty::Lng),
|
||||
"ERL" => (HExpr::Erl, Ty::Lng),
|
||||
@@ -2972,34 +3540,6 @@ impl Sema {
|
||||
// --- Spätere Phasen: dokumentiert, aber noch nicht verfügbar ---
|
||||
_ => {
|
||||
let name: &'static str = match full_name {
|
||||
"INKEY$" => "INKEY$",
|
||||
"INPUT$" => "INPUT$",
|
||||
"CSRLIN" => "CSRLIN",
|
||||
"POS" => "POS",
|
||||
"SCREEN" => "SCREEN",
|
||||
"TAB" => "TAB",
|
||||
"SPC" => "SPC",
|
||||
"NOW" => "NOW",
|
||||
"DATESERIAL" => "DATESERIAL",
|
||||
"TIMESERIAL" => "TIMESERIAL",
|
||||
"DATEVALUE" => "DATEVALUE",
|
||||
"TIMEVALUE" => "TIMEVALUE",
|
||||
"DAY" => "DAY",
|
||||
"MONTH" => "MONTH",
|
||||
"YEAR" => "YEAR",
|
||||
"WEEKDAY" => "WEEKDAY",
|
||||
"HOUR" => "HOUR",
|
||||
"MINUTE" => "MINUTE",
|
||||
"SECOND" => "SECOND",
|
||||
"FORMAT$" => "FORMAT$",
|
||||
"FRE" => "FRE",
|
||||
"EOF" => "EOF",
|
||||
"LOF" => "LOF",
|
||||
"LOC" => "LOC",
|
||||
"SEEK" => "SEEK",
|
||||
"FREEFILE" => "FREEFILE",
|
||||
"FILEATTR" => "FILEATTR",
|
||||
"ENVIRON$" => "ENVIRON$",
|
||||
"MSGBOX" => "MSGBOX",
|
||||
"INPUTBOX$" => "INPUTBOX$",
|
||||
_ => "Funktion",
|
||||
@@ -3110,7 +3650,7 @@ mod tests {
|
||||
#[test]
|
||||
fn argumentanzahl() {
|
||||
assert!(diags("PRINT LEFT$(\"a\")")
|
||||
.contains(&"Argument-count mismatch".to_string()));
|
||||
.contains(&"Argument-count mismatch: LEFT$".to_string()));
|
||||
assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1")
|
||||
.contains(&"Argument-count mismatch".to_string()));
|
||||
}
|
||||
|
||||
498
crates/tb-frontend/tests/inventar.rs
Normal file
498
crates/tb-frontend/tests/inventar.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
//! Abgleich `docs/inventar.md` ↔ Implementierungsstand.
|
||||
//!
|
||||
//! Das Inventar ist die Sollseite der Leitplanke „Vollständigkeit ist das
|
||||
//! Soll"; dieser Test hält es gegen den Code, damit der Status nicht
|
||||
//! verrottet. Prüfregeln siehe `pruefe_status`.
|
||||
//!
|
||||
//! ponytail: Die Tabellen `builtin_fn`/`builtin_stmt`/`banned_feature` sind
|
||||
//! private `match`-Ausdrücke ohne Namensliste. Statt sie öffentlich zu machen
|
||||
//! oder die Namen ein zweites Mal zu pflegen, liest der Test die Stringliterale
|
||||
//! aus dem Quelltext. Ceiling: Wird `sema.rs` umstrukturiert, schlägt
|
||||
//! `abschnitt` fehl und nennt den fehlenden Anker — dann hier nachziehen oder
|
||||
//! die Tabellen als `pub const`-Listen exportieren.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const IMPLEMENTIERT: &str = "implementiert";
|
||||
const OFFEN: &str = "offen";
|
||||
const NON_FEATURE: &str = "Non-Feature";
|
||||
|
||||
fn wurzel() -> PathBuf {
|
||||
// crates/tb-frontend/ → Projektwurzel
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
|
||||
}
|
||||
|
||||
// ---- Inventar lesen --------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Eintrag {
|
||||
name: String,
|
||||
art: String,
|
||||
status: String,
|
||||
fundstelle: String,
|
||||
zeile: usize,
|
||||
}
|
||||
|
||||
fn inventar() -> Vec<Eintrag> {
|
||||
let pfad = wurzel().join("docs/inventar.md");
|
||||
let text = std::fs::read_to_string(&pfad)
|
||||
.unwrap_or_else(|e| panic!("docs/inventar.md nicht lesbar: {e}"));
|
||||
let mut out = Vec::new();
|
||||
for (i, zeile) in text.lines().enumerate() {
|
||||
let zeile = zeile.trim();
|
||||
if !zeile.starts_with("| `") {
|
||||
continue;
|
||||
}
|
||||
let spalten: Vec<&str> = zeile
|
||||
.trim_matches('|')
|
||||
.split('|')
|
||||
.map(|s| s.trim())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
spalten.len(),
|
||||
6,
|
||||
"docs/inventar.md Zeile {}: {} Spalten statt 6 — {zeile}",
|
||||
i + 1,
|
||||
spalten.len()
|
||||
);
|
||||
out.push(Eintrag {
|
||||
name: spalten[0].trim_matches('`').to_string(),
|
||||
art: spalten[1].to_string(),
|
||||
status: spalten[3].to_string(),
|
||||
fundstelle: spalten[4].to_string(),
|
||||
zeile: i + 1,
|
||||
});
|
||||
}
|
||||
assert!(!out.is_empty(), "docs/inventar.md enthält keine Einträge");
|
||||
out
|
||||
}
|
||||
|
||||
// ---- Implementierungsstand aus sema.rs -------------------------------------
|
||||
|
||||
fn sema_quelle() -> String {
|
||||
let pfad = wurzel().join("crates/tb-frontend/src/sema.rs");
|
||||
std::fs::read_to_string(&pfad).unwrap_or_else(|e| panic!("sema.rs nicht lesbar: {e}"))
|
||||
}
|
||||
|
||||
/// Quelltextabschnitt zwischen zwei Ankern (exklusive Endanker).
|
||||
fn abschnitt<'a>(quelle: &'a str, von: &str, bis: &str) -> &'a str {
|
||||
let a = quelle
|
||||
.find(von)
|
||||
.unwrap_or_else(|| panic!("Anker nicht gefunden in sema.rs: {von}"));
|
||||
let rest = &quelle[a..];
|
||||
let b = rest
|
||||
.find(bis)
|
||||
.unwrap_or_else(|| panic!("Endanker nicht gefunden in sema.rs: {bis}"));
|
||||
&rest[..b]
|
||||
}
|
||||
|
||||
/// Alle Stringliterale eines Abschnitts.
|
||||
fn literale(abschnitt: &str) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
let bytes: Vec<char> = abschnitt.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == '"' {
|
||||
let mut s = String::new();
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i] != '"' {
|
||||
s.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
if !s.is_empty() {
|
||||
out.insert(s);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
struct Stand {
|
||||
signaturen: BTreeSet<String>,
|
||||
verboten: BTreeSet<String>,
|
||||
nicht_verfuegbar: BTreeSet<String>,
|
||||
/// Non-Features, die an ihrer Syntax statt an ihrem Namen erkannt werden.
|
||||
syntaktisch: BTreeSet<String>,
|
||||
}
|
||||
|
||||
fn stand() -> Stand {
|
||||
let q = sema_quelle();
|
||||
let fn_tab = abschnitt(&q, "fn builtin_fn(", "/// Deklarierte Non-Features");
|
||||
let ban = abschnitt(
|
||||
&q,
|
||||
"fn banned_feature(",
|
||||
"/// Non-Features, die nicht über ihren Namen",
|
||||
);
|
||||
let stmt_tab = abschnitt(&q, "fn builtin_stmt(", "// ---- Einstiegspunkte");
|
||||
let unsup_fn = abschnitt(
|
||||
&q,
|
||||
"// --- Spätere Phasen: dokumentiert",
|
||||
"(HExpr::Unsupported(name)",
|
||||
);
|
||||
let unsup_stmt = abschnitt(
|
||||
&q,
|
||||
"// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.",
|
||||
r#"_ => "Anweisung""#,
|
||||
);
|
||||
|
||||
let mut signaturen = literale(fn_tab);
|
||||
signaturen.extend(literale(stmt_tab));
|
||||
let mut nicht_verfuegbar = literale(unsup_fn);
|
||||
nicht_verfuegbar.extend(literale(unsup_stmt));
|
||||
// Platzhalter der Sammelzweige sind keine Elementnamen.
|
||||
nicht_verfuegbar.remove("Funktion");
|
||||
nicht_verfuegbar.remove("Anweisung");
|
||||
|
||||
let syn = abschnitt(
|
||||
&q,
|
||||
"pub const SYNTAKTISCH_ABGEWIESEN",
|
||||
"/// Ist `s` ein serieller Gerätename",
|
||||
);
|
||||
|
||||
Stand {
|
||||
signaturen,
|
||||
verboten: literale(ban),
|
||||
nicht_verfuegbar,
|
||||
syntaktisch: literale(syn).into_iter().map(|s| s.to_uppercase()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Regeln ----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn statusvokabular_ist_beschraenkt() {
|
||||
for e in inventar() {
|
||||
assert!(
|
||||
matches!(
|
||||
e.art.as_str(),
|
||||
"Anweisung" | "Funktion" | "Metabefehl" | "Routine"
|
||||
),
|
||||
"Zeile {}: `{}` trägt unzulässige Art `{}`",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.art
|
||||
);
|
||||
assert!(
|
||||
matches!(e.status.as_str(), IMPLEMENTIERT | OFFEN | NON_FEATURE),
|
||||
"Zeile {}: `{}` trägt unzulässigen Status `{}` \
|
||||
(erlaubt: {IMPLEMENTIERT}/{OFFEN}/{NON_FEATURE})",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_features_nennen_eine_fundstelle_in_der_sprachreferenz() {
|
||||
for e in inventar() {
|
||||
if e.status == NON_FEATURE {
|
||||
assert!(
|
||||
e.fundstelle.contains("sprachreferenz.md"),
|
||||
"Zeile {}: `{}` ist Non-Feature, nennt aber keine Fundstelle \
|
||||
in docs/sprachreferenz.md (gefunden: `{}`)",
|
||||
e.zeile,
|
||||
e.name,
|
||||
e.fundstelle
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offene_elemente_haben_keine_fundstelle() {
|
||||
for e in inventar() {
|
||||
if e.status == OFFEN {
|
||||
assert_eq!(
|
||||
e.fundstelle, "-",
|
||||
"Zeile {}: `{}` ist offen, trägt aber die Fundstelle `{}`",
|
||||
e.zeile, e.name, e.fundstelle
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernregel: Inventarstatus und Code dürfen nicht auseinanderlaufen.
|
||||
///
|
||||
/// Geprüft wird nur, was mechanisch belegbar ist — die Bibliothekselemente aus
|
||||
/// `builtin_fn`/`builtin_stmt`, die Non-Feature-Liste und die
|
||||
/// „nicht verfügbar"-Marker. Anweisungen, die der Parser direkt kennt
|
||||
/// (`PRINT`, `IF`, …), und Metabefehle tauchen in keiner dieser Tabellen auf;
|
||||
/// für sie gilt nur die schwächere Regel `parserelemente_sind_keine_non_features`.
|
||||
#[test]
|
||||
fn inventar_stimmt_mit_code_ueberein() {
|
||||
let inv = inventar();
|
||||
let st = stand();
|
||||
let mut fehler: Vec<String> = Vec::new();
|
||||
|
||||
// Ein Name kann zweimal vorkommen (Funktion und Anweisung, z. B. `SCREEN`).
|
||||
// Schlüssel ist der Name in Großschreibung — der Dialekt ist
|
||||
// case-insensitiv, der Code führt die Namen groß.
|
||||
//
|
||||
// ponytail: Einträge mit klammerndem Zusatz (`GET (Grafik)`) sind
|
||||
// syntaktische Varianten eines Tokens, das auch eine unterstützte Form hat.
|
||||
// Sie sind über den Namen nicht prüfbar und bleiben hier außen vor; ihre
|
||||
// Ablehnung liegt im Parser. Ceiling: sobald der Parser eine Liste seiner
|
||||
// abgewiesenen Formen exportiert, hier gegenprüfen.
|
||||
let mut je_name: BTreeMap<String, Vec<&Eintrag>> = BTreeMap::new();
|
||||
for e in &inv {
|
||||
// Syntaktisch erkannte Formen tragen einen klammernden Zusatz oder
|
||||
// stehen namentlich in SYNTAKTISCH_ABGEWIESEN — sie werden unten
|
||||
// gegen diese Liste geprüft, nicht gegen `banned_feature`.
|
||||
if st.syntaktisch.contains(&e.name.to_uppercase()) {
|
||||
assert_eq!(
|
||||
e.status, NON_FEATURE,
|
||||
"Zeile {}: `{}` steht in SYNTAKTISCH_ABGEWIESEN, das Inventar sagt aber `{}`",
|
||||
e.zeile, e.name, e.status
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Ein klammernder Zusatz benennt die Form (`KEY (Ereignis)`); für den
|
||||
// Codeabgleich zählt das Grundtoken davor.
|
||||
let schluessel = e
|
||||
.name
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or(&e.name)
|
||||
.trim()
|
||||
.to_uppercase();
|
||||
je_name.entry(schluessel).or_default().push(e);
|
||||
}
|
||||
|
||||
for (name, eintraege) in &je_name {
|
||||
let stati: BTreeSet<&str> = eintraege.iter().map(|e| e.status.as_str()).collect();
|
||||
let _ = &name;
|
||||
let zeilen: Vec<String> = eintraege.iter().map(|e| e.zeile.to_string()).collect();
|
||||
let wo = format!("Zeile(n) {}", zeilen.join(", "));
|
||||
|
||||
let hat_signatur = st.signaturen.contains(name.as_str());
|
||||
// `ON COM(1) GOSUB …` scheitert am Gerätenamen: für die ON-Formen
|
||||
// zählt das zweite Token. Andere Mehrwortnamen (`LINE INPUT`,
|
||||
// `VIEW PRINT`) sind eigenständige unterstützte Anweisungen und
|
||||
// erben nichts vom Grundtoken.
|
||||
let ist_verboten = match name.strip_prefix("ON ") {
|
||||
Some(geraet) => st.verboten.contains(geraet),
|
||||
None => st.verboten.contains(name.as_str()),
|
||||
};
|
||||
let ist_marker = st.nicht_verfuegbar.contains(name.as_str());
|
||||
|
||||
if ist_verboten && !stati.contains(NON_FEATURE) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Code weist es als Non-Feature ab, Inventar sagt {stati:?}"
|
||||
));
|
||||
}
|
||||
if !ist_verboten && stati.contains(NON_FEATURE) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt Non-Feature, Code weist es nicht ab \
|
||||
(fehlt in banned_feature)"
|
||||
));
|
||||
}
|
||||
if ist_marker && !stati.contains(OFFEN) {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): senkt auf den „nicht verfügbar\"-Marker ab, \
|
||||
Inventar sagt {stati:?}"
|
||||
));
|
||||
}
|
||||
if stati == BTreeSet::from([IMPLEMENTIERT]) && ist_marker {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt implementiert, senkt aber weiterhin \
|
||||
auf den „nicht verfügbar\"-Marker ab"
|
||||
));
|
||||
}
|
||||
// Nur wenn ALLE Formen dieses Namens offen sind — sonst erklärt die
|
||||
// implementierte Form (etwa `DATE$` als Funktion) die Signatur.
|
||||
if stati == BTreeSet::from([OFFEN]) && hat_signatur && !ist_marker && !ist_verboten {
|
||||
fehler.push(format!(
|
||||
"`{name}` ({wo}): Inventar sagt offen, aber der Code hat eine Signatur \
|
||||
ohne „nicht verfügbar\"-Marker — entweder implementiert oder Marker fehlt"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Gegenrichtung: kennt der Code Elemente, die das Inventar nicht führt?
|
||||
// Ein Element darf im Code unter mehreren Schreibweisen stehen: die
|
||||
// Original-Hilfe schreibt die Finanzfunktionen mit Typsuffix (`FV#`), der
|
||||
// Code nimmt beide an. Für den Abgleich zählt das Inventar als getroffen,
|
||||
// wenn eine der Schreibweisen passt.
|
||||
let bekannt = |n: &str| {
|
||||
let up = n.to_uppercase();
|
||||
je_name.contains_key(&up)
|
||||
|| ['#', '!', '&', '%', '@', '$']
|
||||
.iter()
|
||||
.any(|s| je_name.contains_key(&format!("{up}{s}")))
|
||||
};
|
||||
for name in st.signaturen.iter().chain(st.verboten.iter()) {
|
||||
if !bekannt(name) {
|
||||
fehler.push(format!(
|
||||
"`{name}`: im Code bekannt, fehlt aber in docs/inventar.md"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
fehler.is_empty(),
|
||||
"Inventar und Code laufen auseinander ({} Abweichungen):\n {}",
|
||||
fehler.len(),
|
||||
fehler.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abdeckungsstand_wird_ausgewiesen() {
|
||||
let inv = inventar();
|
||||
let zaehle = |s: &str| inv.iter().filter(|e| e.status == s).count();
|
||||
let (i, o, n) = (
|
||||
zaehle(IMPLEMENTIERT),
|
||||
zaehle(OFFEN),
|
||||
zaehle(NON_FEATURE),
|
||||
);
|
||||
println!(
|
||||
"Abdeckung: implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
|
||||
inv.len()
|
||||
);
|
||||
|
||||
// Der Kopf der Tabelle nennt dieselben Zahlen — sonst ist die Doku stale.
|
||||
let text = std::fs::read_to_string(wurzel().join("docs/inventar.md")).unwrap();
|
||||
let erwartet = format!(
|
||||
"implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
|
||||
inv.len()
|
||||
);
|
||||
assert!(
|
||||
text.contains(&erwartet),
|
||||
"Abdeckungszeile in docs/inventar.md stimmt nicht; erwartet: {erwartet}"
|
||||
);
|
||||
assert_eq!(i + o + n, inv.len(), "Statussumme ≠ Einträge");
|
||||
}
|
||||
|
||||
// ---- Namentliche Ablehnung -------------------------------------------------
|
||||
|
||||
/// Ein im Inventar geführtes Element darf nie als unbekannter Bezeichner oder
|
||||
/// mit einem generischen Syntaxfehler enden — die Ablehnung MUSS es nennen
|
||||
/// (Guiding Principle, Capability `sprachinventar`).
|
||||
///
|
||||
/// Geprüft wird stichprobenartig je Ablehnungsweg, nicht über alle 285
|
||||
/// Einträge: die vollständige Zuordnung leistet `inventar_stimmt_mit_code_ueberein`.
|
||||
#[test]
|
||||
fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
|
||||
// (Quelltext, erwartetes Element, erwarteter Meldungsbestandteil)
|
||||
let faelle: &[(&str, &str, &str)] = &[
|
||||
// Non-Feature über die Namensliste
|
||||
("X = PEEK(100)", "PEEK", "Feature unavailable"),
|
||||
("Y = POINT(1, 2)", "POINT", "Feature unavailable"),
|
||||
("CALLS Foo", "CALLS", "Feature unavailable"),
|
||||
// Non-Feature über die Syntax (teilt das Schlüsselwort)
|
||||
("LINE (1, 1)-(2, 2)", "LINE", "Feature unavailable"),
|
||||
("GET (1, 1)-(2, 2), A", "GET (Grafik)", "Feature unavailable"),
|
||||
(r#"OPEN "COM1:9600,N,8,1" AS #1"#, "OPEN COM", "Feature unavailable"),
|
||||
// Ereignisgerät als Funktionsform
|
||||
("ON PEN GOSUB Ziel\nZiel:\nRETURN", "ON PEN", "Feature unavailable"),
|
||||
];
|
||||
|
||||
for (quelle, element, erwartet) in faelle {
|
||||
let a = tb_frontend::analyze_source("probe.bas", quelle);
|
||||
let texte: Vec<String> = a.diagnostics.iter().map(|d| d.message.clone()).collect();
|
||||
assert!(
|
||||
texte.iter().any(|m| m.contains(erwartet)),
|
||||
"`{element}`: erwartete Meldung `{erwartet}`, bekam {texte:?} \
|
||||
(Quelle: {quelle:?})"
|
||||
);
|
||||
assert!(
|
||||
!texte.iter().any(|m| m.contains("not defined")),
|
||||
"`{element}`: wird als unbekannter Bezeichner abgewiesen statt \
|
||||
namentlich — {texte:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gegenprobe: ein Element mit „nicht verfügbar"-Marker kompiliert zwar, endet
|
||||
/// aber zur Laufzeit in einem Fehler, der es benennt — nicht in einem
|
||||
/// generischen Syntaxfehler.
|
||||
#[test]
|
||||
fn offene_elemente_erzeugen_keinen_syntaxfehler() {
|
||||
for quelle in ["CLS", "LOCATE 5, 10", "COLOR 14, 1", r#"OPEN "d.txt" FOR OUTPUT AS #1"#] {
|
||||
let a = tb_frontend::analyze_source("probe.bas", quelle);
|
||||
assert!(
|
||||
a.diagnostics.is_empty(),
|
||||
"{quelle:?}: offenes Element soll übersetzen und erst zur Laufzeit \
|
||||
namentlich abgewiesen werden, bekam {:?}",
|
||||
a.diagnostics
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Jedes Inventarelement besitzt eine Signatur — auch die, deren
|
||||
/// Laufzeitverhalten noch aussteht. Sie dürfen nie als unbekannter Bezeichner
|
||||
/// enden (Capability `sprach-frontend`, Anforderung „Jedes dokumentierte
|
||||
/// Element besitzt eine Signatur").
|
||||
#[test]
|
||||
fn bislang_signaturlose_elemente_haben_signaturen() {
|
||||
let quelle = r#"
|
||||
DIM Zahlungen#(1 TO 3)
|
||||
DIM D#, S$, I%, L&, C@, G!
|
||||
D# = FV#(0.05, 10, -100, 0, 0)
|
||||
D# = PV#(0.05, 10, -100, 0, 0)
|
||||
D# = PMT#(0.05, 10, 1000, 0, 0)
|
||||
D# = NPER#(0.05, -100, 1000, 0, 0)
|
||||
D# = IPMT#(0.05, 1, 10, 1000, 0, 0)
|
||||
D# = PPMT#(0.05, 1, 10, 1000, 0, 0)
|
||||
D# = RATE#(10, -100, 1000, 0, 0, 0.1)
|
||||
D# = NPV#(0.05, Zahlungen#())
|
||||
D# = IRR#(Zahlungen#(), 0.1)
|
||||
D# = MIRR#(Zahlungen#(), 0.05, 0.06)
|
||||
D# = SLN#(1000, 100, 10)
|
||||
D# = SYD#(1000, 100, 10, 1)
|
||||
D# = DDB#(1000, 100, 10, 1)
|
||||
S$ = MKI$(1)
|
||||
S$ = MKL$(1)
|
||||
S$ = MKS$(1)
|
||||
S$ = MKD$(1)
|
||||
S$ = MKC$(1)
|
||||
S$ = MKSMBF$(1)
|
||||
S$ = MKDMBF$(1)
|
||||
I% = CVI("ab")
|
||||
L& = CVL("abcd")
|
||||
G! = CVS("abcd")
|
||||
D# = CVD("abcdefgh")
|
||||
C@ = CVC("abcdefgh")
|
||||
G! = CVSMBF("abcd")
|
||||
D# = CVDMBF("abcdefgh")
|
||||
S$ = CURDIR$
|
||||
S$ = DIR$("*.txt")
|
||||
I% = LPOS(0)
|
||||
L& = STACK
|
||||
I% = ERDEV
|
||||
S$ = ERDEV$
|
||||
CHDRIVE "C"
|
||||
STACK 2048
|
||||
SetFormatCC 49
|
||||
"#;
|
||||
let a = tb_frontend::analyze_source("signaturen.bas", quelle);
|
||||
let unbekannt: Vec<&tb_frontend::Diagnostic> = a
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|d| d.message.contains("not defined"))
|
||||
.collect();
|
||||
assert!(
|
||||
unbekannt.is_empty(),
|
||||
"Elemente werden als unbekannter Bezeichner abgewiesen: {unbekannt:?}"
|
||||
);
|
||||
assert!(
|
||||
a.diagnostics.is_empty(),
|
||||
"unerwartete Diagnosen: {:?}",
|
||||
a.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
/// Die Signatur wirkt auch dort, wo das Laufzeitverhalten noch aussteht:
|
||||
/// eine falsche Argumentanzahl wird gemeldet und nennt das Element.
|
||||
#[test]
|
||||
fn signatur_greift_ohne_laufzeitverhalten() {
|
||||
let a = tb_frontend::analyze_source("argzahl.bas", "S$ = MKL$(1, 2)");
|
||||
let texte: Vec<String> = a.diagnostics.iter().map(|d| d.message.clone()).collect();
|
||||
assert!(
|
||||
texte.iter().any(|m| m.contains("MKL$")),
|
||||
"Diagnose nennt `MKL$` nicht: {texte:?}"
|
||||
);
|
||||
}
|
||||
@@ -7,5 +7,6 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
unicode-width.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,112 +1,199 @@
|
||||
//! Konsolen-Druckzustand: Spaltenverfolgung, PRINT-Formatierung,
|
||||
//! 14-Zeichen-Druckzonen, TAB/SPC. Wirkung ausschließlich über `Host`.
|
||||
//! Konsolen-Druckzustand: PRINT-Formatierung, 14-Zeichen-Druckzonen, TAB/SPC.
|
||||
//! Wirkung ausschließlich auf dem [`TextScreen`].
|
||||
//!
|
||||
//! Die Druckspalte wird nicht mehr eigenständig mitgezählt, sondern vom
|
||||
//! Bildschirm gelesen (`POS`). Damit stimmen Druckzonen und Cursor auch
|
||||
//! nach `LOCATE`, `CLS` oder einem Umbruch am rechten Rand überein.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::format::format_print;
|
||||
use crate::host::Host;
|
||||
use crate::fileio::Dateien;
|
||||
use crate::screen::TextScreen;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Breite einer Druckzone (Vorbild: 14 Zeichen).
|
||||
pub const ZONE_WIDTH: usize = 14;
|
||||
|
||||
/// Druckzustand (Spalte 0-basiert, in Zeichen).
|
||||
/// Wohin `PRINT` schreibt.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Ziel {
|
||||
#[default]
|
||||
Bildschirm,
|
||||
/// `PRINT #n` — in eine offene Datei.
|
||||
Datei(i32),
|
||||
/// `LPRINT` — in den Druckerkanal.
|
||||
Drucker,
|
||||
}
|
||||
|
||||
/// PRINT-Verhalten. Auf dem Bildschirm lebt die Spalte im Bildschirm; für
|
||||
/// Datei und Drucker wird sie hier mitgezählt, damit Druckzonen, `TAB` und
|
||||
/// `SPC` dort genauso wirken.
|
||||
#[derive(Default)]
|
||||
pub struct PrintState {
|
||||
pub col: usize,
|
||||
pub ziel: Ziel,
|
||||
/// Spalte (0-basiert) für Datei- und Druckerausgabe.
|
||||
pub spalte: usize,
|
||||
/// Druckerpuffer (Grundlage von `LPOS`); wird beim Programmende
|
||||
/// ausgegeben.
|
||||
pub drucker: String,
|
||||
}
|
||||
|
||||
impl PrintState {
|
||||
/// Text ausgeben und Spalte nachführen.
|
||||
pub fn write(&mut self, host: &mut dyn Host, s: &str) {
|
||||
host.write(s);
|
||||
/// Aktuelle Druckspalte (0-basiert) des eingestellten Ziels.
|
||||
pub fn col(&self, screen: &TextScreen) -> usize {
|
||||
match self.ziel {
|
||||
Ziel::Bildschirm => screen.pos() - 1,
|
||||
_ => self.spalte,
|
||||
}
|
||||
}
|
||||
|
||||
/// Text an das eingestellte Ziel ausgeben.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
s: &str,
|
||||
) -> Result<(), RuntimeError> {
|
||||
match self.ziel {
|
||||
Ziel::Bildschirm => screen.print(s),
|
||||
Ziel::Drucker => {
|
||||
self.drucker.push_str(s);
|
||||
self.spalte_nachfuehren(s);
|
||||
}
|
||||
Ziel::Datei(n) => {
|
||||
dateien.get(n)?.schreiben(s)?;
|
||||
self.spalte_nachfuehren(s);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spalte nach ausgegebenem Text nachführen (nur für Nicht-Bildschirm).
|
||||
fn spalte_nachfuehren(&mut self, s: &str) {
|
||||
match s.rfind('\n') {
|
||||
Some(i) => self.col = s[i + 1..].chars().count(),
|
||||
None => self.col += s.chars().count(),
|
||||
Some(i) => self.spalte = s[i + 1..].chars().count(),
|
||||
None => self.spalte += s.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
|
||||
/// Leerzeichen, Strings unverändert.
|
||||
pub fn print_value(&mut self, host: &mut dyn Host, v: &Value) {
|
||||
match v {
|
||||
Value::Str(s) => {
|
||||
let s = s.clone();
|
||||
self.write(host, &s);
|
||||
}
|
||||
_ => {
|
||||
let s = format_print(v);
|
||||
self.write(host, &s);
|
||||
}
|
||||
}
|
||||
pub fn print_value(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
v: &Value,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let text = match v {
|
||||
Value::Str(s) => s.to_string(),
|
||||
_ => format_print(v),
|
||||
};
|
||||
self.write(screen, dateien, &text)
|
||||
}
|
||||
|
||||
/// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
|
||||
pub fn print_comma(&mut self, host: &mut dyn Host) {
|
||||
let next = (self.col / ZONE_WIDTH + 1) * ZONE_WIDTH;
|
||||
let pad = next - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
pub fn print_comma(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let col = self.col(screen);
|
||||
let next = (col / ZONE_WIDTH + 1) * ZONE_WIDTH;
|
||||
self.write(screen, dateien, &" ".repeat(next - col))
|
||||
}
|
||||
|
||||
/// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
|
||||
/// dahinter, zuerst Zeilenumbruch (Vorbild).
|
||||
pub fn print_tab(&mut self, host: &mut dyn Host, n: i32) {
|
||||
pub fn print_tab(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
n: i32,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let target = (n.max(1) as usize) - 1;
|
||||
if self.col > target {
|
||||
self.write(host, "\n");
|
||||
if self.col(screen) > target {
|
||||
self.write(screen, dateien, "\n")?;
|
||||
}
|
||||
if target > self.col {
|
||||
let pad = target - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
let col = self.col(screen);
|
||||
if target > col {
|
||||
self.write(screen, dateien, &" ".repeat(target - col))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SPC(n)` — n Leerzeichen.
|
||||
pub fn print_spc(&mut self, host: &mut dyn Host, n: i32) {
|
||||
pub fn print_spc(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
n: i32,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if n > 0 {
|
||||
self.write(host, &" ".repeat(n as usize));
|
||||
self.write(screen, dateien, &" ".repeat(n as usize))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_newline(&mut self, host: &mut dyn Host) {
|
||||
self.write(host, "\n");
|
||||
pub fn print_newline(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.write(screen, dateien, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host::CaptureHost;
|
||||
|
||||
/// Zeile `z` (1-basiert) als getrimmter Text.
|
||||
fn zeile(s: &TextScreen, z: usize) -> String {
|
||||
let t: String = (1..=s.cols()).map(|c| s.cell(z, c).ch).collect();
|
||||
t.trim_end().to_string()
|
||||
}
|
||||
|
||||
fn ps() -> (PrintState, TextScreen, Dateien) {
|
||||
(PrintState::default(), TextScreen::new(), Dateien::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn druckzonen() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
// "a" (1 Zeichen) , → Spalte 14
|
||||
ps.write(&mut h, "a");
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "b");
|
||||
assert_eq!(h.output, "a b");
|
||||
assert_eq!(ps.col, 15);
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
// "a" (1 Zeichen) , → Spalte 15 (1-basiert)
|
||||
ps.write(&mut s, &mut d, "a").unwrap();
|
||||
ps.print_comma(&mut s, &mut d).unwrap();
|
||||
ps.write(&mut s, &mut d, "b").unwrap();
|
||||
assert_eq!(zeile(&s, 1), "a b");
|
||||
assert_eq!(s.pos(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volle_zone_springt_zur_uebernaechsten() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.write(&mut h, "12345678901234"); // 14 Zeichen, Zone voll
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "x");
|
||||
// x beginnt in Spalte 29 (1-basiert) = Index 28
|
||||
assert_eq!(h.output.chars().count(), 29);
|
||||
assert!(h.output.ends_with("x"));
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
ps.write(&mut s, &mut d, "12345678901234").unwrap(); // Zone voll
|
||||
ps.print_comma(&mut s, &mut d).unwrap();
|
||||
ps.write(&mut s, &mut d, "x").unwrap();
|
||||
// x beginnt in Spalte 29 (1-basiert)
|
||||
assert_eq!(s.cell(1, 29).ch, 'x');
|
||||
assert_eq!(s.pos(), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zahlen_mit_vorzeichenspalte() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.print_value(&mut h, &Value::Int(1));
|
||||
ps.print_value(&mut h, &Value::Int(-2));
|
||||
ps.print_newline(&mut h);
|
||||
assert_eq!(h.output, " 1 -2 \n");
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
ps.print_value(&mut s, &mut d, &Value::Int(1)).unwrap();
|
||||
ps.print_value(&mut s, &mut d, &Value::Int(-2)).unwrap();
|
||||
ps.print_newline(&mut s, &mut d).unwrap();
|
||||
assert_eq!(zeile(&s, 1), " 1 -2");
|
||||
assert_eq!(s.csrlin(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn druckzone_folgt_dem_cursor_nach_locate() {
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
s.locate(3, 5).unwrap();
|
||||
ps.print_comma(&mut s, &mut d).unwrap(); // Spalte 4 → nächste Zone 14
|
||||
assert_eq!(s.pos(), 15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,199 @@
|
||||
//! Datums- und Zeitfunktionen der Laufzeitbibliothek.
|
||||
//!
|
||||
//! Serielle Datumswerte folgen der Zählweise des Vorbilds: der ganzzahlige
|
||||
//! Anteil zählt Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900), der
|
||||
//! Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
|
||||
//!
|
||||
//! ponytail: Die Uhr läuft in UTC — die Standardbibliothek kennt keine
|
||||
//! Zeitzonen, und eine Zeitzonenbibliothek wäre die erste schwere
|
||||
//! Abhängigkeit der Laufzeit. Die Abweichung steht in der Sprachreferenz.
|
||||
//! Ceiling: sobald lokale Zeit gefordert ist, hier eine Zeitzonenquelle
|
||||
//! einhängen — der Rest des Moduls rechnet bereits auf Sekunden seit Epoche.
|
||||
|
||||
// Platzhalter — wird in Phase 3 ausgearbeitet (siehe PLAN.md)
|
||||
/// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0).
|
||||
const EPOCH_OFFSET_TAGE: i64 = 25_569;
|
||||
|
||||
/// Tage → (Jahr, Monat, Tag) ab Unix-Epoche (Howard Hinnant).
|
||||
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let doe = (z - era * 146_097) as u64;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||||
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
|
||||
(y + i64::from(m <= 2), m, d)
|
||||
}
|
||||
|
||||
/// (Jahr, Monat, Tag) → Tage ab Unix-Epoche.
|
||||
pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
|
||||
let y = y - i64::from(m <= 2);
|
||||
let era = if y >= 0 { y } else { y - 399 } / 400;
|
||||
let yoe = (y - era * 400) as u64;
|
||||
let mp = if m > 2 { m - 3 } else { m + 9 } as u64;
|
||||
let doy = (153 * mp + 2) / 5 + u64::from(d) - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146_097 + doe as i64 - 719_468
|
||||
}
|
||||
|
||||
/// Gültige Kalenderangabe? (Monat 1–12, Tag im Monat.)
|
||||
pub fn gueltig(y: i64, m: i64, d: i64) -> bool {
|
||||
if !(1..=12).contains(&m) || d < 1 {
|
||||
return false;
|
||||
}
|
||||
let tage = tage_im_monat(y, m as u32);
|
||||
d <= tage as i64
|
||||
}
|
||||
|
||||
fn tage_im_monat(y: i64, m: u32) -> u32 {
|
||||
match m {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
2 if schaltjahr(y) => 29,
|
||||
2 => 28,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn schaltjahr(y: i64) -> bool {
|
||||
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
|
||||
}
|
||||
|
||||
/// Serieller Wert aus Jahr/Monat/Tag.
|
||||
pub fn serial_aus_ymd(y: i64, m: u32, d: u32) -> f64 {
|
||||
(days_from_civil(y, m, d) + EPOCH_OFFSET_TAGE) as f64
|
||||
}
|
||||
|
||||
/// (Jahr, Monat, Tag) aus einem seriellen Wert.
|
||||
pub fn ymd_aus_serial(s: f64) -> (i64, u32, u32) {
|
||||
civil_from_days(s.floor() as i64 - EPOCH_OFFSET_TAGE)
|
||||
}
|
||||
|
||||
/// Tageszeit eines seriellen Werts als (Stunde, Minute, Sekunde).
|
||||
pub fn hms_aus_serial(s: f64) -> (u32, u32, u32) {
|
||||
// Auf die Sekunde runden, damit 0.5 exakt 12:00:00 ergibt.
|
||||
let sekunden = ((s - s.floor()) * 86_400.0).round() as i64;
|
||||
let sekunden = sekunden.rem_euclid(86_400);
|
||||
(
|
||||
(sekunden / 3600) as u32,
|
||||
((sekunden / 60) % 60) as u32,
|
||||
(sekunden % 60) as u32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Wochentag: 1 = Sonntag … 7 = Samstag.
|
||||
pub fn weekday(s: f64) -> u32 {
|
||||
// Der 1.1.1970 war ein Donnerstag (Index 4 in dieser Zählung).
|
||||
let tage = s.floor() as i64 - EPOCH_OFFSET_TAGE;
|
||||
(tage.rem_euclid(7) as u32 + 4) % 7 + 1
|
||||
}
|
||||
|
||||
/// Tageszeit als serieller Bruchteil.
|
||||
pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 {
|
||||
(h * 3600 + m * 60 + s) as f64 / 86_400.0
|
||||
}
|
||||
|
||||
/// Sekunden seit der Unix-Epoche (UTC).
|
||||
pub fn jetzt_sekunden() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `NOW` — aktueller Zeitpunkt als serieller Wert.
|
||||
pub fn jetzt_serial(offset_sekunden: i64) -> f64 {
|
||||
let s = jetzt_sekunden() + offset_sekunden;
|
||||
s as f64 / 86_400.0 + EPOCH_OFFSET_TAGE as f64
|
||||
}
|
||||
|
||||
/// `DATEVALUE` — Datum aus einem Text lesen (`MM-TT-JJJJ`, `MM/TT/JJJJ`,
|
||||
/// zweistelliges Jahr wird als 19xx/20xx gedeutet wie beim Vorbild).
|
||||
pub fn datevalue(t: &str) -> Option<f64> {
|
||||
let teile: Vec<&str> = t
|
||||
.trim()
|
||||
.split(['-', '/', '.'])
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect();
|
||||
if teile.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let m: i64 = teile[0].trim().parse().ok()?;
|
||||
let d: i64 = teile[1].trim().parse().ok()?;
|
||||
let mut y: i64 = teile[2].trim().parse().ok()?;
|
||||
if y < 100 {
|
||||
y += if y < 80 { 2000 } else { 1900 };
|
||||
}
|
||||
if !gueltig(y, m, d) {
|
||||
return None;
|
||||
}
|
||||
Some(serial_aus_ymd(y, m as u32, d as u32))
|
||||
}
|
||||
|
||||
/// `TIMEVALUE` — Tageszeit aus einem Text lesen (`HH:MM[:SS]`).
|
||||
pub fn timevalue(t: &str) -> Option<f64> {
|
||||
let teile: Vec<&str> = t.trim().split(':').collect();
|
||||
if !(2..=3).contains(&teile.len()) {
|
||||
return None;
|
||||
}
|
||||
let h: i64 = teile[0].trim().parse().ok()?;
|
||||
let m: i64 = teile[1].trim().parse().ok()?;
|
||||
let s: i64 = teile.get(2).map_or(Ok(0), |x| x.trim().parse()).ok()?;
|
||||
if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&s) {
|
||||
return None;
|
||||
}
|
||||
Some(serial_aus_hms(h, m, s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serienwert_und_rueckumwandlung() {
|
||||
let s = serial_aus_ymd(1992, 3, 15);
|
||||
assert_eq!(ymd_aus_serial(s), (1992, 3, 15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zaehlweise_des_vorbilds() {
|
||||
// Serie 2 = 1. Januar 1900.
|
||||
assert_eq!(serial_aus_ymd(1900, 1, 1), 2.0);
|
||||
assert_eq!(ymd_aus_serial(2.0), (1900, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wochentag_sonntag_ist_eins() {
|
||||
// 15. März 1992 war ein Sonntag.
|
||||
assert_eq!(weekday(serial_aus_ymd(1992, 3, 15)), 1);
|
||||
// 16. März 1992 war ein Montag.
|
||||
assert_eq!(weekday(serial_aus_ymd(1992, 3, 16)), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tageszeit() {
|
||||
let s = serial_aus_hms(13, 45, 30);
|
||||
assert_eq!(hms_aus_serial(s), (13, 45, 30));
|
||||
assert_eq!(hms_aus_serial(0.5), (12, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gueltigkeitspruefung() {
|
||||
assert!(gueltig(1992, 2, 29)); // Schaltjahr
|
||||
assert!(!gueltig(1993, 2, 29));
|
||||
assert!(!gueltig(1992, 13, 1));
|
||||
assert!(!gueltig(1992, 4, 31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_in_serienwert() {
|
||||
assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15)));
|
||||
assert_eq!(datevalue("3/15/92"), Some(serial_aus_ymd(1992, 3, 15)));
|
||||
assert_eq!(datevalue("13-01-1992"), None);
|
||||
assert_eq!(timevalue("13:45:30"), Some(serial_aus_hms(13, 45, 30)));
|
||||
assert_eq!(timevalue("13:45"), Some(serial_aus_hms(13, 45, 0)));
|
||||
assert_eq!(timevalue("25:00"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,660 @@
|
||||
//! Datei-E/A der Laufzeitbibliothek (sequenziell, Random Access, binär).
|
||||
//! Datei-E/A: `OPEN`/`CLOSE`, sequenzieller, wahlfreier und binärer Zugriff,
|
||||
//! Recordpuffer mit Festtextfeldern, Positions- und Statusfunktionen sowie
|
||||
//! die Dateisystem-Anweisungen.
|
||||
//!
|
||||
//! Feste Strings in Recordpuffern liegen als **UTF-32** (4 Bytes je Zeichen),
|
||||
//! damit `STRING * n` unabhängig vom Inhalt genau `4 · n` Bytes belegt und
|
||||
//! die Recordlänge konstant bleibt (Entscheidung 2026-09-02). Binärdateien
|
||||
//! sind dadurch bewusst nicht mit denen des Vorbilds austauschbar.
|
||||
|
||||
// Platzhalter — wird in Phase 3 ausgearbeitet (siehe PLAN.md)
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
|
||||
/// Zugriffsart einer offenen Datei.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Modus {
|
||||
Input,
|
||||
Output,
|
||||
Append,
|
||||
Random,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Modus {
|
||||
/// Aus der Kurzsyntax `OPEN "I", #1, …`.
|
||||
pub fn aus_text(s: &str) -> Option<Modus> {
|
||||
match s.trim().to_ascii_uppercase().chars().next()? {
|
||||
'I' => Some(Modus::Input),
|
||||
'O' => Some(Modus::Output),
|
||||
'A' => Some(Modus::Append),
|
||||
'R' => Some(Modus::Random),
|
||||
'B' => Some(Modus::Binary),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein Feld des Recordpuffers (`FIELD`): die gebundene Variable und ihr
|
||||
/// Bereich im Puffer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Feld {
|
||||
pub referenz: crate::value::VarRef,
|
||||
pub start: usize,
|
||||
pub laenge: usize,
|
||||
}
|
||||
|
||||
/// Eine offene Datei.
|
||||
#[derive(Debug)]
|
||||
pub struct Datei {
|
||||
pub modus: Modus,
|
||||
pub pfad: PathBuf,
|
||||
datei: File,
|
||||
/// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
|
||||
pub reclen: usize,
|
||||
/// Recordpuffer für `FIELD`/`GET`/`PUT`.
|
||||
pub puffer: Vec<u8>,
|
||||
pub felder: Vec<Feld>,
|
||||
/// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
|
||||
/// jeweils 1-basiert.
|
||||
pub position: u64,
|
||||
/// Zeilenpuffer für sequenzielles Lesen.
|
||||
leser: Option<BufReader<File>>,
|
||||
}
|
||||
|
||||
impl Datei {
|
||||
fn oeffnen(pfad: &Path, modus: Modus, reclen: usize) -> Result<Datei, RuntimeError> {
|
||||
let mut opt = OpenOptions::new();
|
||||
match modus {
|
||||
Modus::Input => {
|
||||
opt.read(true);
|
||||
}
|
||||
Modus::Output => {
|
||||
opt.write(true).create(true).truncate(true);
|
||||
}
|
||||
Modus::Append => {
|
||||
opt.write(true).create(true).append(true);
|
||||
}
|
||||
Modus::Random | Modus::Binary => {
|
||||
opt.read(true).write(true).create(true);
|
||||
}
|
||||
}
|
||||
let datei = opt.open(pfad).map_err(fehler_aus_io)?;
|
||||
let leser = match modus {
|
||||
Modus::Input => Some(BufReader::new(
|
||||
File::open(pfad).map_err(fehler_aus_io)?,
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
let reclen = if modus == Modus::Random { reclen.max(1) } else { 1 };
|
||||
Ok(Datei {
|
||||
modus,
|
||||
pfad: pfad.to_path_buf(),
|
||||
datei,
|
||||
reclen,
|
||||
puffer: vec![b' '; reclen],
|
||||
felder: Vec::new(),
|
||||
position: 1,
|
||||
leser,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dateigröße in Bytes.
|
||||
pub fn laenge(&self) -> Result<u64, RuntimeError> {
|
||||
self.datei.metadata().map(|m| m.len()).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// `EOF` — bei sequenziellem Lesen: nichts mehr im Puffer; sonst:
|
||||
/// Position hinter dem letzten Datensatz bzw. Byte.
|
||||
pub fn eof(&mut self) -> Result<bool, RuntimeError> {
|
||||
match self.modus {
|
||||
Modus::Input => {
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
Ok(l.fill_buf().map_err(fehler_aus_io)?.is_empty())
|
||||
}
|
||||
Modus::Random => Ok((self.position - 1) * self.reclen as u64 >= self.laenge()?),
|
||||
Modus::Binary => Ok(self.position > self.laenge()?),
|
||||
// Beim Schreiben ist das Ende immer erreicht.
|
||||
Modus::Output | Modus::Append => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text anhängen (`PRINT #`, `WRITE #`).
|
||||
pub fn schreiben(&mut self, text: &str) -> Result<(), RuntimeError> {
|
||||
if !matches!(self.modus, Modus::Output | Modus::Append) {
|
||||
return Err(RuntimeError(54)); // Bad file mode
|
||||
}
|
||||
self.datei
|
||||
.write_all(text.as_bytes())
|
||||
.map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
|
||||
pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
|
||||
if self.modus != Modus::Input {
|
||||
return Err(RuntimeError(54));
|
||||
}
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
let mut roh = Vec::new();
|
||||
let n = l.read_until(b'\n', &mut roh).map_err(fehler_aus_io)?;
|
||||
if n == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
while roh.last() == Some(&b'\n') || roh.last() == Some(&b'\r') {
|
||||
roh.pop();
|
||||
}
|
||||
Ok(Some(String::from_utf8_lossy(&roh).into_owned()))
|
||||
}
|
||||
|
||||
/// Datensatz `nr` (1-basiert) in den Puffer lesen.
|
||||
pub fn record_lesen(&mut self, nr: u64) -> Result<(), RuntimeError> {
|
||||
let offset = (nr - 1) * self.reclen as u64;
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(fehler_aus_io)?;
|
||||
self.puffer = vec![b' '; self.reclen];
|
||||
// Ein unvollständig gelesener Satz bleibt mit Leerzeichen gefüllt.
|
||||
let mut gelesen = 0;
|
||||
while gelesen < self.reclen {
|
||||
match self.datei.read(&mut self.puffer[gelesen..]) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => gelesen += n,
|
||||
Err(e) => return Err(fehler_aus_io(e)),
|
||||
}
|
||||
}
|
||||
self.position = nr + 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Puffer als Datensatz `nr` schreiben.
|
||||
pub fn record_schreiben(&mut self, nr: u64) -> Result<(), RuntimeError> {
|
||||
let offset = (nr - 1) * self.reclen as u64;
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(fehler_aus_io)?;
|
||||
let puffer = std::mem::take(&mut self.puffer);
|
||||
self.datei.write_all(&puffer).map_err(fehler_aus_io)?;
|
||||
self.puffer = puffer;
|
||||
self.position = nr + 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `n` Bytes ab Position `pos` (1-basiert) lesen.
|
||||
pub fn bytes_lesen(&mut self, pos: u64, n: usize) -> Result<Vec<u8>, RuntimeError> {
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(pos - 1))
|
||||
.map_err(fehler_aus_io)?;
|
||||
let mut buf = vec![0u8; n];
|
||||
let mut gelesen = 0;
|
||||
while gelesen < n {
|
||||
match self.datei.read(&mut buf[gelesen..]) {
|
||||
Ok(0) => break,
|
||||
Ok(k) => gelesen += k,
|
||||
Err(e) => return Err(fehler_aus_io(e)),
|
||||
}
|
||||
}
|
||||
if gelesen < n {
|
||||
return Err(RuntimeError(62)); // Input past end of file
|
||||
}
|
||||
self.position = pos + n as u64;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Bytes ab Position `pos` (1-basiert) schreiben.
|
||||
pub fn bytes_schreiben(&mut self, pos: u64, daten: &[u8]) -> Result<(), RuntimeError> {
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(pos - 1))
|
||||
.map_err(fehler_aus_io)?;
|
||||
self.datei.write_all(daten).map_err(fehler_aus_io)?;
|
||||
self.position = pos + daten.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schliessen(&mut self) -> Result<(), RuntimeError> {
|
||||
self.datei.flush().map_err(fehler_aus_io)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tabelle der offenen Dateien, nach Dateinummer.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dateien {
|
||||
offen: BTreeMap<i32, Datei>,
|
||||
}
|
||||
|
||||
impl Dateien {
|
||||
/// `OPEN` — Datei an eine Nummer binden.
|
||||
pub fn oeffnen(
|
||||
&mut self,
|
||||
nummer: i32,
|
||||
pfad: &str,
|
||||
modus: Modus,
|
||||
reclen: usize,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if nummer < 1 {
|
||||
return Err(RuntimeError(52)); // Bad file name or number
|
||||
}
|
||||
if self.offen.contains_key(&nummer) {
|
||||
return Err(RuntimeError(55)); // File already open
|
||||
}
|
||||
let p = pfad_normieren(pfad);
|
||||
if modus == Modus::Input && !p.exists() {
|
||||
return Err(RuntimeError(53)); // File not found
|
||||
}
|
||||
self.offen.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&mut self, nummer: i32) -> Result<&mut Datei, RuntimeError> {
|
||||
self.offen.get_mut(&nummer).ok_or(RuntimeError(52))
|
||||
}
|
||||
|
||||
pub fn ist_offen(&self, nummer: i32) -> bool {
|
||||
self.offen.contains_key(&nummer)
|
||||
}
|
||||
|
||||
/// `CLOSE #n` — eine Datei schließen.
|
||||
pub fn schliessen(&mut self, nummer: i32) -> Result<(), RuntimeError> {
|
||||
match self.offen.remove(&nummer) {
|
||||
Some(mut d) => d.schliessen(),
|
||||
None => Ok(()), // CLOSE auf eine geschlossene Nummer ist folgenlos
|
||||
}
|
||||
}
|
||||
|
||||
/// `CLOSE` / `RESET` — alle Dateien schließen.
|
||||
pub fn alle_schliessen(&mut self) -> Result<(), RuntimeError> {
|
||||
let nummern: Vec<i32> = self.offen.keys().copied().collect();
|
||||
for n in nummern {
|
||||
self.schliessen(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Zu welcher Datei und welchem Bereich gehört diese Variable?
|
||||
pub fn feld_finden(&self, r: &crate::value::VarRef) -> Option<(i32, usize, usize)> {
|
||||
for (n, d) in &self.offen {
|
||||
for f in &d.felder {
|
||||
if gleiche_referenz(&f.referenz, r) {
|
||||
return Some((*n, f.start, f.laenge));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `FREEFILE` — kleinste freie Nummer.
|
||||
pub fn freie_nummer(&self) -> i32 {
|
||||
(1..).find(|n| !self.offen.contains_key(n)).unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeigen zwei Referenzen auf denselben Speicherort?
|
||||
fn gleiche_referenz(a: &crate::value::VarRef, b: &crate::value::VarRef) -> bool {
|
||||
use crate::value::VarRef::*;
|
||||
match (a, b) {
|
||||
(Global(x), Global(y)) => x == y,
|
||||
(Stack(x), Stack(y)) => x == y,
|
||||
(Elem(x, i), Elem(y, j)) => std::rc::Rc::ptr_eq(x, y) && i == j,
|
||||
(Field(x, p), Field(y, q)) => std::rc::Rc::ptr_eq(x, y) && p == q,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Recordpuffer: feste Strings als UTF-32 ---------------------------------
|
||||
|
||||
/// Bytes je Zeichen eines festen Strings im Recordpuffer.
|
||||
pub const UTF32_BREITE: usize = 4;
|
||||
|
||||
/// Festen String in den Puffer schreiben: links- bzw. rechtsbündig,
|
||||
/// überlange Werte werden abgeschnitten.
|
||||
pub fn feld_setzen(puffer: &mut [u8], start: usize, laenge: usize, wert: &str, rechts: bool) {
|
||||
let zeichen: Vec<char> = wert.chars().take(laenge).collect();
|
||||
let fehlt = laenge - zeichen.len();
|
||||
let mut alle: Vec<char> = Vec::with_capacity(laenge);
|
||||
if rechts {
|
||||
alle.extend(std::iter::repeat_n(' ', fehlt));
|
||||
alle.extend(zeichen);
|
||||
} else {
|
||||
alle.extend(zeichen);
|
||||
alle.extend(std::iter::repeat_n(' ', fehlt));
|
||||
}
|
||||
for (i, c) in alle.iter().enumerate() {
|
||||
let off = start + i * UTF32_BREITE;
|
||||
if off + UTF32_BREITE <= puffer.len() {
|
||||
puffer[off..off + UTF32_BREITE].copy_from_slice(&(*c as u32).to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Festen String aus dem Puffer lesen.
|
||||
pub fn feld_lesen(puffer: &[u8], start: usize, laenge: usize) -> String {
|
||||
let mut s = String::with_capacity(laenge);
|
||||
for i in 0..laenge {
|
||||
let off = start + i * UTF32_BREITE;
|
||||
if off + UTF32_BREITE > puffer.len() {
|
||||
break;
|
||||
}
|
||||
let code = u32::from_le_bytes([
|
||||
puffer[off],
|
||||
puffer[off + 1],
|
||||
puffer[off + 2],
|
||||
puffer[off + 3],
|
||||
]);
|
||||
s.push(char::from_u32(code).unwrap_or(' '));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ---- Werte im Recordpuffer --------------------------------------------------
|
||||
|
||||
use crate::value::{RecordObj, TypeInit, UdtLayout, Value};
|
||||
|
||||
/// Bytebreite eines Feldtyps im Recordpuffer. Feste Strings zählen
|
||||
/// `UTF32_BREITE` Bytes je Zeichen; Strings variabler Länge sind in Records
|
||||
/// nicht zulässig (Vorbild: nur `STRING * n`).
|
||||
pub fn breite(t: &TypeInit, layouts: &[UdtLayout]) -> Option<usize> {
|
||||
Some(match t {
|
||||
TypeInit::Int => 2,
|
||||
TypeInit::Lng | TypeInit::Sng => 4,
|
||||
TypeInit::Dbl | TypeInit::Cur => 8,
|
||||
TypeInit::FixedStr(n) => *n as usize * UTF32_BREITE,
|
||||
TypeInit::Udt(i) => {
|
||||
let felder = &layouts.get(*i as usize)?.fields;
|
||||
let mut summe = 0;
|
||||
for f in felder {
|
||||
summe += breite(f, layouts)?;
|
||||
}
|
||||
summe
|
||||
}
|
||||
TypeInit::Str | TypeInit::Empty => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wert in den Puffer schreiben; liefert die belegte Bytezahl.
|
||||
pub fn wert_schreiben(
|
||||
puffer: &mut [u8],
|
||||
offset: usize,
|
||||
wert: &Value,
|
||||
typ: &TypeInit,
|
||||
layouts: &[UdtLayout],
|
||||
) -> Result<usize, RuntimeError> {
|
||||
let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
|
||||
if offset + n > puffer.len() {
|
||||
return Err(RuntimeError(59)); // Bad record length
|
||||
}
|
||||
match (typ, wert) {
|
||||
(TypeInit::Int, v) => puffer[offset..offset + 2]
|
||||
.copy_from_slice(&(crate::value::as_f64(v) as i16).to_le_bytes()),
|
||||
(TypeInit::Lng, v) => puffer[offset..offset + 4]
|
||||
.copy_from_slice(&(crate::value::as_f64(v) as i32).to_le_bytes()),
|
||||
(TypeInit::Sng, v) => puffer[offset..offset + 4]
|
||||
.copy_from_slice(&(crate::value::as_f64(v) as f32).to_le_bytes()),
|
||||
(TypeInit::Dbl, v) => {
|
||||
puffer[offset..offset + 8].copy_from_slice(&crate::value::as_f64(v).to_le_bytes())
|
||||
}
|
||||
(TypeInit::Cur, Value::Cur(c)) => {
|
||||
puffer[offset..offset + 8].copy_from_slice(&c.to_le_bytes())
|
||||
}
|
||||
(TypeInit::Cur, v) => puffer[offset..offset + 8]
|
||||
.copy_from_slice(&crate::value::f64_to_cur(crate::value::as_f64(v))?.to_le_bytes()),
|
||||
(TypeInit::FixedStr(len), Value::Str(s)) => {
|
||||
feld_setzen(puffer, offset, *len as usize, s, false)
|
||||
}
|
||||
(TypeInit::Udt(i), Value::Rec(r)) => {
|
||||
let felder = &layouts.get(*i as usize).ok_or(RuntimeError::TYPE_MISMATCH)?.fields;
|
||||
let rec = r.borrow();
|
||||
let mut off = offset;
|
||||
for (ft, fv) in felder.iter().zip(rec.fields.iter()) {
|
||||
off += wert_schreiben(puffer, off, fv, ft, layouts)?;
|
||||
}
|
||||
}
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Wert aus dem Puffer lesen.
|
||||
pub fn wert_lesen(
|
||||
puffer: &[u8],
|
||||
offset: usize,
|
||||
typ: &TypeInit,
|
||||
layouts: &[UdtLayout],
|
||||
) -> Result<(Value, usize), RuntimeError> {
|
||||
let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
|
||||
if offset + n > puffer.len() {
|
||||
return Err(RuntimeError(59));
|
||||
}
|
||||
let b = &puffer[offset..offset + n];
|
||||
let v = match typ {
|
||||
TypeInit::Int => Value::Int(i16::from_le_bytes([b[0], b[1]])),
|
||||
TypeInit::Lng => Value::Lng(i32::from_le_bytes([b[0], b[1], b[2], b[3]])),
|
||||
TypeInit::Sng => Value::Sng(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
|
||||
TypeInit::Dbl => Value::Dbl(f64::from_le_bytes(b[..8].try_into().unwrap())),
|
||||
TypeInit::Cur => Value::Cur(i64::from_le_bytes(b[..8].try_into().unwrap())),
|
||||
TypeInit::FixedStr(len) => {
|
||||
Value::Str(std::rc::Rc::from(feld_lesen(puffer, offset, *len as usize).as_str()))
|
||||
}
|
||||
TypeInit::Udt(i) => {
|
||||
let felder = &layouts.get(*i as usize).ok_or(RuntimeError::TYPE_MISMATCH)?.fields;
|
||||
let mut off = offset;
|
||||
let mut werte = Vec::with_capacity(felder.len());
|
||||
for ft in felder {
|
||||
let (v, k) = wert_lesen(puffer, off, ft, layouts)?;
|
||||
werte.push(v);
|
||||
off += k;
|
||||
}
|
||||
Value::Rec(std::rc::Rc::new(std::cell::RefCell::new(RecordObj {
|
||||
fields: werte,
|
||||
})))
|
||||
}
|
||||
TypeInit::Str | TypeInit::Empty => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
};
|
||||
Ok((v, n))
|
||||
}
|
||||
|
||||
// ---- Pfade und Dateisystem --------------------------------------------------
|
||||
|
||||
/// Pfadtrennzeichen des Vorbilds (`\`) auf die Plattform abbilden.
|
||||
pub fn pfad_normieren(p: &str) -> PathBuf {
|
||||
if std::path::MAIN_SEPARATOR == '\\' {
|
||||
PathBuf::from(p)
|
||||
} else {
|
||||
PathBuf::from(p.replace('\\', "/"))
|
||||
}
|
||||
}
|
||||
|
||||
fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
|
||||
use std::io::ErrorKind::*;
|
||||
match e.kind() {
|
||||
NotFound => RuntimeError(53), // File not found
|
||||
PermissionDenied => RuntimeError(70), // Permission denied
|
||||
AlreadyExists => RuntimeError(58), // File already exists
|
||||
_ => RuntimeError(57), // Device I/O error
|
||||
}
|
||||
}
|
||||
|
||||
/// `KILL` — Datei löschen.
|
||||
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
std::fs::remove_file(pfad_normieren(pfad)).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// `NAME alt AS neu`.
|
||||
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
|
||||
let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
|
||||
if !a.exists() {
|
||||
return Err(RuntimeError(53));
|
||||
}
|
||||
if n.exists() {
|
||||
return Err(RuntimeError(58)); // File already exists
|
||||
}
|
||||
std::fs::rename(a, n).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// `MKDIR` / `RMDIR` / `CHDIR`.
|
||||
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
let p = pfad_normieren(pfad);
|
||||
if p.parent().is_some_and(|e| !e.as_os_str().is_empty() && !e.exists()) {
|
||||
return Err(RuntimeError(76)); // Path not found
|
||||
}
|
||||
std::fs::create_dir(p).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
std::fs::remove_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => RuntimeError(76),
|
||||
_ => fehler_aus_io(e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verzeichnis_wechseln(pfad: &str) -> Result<(), RuntimeError> {
|
||||
std::env::set_current_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => RuntimeError(76),
|
||||
_ => fehler_aus_io(e),
|
||||
})
|
||||
}
|
||||
|
||||
/// `CURDIR$`.
|
||||
pub fn aktuelles_verzeichnis() -> String {
|
||||
std::env::current_dir()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Verzeichnissuche für `DIR$` und `FILES`: alle Namen, die auf das Muster
|
||||
/// passen (`*` und `?` wie beim Vorbild).
|
||||
pub fn suchen(muster: &str) -> Result<Vec<String>, RuntimeError> {
|
||||
let p = pfad_normieren(muster);
|
||||
let (verzeichnis, maske) = match p.file_name() {
|
||||
Some(n) => (
|
||||
p.parent()
|
||||
.filter(|e| !e.as_os_str().is_empty())
|
||||
.map(|e| e.to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from(".")),
|
||||
n.to_string_lossy().into_owned(),
|
||||
),
|
||||
None => (p.clone(), "*".to_string()),
|
||||
};
|
||||
let eintraege = std::fs::read_dir(&verzeichnis).map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => RuntimeError(76),
|
||||
_ => fehler_aus_io(e),
|
||||
})?;
|
||||
let mut namen: Vec<String> = eintraege
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| passt(n, &maske))
|
||||
.collect();
|
||||
namen.sort();
|
||||
Ok(namen)
|
||||
}
|
||||
|
||||
/// Musterabgleich mit `*` (beliebig viele Zeichen) und `?` (genau eines).
|
||||
pub fn passt(name: &str, muster: &str) -> bool {
|
||||
let n: Vec<char> = name.chars().collect();
|
||||
let m: Vec<char> = muster.chars().collect();
|
||||
fn rek(n: &[char], m: &[char]) -> bool {
|
||||
match m.first() {
|
||||
None => n.is_empty(),
|
||||
Some('*') => rek(n, &m[1..]) || (!n.is_empty() && rek(&n[1..], m)),
|
||||
Some('?') => !n.is_empty() && rek(&n[1..], &m[1..]),
|
||||
Some(c) => {
|
||||
!n.is_empty()
|
||||
&& n[0].eq_ignore_ascii_case(c)
|
||||
&& rek(&n[1..], &m[1..])
|
||||
}
|
||||
}
|
||||
}
|
||||
rek(&n, &m)
|
||||
}
|
||||
|
||||
/// Elemente einer `INPUT #`-Zeile trennen: Komma trennt, Anführungszeichen
|
||||
/// klammern, Leerraum am Rand fällt weg.
|
||||
pub fn felder_trennen(zeile: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut aktuell = String::new();
|
||||
let mut in_quote = false;
|
||||
let mut hatte_quote = false;
|
||||
for c in zeile.chars() {
|
||||
match c {
|
||||
'"' if !in_quote && aktuell.trim().is_empty() => {
|
||||
in_quote = true;
|
||||
hatte_quote = true;
|
||||
aktuell.clear();
|
||||
}
|
||||
'"' if in_quote => in_quote = false,
|
||||
',' if !in_quote => {
|
||||
out.push(if hatte_quote {
|
||||
std::mem::take(&mut aktuell)
|
||||
} else {
|
||||
std::mem::take(&mut aktuell).trim().to_string()
|
||||
});
|
||||
hatte_quote = false;
|
||||
}
|
||||
_ => aktuell.push(c),
|
||||
}
|
||||
}
|
||||
out.push(if hatte_quote {
|
||||
aktuell
|
||||
} else {
|
||||
aktuell.trim().to_string()
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Ein Element für `WRITE #` aufbereiten: Strings in Anführungszeichen.
|
||||
pub fn write_element(text: &str, ist_string: bool) -> String {
|
||||
if ist_string {
|
||||
format!("\"{text}\"")
|
||||
} else {
|
||||
text.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn muster_mit_stern_und_fragezeichen() {
|
||||
assert!(passt("test.txt", "*.txt"));
|
||||
assert!(passt("test.txt", "t?st.txt"));
|
||||
assert!(!passt("test.txt", "*.bas"));
|
||||
assert!(passt("TEST.TXT", "*.txt")); // Vorbild: Groß/klein egal
|
||||
assert!(passt("a", "*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn festes_feld_ist_inhaltsunabhaengig_gleich_lang() {
|
||||
let mut p = vec![0u8; 10 * UTF32_BREITE];
|
||||
feld_setzen(&mut p, 0, 10, "abc", false);
|
||||
assert_eq!(feld_lesen(&p, 0, 10), "abc ");
|
||||
feld_setzen(&mut p, 0, 10, "äöüßÄÖÜ", false);
|
||||
assert_eq!(feld_lesen(&p, 0, 10), "äöüßÄÖÜ ");
|
||||
// Der Puffer hat in beiden Fällen dieselbe Länge.
|
||||
assert_eq!(p.len(), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rset_schneidet_ab_und_richtet_rechts_aus() {
|
||||
let mut p = vec![0u8; 5 * UTF32_BREITE];
|
||||
feld_setzen(&mut p, 0, 5, "ABCDEFG", true);
|
||||
assert_eq!(feld_lesen(&p, 0, 5), "ABCDE");
|
||||
feld_setzen(&mut p, 0, 5, "xy", true);
|
||||
assert_eq!(feld_lesen(&p, 0, 5), " xy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn felder_einer_input_zeile() {
|
||||
assert_eq!(felder_trennen("a,b,c"), vec!["a", "b", "c"]);
|
||||
assert_eq!(felder_trennen(" 1 , 2 "), vec!["1", "2"]);
|
||||
assert_eq!(felder_trennen("\"a,b\",5"), vec!["a,b", "5"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modus_aus_kurzsyntax() {
|
||||
assert_eq!(Modus::aus_text("I"), Some(Modus::Input));
|
||||
assert_eq!(Modus::aus_text("output"), Some(Modus::Output));
|
||||
assert_eq!(Modus::aus_text("x"), None);
|
||||
}
|
||||
}
|
||||
|
||||
377
crates/tb-runtime/src/finanz.rs
Normal file
377
crates/tb-runtime/src/finanz.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
//! Finanzmathematische Funktionen des Vorbilds (`FV#`, `PV#`, `Pmt#`,
|
||||
//! `IPmt#`, `PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`,
|
||||
//! `SYD#`, `DDB#`).
|
||||
//!
|
||||
//! Vorzeichenkonvention wie beim Vorbild: ausgehende Zahlungen sind negativ,
|
||||
//! eingehende positiv. Das Fälligkeitsargument (`faellig`) ist 0 für
|
||||
//! nachschüssige und 1 für vorschüssige Zahlung. Gerechnet wird in DOUBLE.
|
||||
//!
|
||||
//! `Rate#` und `IRR#` nähern sich iterativ; die Abbruchbedingung steht in
|
||||
//! [`ITERATIONEN`] und [`TOLERANZ`] und ist in docs/bibliothek.md
|
||||
//! dokumentiert.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
|
||||
/// Höchstzahl der Näherungsschritte von `Rate#` und `IRR#`.
|
||||
pub const ITERATIONEN: usize = 100;
|
||||
/// Abbruchschranke der Näherung (Betrag der Restabweichung).
|
||||
pub const TOLERANZ: f64 = 1e-9;
|
||||
|
||||
type R = Result<f64, RuntimeError>;
|
||||
|
||||
fn ungueltig() -> RuntimeError {
|
||||
RuntimeError::ILLEGAL_FUNCTION_CALL
|
||||
}
|
||||
|
||||
/// `(1 + zins)^perioden`.
|
||||
fn faktor(zins: f64, perioden: f64) -> f64 {
|
||||
(1.0 + zins).powf(perioden)
|
||||
}
|
||||
|
||||
/// Barwertfaktor der Rentenzahlungen.
|
||||
fn rentenfaktor(zins: f64, perioden: f64, faellig: f64) -> f64 {
|
||||
if zins == 0.0 {
|
||||
perioden
|
||||
} else {
|
||||
(1.0 + zins * faellig) * (faktor(zins, perioden) - 1.0) / zins
|
||||
}
|
||||
}
|
||||
|
||||
/// `FV#` — Endwert.
|
||||
pub fn fv(zins: f64, perioden: f64, zahlung: f64, barwert: f64, faellig: f64) -> R {
|
||||
if zins <= -1.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok(-(barwert * faktor(zins, perioden) + zahlung * rentenfaktor(zins, perioden, faellig)))
|
||||
}
|
||||
|
||||
/// `PV#` — Barwert.
|
||||
pub fn pv(zins: f64, perioden: f64, zahlung: f64, endwert: f64, faellig: f64) -> R {
|
||||
if zins <= -1.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok(-(endwert + zahlung * rentenfaktor(zins, perioden, faellig)) / faktor(zins, perioden))
|
||||
}
|
||||
|
||||
/// `Pmt#` — Zahlung je Periode.
|
||||
pub fn pmt(zins: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
|
||||
if perioden == 0.0 || zins <= -1.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let rf = rentenfaktor(zins, perioden, faellig);
|
||||
if rf == 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok(-(endwert + barwert * faktor(zins, perioden)) / rf)
|
||||
}
|
||||
|
||||
/// `NPer#` — Anzahl Perioden.
|
||||
pub fn nper(zins: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
|
||||
if zins <= -1.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
if zins == 0.0 {
|
||||
if zahlung == 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
return Ok(-(barwert + endwert) / zahlung);
|
||||
}
|
||||
let z = zahlung * (1.0 + zins * faellig);
|
||||
let zaehler = z - endwert * zins;
|
||||
let nenner = barwert * zins + z;
|
||||
if nenner == 0.0 || zaehler / nenner <= 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok((zaehler / nenner).ln() / (1.0 + zins).ln())
|
||||
}
|
||||
|
||||
/// `IPmt#` — Zinsanteil der Periode `periode` (1-basiert).
|
||||
pub fn ipmt(
|
||||
zins: f64,
|
||||
periode: f64,
|
||||
perioden: f64,
|
||||
barwert: f64,
|
||||
endwert: f64,
|
||||
faellig: f64,
|
||||
) -> R {
|
||||
if periode < 1.0 || periode > perioden {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
|
||||
if faellig != 0.0 && periode == 1.0 {
|
||||
// Vorschüssig fällt in der ersten Periode kein Zins an.
|
||||
return Ok(0.0);
|
||||
}
|
||||
let vorher = if faellig != 0.0 {
|
||||
fv(zins, periode - 2.0, zahlung, barwert, faellig)?
|
||||
} else {
|
||||
fv(zins, periode - 1.0, zahlung, barwert, faellig)?
|
||||
};
|
||||
Ok(vorher * zins)
|
||||
}
|
||||
|
||||
/// `PPmt#` — Tilgungsanteil der Periode.
|
||||
pub fn ppmt(
|
||||
zins: f64,
|
||||
periode: f64,
|
||||
perioden: f64,
|
||||
barwert: f64,
|
||||
endwert: f64,
|
||||
faellig: f64,
|
||||
) -> R {
|
||||
let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
|
||||
let zinsanteil = ipmt(zins, periode, perioden, barwert, endwert, faellig)?;
|
||||
Ok(zahlung - zinsanteil)
|
||||
}
|
||||
|
||||
/// `Rate#` — Zinssatz je Periode, iterativ genähert.
|
||||
pub fn rate(
|
||||
perioden: f64,
|
||||
zahlung: f64,
|
||||
barwert: f64,
|
||||
endwert: f64,
|
||||
faellig: f64,
|
||||
schaetzung: f64,
|
||||
) -> R {
|
||||
if perioden <= 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
// Sekantenverfahren auf f(r) = FV(r) − Sollendwert.
|
||||
let f = |r: f64| -> Option<f64> {
|
||||
if r <= -1.0 {
|
||||
return None;
|
||||
}
|
||||
Some(barwert * faktor(r, perioden) + zahlung * rentenfaktor(r, perioden, faellig) + endwert)
|
||||
};
|
||||
let mut r0 = schaetzung;
|
||||
let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 };
|
||||
let (mut f0, mut f1) = match (f(r0), f(r1)) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
_ => return Err(ungueltig()),
|
||||
};
|
||||
for _ in 0..ITERATIONEN {
|
||||
if f1.abs() < TOLERANZ {
|
||||
return Ok(r1);
|
||||
}
|
||||
if (f1 - f0).abs() < f64::MIN_POSITIVE {
|
||||
break;
|
||||
}
|
||||
let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
|
||||
let Some(f2) = f(r2) else { break };
|
||||
r0 = r1;
|
||||
f0 = f1;
|
||||
r1 = r2;
|
||||
f1 = f2;
|
||||
}
|
||||
if f1.abs() < TOLERANZ * 1e3 {
|
||||
Ok(r1)
|
||||
} else {
|
||||
Err(ungueltig())
|
||||
}
|
||||
}
|
||||
|
||||
/// `NPV#` — Kapitalwert einer Zahlungsreihe (erste Zahlung am Ende der
|
||||
/// ersten Periode).
|
||||
pub fn npv(zins: f64, werte: &[f64]) -> R {
|
||||
if zins <= -1.0 || werte.is_empty() {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let mut summe = 0.0;
|
||||
let mut nenner = 1.0 + zins;
|
||||
for w in werte {
|
||||
summe += w / nenner;
|
||||
nenner *= 1.0 + zins;
|
||||
}
|
||||
Ok(summe)
|
||||
}
|
||||
|
||||
/// `IRR#` — interner Zinsfuß, iterativ genähert.
|
||||
pub fn irr(werte: &[f64], schaetzung: f64) -> R {
|
||||
// Ohne Vorzeichenwechsel gibt es keine Lösung.
|
||||
let hat_plus = werte.iter().any(|w| *w > 0.0);
|
||||
let hat_minus = werte.iter().any(|w| *w < 0.0);
|
||||
if !hat_plus || !hat_minus {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let f = |r: f64| -> Option<f64> {
|
||||
if r <= -1.0 {
|
||||
return None;
|
||||
}
|
||||
let mut summe = 0.0;
|
||||
let mut nenner = 1.0;
|
||||
for w in werte {
|
||||
summe += w / nenner;
|
||||
nenner *= 1.0 + r;
|
||||
}
|
||||
Some(summe)
|
||||
};
|
||||
let mut r0 = schaetzung;
|
||||
let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 };
|
||||
let (mut f0, mut f1) = match (f(r0), f(r1)) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
_ => return Err(ungueltig()),
|
||||
};
|
||||
for _ in 0..ITERATIONEN {
|
||||
if f1.abs() < TOLERANZ {
|
||||
return Ok(r1);
|
||||
}
|
||||
if (f1 - f0).abs() < f64::MIN_POSITIVE {
|
||||
break;
|
||||
}
|
||||
let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
|
||||
let Some(f2) = f(r2) else { break };
|
||||
r0 = r1;
|
||||
f0 = f1;
|
||||
r1 = r2;
|
||||
f1 = f2;
|
||||
}
|
||||
if f1.abs() < TOLERANZ * 1e3 {
|
||||
Ok(r1)
|
||||
} else {
|
||||
Err(ungueltig())
|
||||
}
|
||||
}
|
||||
|
||||
/// `MIRR#` — modifizierter interner Zinsfuß.
|
||||
pub fn mirr(werte: &[f64], finanzierungszins: f64, wiederanlagezins: f64) -> R {
|
||||
let n = werte.len();
|
||||
if n < 2 || finanzierungszins <= -1.0 || wiederanlagezins <= -1.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let negative: Vec<f64> = werte.iter().map(|w| w.min(0.0)).collect();
|
||||
let positive: Vec<f64> = werte.iter().map(|w| w.max(0.0)).collect();
|
||||
let bw_neg = npv(finanzierungszins, &negative)?;
|
||||
let bw_pos = npv(wiederanlagezins, &positive)?;
|
||||
if bw_neg == 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let n = n as f64;
|
||||
let verhaeltnis = -bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins));
|
||||
if verhaeltnis <= 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok(verhaeltnis.powf(1.0 / (n - 1.0)) - 1.0)
|
||||
}
|
||||
|
||||
/// `SLN#` — lineare Abschreibung.
|
||||
pub fn sln(anschaffung: f64, restwert: f64, nutzungsdauer: f64) -> R {
|
||||
if nutzungsdauer == 0.0 {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok((anschaffung - restwert) / nutzungsdauer)
|
||||
}
|
||||
|
||||
/// `SYD#` — arithmetisch-degressive Abschreibung.
|
||||
pub fn syd(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
|
||||
if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
Ok((anschaffung - restwert) * (nutzungsdauer - periode + 1.0) * 2.0
|
||||
/ (nutzungsdauer * (nutzungsdauer + 1.0)))
|
||||
}
|
||||
|
||||
/// `DDB#` — geometrisch-degressive Abschreibung (doppelter linearer Satz).
|
||||
pub fn ddb(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
|
||||
if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
|
||||
return Err(ungueltig());
|
||||
}
|
||||
let mut buchwert = anschaffung;
|
||||
let mut betrag = 0.0;
|
||||
let satz = 2.0 / nutzungsdauer;
|
||||
for _ in 0..(periode as u64) {
|
||||
betrag = (buchwert * satz).min(buchwert - restwert).max(0.0);
|
||||
buchwert -= betrag;
|
||||
}
|
||||
Ok(betrag)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn nah(a: f64, b: f64) {
|
||||
assert!((a - b).abs() < 1e-6, "{a} ≠ {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endwert_einer_rentenzahlung() {
|
||||
// 10 Perioden, 5 % Zins, Zahlung 100 nachschüssig, Barwert 0.
|
||||
nah(fv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap(), 1257.789253);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn barwert_und_endwert_sind_gegenlaeufig() {
|
||||
let b = pv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap();
|
||||
nah(fv(0.05, 10.0, -100.0, b, 0.0).unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zins_und_tilgung_ergeben_die_zahlung() {
|
||||
let (r, n, bw) = (0.05, 10.0, 1000.0);
|
||||
let z = pmt(r, n, bw, 0.0, 0.0).unwrap();
|
||||
for periode in 1..=10 {
|
||||
let i = ipmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
|
||||
let t = ppmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
|
||||
nah(i + t, z);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zinssatz_wird_zurueckgerechnet() {
|
||||
let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
|
||||
nah(rate(10.0, z, 1000.0, 0.0, 0.0, 0.1).unwrap(), 0.05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nper_zaehlt_die_perioden() {
|
||||
let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
|
||||
nah(nper(0.05, z, 1000.0, 0.0, 0.0).unwrap(), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kapitalwert_und_interner_zinsfuss() {
|
||||
let reihe = [-1000.0, 400.0, 400.0, 400.0];
|
||||
let r = irr(&reihe, 0.1).unwrap();
|
||||
// Bei r ist der Kapitalwert (inkl. Anfangsauszahlung) null.
|
||||
let mut summe = 0.0;
|
||||
let mut nenner = 1.0;
|
||||
for w in reihe {
|
||||
summe += w / nenner;
|
||||
nenner *= 1.0 + r;
|
||||
}
|
||||
nah(summe, 0.0);
|
||||
nah(npv(0.05, &[100.0, 100.0]).unwrap(), 185.941043);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irr_ohne_vorzeichenwechsel_ist_fehler() {
|
||||
assert!(irr(&[100.0, 200.0], 0.1).is_err());
|
||||
assert!(irr(&[-100.0, -200.0], 0.1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abschreibungen() {
|
||||
nah(sln(1000.0, 100.0, 10.0).unwrap(), 90.0);
|
||||
// SYD: Summe über alle Perioden = Abschreibungsvolumen.
|
||||
let summe: f64 = (1..=10).map(|p| syd(1000.0, 100.0, 10.0, p as f64).unwrap()).sum();
|
||||
nah(summe, 900.0);
|
||||
nah(syd(1000.0, 100.0, 10.0, 1.0).unwrap(), 163.636363);
|
||||
nah(ddb(1000.0, 100.0, 10.0, 1.0).unwrap(), 200.0);
|
||||
nah(ddb(1000.0, 100.0, 10.0, 2.0).unwrap(), 160.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ungueltige_argumente() {
|
||||
assert!(sln(1.0, 0.0, 0.0).is_err());
|
||||
assert!(syd(1.0, 0.0, 10.0, 11.0).is_err());
|
||||
assert!(ipmt(0.05, 11.0, 10.0, 1000.0, 0.0, 0.0).is_err());
|
||||
assert!(pmt(0.05, 0.0, 1000.0, 0.0, 0.0).is_err());
|
||||
assert!(nper(0.0, 0.0, 1000.0, 0.0, 0.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mirr_liegt_zwischen_den_zinsen() {
|
||||
let r = mirr(&[-1000.0, 400.0, 400.0, 400.0], 0.05, 0.06).unwrap();
|
||||
assert!(r > 0.0 && r < 0.3, "{r}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
|
||||
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
|
||||
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
|
||||
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
|
||||
pub mod host; // Host-Trait (Konsole, Capture) — Entscheidung D3
|
||||
pub mod host;
|
||||
pub mod screen;
|
||||
pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
|
||||
pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
|
||||
pub mod snapshot; // Bildschirmzustand als vergleichbarer Text (Korpus) // Zellenpuffer des Textbildschirms (ohne Terminalbezug) // Host-Trait (Konsole, Capture) — Entscheidung D3
|
||||
pub mod datetime; // DATE$, TIME$, TIMER
|
||||
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
|
||||
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
|
||||
|
||||
557
crates/tb-runtime/src/screen.rs
Normal file
557
crates/tb-runtime/src/screen.rs
Normal file
@@ -0,0 +1,557 @@
|
||||
//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
|
||||
//! Cursor, Scrollen.
|
||||
//!
|
||||
//! Der Puffer liegt in der Laufzeitbibliothek und ist frei von jeder
|
||||
//! Terminal-Abhängigkeit: `PRINT`, `LOCATE`, `CLS`, `COLOR` und `SCREEN()`
|
||||
//! sind Bibliothekselemente und müssen ihn erreichen, und jedes
|
||||
//! Bildschirmverhalten bleibt ohne Terminal prüfbar. Die Darstellung liegt in
|
||||
//! `tb-ui` (`ScreenWidget`).
|
||||
//!
|
||||
//! Entscheidungen (siehe PLAN.md):
|
||||
//! - durchgängig Unicode, keine CP437-Emulation
|
||||
//! - **dynamische Größe**: der Bildschirm folgt der Terminalgröße; unterhalb
|
||||
//! der Mindestgröße (80×25) wird nur ein Hinweis gerendert (btop-artig),
|
||||
//! größere Terminals werden voll genutzt.
|
||||
//!
|
||||
//! Zeichen belegen so viele Zellen, wie ihre Darstellungsbreite verlangt
|
||||
//! (Entscheidung 2026-09-02): schmale Zeichen eine, breite (Emoji, CJK)
|
||||
//! zwei. Die zweite Zelle ist als Fortsetzung markiert und wird nie
|
||||
//! eigenständig beschrieben.
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
|
||||
pub const MIN_COLS: usize = 80;
|
||||
pub const MIN_ROWS: usize = 25;
|
||||
|
||||
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub ch: char,
|
||||
/// Vordergrund 0–15 (klassische Palette).
|
||||
pub fg: u8,
|
||||
/// Hintergrund 0–7.
|
||||
pub bg: u8,
|
||||
/// Zweite Hälfte eines breiten Zeichens. Solche Zellen tragen kein
|
||||
/// eigenes Zeichen und werden bei der Darstellung übersprungen.
|
||||
pub fortsetzung: bool,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Cell { ch: ' ', fg: 7, bg: 0, fortsetzung: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Der emulierte Textbildschirm mit dynamischer Größe.
|
||||
///
|
||||
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
|
||||
/// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextScreen {
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
cells: Vec<Cell>,
|
||||
/// Cursorposition, 0-basiert intern.
|
||||
cur_row: usize,
|
||||
cur_col: usize,
|
||||
pub cursor_visible: bool,
|
||||
/// Aktuelle Ausgabefarben (`COLOR`).
|
||||
pub fg: u8,
|
||||
pub bg: u8,
|
||||
/// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
|
||||
view_top: usize,
|
||||
view_bottom: usize,
|
||||
/// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
|
||||
/// Scrollbereich folgt dann der Bildschirmgröße.
|
||||
view_full: bool,
|
||||
/// Je Zeile die größte beschriebene Spalte (0-basiert), `None` für
|
||||
/// unberührte Zeilen. Grundlage des getrimmten Snapshots: nachgestellte
|
||||
/// Leerzeichen, die `PRINT` selbst ausgibt, zählen als belegt, ein nie
|
||||
/// beschriebener Zeilenrest nicht.
|
||||
belegt: Vec<Option<usize>>,
|
||||
/// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
|
||||
/// Neuzeichnen aufgefordert.
|
||||
veraendert: bool,
|
||||
}
|
||||
|
||||
impl Default for TextScreen {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextScreen {
|
||||
/// Bildschirm in Mindestgröße (80×25).
|
||||
pub fn new() -> Self {
|
||||
Self::with_size(MIN_COLS, MIN_ROWS)
|
||||
}
|
||||
|
||||
/// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
|
||||
pub fn with_size(cols: usize, rows: usize) -> Self {
|
||||
let cols = cols.max(MIN_COLS);
|
||||
let rows = rows.max(MIN_ROWS);
|
||||
TextScreen {
|
||||
cols,
|
||||
rows,
|
||||
cells: vec![Cell::default(); cols * rows],
|
||||
cur_row: 0,
|
||||
cur_col: 0,
|
||||
cursor_visible: true,
|
||||
fg: 7,
|
||||
bg: 0,
|
||||
view_top: 0,
|
||||
view_bottom: rows - 1,
|
||||
view_full: true,
|
||||
belegt: vec![None; rows],
|
||||
veraendert: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cols(&self) -> usize {
|
||||
self.cols
|
||||
}
|
||||
pub fn rows(&self) -> usize {
|
||||
self.rows
|
||||
}
|
||||
|
||||
/// An neue Terminalgröße anpassen: Inhalt bleibt oben links erhalten,
|
||||
/// neue Zellen sind leer. Unterhalb der Mindestgröße bleibt der Puffer
|
||||
/// bei 80×25 (das Widget zeigt dann den Zu-klein-Hinweis).
|
||||
pub fn resize(&mut self, cols: usize, rows: usize) {
|
||||
self.veraendert = true;
|
||||
let cols = cols.max(MIN_COLS);
|
||||
let rows = rows.max(MIN_ROWS);
|
||||
if cols == self.cols && rows == self.rows {
|
||||
return;
|
||||
}
|
||||
let mut cells =
|
||||
vec![Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false }; cols * rows];
|
||||
for row in 0..self.rows.min(rows) {
|
||||
for col in 0..self.cols.min(cols) {
|
||||
cells[row * cols + col] = self.cells[row * self.cols + col];
|
||||
}
|
||||
}
|
||||
self.cells = cells;
|
||||
let mut belegt = vec![None; rows];
|
||||
for r in 0..self.rows.min(rows) {
|
||||
belegt[r] = self.belegt[r].map(|c| c.min(cols - 1));
|
||||
}
|
||||
self.belegt = belegt;
|
||||
self.cols = cols;
|
||||
self.rows = rows;
|
||||
self.cur_row = self.cur_row.min(rows - 1);
|
||||
self.cur_col = self.cur_col.min(cols - 1);
|
||||
if self.view_full || self.view_top > rows - 1 {
|
||||
// Ohne eigenes VIEW PRINT folgt der Bereich dem Bildschirm; ein
|
||||
// Bereich, der ganz vom Schirm gefallen ist, wäre leer und fällt
|
||||
// auf den vollen Bildschirm zurück.
|
||||
self.view_top = 0;
|
||||
self.view_bottom = rows - 1;
|
||||
self.view_full = true;
|
||||
} else {
|
||||
self.view_bottom = self.view_bottom.min(rows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// `CLS`: Scrollbereich mit aktueller Hintergrundfarbe löschen,
|
||||
/// Cursor an den Anfang des Bereichs.
|
||||
pub fn cls(&mut self) {
|
||||
self.veraendert = true;
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
|
||||
for row in self.view_top..=self.view_bottom {
|
||||
self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
|
||||
}
|
||||
self.cur_row = self.view_top;
|
||||
self.cur_col = 0;
|
||||
for r in self.view_top..=self.view_bottom {
|
||||
self.belegt[r] = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Hat sich seit der letzten Quittung etwas geändert?
|
||||
pub fn ist_veraendert(&self) -> bool {
|
||||
self.veraendert
|
||||
}
|
||||
|
||||
/// Änderung quittieren (nach dem Anzeigen).
|
||||
pub fn veraenderung_quittieren(&mut self) {
|
||||
self.veraendert = false;
|
||||
}
|
||||
|
||||
/// Anzahl belegter Zeilen (1-basiert bis zur letzten beschriebenen),
|
||||
/// 0 wenn nichts ausgegeben wurde.
|
||||
pub fn belegte_zeilen(&self) -> usize {
|
||||
self.belegt
|
||||
.iter()
|
||||
.rposition(|b| b.is_some())
|
||||
.map_or(0, |i| i + 1)
|
||||
}
|
||||
|
||||
/// Breite der belegten Zeile `z` (1-basiert); 0 für unberührte Zeilen.
|
||||
pub fn belegte_breite(&self, z: usize) -> usize {
|
||||
self.belegt.get(z - 1).copied().flatten().map_or(0, |c| c + 1)
|
||||
}
|
||||
|
||||
/// `COLOR vg, hg` (vg 0–31, hg 0–7). Blinkende Vordergrundfarben
|
||||
/// (16–31) werden als „hell" simuliert (Entscheidung 2026-09-02):
|
||||
/// echtes Terminal-Blinken ist nicht überall verfügbar.
|
||||
pub fn set_color(&mut self, fg: u8, bg: u8) {
|
||||
self.veraendert = true;
|
||||
let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
|
||||
self.fg = fg & 0x0F;
|
||||
self.bg = bg & 0x07;
|
||||
}
|
||||
|
||||
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
|
||||
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> {
|
||||
if row < 1 || row > self.rows || col < 1 || col > self.cols {
|
||||
return Err(());
|
||||
}
|
||||
self.cur_row = row - 1;
|
||||
// Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
|
||||
let mut c = col - 1;
|
||||
if c > 0 && self.cells[self.cur_row * self.cols + c].fortsetzung {
|
||||
c -= 1;
|
||||
}
|
||||
self.cur_col = c;
|
||||
self.veraendert = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `CSRLIN` (1-basiert).
|
||||
pub fn csrlin(&self) -> usize {
|
||||
self.cur_row + 1
|
||||
}
|
||||
|
||||
/// `POS(0)` (1-basiert).
|
||||
pub fn pos(&self) -> usize {
|
||||
self.cur_col + 1
|
||||
}
|
||||
|
||||
/// `VIEW PRINT oben TO unten` (1-basiert).
|
||||
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> {
|
||||
if top < 1 || bottom > self.rows || top > bottom {
|
||||
return Err(());
|
||||
}
|
||||
self.view_top = top - 1;
|
||||
self.view_bottom = bottom - 1;
|
||||
self.view_full = top == 1 && bottom == self.rows;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cell(&self, row: usize, col: usize) -> Cell {
|
||||
self.cells[(row - 1) * self.cols + (col - 1)]
|
||||
}
|
||||
|
||||
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
|
||||
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
|
||||
/// `\r` setzt an den Zeilenanfang.
|
||||
pub fn print(&mut self, text: &str) {
|
||||
self.veraendert = true;
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'\n' => self.newline(),
|
||||
'\r' => self.cur_col = 0,
|
||||
_ => self.zeichen_setzen(ch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein Zeichen an der Cursorposition setzen und den Cursor um seine
|
||||
/// Darstellungsbreite vorrücken. Ein breites Zeichen, das nicht mehr
|
||||
/// vollständig in die Zeile passt, wandert als Ganzes in die nächste;
|
||||
/// die letzte Spalte bleibt dann leer.
|
||||
fn zeichen_setzen(&mut self, ch: char) {
|
||||
// ponytail: Breite 0 (kombinierende Zeichen) wird wie 1 behandelt —
|
||||
// sie bekommen eine eigene Zelle statt sich an die vorige zu hängen.
|
||||
// Ceiling: echte Kombination erst, wenn ein Korpusprogramm sie braucht.
|
||||
let breite = ch.width().unwrap_or(1).max(1);
|
||||
if self.cur_col + breite > self.cols {
|
||||
self.newline();
|
||||
}
|
||||
let attr = (self.fg, self.bg);
|
||||
let start = self.cur_row * self.cols + self.cur_col;
|
||||
self.haelften_freimachen(self.cur_col, breite);
|
||||
self.cells[start] = Cell { ch, fg: attr.0, bg: attr.1, fortsetzung: false };
|
||||
for i in 1..breite {
|
||||
self.cells[start + i] =
|
||||
Cell { ch: ' ', fg: attr.0, bg: attr.1, fortsetzung: true };
|
||||
}
|
||||
let letzte = self.cur_col + breite - 1;
|
||||
let b = &mut self.belegt[self.cur_row];
|
||||
*b = Some(b.map_or(letzte, |c| c.max(letzte)));
|
||||
self.cur_col += breite;
|
||||
if self.cur_col >= self.cols {
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
|
||||
/// Vor dem Schreiben angeschnittene breite Zeichen auflösen: wird auf
|
||||
/// eine Fortsetzung geschrieben, verliert die Hälfte davor ihr Zeichen;
|
||||
/// überschreibt der neue Text den Anfang eines breiten Zeichens, wird
|
||||
/// dessen Fortsetzung geleert. Sonst blieben Geisterzeichen stehen.
|
||||
fn haelften_freimachen(&mut self, col: usize, breite: usize) {
|
||||
let zeilenanfang = self.cur_row * self.cols;
|
||||
let leer = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
|
||||
if col > 0 && self.cells[zeilenanfang + col].fortsetzung {
|
||||
self.cells[zeilenanfang + col - 1] = leer;
|
||||
}
|
||||
let hinter = col + breite;
|
||||
if hinter < self.cols && self.cells[zeilenanfang + hinter].fortsetzung {
|
||||
self.cells[zeilenanfang + hinter] = leer;
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeilenumbruch inkl. Scrollen im Scrollbereich.
|
||||
fn newline(&mut self) {
|
||||
self.cur_col = 0;
|
||||
if self.cur_row >= self.view_bottom {
|
||||
self.scroll_up();
|
||||
self.cur_row = self.view_bottom;
|
||||
} else {
|
||||
self.cur_row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rückschritt: Cursor eine Spalte zurück und die Zelle leeren.
|
||||
/// Am Zeilenanfang passiert nichts (Vorbild).
|
||||
pub fn rueckschritt(&mut self) {
|
||||
if self.cur_col == 0 {
|
||||
return;
|
||||
}
|
||||
self.cur_col -= 1;
|
||||
// War das gelöschte eine Fortsetzung, gehört die Hälfte davor dazu.
|
||||
if self.cells[self.cur_row * self.cols + self.cur_col].fortsetzung && self.cur_col > 0 {
|
||||
self.cells[self.cur_row * self.cols + self.cur_col] =
|
||||
Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
|
||||
self.cur_col -= 1;
|
||||
}
|
||||
self.cells[self.cur_row * self.cols + self.cur_col] =
|
||||
Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
|
||||
// Die gelöschte Zelle gilt nicht mehr als belegt.
|
||||
let b = &mut self.belegt[self.cur_row];
|
||||
*b = match (self.cur_col, *b) {
|
||||
(0, _) => None,
|
||||
(c, Some(alt)) => Some(alt.min(c - 1)),
|
||||
(c, None) => Some(c - 1),
|
||||
};
|
||||
self.veraendert = true;
|
||||
}
|
||||
|
||||
/// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
|
||||
pub fn scroll_up(&mut self) {
|
||||
self.veraendert = true;
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
|
||||
for row in self.view_top..self.view_bottom {
|
||||
let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
|
||||
a[row * self.cols..].copy_from_slice(&b[..self.cols]);
|
||||
}
|
||||
self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols]
|
||||
.fill(blank);
|
||||
for row in self.view_top..self.view_bottom {
|
||||
self.belegt[row] = self.belegt[row + 1];
|
||||
}
|
||||
self.belegt[self.view_bottom] = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn print_schreibt_und_bewegt_cursor() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("AB");
|
||||
assert_eq!(s.cell(1, 1).ch, 'A');
|
||||
assert_eq!(s.cell(1, 2).ch, 'B');
|
||||
assert_eq!((s.csrlin(), s.pos()), (1, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_zeichen_belegen_eine_zelle() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("Ä☃");
|
||||
assert_eq!(s.cell(1, 1).ch, 'Ä');
|
||||
assert_eq!(s.cell(1, 2).ch, '☃');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breites_zeichen_belegt_zwei_zellen() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("中a");
|
||||
assert_eq!(s.cell(1, 1).ch, '中');
|
||||
assert!(s.cell(1, 2).fortsetzung);
|
||||
assert_eq!(s.cell(1, 3).ch, 'a');
|
||||
assert_eq!(s.pos(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breites_zeichen_am_rechten_rand_bricht_ganz_um() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print(&"x".repeat(79)); // Cursor auf Spalte 80
|
||||
assert_eq!(s.pos(), 80);
|
||||
s.print("中");
|
||||
// Letzte Spalte bleibt leer, das Zeichen steht in Zeile 2.
|
||||
assert_eq!(s.cell(1, 80).ch, ' ');
|
||||
assert_eq!(s.cell(2, 1).ch, '中');
|
||||
assert!(s.cell(2, 2).fortsetzung);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ueberschreiben_einer_haelfte_leert_die_andere() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("中");
|
||||
// Auf den Anfang schreiben → Fortsetzung muss weg.
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("a");
|
||||
assert_eq!(s.cell(1, 1).ch, 'a');
|
||||
assert!(!s.cell(1, 2).fortsetzung);
|
||||
assert_eq!(s.cell(1, 2).ch, ' ');
|
||||
|
||||
// Auf die Fortsetzung schreiben → Anfang muss weg.
|
||||
let mut s2 = TextScreen::new();
|
||||
s2.print("中");
|
||||
s2.locate(1, 2).unwrap(); // klemmt auf den Zeichenanfang
|
||||
assert_eq!(s2.pos(), 1);
|
||||
s2.locate(1, 3).unwrap();
|
||||
s2.print("b");
|
||||
assert_eq!(s2.cell(1, 3).ch, 'b');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_auf_fortsetzung_trifft_den_zeichenanfang() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("中");
|
||||
s.locate(1, 2).unwrap();
|
||||
assert_eq!(s.pos(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rueckschritt_loescht_ein_breites_zeichen_ganz() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("a中");
|
||||
assert_eq!(s.pos(), 4);
|
||||
s.rueckschritt();
|
||||
assert_eq!(s.pos(), 2);
|
||||
assert_eq!(s.cell(1, 2).ch, ' ');
|
||||
assert!(!s.cell(1, 2).fortsetzung);
|
||||
assert_eq!(s.cell(1, 3).ch, ' ');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn umbruch_am_rechten_rand() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print(&"x".repeat(81));
|
||||
assert_eq!(s.cell(1, 80).ch, 'x');
|
||||
assert_eq!(s.cell(2, 1).ch, 'x');
|
||||
assert_eq!((s.csrlin(), s.pos()), (2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrollen_am_unteren_rand() {
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(25, 1).unwrap();
|
||||
s.print("unten\n"); // erzwingt Scroll
|
||||
assert_eq!(s.cell(24, 1).ch, 'u');
|
||||
assert_eq!(s.cell(25, 1).ch, ' ');
|
||||
assert_eq!(s.csrlin(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_print_begrenzt_scrollen() {
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("kopf");
|
||||
s.view_print(3, 5).unwrap();
|
||||
s.locate(5, 1).unwrap();
|
||||
s.print("a\nb"); // scrollt nur Zeilen 3–5
|
||||
assert_eq!(s.cell(1, 1).ch, 'k');
|
||||
assert_eq!(s.cell(4, 1).ch, 'a');
|
||||
assert_eq!(s.cell(5, 1).ch, 'b');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_prueft_grenzen() {
|
||||
let mut s = TextScreen::new();
|
||||
assert!(s.locate(0, 1).is_err());
|
||||
assert!(s.locate(26, 1).is_err());
|
||||
assert!(s.locate(25, 80).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamische_groesse_und_resize() {
|
||||
let mut s = TextScreen::with_size(120, 40);
|
||||
assert_eq!((s.cols(), s.rows()), (120, 40));
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("!");
|
||||
assert_eq!(s.cell(40, 1).ch, '!');
|
||||
|
||||
// Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("K");
|
||||
s.resize(100, 30);
|
||||
assert_eq!((s.cols(), s.rows()), (100, 30));
|
||||
assert_eq!(s.cell(1, 1).ch, 'K');
|
||||
|
||||
// Unter Minimum wird auf 80×25 geklemmt
|
||||
s.resize(10, 5);
|
||||
assert_eq!((s.cols(), s.rows()), (80, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blink_wird_als_hell_simuliert() {
|
||||
let mut s = TextScreen::new();
|
||||
s.set_color(17, 0); // blinkend Blau → helles Blau
|
||||
assert_eq!(s.fg, 9);
|
||||
s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
|
||||
assert_eq!(s.fg, 15);
|
||||
s.set_color(7, 0);
|
||||
assert_eq!(s.fg, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_klemmt_cursor_und_scrollbereich() {
|
||||
let mut s = TextScreen::with_size(120, 40);
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("K");
|
||||
s.view_print(30, 38).unwrap();
|
||||
s.locate(38, 5).unwrap();
|
||||
|
||||
s.resize(80, 25);
|
||||
// Inhalt oben links bleibt.
|
||||
assert_eq!(s.cell(1, 1).ch, 'K');
|
||||
// Cursor liegt in den neuen Grenzen.
|
||||
assert!(s.csrlin() <= s.rows() && s.pos() <= s.cols());
|
||||
// Der Bereich lag ganz außerhalb → voller Bildschirm.
|
||||
s.locate(25, 1).unwrap();
|
||||
s.print("u\n");
|
||||
assert_eq!(s.cell(24, 1).ch, 'u');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_klemmt_nur_den_unteren_rand_wenn_der_bereich_bleibt() {
|
||||
let mut s = TextScreen::with_size(100, 40);
|
||||
s.view_print(5, 38).unwrap();
|
||||
s.resize(100, 30);
|
||||
// Oberer Rand bleibt bei 5, unterer klemmt auf 30.
|
||||
s.locate(30, 1).unwrap();
|
||||
s.print("a\nb");
|
||||
assert_eq!(s.cell(29, 1).ch, 'a');
|
||||
assert_eq!(s.cell(30, 1).ch, 'b');
|
||||
// Zeile 4 liegt außerhalb und bleibt unberührt.
|
||||
assert_eq!(s.cell(4, 1).ch, ' ');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_folgt_vollem_scrollbereich() {
|
||||
let mut s = TextScreen::new();
|
||||
s.resize(90, 40);
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("a\nb"); // Scroll am neuen unteren Rand
|
||||
assert_eq!(s.cell(39, 1).ch, 'a');
|
||||
assert_eq!(s.cell(40, 1).ch, 'b');
|
||||
}
|
||||
|
||||
}
|
||||
141
crates/tb-runtime/src/snapshot.rs
Normal file
141
crates/tb-runtime/src/snapshot.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Snapshot des Bildschirms als vergleichbarer Text.
|
||||
//!
|
||||
//! Grundlage des Kompatibilitäts-Korpus (Entscheidung 2026-09-03, D3):
|
||||
//! ein Byte-Strom könnte `LOCATE`/`COLOR` nicht abbilden, ein voller
|
||||
//! 80×25-Abzug wäre für ein `PRINT "hi"` fast nur Rauschen. Der Snapshot
|
||||
//! ist deshalb auf die belegte Fläche getrimmt — nachgestellte Leerzeichen,
|
||||
//! die `PRINT` selbst ausgibt, gehören dazu.
|
||||
//!
|
||||
//! Die Attributebene entsteht nur, wenn das Programm überhaupt Farben
|
||||
//! gesetzt hat; sonst wäre sie eine Fläche identischer Werte.
|
||||
|
||||
use crate::screen::{Cell, TextScreen};
|
||||
|
||||
/// Trennzeile vor der Attributebene.
|
||||
pub const ATTRIBUT_MARKE: &str = "--- Attribute (Vordergrund/Hintergrund je Zelle, hex) ---";
|
||||
|
||||
/// Standardattribut eines unberührten Bildschirms.
|
||||
fn ist_standard(c: &Cell) -> bool {
|
||||
c.fg == Cell::default().fg && c.bg == Cell::default().bg
|
||||
}
|
||||
|
||||
/// Enthält die belegte Fläche eine vom Standard abweichende Farbe?
|
||||
pub fn hat_farben(screen: &TextScreen) -> bool {
|
||||
(1..=screen.belegte_zeilen())
|
||||
.any(|r| (1..=screen.belegte_breite(r)).any(|c| !ist_standard(&screen.cell(r, c))))
|
||||
}
|
||||
|
||||
/// Textbild der belegten Fläche, je Zeile bis zu ihrer eigenen belegten
|
||||
/// Breite. Leerer String, wenn nichts ausgegeben wurde.
|
||||
pub fn text(screen: &TextScreen) -> String {
|
||||
let mut out = String::new();
|
||||
for r in 1..=screen.belegte_zeilen() {
|
||||
for c in 1..=screen.belegte_breite(r) {
|
||||
// Fortsetzungszellen gehören zum Zeichen davor und werden nicht
|
||||
// eigens ausgegeben — sonst stünde hinter jedem breiten Zeichen
|
||||
// ein Leerzeichen und die Spalten der Datei liefen auseinander.
|
||||
let z = screen.cell(r, c);
|
||||
if !z.fortsetzung {
|
||||
out.push(z.ch);
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Attributebene: je Zelle zwei Hexziffern (Vordergrund, Hintergrund),
|
||||
/// zeilenweise gleich breit wie das Textbild.
|
||||
pub fn attribute(screen: &TextScreen) -> String {
|
||||
let mut out = String::new();
|
||||
for r in 1..=screen.belegte_zeilen() {
|
||||
for c in 1..=screen.belegte_breite(r) {
|
||||
let cell = screen.cell(r, c);
|
||||
if cell.fortsetzung {
|
||||
continue; // gehört zum Zeichen davor (siehe `text`)
|
||||
}
|
||||
out.push(char::from_digit(cell.fg as u32, 16).unwrap_or('?'));
|
||||
out.push(char::from_digit(cell.bg as u32, 16).unwrap_or('?'));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Vollständiger Snapshot: Textbild, bei gesetzten Farben gefolgt von
|
||||
/// Marke und Attributebene.
|
||||
pub fn snapshot(screen: &TextScreen) -> String {
|
||||
let mut s = text(screen);
|
||||
if hat_farben(screen) {
|
||||
s.push_str(ATTRIBUT_MARKE);
|
||||
s.push('\n');
|
||||
s.push_str(&attribute(screen));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn getrimmt_auf_die_belegte_flaeche() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("hi\n");
|
||||
// Zwei Zeichen in Zeile 1 — kein 80×25-Abzug.
|
||||
assert_eq!(snapshot(&s), "hi\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nachgestellte_leerzeichen_bleiben_erhalten() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print(" 1 2 3 "); // wie PRINT 1; 2; 3
|
||||
assert_eq!(snapshot(&s), " 1 2 3 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_farben_keine_attributebene() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("abc");
|
||||
assert!(!snapshot(&s).contains(ATTRIBUT_MARKE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mit_farben_kommt_die_attributebene() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("a");
|
||||
s.set_color(14, 1);
|
||||
s.print("b");
|
||||
let snap = snapshot(&s);
|
||||
assert!(snap.starts_with("ab\n"));
|
||||
assert!(snap.contains(ATTRIBUT_MARKE));
|
||||
// Zelle 1 Standard (70), Zelle 2 gelb auf blau (e1)
|
||||
assert!(snap.ends_with("70e1\n"), "{snap:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_erweitert_die_flaeche() {
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(3, 5).unwrap();
|
||||
s.print("x");
|
||||
let snap = snapshot(&s);
|
||||
let zeilen: Vec<&str> = snap.lines().collect();
|
||||
assert_eq!(zeilen.len(), 3);
|
||||
assert_eq!(zeilen[2], " x");
|
||||
// Unberührte Zeilen bleiben leer statt auf die Breite aufgefüllt.
|
||||
assert_eq!(zeilen[0], "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breites_zeichen_erscheint_einmal() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("中a");
|
||||
// Ein Zeichen, kein nachgestelltes Leerzeichen für die Fortsetzung.
|
||||
assert_eq!(snapshot(&s), "中a\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leerer_bildschirm_ergibt_leeren_snapshot() {
|
||||
assert_eq!(snapshot(&TextScreen::new()), "");
|
||||
}
|
||||
}
|
||||
487
crates/tb-runtime/src/using.rs
Normal file
487
crates/tb-runtime/src/using.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! `PRINT USING`, `LPRINT USING` und `FORMAT$`: Formatzeichenketten des
|
||||
//! Vorbilds.
|
||||
//!
|
||||
//! Die Formatzeichenkette beschreibt Felder; jeder Wert füllt das nächste
|
||||
//! Feld. Sind noch Werte übrig, wenn die Zeichenkette zu Ende ist, beginnt
|
||||
//! sie von vorn. Passt ein Wert nicht in sein Feld, wird er vollständig
|
||||
//! ausgegeben und mit `%` gekennzeichnet.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::value::{as_f64, Value};
|
||||
|
||||
/// Numerisches Feld.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
struct NumFeld {
|
||||
/// Ziffernstellen vor dem Dezimalpunkt (ohne die Stellen aus `$$`/`**`).
|
||||
vor: usize,
|
||||
/// Ziffernstellen nach dem Dezimalpunkt.
|
||||
nach: usize,
|
||||
/// Dezimalpunkt vorhanden?
|
||||
punkt: bool,
|
||||
/// Tausendertrennung (`,` vor dem Punkt).
|
||||
tausender: bool,
|
||||
/// `+` vorangestellt bzw. nachgestellt.
|
||||
plus_vorn: bool,
|
||||
plus_hinten: bool,
|
||||
/// `-` nachgestellt (nur für negative Werte).
|
||||
minus_hinten: bool,
|
||||
/// `$$` — mitlaufendes Währungszeichen.
|
||||
waehrung: bool,
|
||||
/// `**` — führende Stellen mit `*` füllen.
|
||||
sterne: bool,
|
||||
/// `^^^^` — Exponentialform.
|
||||
exponent: bool,
|
||||
}
|
||||
|
||||
/// Stringfeld.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum StrFeld {
|
||||
/// `!` — nur das erste Zeichen.
|
||||
Erstes,
|
||||
/// `\ \` — feste Länge (Anzahl Zeichen inklusive beider Schrägstriche).
|
||||
Fest(usize),
|
||||
/// `&` — der ganze String.
|
||||
Ganz,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Teil {
|
||||
Literal(String),
|
||||
Zahl(NumFeld),
|
||||
Text(StrFeld),
|
||||
}
|
||||
|
||||
/// Währungszeichen für `$$` (siehe `SetFormatCC`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Waehrung {
|
||||
pub zeichen: String,
|
||||
}
|
||||
|
||||
impl Default for Waehrung {
|
||||
fn default() -> Self {
|
||||
Waehrung { zeichen: "$".into() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Zerlegen der Formatzeichenkette ---------------------------------------
|
||||
|
||||
fn zerlege(fmt: &str) -> Vec<Teil> {
|
||||
let z: Vec<char> = fmt.chars().collect();
|
||||
let mut teile = Vec::new();
|
||||
let mut lit = String::new();
|
||||
let mut i = 0;
|
||||
let schiebe = |lit: &mut String, teile: &mut Vec<Teil>| {
|
||||
if !lit.is_empty() {
|
||||
teile.push(Teil::Literal(std::mem::take(lit)));
|
||||
}
|
||||
};
|
||||
|
||||
while i < z.len() {
|
||||
match z[i] {
|
||||
// `_` macht das Folgezeichen literal.
|
||||
'_' if i + 1 < z.len() => {
|
||||
lit.push(z[i + 1]);
|
||||
i += 2;
|
||||
}
|
||||
'!' => {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Erstes));
|
||||
i += 1;
|
||||
}
|
||||
'&' => {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Ganz));
|
||||
i += 1;
|
||||
}
|
||||
// `\ \` — Leerzeichen zwischen zwei Schrägstrichen.
|
||||
'\\' => {
|
||||
let mut j = i + 1;
|
||||
while j < z.len() && z[j] == ' ' {
|
||||
j += 1;
|
||||
}
|
||||
if j < z.len() && z[j] == '\\' {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Fest(j - i + 1)));
|
||||
i = j + 1;
|
||||
} else {
|
||||
lit.push('\\');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'#' | '+' | '.' | '*' | '$' => {
|
||||
if let Some((feld, weiter)) = zahlfeld(&z, i) {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Zahl(feld));
|
||||
i = weiter;
|
||||
} else {
|
||||
lit.push(z[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
lit.push(c);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile
|
||||
}
|
||||
|
||||
/// Versucht ab `start` ein numerisches Feld zu lesen.
|
||||
fn zahlfeld(z: &[char], start: usize) -> Option<(NumFeld, usize)> {
|
||||
let mut f = NumFeld::default();
|
||||
let mut i = start;
|
||||
|
||||
if z[i] == '+' {
|
||||
f.plus_vorn = true;
|
||||
i += 1;
|
||||
}
|
||||
// `**$`, `**`, `$$` belegen selbst Ziffernstellen.
|
||||
if i + 1 < z.len() && z[i] == '*' && z[i + 1] == '*' {
|
||||
f.sterne = true;
|
||||
f.vor += 2;
|
||||
i += 2;
|
||||
if i < z.len() && z[i] == '$' {
|
||||
f.waehrung = true;
|
||||
f.vor += 1;
|
||||
i += 1;
|
||||
}
|
||||
} else if i + 1 < z.len() && z[i] == '$' && z[i + 1] == '$' {
|
||||
f.waehrung = true;
|
||||
f.vor += 2;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
let ziffern_start = i;
|
||||
while i < z.len() {
|
||||
match z[i] {
|
||||
'#' => {
|
||||
f.vor += 1;
|
||||
i += 1;
|
||||
}
|
||||
',' if i + 1 < z.len() && (z[i + 1] == '#' || z[i + 1] == '.') => {
|
||||
f.tausender = true;
|
||||
i += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
// Ohne jede Ziffernstelle ist es kein Zahlfeld.
|
||||
if i == ziffern_start && !f.waehrung && !f.sterne {
|
||||
return None;
|
||||
}
|
||||
if i < z.len() && z[i] == '.' {
|
||||
f.punkt = true;
|
||||
i += 1;
|
||||
while i < z.len() && z[i] == '#' {
|
||||
f.nach += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
// Exponentialform: vier oder mehr `^`.
|
||||
let exp_start = i;
|
||||
while i < z.len() && z[i] == '^' {
|
||||
i += 1;
|
||||
}
|
||||
if i - exp_start >= 4 {
|
||||
f.exponent = true;
|
||||
} else {
|
||||
i = exp_start;
|
||||
}
|
||||
if i < z.len() && z[i] == '+' {
|
||||
f.plus_hinten = true;
|
||||
i += 1;
|
||||
} else if i < z.len() && z[i] == '-' {
|
||||
f.minus_hinten = true;
|
||||
i += 1;
|
||||
}
|
||||
Some((f, i))
|
||||
}
|
||||
|
||||
// ---- Werte formatieren ------------------------------------------------------
|
||||
|
||||
fn zahl_formatieren(f: &NumFeld, wert: f64, waehrung: &Waehrung) -> String {
|
||||
let negativ = wert < 0.0 || (wert == 0.0 && wert.is_sign_negative());
|
||||
let betrag = wert.abs();
|
||||
|
||||
if f.exponent {
|
||||
return exponent_formatieren(f, wert);
|
||||
}
|
||||
|
||||
// Ziffern runden (kaufmännisch zur geraden Zahl wie sonst auch).
|
||||
let gerundet = runde(betrag, f.nach);
|
||||
let mut ganz = format!("{:.0}", gerundet.trunc());
|
||||
let bruch = if f.punkt {
|
||||
let b = gerundet - gerundet.trunc();
|
||||
let s = format!("{:.*}", f.nach, b);
|
||||
// "0.25" → "25"
|
||||
s.split_once('.').map(|(_, r)| r.to_string()).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// Die Tausendertrennung fügt Zeichen hinzu, die keine Ziffernstelle
|
||||
// belegen; der Überlauf entscheidet sich an der Ziffernzahl.
|
||||
let ziffern = ganz.chars().count();
|
||||
if f.tausender {
|
||||
ganz = tausender_gruppieren(&ganz);
|
||||
}
|
||||
|
||||
// Vorzeichen mit eigener Position: `+` vorn bzw. `+`/`-` hinten stehen
|
||||
// neben dem Zahlenfeld, nicht darin. Ohne Vorzeichenangabe teilt sich ein
|
||||
// `-` die Ziffernstellen.
|
||||
let (vorz_vorn, vorz_hinten, minus_im_feld) = if f.plus_vorn {
|
||||
(if negativ { "-" } else { "+" }.to_string(), String::new(), false)
|
||||
} else if f.plus_hinten {
|
||||
(String::new(), if negativ { "-" } else { "+" }.to_string(), false)
|
||||
} else if f.minus_hinten {
|
||||
(String::new(), if negativ { "-" } else { " " }.to_string(), false)
|
||||
} else {
|
||||
(String::new(), String::new(), negativ)
|
||||
};
|
||||
|
||||
// Inhalt der Ziffernstellen, rechtsbündig: Vorzeichen (falls im Feld),
|
||||
// Währungszeichen, Ziffern. `$$`/`**` haben ihre Stellen mitgebracht.
|
||||
let mut inhalt = String::new();
|
||||
if minus_im_feld {
|
||||
inhalt.push('-');
|
||||
}
|
||||
if f.waehrung {
|
||||
inhalt.push_str(&waehrung.zeichen);
|
||||
}
|
||||
inhalt.push_str(&ganz);
|
||||
|
||||
let ueberlauf = ziffern + usize::from(minus_im_feld) + usize::from(f.waehrung) > f.vor;
|
||||
let gefuellt = if ueberlauf {
|
||||
inhalt
|
||||
} else {
|
||||
// Trennzeichen belegen keine Ziffernstelle und können die Breite
|
||||
// überschreiten, ohne Überlauf zu sein.
|
||||
let fehlt = f.vor.saturating_sub(inhalt.chars().count());
|
||||
let fueller = if f.sterne { '*' } else { ' ' };
|
||||
let mut t: String = std::iter::repeat_n(fueller, fehlt).collect();
|
||||
t.push_str(&inhalt);
|
||||
t
|
||||
};
|
||||
|
||||
let mut out = String::new();
|
||||
if ueberlauf {
|
||||
out.push('%');
|
||||
}
|
||||
out.push_str(&vorz_vorn);
|
||||
out.push_str(&gefuellt);
|
||||
if f.punkt {
|
||||
out.push('.');
|
||||
out.push_str(&bruch);
|
||||
}
|
||||
out.push_str(&vorz_hinten);
|
||||
out
|
||||
}
|
||||
|
||||
/// Kaufmännische Rundung zur geraden Zahl auf `stellen` Nachkommastellen.
|
||||
fn runde(x: f64, stellen: usize) -> f64 {
|
||||
let faktor = 10f64.powi(stellen as i32);
|
||||
let skaliert = x * faktor;
|
||||
let ab = skaliert.floor();
|
||||
let rest = skaliert - ab;
|
||||
let gerundet = if (rest - 0.5).abs() < f64::EPSILON * skaliert.abs().max(1.0) {
|
||||
if (ab as i64) % 2 == 0 {
|
||||
ab
|
||||
} else {
|
||||
ab + 1.0
|
||||
}
|
||||
} else {
|
||||
skaliert.round()
|
||||
};
|
||||
gerundet / faktor
|
||||
}
|
||||
|
||||
fn tausender_gruppieren(s: &str) -> String {
|
||||
let z: Vec<char> = s.chars().collect();
|
||||
let mut out = String::new();
|
||||
for (i, c) in z.iter().enumerate() {
|
||||
if i > 0 && (z.len() - i) % 3 == 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(*c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn exponent_formatieren(f: &NumFeld, wert: f64) -> String {
|
||||
let stellen = f.nach;
|
||||
let s = format!("{:.*E}", stellen, wert);
|
||||
// Rust liefert `1.5E3`; das Vorbild schreibt `1.5E+03`.
|
||||
match s.split_once('E') {
|
||||
Some((m, e)) => {
|
||||
let (vz, betrag) = match e.strip_prefix('-') {
|
||||
Some(r) => ('-', r),
|
||||
None => ('+', e.trim_start_matches('+')),
|
||||
};
|
||||
format!("{m}E{vz}{betrag:0>2}")
|
||||
}
|
||||
None => s,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_formatieren(f: &StrFeld, s: &str) -> String {
|
||||
match f {
|
||||
StrFeld::Erstes => s.chars().take(1).collect(),
|
||||
StrFeld::Ganz => s.to_string(),
|
||||
StrFeld::Fest(n) => {
|
||||
let mut t: String = s.chars().take(*n).collect();
|
||||
let fehlt = n.saturating_sub(t.chars().count());
|
||||
t.extend(std::iter::repeat_n(' ', fehlt));
|
||||
t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Einstiegspunkt ---------------------------------------------------------
|
||||
|
||||
/// Werte nach `fmt` formatieren. Die Zeichenkette wird so oft wiederholt,
|
||||
/// wie Werte übrig sind; nach dem letzten Wert folgen noch die Literale bis
|
||||
/// zum nächsten Feld.
|
||||
pub fn using(fmt: &str, werte: &[Value], waehrung: &Waehrung) -> Result<String, RuntimeError> {
|
||||
let teile = zerlege(fmt);
|
||||
let hat_feld = teile
|
||||
.iter()
|
||||
.any(|t| matches!(t, Teil::Zahl(_) | Teil::Text(_)));
|
||||
if !hat_feld {
|
||||
if werte.is_empty() {
|
||||
return Ok(fmt.to_string());
|
||||
}
|
||||
// Werte ohne Feld sind ein Bedienfehler (Vorbild: Fehler 5).
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
let mut i = 0usize; // Index im Teile-Vektor
|
||||
let mut w = 0usize; // Index im Wertevektor
|
||||
// Schutz gegen Endlosläufe bei Formaten ohne Fortschritt.
|
||||
let mut runden = 0usize;
|
||||
|
||||
while w < werte.len() {
|
||||
if i >= teile.len() {
|
||||
i = 0;
|
||||
runden += 1;
|
||||
if runden > werte.len() + 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match &teile[i] {
|
||||
Teil::Literal(l) => out.push_str(l),
|
||||
Teil::Zahl(f) => {
|
||||
if matches!(werte[w], Value::Str(_)) {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
}
|
||||
let v = as_f64(&werte[w]);
|
||||
out.push_str(&zahl_formatieren(f, v, waehrung));
|
||||
w += 1;
|
||||
}
|
||||
Teil::Text(f) => {
|
||||
let Value::Str(s) = &werte[w] else {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
};
|
||||
out.push_str(&text_formatieren(f, s));
|
||||
w += 1;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
// Nachlaufende Literale bis zum nächsten Feld.
|
||||
while i < teile.len() {
|
||||
match &teile[i] {
|
||||
Teil::Literal(l) => out.push_str(l),
|
||||
_ => break,
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
fn u(fmt: &str, werte: &[Value]) -> String {
|
||||
using(fmt, werte, &Waehrung::default()).unwrap()
|
||||
}
|
||||
fn z(x: f64) -> Value {
|
||||
Value::Dbl(x)
|
||||
}
|
||||
fn s(x: &str) -> Value {
|
||||
Value::Str(Rc::from(x))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numerisches_feld_mit_nachkommastellen() {
|
||||
assert_eq!(u("###.##", &[z(3.14159)]), " 3.14");
|
||||
assert_eq!(u("##.##", &[z(-1.5)]), "-1.50");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feldueberlauf_bekommt_prozentzeichen() {
|
||||
let r = u("##", &[z(12345.0)]);
|
||||
assert!(r.starts_with('%'), "{r}");
|
||||
assert!(r.contains("12345"), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_wird_wiederverwendet() {
|
||||
assert_eq!(u("## ", &[z(1.0), z(2.0), z(3.0)]), " 1 2 3 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tausendertrennung() {
|
||||
assert_eq!(u("#######", &[z(1234567.0)]), "1234567");
|
||||
assert_eq!(u("##,#####", &[z(1234567.0)]), "1,234,567");
|
||||
// Mehr Ziffern als Stellen → Überlauf mit %-Präfix.
|
||||
assert!(u("##,####", &[z(1234567.0)]).starts_with('%'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vorzeichenfuehrung() {
|
||||
assert_eq!(u("+###", &[z(12.0)]), "+ 12");
|
||||
assert_eq!(u("###-", &[z(-12.0)]), " 12-");
|
||||
assert_eq!(u("###-", &[z(12.0)]), " 12 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waehrung_und_sterne() {
|
||||
// `$$` bringt zwei Stellen mit, eine davon ist das Zeichen selbst.
|
||||
assert_eq!(u("$$###", &[z(12.0)]), " $12");
|
||||
assert_eq!(u("**###", &[z(12.0)]), "***12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringfelder() {
|
||||
assert_eq!(u("!", &[s("abc")]), "a");
|
||||
assert_eq!(u("&", &[s("abc")]), "abc");
|
||||
assert_eq!(u("\\ \\", &[s("abcdef")]), "abcd");
|
||||
assert_eq!(u("\\ \\", &[s("ab")]), "ab ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literale_und_unterstrich() {
|
||||
assert_eq!(u("Wert: ##!", &[z(7.0), s("x")]), "Wert: 7x");
|
||||
assert_eq!(u("_###", &[z(7.0)]), "# 7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exponentialform() {
|
||||
assert_eq!(u("#.##^^^^", &[z(1234.0)]), "1.23E+03");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_feld_aber_mit_werten_ist_fehler_5() {
|
||||
let e = using("nur Text", &[z(1.0)], &Waehrung::default()).unwrap_err();
|
||||
assert_eq!(e, RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_feld_und_ohne_werte_bleibt_der_text() {
|
||||
assert_eq!(u("nur Text", &[]), "nur Text");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-runtime.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, Key
|
||||
use crossterm::execute;
|
||||
use std::io::stdout;
|
||||
use std::time::Duration;
|
||||
use tb_ui::screen::TextScreen;
|
||||
use tb_ui::screen::{ScreenWidget, TextScreen};
|
||||
|
||||
fn testbild(s: &mut TextScreen) {
|
||||
s.set_color(15, 1);
|
||||
@@ -61,7 +61,7 @@ fn main() -> anyhow::Result<()> {
|
||||
status(&mut screen, "Bereit. Tasten/Maus testen, Esc beendet.");
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| f.render_widget(&screen, f.area()))?;
|
||||
terminal.draw(|f| f.render_widget(ScreenWidget(&screen), f.area()))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
|
||||
150
crates/tb-ui/src/host.rs
Normal file
150
crates/tb-ui/src/host.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
//! Terminal-Host: stellt den Zellenpuffer über Ratatui dar und liefert
|
||||
//! Tastatur- und Größenereignisse aus crossterm.
|
||||
//!
|
||||
//! Er lebt hier statt in `tb-runtime`, damit die Ausführungsschicht ohne
|
||||
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
|
||||
|
||||
use std::io::{self, Stdout};
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
KeyModifiers,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use tb_runtime::host::{taste, Ereignis, Host};
|
||||
use tb_runtime::screen::TextScreen;
|
||||
|
||||
use crate::screen::ScreenWidget;
|
||||
|
||||
/// Host für laufende Programme im Terminal.
|
||||
///
|
||||
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
|
||||
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
|
||||
pub struct TerminalHost {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
}
|
||||
|
||||
impl TerminalHost {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
enable_raw_mode()?;
|
||||
let mut out = io::stdout();
|
||||
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let terminal = Terminal::new(CrosstermBackend::new(out))?;
|
||||
Ok(TerminalHost { terminal })
|
||||
}
|
||||
|
||||
/// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt.
|
||||
pub fn groesse(&self) -> io::Result<(usize, usize)> {
|
||||
let s = self.terminal.size()?;
|
||||
Ok((s.width as usize, s.height as usize))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalHost {
|
||||
fn drop(&mut self) {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
let _ = self.terminal.show_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for TerminalHost {
|
||||
fn present(&mut self, screen: &TextScreen) {
|
||||
let _ = self
|
||||
.terminal
|
||||
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
|
||||
}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
loop {
|
||||
if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
match event::read() {
|
||||
Ok(Event::Key(k)) if k.kind == KeyEventKind::Press => {
|
||||
if let Some(e) = taste_zu_ereignis(k) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
return Some(Ereignis::Groesse {
|
||||
cols: cols as usize,
|
||||
rows: rows as usize,
|
||||
})
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => return Some(Ereignis::Ende),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
|
||||
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
// Strg+Untbr bzw. Strg+C: Abbruchwunsch.
|
||||
if k.modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& matches!(k.code, KeyCode::Char('c') | KeyCode::Pause)
|
||||
{
|
||||
return Some(Ereignis::Abbruch);
|
||||
}
|
||||
let s = match k.code {
|
||||
KeyCode::Char(c) => c.to_string(),
|
||||
KeyCode::Enter => taste::ENTER.to_string(),
|
||||
KeyCode::Backspace => taste::BACKSPACE.to_string(),
|
||||
KeyCode::Esc => taste::ESC.to_string(),
|
||||
KeyCode::Tab => taste::TAB.to_string(),
|
||||
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
|
||||
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
|
||||
KeyCode::Home => taste::sonder(71),
|
||||
KeyCode::Up => taste::sonder(72),
|
||||
KeyCode::PageUp => taste::sonder(73),
|
||||
KeyCode::Left => taste::sonder(75),
|
||||
KeyCode::Right => taste::sonder(77),
|
||||
KeyCode::End => taste::sonder(79),
|
||||
KeyCode::Down => taste::sonder(80),
|
||||
KeyCode::PageDown => taste::sonder(81),
|
||||
KeyCode::Insert => taste::sonder(82),
|
||||
KeyCode::Delete => taste::sonder(83),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Ereignis::Taste(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zeichentaste_wird_zu_inkey_form() {
|
||||
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sondertaste_traegt_fuehrendes_nullzeichen() {
|
||||
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
|
||||
let Some(Ereignis::Taste(s)) = taste_zu_ereignis(k) else {
|
||||
panic!("keine Taste");
|
||||
};
|
||||
assert_eq!(s.chars().count(), 2);
|
||||
assert_eq!(s.chars().next(), Some('\0'));
|
||||
assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strg_c_ist_abbruch() {
|
||||
let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Abbruch));
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@
|
||||
//! Tastatur- und Mausereignisse.
|
||||
|
||||
pub mod screen;
|
||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
pub mod forms;
|
||||
pub mod events;
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
|
||||
//! Cursor, Scrollen — gerendert als Ratatui-Widget.
|
||||
//! Darstellung des Textbildschirms als Ratatui-Widget.
|
||||
//!
|
||||
//! Entscheidungen (siehe PLAN.md):
|
||||
//! - durchgängig Unicode, keine CP437-Emulation
|
||||
//! - **dynamische Größe**: der Bildschirm folgt der Terminalgröße; unterhalb
|
||||
//! der Mindestgröße (80×25) wird nur ein Hinweis gerendert (btop-artig),
|
||||
//! größere Terminals werden voll genutzt.
|
||||
//!
|
||||
//! Das Zellenmodell ist strikt 1 Zeichen = 1 Zelle; Zeichen mit
|
||||
//! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md).
|
||||
//! Der Zellenpuffer selbst lebt in `tb_runtime::screen` (Entscheidung
|
||||
//! 2026-09-03) — hier bleibt nur die Anbindung an das Terminal: die
|
||||
//! Farbabbildung und das Widget. Da `Widget` und `TextScreen` beide fremd
|
||||
//! sind, trägt das Widget ein Newtype.
|
||||
|
||||
pub use tb_runtime::screen::{Cell, TextScreen, MIN_COLS, MIN_ROWS};
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
/// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
|
||||
pub const MIN_COLS: usize = 80;
|
||||
pub const MIN_ROWS: usize = 25;
|
||||
|
||||
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub ch: char,
|
||||
/// Vordergrund 0–15 (klassische Palette).
|
||||
pub fg: u8,
|
||||
/// Hintergrund 0–7.
|
||||
pub bg: u8,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Cell { ch: ' ', fg: 7, bg: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Abbildung der klassischen 16-Farben-Palette auf ANSI-Indexfarben.
|
||||
/// (Klassisch: 1 = Blau, 4 = Rot — ANSI: 1 = Rot, 4 = Blau usw.)
|
||||
pub fn basic_color(n: u8) -> Color {
|
||||
@@ -42,200 +19,14 @@ pub fn basic_color(n: u8) -> Color {
|
||||
Color::Indexed(MAP[(n & 0x0F) as usize])
|
||||
}
|
||||
|
||||
/// Der emulierte Textbildschirm mit dynamischer Größe.
|
||||
///
|
||||
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
|
||||
/// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextScreen {
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
cells: Vec<Cell>,
|
||||
/// Cursorposition, 0-basiert intern.
|
||||
cur_row: usize,
|
||||
cur_col: usize,
|
||||
pub cursor_visible: bool,
|
||||
/// Aktuelle Ausgabefarben (`COLOR`).
|
||||
pub fg: u8,
|
||||
pub bg: u8,
|
||||
/// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
|
||||
view_top: usize,
|
||||
view_bottom: usize,
|
||||
/// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
|
||||
/// Scrollbereich folgt dann der Bildschirmgröße.
|
||||
view_full: bool,
|
||||
}
|
||||
|
||||
impl Default for TextScreen {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextScreen {
|
||||
/// Bildschirm in Mindestgröße (80×25).
|
||||
pub fn new() -> Self {
|
||||
Self::with_size(MIN_COLS, MIN_ROWS)
|
||||
}
|
||||
|
||||
/// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
|
||||
pub fn with_size(cols: usize, rows: usize) -> Self {
|
||||
let cols = cols.max(MIN_COLS);
|
||||
let rows = rows.max(MIN_ROWS);
|
||||
TextScreen {
|
||||
cols,
|
||||
rows,
|
||||
cells: vec![Cell::default(); cols * rows],
|
||||
cur_row: 0,
|
||||
cur_col: 0,
|
||||
cursor_visible: true,
|
||||
fg: 7,
|
||||
bg: 0,
|
||||
view_top: 0,
|
||||
view_bottom: rows - 1,
|
||||
view_full: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cols(&self) -> usize {
|
||||
self.cols
|
||||
}
|
||||
pub fn rows(&self) -> usize {
|
||||
self.rows
|
||||
}
|
||||
|
||||
/// An neue Terminalgröße anpassen: Inhalt bleibt oben links erhalten,
|
||||
/// neue Zellen sind leer. Unterhalb der Mindestgröße bleibt der Puffer
|
||||
/// bei 80×25 (das Widget zeigt dann den Zu-klein-Hinweis).
|
||||
pub fn resize(&mut self, cols: usize, rows: usize) {
|
||||
let cols = cols.max(MIN_COLS);
|
||||
let rows = rows.max(MIN_ROWS);
|
||||
if cols == self.cols && rows == self.rows {
|
||||
return;
|
||||
}
|
||||
let mut cells = vec![Cell { ch: ' ', fg: self.fg, bg: self.bg }; cols * rows];
|
||||
for row in 0..self.rows.min(rows) {
|
||||
for col in 0..self.cols.min(cols) {
|
||||
cells[row * cols + col] = self.cells[row * self.cols + col];
|
||||
}
|
||||
}
|
||||
self.cells = cells;
|
||||
self.cols = cols;
|
||||
self.rows = rows;
|
||||
self.cur_row = self.cur_row.min(rows - 1);
|
||||
self.cur_col = self.cur_col.min(cols - 1);
|
||||
if self.view_full {
|
||||
self.view_top = 0;
|
||||
self.view_bottom = rows - 1;
|
||||
} else {
|
||||
self.view_top = self.view_top.min(rows - 1);
|
||||
self.view_bottom = self.view_bottom.min(rows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// `CLS`: Scrollbereich mit aktueller Hintergrundfarbe löschen,
|
||||
/// Cursor an den Anfang des Bereichs.
|
||||
pub fn cls(&mut self) {
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
|
||||
for row in self.view_top..=self.view_bottom {
|
||||
self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
|
||||
}
|
||||
self.cur_row = self.view_top;
|
||||
self.cur_col = 0;
|
||||
}
|
||||
|
||||
/// `COLOR vg, hg` (vg 0–31, hg 0–7). Blinkende Vordergrundfarben
|
||||
/// (16–31) werden als „hell" simuliert (Entscheidung 2026-09-02):
|
||||
/// echtes Terminal-Blinken ist nicht überall verfügbar.
|
||||
pub fn set_color(&mut self, fg: u8, bg: u8) {
|
||||
let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
|
||||
self.fg = fg & 0x0F;
|
||||
self.bg = bg & 0x07;
|
||||
}
|
||||
|
||||
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
|
||||
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> {
|
||||
if row < 1 || row > self.rows || col < 1 || col > self.cols {
|
||||
return Err(());
|
||||
}
|
||||
self.cur_row = row - 1;
|
||||
self.cur_col = col - 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `CSRLIN` (1-basiert).
|
||||
pub fn csrlin(&self) -> usize {
|
||||
self.cur_row + 1
|
||||
}
|
||||
|
||||
/// `POS(0)` (1-basiert).
|
||||
pub fn pos(&self) -> usize {
|
||||
self.cur_col + 1
|
||||
}
|
||||
|
||||
/// `VIEW PRINT oben TO unten` (1-basiert).
|
||||
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> {
|
||||
if top < 1 || bottom > self.rows || top > bottom {
|
||||
return Err(());
|
||||
}
|
||||
self.view_top = top - 1;
|
||||
self.view_bottom = bottom - 1;
|
||||
self.view_full = top == 1 && bottom == self.rows;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cell(&self, row: usize, col: usize) -> Cell {
|
||||
self.cells[(row - 1) * self.cols + (col - 1)]
|
||||
}
|
||||
|
||||
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
|
||||
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
|
||||
/// `\r` setzt an den Zeilenanfang.
|
||||
pub fn print(&mut self, text: &str) {
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'\n' => self.newline(),
|
||||
'\r' => self.cur_col = 0,
|
||||
_ => {
|
||||
self.cells[self.cur_row * self.cols + self.cur_col] =
|
||||
Cell { ch, fg: self.fg, bg: self.bg };
|
||||
self.cur_col += 1;
|
||||
if self.cur_col >= self.cols {
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeilenumbruch inkl. Scrollen im Scrollbereich.
|
||||
fn newline(&mut self) {
|
||||
self.cur_col = 0;
|
||||
if self.cur_row >= self.view_bottom {
|
||||
self.scroll_up();
|
||||
self.cur_row = self.view_bottom;
|
||||
} else {
|
||||
self.cur_row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
|
||||
pub fn scroll_up(&mut self) {
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
|
||||
for row in self.view_top..self.view_bottom {
|
||||
let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
|
||||
a[row * self.cols..].copy_from_slice(&b[..self.cols]);
|
||||
}
|
||||
self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols]
|
||||
.fill(blank);
|
||||
}
|
||||
}
|
||||
/// Zeichenbares Gegenstück zu [`TextScreen`].
|
||||
pub struct ScreenWidget<'a>(pub &'a TextScreen);
|
||||
|
||||
/// Rendert den Bildschirm oben links in die verfügbare Fläche. Ist das
|
||||
/// Terminal kleiner als die Mindestgröße, erscheint nur ein Hinweis
|
||||
/// (btop-artig); die Anwendung ruft bei Resize `TextScreen::resize` auf,
|
||||
/// damit Puffer und Terminal deckungsgleich bleiben.
|
||||
impl Widget for &TextScreen {
|
||||
impl Widget for ScreenWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if (area.width as usize) < MIN_COLS || (area.height as usize) < MIN_ROWS {
|
||||
let msg = format!(
|
||||
@@ -245,11 +36,16 @@ impl Widget for &TextScreen {
|
||||
buf.set_string(area.x, area.y, msg, Style::default().fg(Color::Red));
|
||||
return;
|
||||
}
|
||||
let cols = self.cols.min(area.width as usize);
|
||||
let rows = self.rows.min(area.height as usize);
|
||||
let cols = self.0.cols().min(area.width as usize);
|
||||
let rows = self.0.rows().min(area.height as usize);
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let c = self.cells[row * self.cols + col];
|
||||
let c = self.0.cell(row + 1, col + 1);
|
||||
if c.fortsetzung {
|
||||
// Die Zelle gehört zum breiten Zeichen davor; das Terminal
|
||||
// belegt sie beim Zeichnen selbst mit.
|
||||
continue;
|
||||
}
|
||||
if let Some(cell) = buf.cell_mut((area.x + col as u16, area.y + row as u16)) {
|
||||
let mut s = String::new();
|
||||
s.push(c.ch);
|
||||
@@ -262,8 +58,8 @@ impl Widget for &TextScreen {
|
||||
}
|
||||
// Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in
|
||||
// der Forms-/Runtime-Schicht später gezielt gesteuert).
|
||||
if self.cursor_visible && self.cur_row < rows && self.cur_col < cols {
|
||||
let (cx, cy) = (area.x + self.cur_col as u16, area.y + self.cur_row as u16);
|
||||
if self.0.cursor_visible && self.0.csrlin() <= rows && self.0.pos() <= cols {
|
||||
let (cx, cy) = (area.x + (self.0.pos() - 1) as u16, area.y + (self.0.csrlin() - 1) as u16);
|
||||
if let Some(cell) = buf.cell_mut((cx, cy)) {
|
||||
cell.set_style(
|
||||
cell.style().add_modifier(ratatui::style::Modifier::REVERSED),
|
||||
@@ -277,104 +73,6 @@ impl Widget for &TextScreen {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn print_schreibt_und_bewegt_cursor() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("AB");
|
||||
assert_eq!(s.cell(1, 1).ch, 'A');
|
||||
assert_eq!(s.cell(1, 2).ch, 'B');
|
||||
assert_eq!((s.csrlin(), s.pos()), (1, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_zeichen_belegen_eine_zelle() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("Ä☃");
|
||||
assert_eq!(s.cell(1, 1).ch, 'Ä');
|
||||
assert_eq!(s.cell(1, 2).ch, '☃');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn umbruch_am_rechten_rand() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print(&"x".repeat(81));
|
||||
assert_eq!(s.cell(1, 80).ch, 'x');
|
||||
assert_eq!(s.cell(2, 1).ch, 'x');
|
||||
assert_eq!((s.csrlin(), s.pos()), (2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrollen_am_unteren_rand() {
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(25, 1).unwrap();
|
||||
s.print("unten\n"); // erzwingt Scroll
|
||||
assert_eq!(s.cell(24, 1).ch, 'u');
|
||||
assert_eq!(s.cell(25, 1).ch, ' ');
|
||||
assert_eq!(s.csrlin(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_print_begrenzt_scrollen() {
|
||||
let mut s = TextScreen::new();
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("kopf");
|
||||
s.view_print(3, 5).unwrap();
|
||||
s.locate(5, 1).unwrap();
|
||||
s.print("a\nb"); // scrollt nur Zeilen 3–5
|
||||
assert_eq!(s.cell(1, 1).ch, 'k');
|
||||
assert_eq!(s.cell(4, 1).ch, 'a');
|
||||
assert_eq!(s.cell(5, 1).ch, 'b');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_prueft_grenzen() {
|
||||
let mut s = TextScreen::new();
|
||||
assert!(s.locate(0, 1).is_err());
|
||||
assert!(s.locate(26, 1).is_err());
|
||||
assert!(s.locate(25, 80).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamische_groesse_und_resize() {
|
||||
let mut s = TextScreen::with_size(120, 40);
|
||||
assert_eq!((s.cols(), s.rows()), (120, 40));
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("!");
|
||||
assert_eq!(s.cell(40, 1).ch, '!');
|
||||
|
||||
// Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("K");
|
||||
s.resize(100, 30);
|
||||
assert_eq!((s.cols(), s.rows()), (100, 30));
|
||||
assert_eq!(s.cell(1, 1).ch, 'K');
|
||||
|
||||
// Unter Minimum wird auf 80×25 geklemmt
|
||||
s.resize(10, 5);
|
||||
assert_eq!((s.cols(), s.rows()), (80, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blink_wird_als_hell_simuliert() {
|
||||
let mut s = TextScreen::new();
|
||||
s.set_color(17, 0); // blinkend Blau → helles Blau
|
||||
assert_eq!(s.fg, 9);
|
||||
s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
|
||||
assert_eq!(s.fg, 15);
|
||||
s.set_color(7, 0);
|
||||
assert_eq!(s.fg, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_folgt_vollem_scrollbereich() {
|
||||
let mut s = TextScreen::new();
|
||||
s.resize(90, 40);
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("a\nb"); // Scroll am neuen unteren Rand
|
||||
assert_eq!(s.cell(39, 1).ch, 'a');
|
||||
assert_eq!(s.cell(40, 1).ch, 'b');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn farbabbildung() {
|
||||
assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau
|
||||
|
||||
@@ -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 ------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
304
docs/bibliothek.md
Normal file
304
docs/bibliothek.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Bibliotheksreferenz
|
||||
|
||||
Die Laufzeitbibliothek **so wie implementiert**. Der Umfang wird von
|
||||
[docs/inventar.md](inventar.md) bestimmt; dort steht auch, was noch offen
|
||||
ist. Abweichungen vom Vorbild sind in
|
||||
[docs/sprachreferenz.md](sprachreferenz.md) unter „Abweichungen"
|
||||
gesammelt.
|
||||
|
||||
Ein automatischer Test hält das Inventar gegen den Code; diese Referenz
|
||||
beschreibt das Verhalten dahinter.
|
||||
|
||||
|
||||
## Bildschirm
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `CLS` | Anweisung |
|
||||
| `COLOR` | Anweisung |
|
||||
| `CSRLIN` | Funktion |
|
||||
| `INPUT` | Anweisung |
|
||||
| `LINE INPUT` | Anweisung |
|
||||
| `LOCATE` | Anweisung |
|
||||
| `POS` | Funktion |
|
||||
| `PRINT` | Anweisung |
|
||||
| `SCREEN` | Anweisung |
|
||||
| `SCREEN` | Funktion |
|
||||
| `SPC` | Funktion |
|
||||
| `TAB` | Funktion |
|
||||
| `VIEW PRINT` | Anweisung |
|
||||
| `WIDTH` | Anweisung |
|
||||
|
||||
Alle Anweisungen wirken auf den Zellenpuffer der aktuellen
|
||||
Bildschirmgröße — 80×25 ist nirgends eine feste Grenze. Ausgelassene
|
||||
Argumente von `LOCATE` und `COLOR` lassen den bisherigen Wert stehen.
|
||||
`LOCATE` außerhalb der Grenzen und `SCREEN(zeile, spalte)` außerhalb
|
||||
lösen Fehler 5 aus. Die Funktionsform `SCREEN` liefert ohne drittes
|
||||
Argument den Zeichencode, mit drittem das Farbattribut
|
||||
(Hintergrund·16 + Vordergrund). `WIDTH` hebt den Puffer höchstens an.
|
||||
`VIEW PRINT` ohne Argumente setzt den Scrollbereich auf den vollen
|
||||
Bildschirm zurück.
|
||||
|
||||
## Datum/Zeit
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `DATE$` | Anweisung |
|
||||
| `DATE$` | Funktion |
|
||||
| `DATESERIAL` | Funktion |
|
||||
| `DATEVALUE` | Funktion |
|
||||
| `DAY` | Funktion |
|
||||
| `HOUR` | Funktion |
|
||||
| `MINUTE` | Funktion |
|
||||
| `MONTH` | Funktion |
|
||||
| `NOW` | Funktion |
|
||||
| `SECOND` | Funktion |
|
||||
| `TIME$` | Anweisung |
|
||||
| `TIME$` | Funktion |
|
||||
| `TIMER` | Funktion |
|
||||
| `TIMESERIAL` | Funktion |
|
||||
| `TIMEVALUE` | Funktion |
|
||||
| `WEEKDAY` | Funktion |
|
||||
| `YEAR` | Funktion |
|
||||
|
||||
Serielle Werte zählen Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900);
|
||||
der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
|
||||
Ungültige Datums- oder Zeitangaben lösen Fehler 5 aus.
|
||||
|
||||
Die Uhr läuft in **UTC** (dokumentierte Abweichung). `DATE$ = …` und
|
||||
`TIME$ = …` stellen nicht die Systemuhr, sondern einen
|
||||
programmeigenen Versatz, den alle Zeitfunktionen mitrechnen.
|
||||
|
||||
## Deklaration
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `COMMON` | Anweisung |
|
||||
| `CONST` | Anweisung |
|
||||
| `DATA` | Anweisung |
|
||||
| `DEFtype` | Anweisung |
|
||||
| `DIM` | Anweisung |
|
||||
| `ERASE` | Anweisung |
|
||||
| `LBOUND` | Funktion |
|
||||
| `LET` | Anweisung |
|
||||
| `OPTION BASE` | Anweisung |
|
||||
| `READ` | Anweisung |
|
||||
| `REDIM` | Anweisung |
|
||||
| `REM` | Anweisung |
|
||||
| `RESTORE` | Anweisung |
|
||||
| `SHARED` | Anweisung |
|
||||
| `STATIC` | Anweisung |
|
||||
| `SWAP` | Anweisung |
|
||||
| `TYPE` | Anweisung |
|
||||
| `UBOUND` | Funktion |
|
||||
|
||||
## Ereignisse
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `DOEVENTS` | Funktion |
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `ERL` | Funktion |
|
||||
| `ERR` | Funktion |
|
||||
| `ERROR` | Anweisung |
|
||||
| `ON ERROR` | Anweisung |
|
||||
| `RESUME` | Anweisung |
|
||||
|
||||
## Finanzen
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `DDB#` | Funktion |
|
||||
| `FV#` | Funktion |
|
||||
| `IPMT#` | Funktion |
|
||||
| `IRR#` | Funktion |
|
||||
| `MIRR#` | Funktion |
|
||||
| `NPER#` | Funktion |
|
||||
| `NPV#` | Funktion |
|
||||
| `PMT#` | Funktion |
|
||||
| `PPMT#` | Funktion |
|
||||
| `PV#` | Funktion |
|
||||
| `RATE#` | Funktion |
|
||||
| `SLN#` | Funktion |
|
||||
| `SYD#` | Funktion |
|
||||
|
||||
Alle Funktionen rechnen in DOUBLE. Vorzeichenkonvention wie beim
|
||||
Vorbild: ausgehende Zahlungen sind negativ, eingehende positiv. Das
|
||||
letzte Argument der Renten-Funktionen ist die Fälligkeit — 0
|
||||
nachschüssig, 1 vorschüssig.
|
||||
|
||||
`Rate#` und `IRR#` nähern sich mit dem Sekantenverfahren.
|
||||
**Abbruchbedingung:** höchstens 100 Schritte; erreicht wird die Lösung,
|
||||
wenn der Betrag der Restabweichung unter 1e-9 fällt. Bleibt sie über
|
||||
1e-6, gilt die Näherung als erfolglos und es tritt Fehler 5 auf —
|
||||
ein stiller Ersatzwert wird nie geliefert. Eine Zahlungsreihe ohne
|
||||
Vorzeichenwechsel weist `IRR#` sofort mit Fehler 5 ab.
|
||||
|
||||
Weitere Fehler 5: Periode außerhalb der Laufzeit bei
|
||||
`IPmt#`/`PPmt#`/`SYD#`/`DDB#`, Nutzungsdauer null bei `SLN#`,
|
||||
Perioden null bei `Pmt#`, Zinssatz ≤ −1.
|
||||
|
||||
## Formatierung
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `FORMAT$` | Funktion |
|
||||
| `PRINT USING` | Anweisung |
|
||||
| `SetFormatCC` | Routine |
|
||||
|
||||
`PRINT USING`, `LPRINT USING` und `FORMAT$` teilen dieselbe
|
||||
Formatzeichenkette. Numerische Platzhalter: `#` Ziffernstelle,
|
||||
`.` Dezimalpunkt, `,` Tausendertrennung, `+`/`-` Vorzeichenführung
|
||||
(vorn oder hinten, mit eigener Stelle), `$$` mitlaufendes
|
||||
Währungszeichen und `**` Sternfüllung (beide bringen zwei Stellen
|
||||
mit), `^^^^` Exponentialform. String-Platzhalter: `!` erstes Zeichen,
|
||||
`&` ganzer String, `\ \` feste Länge. `_` macht das Folgezeichen
|
||||
literal.
|
||||
|
||||
Passt ein Wert nicht in sein Feld, wird er vollständig ausgegeben und
|
||||
mit `%` gekennzeichnet. Die Formatzeichenkette wird wiederholt,
|
||||
solange Werte übrig sind. Eine Zeichenkette ohne Feld bei vorhandenen
|
||||
Werten löst Fehler 5 aus. `SetFormatCC` setzt das Währungszeichen
|
||||
über einen Ländercode; nicht aufgeführte Codes behalten `$`.
|
||||
|
||||
## Klang
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `BEEP` | Anweisung |
|
||||
|
||||
## Mathematik
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `ABS` | Funktion |
|
||||
| `ATN` | Funktion |
|
||||
| `CCUR` | Funktion |
|
||||
| `CDBL` | Funktion |
|
||||
| `CINT` | Funktion |
|
||||
| `CLNG` | Funktion |
|
||||
| `COS` | Funktion |
|
||||
| `CSNG` | Funktion |
|
||||
| `EXP` | Funktion |
|
||||
| `FIX` | Funktion |
|
||||
| `INT` | Funktion |
|
||||
| `LOG` | Funktion |
|
||||
| `RANDOMIZE` | Anweisung |
|
||||
| `RND` | Funktion |
|
||||
| `SGN` | Funktion |
|
||||
| `SIN` | Funktion |
|
||||
| `SQR` | Funktion |
|
||||
| `TAN` | Funktion |
|
||||
|
||||
Rundung ist kaufmännisch-symmetrisch zur geraden Zahl
|
||||
(`CINT(0.5)` = 0, `CINT(1.5)` = 2, `CINT(2.5)` = 2). `INT` rundet ab,
|
||||
`FIX` in Richtung null. Überschreitet ein Ergebnis den Zieltyp, tritt
|
||||
Fehler 6 auf; `SQR`/`LOG` mit unzulässigem Argument lösen Fehler 5
|
||||
aus, Division durch null Fehler 11.
|
||||
|
||||
Der Zufallsgenerator ist der 24-Bit-LCG des Vorbilds
|
||||
(`zustand = (zustand · &HFD43FD + &HC39EC3) MOD 2^24`). `RND(0)`
|
||||
wiederholt den letzten Wert, `RND(n)` mit `n < 0` sät neu aus dem
|
||||
Argument. `RANDOMIZE saat` liefert bei gleicher Saat stets dieselbe
|
||||
Folge, unabhängig von vorherigen `RND`-Aufrufen; ohne Argument wird
|
||||
der Startwert abgefragt.
|
||||
|
||||
## Metabefehl
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `$DYNAMIC` | Metabefehl |
|
||||
| `$STATIC` | Metabefehl |
|
||||
|
||||
## Prozeduren
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `CALL` | Anweisung |
|
||||
| `DECLARE` | Anweisung |
|
||||
| `DEF FN` | Anweisung |
|
||||
| `FUNCTION` | Anweisung |
|
||||
| `SUB` | Anweisung |
|
||||
|
||||
## Steuerfluss
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `DO...LOOP` | Anweisung |
|
||||
| `END` | Anweisung |
|
||||
| `EXIT` | Anweisung |
|
||||
| `FOR...NEXT` | Anweisung |
|
||||
| `GOSUB` | Anweisung |
|
||||
| `GOTO` | Anweisung |
|
||||
| `IF...THEN...ELSE` | Anweisung |
|
||||
| `ON...GOSUB` | Anweisung |
|
||||
| `ON...GOTO` | Anweisung |
|
||||
| `RETURN` | Anweisung |
|
||||
| `SELECT CASE` | Anweisung |
|
||||
| `STOP` | Anweisung |
|
||||
| `WHILE...WEND` | Anweisung |
|
||||
|
||||
## Strings
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `ASC` | Funktion |
|
||||
| `CHR$` | Funktion |
|
||||
| `HEX$` | Funktion |
|
||||
| `INSTR` | Funktion |
|
||||
| `LCASE$` | Funktion |
|
||||
| `LEFT$` | Funktion |
|
||||
| `LEN` | Funktion |
|
||||
| `LTRIM$` | Funktion |
|
||||
| `MID$` | Anweisung |
|
||||
| `MID$` | Funktion |
|
||||
| `OCT$` | Funktion |
|
||||
| `RIGHT$` | Funktion |
|
||||
| `RTRIM$` | Funktion |
|
||||
| `SPACE$` | Funktion |
|
||||
| `STR$` | Funktion |
|
||||
| `STRING$` | Funktion |
|
||||
| `UCASE$` | Funktion |
|
||||
| `VAL` | Funktion |
|
||||
|
||||
Zeichen sind Unicode-Codepoints; `LEN` zählt Zeichen, nicht Bytes
|
||||
(dokumentierte Abweichung). Randfälle: `VAL` liest das führende
|
||||
Zahlenpräfix und überspringt Leerraum, versteht `&H`/`&O`;
|
||||
`STR$` stellt nicht-negativen Zahlen ein Leerzeichen voran;
|
||||
`LEFT$`/`RIGHT$` mit Länge über der Stringlänge liefern den ganzen
|
||||
String; `ASC("")` sowie eine Startposition kleiner 1 bei
|
||||
`MID$`/`INSTR` lösen Fehler 5 aus. `STRING$` nimmt einen Zeichencode
|
||||
oder einen String (dessen erstes Zeichen zählt).
|
||||
|
||||
## System
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `COMMAND$` | Funktion |
|
||||
| `SLEEP` | Anweisung |
|
||||
| `SYSTEM` | Anweisung |
|
||||
|
||||
`ENVIRON$` liest Umgebungsvariablen nach Namen, `COMMAND$` liefert die
|
||||
Aufrufargumente ohne Programmnamen. Nicht abbildbare Größen liefern
|
||||
einen dokumentierten Ersatzwert statt still zu variieren.
|
||||
|
||||
## Tastatur
|
||||
|
||||
| Element | Art |
|
||||
|---|---|
|
||||
| `INKEY$` | Funktion |
|
||||
| `INPUT$` | Funktion |
|
||||
| `KEY` | Anweisung |
|
||||
|
||||
`INKEY$` blockiert nicht: leerer String bei leerem Puffer, ein Zeichen
|
||||
bei einer Zeichentaste, zwei Zeichen mit führendem Nullzeichen bei
|
||||
einer Sondertaste. `INPUT$(n)` liest genau `n` Zeichen und blockiert
|
||||
dabei, ohne sie am Bildschirm zu wiederholen.
|
||||
|
||||
`KEY n, text$` belegt ein Funktionstasten-Makro (n = 1–10 sowie 30/31,
|
||||
höchstens 15 Zeichen), `KEY LIST` gibt alle zwölf untereinander aus,
|
||||
`KEY ON`/`KEY OFF` blendet die Softkey-Zeile ein und aus.
|
||||
312
docs/inventar.md
Normal file
312
docs/inventar.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Vollständigkeits-Inventar
|
||||
|
||||
Sollseite der Leitplanke „Vollständigkeit ist das Soll": jedes in der
|
||||
Original-Hilfe dokumentierte Sprachelement mit Abdeckungsstatus. Diese
|
||||
Tabelle ist Abnahmekriterium für Phase 6 und wird vom Test
|
||||
`inventar_stimmt_mit_code_ueberein` gegen den Code geprüft — sie kann
|
||||
nicht falsch werden, ohne dass die Testsuite rot wird.
|
||||
|
||||
**Quellen.** `bas7advr` = Themenliste von `bas7advr.hlp` (BASIC 7
|
||||
Advisor). `qb45advr` = Gegenprobe über `qb45advr.hlp`, nötig weil die
|
||||
erste Liste nachweislich unvollständig ist. `sprachreferenz` =
|
||||
docs/sprachreferenz.md für Elemente des Forms-Teils, die keine der beiden
|
||||
Themenlisten führt. Erhebung siehe
|
||||
`openspec/changes/phase-3-runtime-bildschirm/rohliste-original-hilfe.md`.
|
||||
|
||||
**Status.** `implementiert` = Frontend senkt ab und die Laufzeit führt
|
||||
aus. `offen` = dokumentiert, noch nicht verfügbar; die Verwendung wird
|
||||
namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
|
||||
„Abweichungen" der Sprachreferenz.
|
||||
|
||||
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
|
||||
Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
|
||||
**Abdeckung.** implementiert 195 · offen 37 · Non-Feature 53 · gesamt 285
|
||||
|
||||
| Name | Art | Gruppe | Status | Fundstelle | Quelle |
|
||||
|---|---|---|---|---|---|
|
||||
| `CLS` | Anweisung | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `COLOR` | Anweisung | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `CSRLIN` | Funktion | Bildschirm | implementiert | tb-runtime::screen | qb45advr |
|
||||
| `INPUT` | Anweisung | Bildschirm | implementiert | tb-vm::interp | bas7advr |
|
||||
| `LINE INPUT` | Anweisung | Bildschirm | implementiert | tb-vm::interp | bas7advr |
|
||||
| `LOCATE` | Anweisung | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `POS` | Funktion | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `PRINT` | Anweisung | Bildschirm | implementiert | tb-runtime::console | bas7advr |
|
||||
| `SCREEN` | Anweisung | Bildschirm | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `SCREEN` | Funktion | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `SPC` | Funktion | Bildschirm | implementiert | tb-runtime::console | bas7advr |
|
||||
| `TAB` | Funktion | Bildschirm | implementiert | tb-runtime::console | bas7advr |
|
||||
| `VIEW PRINT` | Anweisung | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `WIDTH` | Anweisung | Bildschirm | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `CHDIR` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CHDRIVE` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CLOSE` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CURDIR$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVC` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVD` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVDMBF` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVI` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVL` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVS` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `CVSMBF` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `DIR$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `EOF` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `FIELD` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `FILEATTR` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `FILES` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `FREEFILE` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `GET` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `INPUT #` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `KILL` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LINE INPUT #` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LOC` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LOCK` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LOF` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LPOS` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LPRINT` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LPRINT USING` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `LSET` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKC$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKD$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKDIR` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKDMBF$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKI$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKL$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKS$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `MKSMBF$` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `NAME` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `OPEN` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `PRINT #` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `PUT` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `RESET` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `RMDIR` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `RSET` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `SEEK` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `SEEK` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `SHELL` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `SHELL` | Funktion | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `UNLOCK` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `WRITE` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `WRITE #` | Anweisung | Datei-E/A | implementiert | tb-runtime::fileio | bas7advr |
|
||||
| `DATE$` | Anweisung | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `DATE$` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | qb45advr |
|
||||
| `DATESERIAL` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `DATEVALUE` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `DAY` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `HOUR` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `MINUTE` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `MONTH` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `NOW` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `SECOND` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `TIME$` | Anweisung | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `TIME$` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `TIMER` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `TIMESERIAL` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `TIMEVALUE` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `WEEKDAY` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `YEAR` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
|
||||
| `COMMON` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `CONST` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DATA` | Anweisung | Deklaration | implementiert | tb-vm::interp | bas7advr |
|
||||
| `DEFtype` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DIM` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `ERASE` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `LBOUND` | Funktion | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `LET` | Anweisung | Deklaration | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `OPTION BASE` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `READ` | Anweisung | Deklaration | implementiert | tb-vm::interp | bas7advr |
|
||||
| `REDIM` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `REM` | Anweisung | Deklaration | implementiert | tb-frontend::lexer | bas7advr |
|
||||
| `RESTORE` | Anweisung | Deklaration | implementiert | tb-vm::interp | bas7advr |
|
||||
| `SHARED` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `STATIC` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `SWAP` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `TYPE` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `UBOUND` | Funktion | Deklaration | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DOEVENTS` | Funktion | Ereignisse | implementiert | tb-runtime::builtins | sprachreferenz |
|
||||
| `EVENT` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `KEY (Ereignis)` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `ON KEY` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `ON PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ON PLAY` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ON SIGNAL` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `ON STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ON TIMER` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `ON UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PLAY (Ereignis)` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `SetUEvent` | Routine | Ereignisse | offen | - | bas7advr |
|
||||
| `SIGNAL` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `TIMER` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr |
|
||||
| `ERL` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `ERR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `ERR` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `ERROR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `ON ERROR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `RESUME` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
|
||||
| `DDB#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `FV#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `IPMT#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `IRR#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `MIRR#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `NPER#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `NPV#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `PMT#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `PPMT#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `PV#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `RATE#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `SLN#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `SYD#` | Funktion | Finanzen | implementiert | tb-runtime::finanz | bas7advr |
|
||||
| `FORMAT$` | Funktion | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `PRINT USING` | Anweisung | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `SetFormatCC` | Routine | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `INPUTBOX$` | Funktion | Forms | offen | - | sprachreferenz |
|
||||
| `MSGBOX` | Anweisung | Forms | offen | - | sprachreferenz |
|
||||
| `MSGBOX` | Funktion | Forms | offen | - | sprachreferenz |
|
||||
| `CIRCLE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `DRAW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `GET (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `LINE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PAINT` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PALETTE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PCOPY` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PMAP` | Funktion | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `POINT` | Funktion | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PRESET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PSET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PUT (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VIEW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `WINDOW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ABSOLUTE` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `BLOAD` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `BSAVE` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `DEF SEG` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `INP` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `INTERRUPT` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `INTERRUPTX` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `IOCTL` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `IOCTL$` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `OUT` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PEEK` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PEN` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `POKE` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `STICK` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `STRIG` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VARPTR` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VARPTR$` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VARSEG` | Funktion | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `WAIT` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `BEGINTRANS` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `BOF` | Funktion | ISAM | offen | - | bas7advr |
|
||||
| `COMMITTRANS` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `CREATEINDEX` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `DELETE` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `DELETEINDEX` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `DELETETABLE` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `GETINDEX$` | Funktion | ISAM | offen | - | bas7advr |
|
||||
| `INSERT` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `MOVEFIRST` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `MOVELAST` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `MOVENEXT` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `MOVEPREVIOUS` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `RETRIEVE` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `ROLLBACK` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `SAVEPOINT` | Funktion | ISAM | offen | - | bas7advr |
|
||||
| `SEEKEQ` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `SEEKGE` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `SEEKGT` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `SETINDEX` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `SETMEM` | Funktion | ISAM | offen | - | bas7advr |
|
||||
| `UPDATE` | Anweisung | ISAM | offen | - | bas7advr |
|
||||
| `CALLS` | Anweisung | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `SADD` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `SSEG` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `SSEGADD` | Funktion | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `StringAddress` | Routine | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `StringAssign` | Routine | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `StringLength` | Routine | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `StringRelease` | Routine | Interlanguage | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `BEEP` | Anweisung | Klang | implementiert | tb-runtime::builtins | qb45advr |
|
||||
| `PLAY` | Anweisung | Klang | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `SOUND` | Anweisung | Klang | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `COM` | Anweisung | Kommunikation | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ON COM` | Anweisung | Kommunikation | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `OPEN COM` | Anweisung | Kommunikation | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ABS` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `ATN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `CCUR` | Funktion | Mathematik | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `CDBL` | Funktion | Mathematik | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `CINT` | Funktion | Mathematik | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `CLNG` | Funktion | Mathematik | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `COS` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `CSNG` | Funktion | Mathematik | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `EXP` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `FIX` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `INT` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `LOG` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `RANDOMIZE` | Anweisung | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `RND` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `SGN` | Funktion | Mathematik | implementiert | tb-runtime::math | qb45advr |
|
||||
| `SIN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `SQR` | Funktion | Mathematik | implementiert | tb-runtime::math | qb45advr |
|
||||
| `TAN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `$DYNAMIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
|
||||
| `$INCLUDE` | Metabefehl | Metabefehl | offen | - | bas7advr |
|
||||
| `$STATIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
|
||||
| `CALL` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DECLARE` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DEF FN` | Anweisung | Prozeduren | implementiert | tb-vm::interp | bas7advr |
|
||||
| `FUNCTION` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `SUB` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DO...LOOP` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `END` | Anweisung | Steuerfluss | implementiert | tb-vm::interp | bas7advr |
|
||||
| `EXIT` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `FOR...NEXT` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `GOSUB` | Anweisung | Steuerfluss | implementiert | tb-vm::interp | bas7advr |
|
||||
| `GOTO` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `IF...THEN...ELSE` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `ON...GOSUB` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `ON...GOTO` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `RETURN` | Anweisung | Steuerfluss | implementiert | tb-vm::interp | bas7advr |
|
||||
| `SELECT CASE` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `STOP` | Anweisung | Steuerfluss | implementiert | tb-vm::interp | bas7advr |
|
||||
| `WHILE...WEND` | Anweisung | Steuerfluss | implementiert | tb-vm::codegen | bas7advr |
|
||||
| `ASC` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `CHR$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `HEX$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `INSTR` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `LCASE$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `LEFT$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `LEN` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `LTRIM$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `MID$` | Anweisung | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `MID$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `OCT$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `RIGHT$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `RTRIM$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `SPACE$` | Funktion | Strings | implementiert | tb-runtime::strings | qb45advr |
|
||||
| `STR$` | Funktion | Strings | implementiert | tb-runtime::format | bas7advr |
|
||||
| `STRING$` | Funktion | Strings | implementiert | tb-runtime::strings | qb45advr |
|
||||
| `UCASE$` | Funktion | Strings | implementiert | tb-runtime::strings | bas7advr |
|
||||
| `VAL` | Funktion | Strings | implementiert | tb-runtime::format | bas7advr |
|
||||
| `CHAIN` | Anweisung | System | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `CLEAR` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `COMMAND$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `ENVIRON` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `ENVIRON$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `ERDEV` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `ERDEV$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `FRE` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `RUN` | Anweisung | System | offen | - | bas7advr |
|
||||
| `SLEEP` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `STACK` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `STACK` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `SYSTEM` | Anweisung | System | implementiert | tb-vm::interp | bas7advr |
|
||||
| `TROFF` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `TRON` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `INKEY$` | Funktion | Tastatur | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `INPUT$` | Funktion | Tastatur | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `KEY` | Anweisung | Tastatur | implementiert | tb-runtime::builtins | bas7advr |
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
Rekonstruierte Referenz des Dialekts (DOS-BASIC, Stand 1992: prozedurale
|
||||
BASIC-Familie mit Forms-Erweiterung). Dieses Dokument ist die verbindliche
|
||||
Grundlage für Frontend und Runtime. Unklare Detailfragen sind mit `TODO`
|
||||
markiert und werden per Testkorpus geklärt bzw. entschieden.
|
||||
Grundlage für Frontend und Runtime. Offene Detailfragen sind ausgeräumt
|
||||
(Stand 2026-09-04); wo die Original-Dokumentation schweigt, steht an Ort
|
||||
und Stelle eine datierte Entscheidung mit Begründung.
|
||||
|
||||
**Primärquellen:** Original-Hilfedateien des Vorbilds als HTML unter
|
||||
https://dos-help.soulsphere.org/ (`qck.hlp` Quick Reference, `advr.hlp`
|
||||
Details, `ener.hlp` Fehlermeldungen; Vergleich PDS 7.1: `bas7qck.hlp`)
|
||||
sowie das Original-README der Professional Edition. Aussagen ohne
|
||||
TODO-Markierung sind daraus belegt.
|
||||
ausgewiesene Entscheidung sind daraus belegt; die Themenlisten von
|
||||
`bas7advr.hlp` und `qb45advr.hlp` bilden zusammen das
|
||||
Vollständigkeits-Inventar (docs/inventar.md).
|
||||
|
||||
**Guiding Principle:** Die Original-Dokumentation führt. Jedes dort
|
||||
dokumentierte Sprachelement wird unterstützt oder — nur nach expliziter
|
||||
@@ -94,7 +97,9 @@ stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
|
||||
- `DIM [SHARED] var[(dims)] [AS typ]` — `AS`-Klausel: INTEGER, LONG, SINGLE,
|
||||
DOUBLE, STRING, STRING * n, CURRENCY, benutzerdefinierter Typ.
|
||||
- `COMMON [SHARED] [/blockname/] liste` — modulübergreifend (Kette
|
||||
CHAIN-kompatibel im Vorbild; TODO: Relevanz ohne CHAIN klären).
|
||||
CHAIN-kompatibel im Vorbild; ohne `CHAIN` bleibt `COMMON` die
|
||||
Verknüpfung mehrerer Module eines Projekts — geklärt mit der
|
||||
Projektverwaltung in Phase 5).
|
||||
- `SHARED` (in Prozedur): Zugriff auf Modulebene-Variablen.
|
||||
- `STATIC` (in Prozedur): Variablen behalten Werte zwischen Aufrufen;
|
||||
`STATIC`-Attribut an `SUB`/`FUNCTION` macht alle lokalen Variablen statisch.
|
||||
@@ -152,7 +157,8 @@ INTEGER −1 (wahr) / 0 (falsch).
|
||||
oder `DEF…`-Regel.
|
||||
- **Parameterübergabe standardmäßig BYREF.** Klammern um ein Argument
|
||||
(`CALL f((x))`) erzwingen Wertübergabe. `BYVAL` nur in `DECLARE` für
|
||||
externe Routinen (entfällt bei uns; TODO: `BYVAL` allgemein erlauben?).
|
||||
externe Routinen (entfällt bei uns; `BYVAL` allgemein zu erlauben wäre
|
||||
eine Erweiterung über den Kernstandard hinaus → Stufe 2, PLAN.md).
|
||||
- Arrays werden mit `name()` übergeben, TYPEs BYREF.
|
||||
- Rekursion erlaubt (außer bei `STATIC`-Semantik-Konflikten).
|
||||
- `DEF FNname (args) = ausdruck` und Block-`DEF FN … END DEF` (mit
|
||||
@@ -194,8 +200,21 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
|
||||
position, `-` nachgestelltes Minus, `$$` führendes $, `**` Füllsterne,
|
||||
`**$` kombiniert, `^^^^` Exponent; String `&` ganz, `!` erstes Zeichen,
|
||||
`\ \` n Zeichen (Leerzeichen + 2); `_` Literal-Escape; alle anderen
|
||||
Zeichen literal. TODO: `%`-Präfix bei Feldüberlauf (QB-Verhalten) in der
|
||||
Vorbild-Hilfe nicht gelistet — per Test klären, wir übernehmen es vorerst.
|
||||
Zeichen literal.
|
||||
- **Feldüberlauf** (geklärt 2026-09-04): Passt ein Wert nicht in sein
|
||||
Feld, wird er vollständig ausgegeben und die Ausgabe mit `%`
|
||||
eingeleitet (`PRINT USING "##"; 12345` → `%12345`). Die Original-Hilfe
|
||||
listet das nicht; wir übernehmen das Verhalten der QB-Familie, weil ein
|
||||
stilles Abschneiden Zahlen verfälschen würde.
|
||||
- Vorzeichenangaben (`+` vorn, `+`/`-` hinten) belegen eine **eigene**
|
||||
Position neben den Ziffernstellen; `$$` und `**` bringen ihre Stellen
|
||||
dagegen mit.
|
||||
- Die Formatzeichenkette wird wiederholt, solange Werte übrig sind. Eine
|
||||
Zeichenkette ohne Feld bei vorhandenen Werten löst Fehler 5 aus.
|
||||
- `SetFormatCC(ländercode)` setzt das Zeichen für `$$`. Nur wenige Codes
|
||||
sind belegt (1 = `$`, 33 = `F`, 39 = `L`, 44 = `£`, 49 = `DM`,
|
||||
81 = `¥`); alle übrigen behalten `$` — die Tabelle des Vorbilds ist
|
||||
nicht überliefert (dokumentierte Abweichung).
|
||||
- `INPUT ["prompt"{;|,}] var, …` (mit `;` vor Prompt: kein „? ");
|
||||
`LINE INPUT` (ganze Zeile in String).
|
||||
- `INKEY$` (nicht blockierend; "" wenn leer; erweiterte Tasten:
|
||||
@@ -205,13 +224,26 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
|
||||
— arbeiten auf der tatsächlichen Bildschirmgröße (dynamisch, s. Abweichungen).
|
||||
- `COLOR [vg][,hg]` (vg 0–31: 16–31 = blinkend — bei uns als „hell"
|
||||
simuliert, Entscheidung 2026-09-02; hg 0–7), `CLS`,
|
||||
`WIDTH` (im Vorbild 80/40 — bei uns wirkungslos, s. Abweichungen),
|
||||
`WIDTH` (hebt den Puffer höchstens an, s. Abweichungen),
|
||||
`VIEW PRINT oben TO unten` (Scrollbereich).
|
||||
- `TAB(n)`, `SPC(n)` in PRINT-Listen.
|
||||
- `BEEP`, `SOUND freq, dauer` (Terminal-Bell / Nicht-Ziel, s. Abweichungen),
|
||||
`PLAY` (Nicht-Ziel).
|
||||
- `KEY n, text$` / `KEY LIST` / `KEY ON/OFF` (Funktionstasten-Makros +
|
||||
Statuszeile) — TODO: Umfang klären.
|
||||
- **Funktionstasten-Makros** (Umfang geklärt 2026-09-03 aus der
|
||||
Original-Hilfe):
|
||||
- `KEY n, text$` belegt ein Makro. `n` ist 1–10 für F1–F10 sowie 30 und
|
||||
31 für F11/F12; andere Werte lösen Fehler 5 aus. `text$` wird nach
|
||||
**15 Zeichen** abgeschnitten.
|
||||
- `KEY LIST` gibt alle zwölf Makros untereinander aus.
|
||||
- `KEY ON` blendet die Softkey-Zeile in der untersten Bildschirmzeile ein
|
||||
(Tastennummer plus die ersten sechs Zeichen des Makros), `KEY OFF`
|
||||
blendet sie aus. Solange sie sichtbar ist, endet der Scrollbereich
|
||||
darüber, damit die Ausgabe sie nicht überschreibt; ein danach gesetztes
|
||||
`VIEW PRINT` hat Vorrang.
|
||||
- `KEY(n) ON|OFF|STOP` **mit** Index ist Ereignissteuerung und kommt mit
|
||||
der Forms-Engine (Phase 4).
|
||||
- Abweichung: ein kompiliertes Programm startet mit **leeren** Makros;
|
||||
die Vorbelegung des Vorbilds gehörte zu dessen Interpreter-Oberfläche.
|
||||
|
||||
## 10. Datei-E/A
|
||||
|
||||
@@ -240,8 +272,20 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
|
||||
`HEX$`, `OCT$`, `LSET`/`RSET` (Anweisungen).
|
||||
|
||||
**Mathematik:** `ABS`, `SGN`, `INT` (abrunden), `FIX` (Richtung 0), `SQR`,
|
||||
`EXP`, `LOG`, `SIN`, `COS`, `TAN`, `ATN`, `RND[(n)]`, `RANDOMIZE [saat]`
|
||||
(kompatibler PRNG! → Testkorpus), `CINT`, `CLNG`, `CSNG`, `CDBL`, `CCUR`.
|
||||
`EXP`, `LOG`, `SIN`, `COS`, `TAN`, `ATN`, `RND[(n)]`, `RANDOMIZE [saat]`,
|
||||
`CINT`, `CLNG`, `CSNG`, `CDBL`, `CCUR`.
|
||||
|
||||
**Zufallszahlen.** Der Generator ist der lineare Kongruenzgenerator des
|
||||
Vorbilds auf 24 Bit:
|
||||
`zustand = (zustand · &H FD43FD + &H C39EC3) MOD 2^24`, Ergebnis
|
||||
`zustand / 2^24` als SINGLE im Bereich [0, 1).
|
||||
`RND` und `RND(n)` mit `n > 0` liefern die nächste Zahl, `RND(0)`
|
||||
wiederholt die zuletzt gelieferte, `RND(n)` mit `n < 0` setzt den Zustand
|
||||
aus dem Argument. `RANDOMIZE saat` bildet aus der Saat 16 Bit, die die
|
||||
Bits 8–23 des Zustands besetzen; das niederwertige Byte ist fest — damit
|
||||
liefert dieselbe Saat stets dieselbe Folge, unabhängig von vorherigen
|
||||
`RND`-Aufrufen. `RANDOMIZE` ohne Argument fragt den Startwert mit
|
||||
`Random Number Seed (-32768 to 32767)?` ab.
|
||||
|
||||
**Datum/Zeit:** `DATE$` (Funktion und Anweisung — Setzen: Nicht-Ziel),
|
||||
`TIME$`, `TIMER` (Sekunden seit Mitternacht, SINGLE); dazu die nativen
|
||||
@@ -287,7 +331,13 @@ Dialoge:
|
||||
- Der Bildschirmpuffer speichert Unicode-Zeichen; Rahmen werden mit
|
||||
Unicode-Box-Drawing gezeichnet.
|
||||
- Zeichen mit Darstellungsbreite 2 (Emoji, CJK) **belegen zwei Zellen**
|
||||
(Entscheidung 2026-09-02; Umsetzung Phase 3).
|
||||
(Entscheidung 2026-09-02, umgesetzt): der Cursor rückt um zwei Spalten
|
||||
vor, `POS` nennt die Anfangsspalte, `LOCATE` auf die zweite Hälfte
|
||||
wirkt auf den Zeichenanfang. Passt ein breites Zeichen nicht mehr in
|
||||
die Zeile, wandert es als Ganzes in die nächste und die letzte Spalte
|
||||
bleibt leer. Wird eine Hälfte überschrieben, verschwindet die andere.
|
||||
Zeichen der Breite 0 (kombinierende Zeichen) belegen vorerst eine
|
||||
eigene Zelle.
|
||||
- `GET`/`PUT`-Records: feste Strings (`STRING * n`) werden als **UTF-32**
|
||||
gespeichert (4 Bytes/Zeichen, feste Record-Länge) — Binärdateien sind
|
||||
dadurch bewusst inkompatibel zum Vorbild (Entscheidung 2026-09-02).
|
||||
@@ -295,20 +345,60 @@ Dialoge:
|
||||
`OUT`/`WAIT`/`BLOAD`/`BSAVE`/`VARPTR`/`VARSEG`/`SADD`/`CALL ABSOLUTE`/
|
||||
`CALL INTERRUPT`/`IOCTL` werden bereits **zur Compile-Zeit** mit der
|
||||
Meldung „Feature unavailable" abgewiesen (kein Laufzeitfehler).
|
||||
3. **`INKEY$` für erweiterte Tasten** liefert weiterhin
|
||||
`CHR$(0) + code`-Sequenzen mit den klassischen Scancodes (F1 = `CHR$(0)+";"`
|
||||
usw.), damit bestehender Code funktioniert. Zusätzliche moderne Tasten:
|
||||
TODO.
|
||||
Ebenso abgewiesen: die Interlanguage-Schnittstelle (`CALLS`, `SSEG`,
|
||||
`SSEGADD`, `StringAddress`/`StringAssign`/`StringLength`/
|
||||
`StringRelease`), die Eingabegeräte `PEN`, `STRIG`, `STICK` und die
|
||||
serielle Schnittstelle (`COM`, `ON COM`, `OPEN "COMn:"`).
|
||||
3. **`INKEY$` für erweiterte Tasten** liefert `CHR$(0) + code`-Sequenzen
|
||||
mit den klassischen Scancodes, damit bestehender Code funktioniert
|
||||
(festgelegt 2026-09-04): F1–F10 = 59–68, Pos1 = 71, ↑ = 72, Bild↑ = 73,
|
||||
← = 75, → = 77, Ende = 79, ↓ = 80, Bild↓ = 81, Einfg = 82, Entf = 83.
|
||||
Zeichentasten liefern ein Zeichen, Eingabe `CHR$(13)`, Rückschritt
|
||||
`CHR$(8)`, Esc `CHR$(27)`, Tabulator `CHR$(9)`. Strg+C und Strg+Untbr
|
||||
sind Abbruch und erscheinen nicht in `INKEY$`. Tasten heutiger
|
||||
Tastaturen ohne Entsprechung im Vorbild (etwa F11/F12 oder Meta-
|
||||
Kombinationen) liefern nichts — eine eigene Kodierung dafür wäre eine
|
||||
Erweiterung und gehört in Stufe 2.
|
||||
4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell.
|
||||
5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `LINE`, `CIRCLE`, `PAINT`,
|
||||
`DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`/`VIEW`, `PMAP`, `PCOPY`)
|
||||
sind Nicht-Ziel (Fehler 73). Hinweis: das Vorbild **hatte** die
|
||||
5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `PRESET`, `LINE`,
|
||||
`CIRCLE`, `PAINT`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`,
|
||||
`VIEW`, `PMAP`, `PCOPY`, `POINT`) sind Nicht-Ziel und werden **zur
|
||||
Compile-Zeit** mit „Feature unavailable" abgewiesen. Die Formen, die
|
||||
sich ein Schlüsselwort mit einer unterstützten Anweisung teilen, erkennt
|
||||
der Compiler an ihrer Syntax: `LINE` ohne `INPUT`, `GET`/`PUT` mit
|
||||
`(`, `VIEW` ohne `PRINT`. Hinweis: das Vorbild **hatte** die
|
||||
Grafikmodi 0–13, aber nicht gleichzeitig mit Forms (dort Fehler 272
|
||||
„Invalid when forms are showing") — wir übernehmen nur den
|
||||
Forms-/Textmodus-Zweig.
|
||||
6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73).
|
||||
5a. **`LPRINT` schreibt in eine Datei** (2026-09-03): einen
|
||||
plattformübergreifenden Druckerkanal gibt es nicht. Der Druckerpuffer
|
||||
geht am Programmende in `LPT1.TXT` im aktuellen Verzeichnis; `LPOS`
|
||||
nennt die Spalte in diesem Puffer.
|
||||
5b. **Satzsperren** (`LOCK`/`UNLOCK`) prüfen nur, ob die Datei offen ist;
|
||||
eine echte Sperre bietet die Plattform nicht einheitlich. `FILEATTR`
|
||||
liefert nur die Modus-Kennzahl (Argument 1); das
|
||||
Betriebssystem-Handle bildet der Dialekt nicht ab und liefert 0.
|
||||
5c. **`CHDRIVE`** ist außerhalb von Windows folgenlos — Laufwerksbuchstaben
|
||||
gibt es dort nicht.
|
||||
6a. **Uhr läuft in UTC** (2026-09-03): `DATE$`, `TIME$`, `TIMER` und `NOW`
|
||||
liefern koordinierte Weltzeit, nicht Ortszeit — die Standardbibliothek
|
||||
von Rust kennt keine Zeitzonen, und eine Zeitzonenbibliothek wäre die
|
||||
erste schwere Abhängigkeit der Laufzeit. `DATE$ = …` und `TIME$ = …`
|
||||
stellen nicht die Systemuhr, sondern einen programmeigenen Versatz;
|
||||
alle Zeitfunktionen rechnen ihn mit ein.
|
||||
6b. **Serielle Datumswerte** zählen Tage ab dem 30.12.1899 (Serie 2 =
|
||||
1.1.1900), der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1
|
||||
für Sonntag. Zweistellige Jahre gelten in `DATESERIAL` als 19xx, in
|
||||
`DATEVALUE` als 20xx unter 80 und sonst als 19xx.
|
||||
7. **Dynamische Bildschirmgröße** (2026-09-02): kein festes 80×25 — der
|
||||
Bildschirm folgt der Terminalgröße (Mindestgröße 80×25, darunter nur
|
||||
ein Hinweis). `CSRLIN`/`POS`/`LOCATE`/`VIEW PRINT` und das
|
||||
`SCREEN`-Objekt arbeiten auf der tatsächlichen Größe. `WIDTH 40/80`
|
||||
wird akzeptiert, ändert aber nichts (die Größe bestimmt das Terminal).
|
||||
80×25 ist überall nur die **untere Schranke der Darstellung**, nie eine
|
||||
Bildschirmgröße im Verhalten des Programms. Ändert sich die Größe
|
||||
**während der Ausführung**, wird das zugestellt: der Inhalt bleibt oben
|
||||
links erhalten, der Cursor wird in die neuen Grenzen geklemmt, und ein
|
||||
`VIEW PRINT`-Bereich wird am unteren Rand geklemmt bzw. — wenn er ganz
|
||||
vom Schirm fällt — auf den vollen Bildschirm zurückgesetzt.
|
||||
|
||||
@@ -41,11 +41,17 @@ enum Value {
|
||||
BYREF-Parameter als Referenz-Slots (Handle auf Variablen-Slot).
|
||||
- GOSUB: **kein** Frame — Rücksprungadresse auf separatem GOSUB-Stack im
|
||||
aktuellen Frame (RETURN prüft diesen zuerst).
|
||||
- **Fehlerbehandlung:** Pro Frame ein Handler-Zustand (`ON ERROR GOTO x`).
|
||||
Laufzeitfehler → VM sucht aktiven Handler im aktuellen Modulkontext
|
||||
(Vorbild: Handler sind modul-/prozedurlokal, TODO: exakte Scoping-Regel
|
||||
testen), setzt `ERR`/`ERL`, springt. `RESUME` nutzt gemerkten
|
||||
Anweisungs-Offset.
|
||||
- **Fehlerbehandlung:** Zwei Ebenen (Scoping geklärt 2026-09-04).
|
||||
`ON LOCAL ERROR GOTO x` setzt einen Handler im aktuellen Frame; sein
|
||||
Sprungziel muss im selben Rumpf liegen. `ON ERROR GOTO x` **ohne**
|
||||
`LOCAL` setzt den modulweiten Handler — auch aus einer Prozedur heraus,
|
||||
und sein Sprungziel liegt dann im Modulrumpf (der Codegenerator löst es
|
||||
über Prozedur 0 auf, die zuerst übersetzt wird).
|
||||
Ein Laufzeitfehler sucht zuerst lokale Handler die Aufrufkette hinauf,
|
||||
dann den modulweiten; beim modulweiten wird bis Frame 0 abgewickelt,
|
||||
weshalb `RESUME NEXT` dort hinter dem Prozeduraufruf fortsetzt. Der
|
||||
Handler setzt `ERR`/`ERL`; `RESUME` nutzt den gemerkten
|
||||
Anweisungs-Offset. Ein Fehler im Handler ist fatal (keine Kaskaden).
|
||||
|
||||
## Ausführungsmodell / Unterbrechbarkeit
|
||||
|
||||
@@ -136,6 +142,20 @@ Windows 11, `cargo bench -p tb-vm`):**
|
||||
- DOUBLE-Schleife: ≈ 5,2 Mio Iterationen/s
|
||||
- SUB-Aufrufe mit BYREF: ≈ 5,2 Mio Aufrufe/s
|
||||
- String-Lasten (MID$/INSTR/Verkettung): ≈ 0,8 Mio Runden/s
|
||||
**Nachmessung nach Phase 3 (2026-09-04, macOS, Apple Silicon,
|
||||
`cargo bench -p tb-vm`)** — die Zustellpunkte des neuen `Host` (Anzeige
|
||||
nur bei geändertem Bildschirm, Ereignisabholung alle 1024 Anweisungen)
|
||||
kosten nichts Messbares:
|
||||
|
||||
- Compile-Budget: Einzelmodul 508 Zeilen **0,77 ms**; Projekt 49 760
|
||||
Zeilen **53 ms** (≈ 950 000 Zeilen/s)
|
||||
- VM-Durchsatz: INTEGER-Schleife **11,0 Mio/s**, DOUBLE **12,2 Mio/s**,
|
||||
SUB-Aufrufe **10,7 Mio/s**, String-Lasten **2,4 Mio Runden/s**
|
||||
|
||||
(Die höheren Zahlen gegenüber 2026-09-02 gehen auf die andere Maschine
|
||||
zurück, nicht auf eine Optimierung — gemessen wurde beide Male derselbe
|
||||
Benchmark.)
|
||||
|
||||
- Einordnung: bei ≈ 13 Instruktionen je Schleifeniteration entspricht
|
||||
das ≈ 60 Mio Instruktionen/s — Vorbild-Programme auf 1992er-Hardware
|
||||
werden um weit mehr als den Faktor 100 übertroffen; für 80×25-
|
||||
@@ -145,14 +165,18 @@ Windows 11, `cargo bench -p tb-vm`):**
|
||||
## Zahlenkonvertierungs-Matrix (Feindesign, 2026-09-02)
|
||||
|
||||
Grundlage: Original-Hilfe (Topics „Arithmetic Operators", „CINT/CLNG",
|
||||
„MOD", „Numeric Types"). Zellen ohne belastbare Fundstelle sind mit
|
||||
`TODO verify` markiert; der Korpustest `konvertierung.bas` verankert die
|
||||
Matrix ausführbar.
|
||||
„MOD", „Numeric Types"). Vier Punkte sind dort **nicht** geregelt; sie
|
||||
wurden am 2026-09-04 entschieden und sind unten als *Projektentscheidung*
|
||||
gekennzeichnet — sie gelten damit als bewusste Festlegung, nicht als
|
||||
rekonstruiertes Vorbildverhalten. Der Korpustest `konvertierung.bas`
|
||||
verankert die Matrix ausführbar.
|
||||
|
||||
**Rangfolge für Promotion:** INTEGER < LONG < CURRENCY < SINGLE < DOUBLE.
|
||||
Ausnahme: CURRENCY gemischt mit SINGLE oder DOUBLE ergibt **DOUBLE**
|
||||
(nicht SINGLE), damit keine Festkomma-Präzision in f32 verloren geht
|
||||
(`TODO verify` gegen PDS-Hilfe; plausibelste Lesart).
|
||||
(nicht SINGLE). *Projektentscheidung 2026-09-04:* Die Original-Hilfe
|
||||
regelt diesen Fall nicht. CURRENCY trägt 19 signifikante Stellen, SINGLE
|
||||
nur 7 — eine Promotion nach SINGLE zerstörte genau die Genauigkeit, für
|
||||
die es den Typ gibt. DOUBLE (16 Stellen) ist die nächstbeste Wahl.
|
||||
|
||||
**Ergebnistyp binärer Operatoren** (l op r; T = promoteter Typ):
|
||||
|
||||
@@ -161,7 +185,7 @@ Ausnahme: CURRENCY gemischt mit SINGLE oder DOUBLE ergibt **DOUBLE**
|
||||
| `+ - *` | T; gleiche Typen bleiben erhalten | Ganzzahl-/CURRENCY-Überlauf → Fehler 6 (auch INTEGER+INTEGER promotet **nicht**) |
|
||||
| `/` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE | Division durch 0 → Fehler 11 |
|
||||
| `\` `MOD` | INTEGER, wenn beide Operanden INTEGER, sonst LONG | Operanden werden **vorher** kaufmännisch (banker's) auf Ganzzahl gerundet; außerhalb LONG → Fehler 6; Divisor 0 → Fehler 11; `MOD`-Vorzeichen wie Dividend |
|
||||
| `^` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE (`TODO verify`) | Rechnung intern in DOUBLE; 0 ^ negativ → Fehler 11; negative Basis mit nicht-ganzem Exponent → Fehler 5 |
|
||||
| `^` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE (*Projektentscheidung 2026-09-04*) | Rechnung intern in DOUBLE; 0 ^ negativ → Fehler 11; negative Basis mit nicht-ganzem Exponent → Fehler 5 |
|
||||
| `= <> < <= > >=` | INTEGER (−1/0) | Vergleich im promoteten Typ; Strings codepoint-weise |
|
||||
| `NOT AND OR XOR EQV IMP` | INTEGER, wenn alle Operanden INTEGER, sonst LONG | bitweise; Operanden vorher banker's-gerundet auf Ganzzahl, außerhalb LONG → Fehler 6 |
|
||||
| unäres `-` | Typ des Operanden | −(−32768) INTEGER → Fehler 6 |
|
||||
@@ -188,13 +212,21 @@ via `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR` — identische Semantik):
|
||||
- INTEGER/LONG: dezimal ohne Dezimalpunkt.
|
||||
- SINGLE: bis 7 signifikante Stellen, kürzeste Darstellung, keine
|
||||
führende Null vor dem Dezimalpunkt (`.5`), keine nachgestellten
|
||||
Nullen; außerhalb des Festformat-Bereichs Exponentialform `mE±xx`
|
||||
(exakte Schwelle `TODO verify`; umgesetzt: Exponentialform, wenn der
|
||||
Dezimalexponent < −7 oder ≥ 7 ist).
|
||||
Nullen; außerhalb des Festformat-Bereichs Exponentialform `mE±xx`.
|
||||
*Projektentscheidung 2026-09-04:* Die Original-Hilfe nennt keine
|
||||
Schwelle. Gewählt ist der Dezimalexponent < −7 oder ≥ 7 — damit bleibt
|
||||
jeder Wert im Festformat, der sich mit den 7 signifikanten Stellen des
|
||||
Typs verlustfrei schreiben lässt (`9999999` fest, `1E+07`
|
||||
exponentiell; `.0000001` fest, `1E-08` exponentiell).
|
||||
- DOUBLE: wie SINGLE mit bis 16 signifikanten Stellen und `D±xx` als
|
||||
Exponentmarke.
|
||||
- CURRENCY: Festformat mit bis zu 4 Nachkommastellen, nachgestellte
|
||||
Nullen entfallen (`TODO verify`).
|
||||
Nullen entfallen (`2.5000@` → `2.5`, `1.10@` → `1.1`).
|
||||
*Projektentscheidung 2026-09-04:* Die Original-Hilfe nennt kein
|
||||
Ausgabeformat. Gewählt ist dieselbe Regel wie bei den übrigen
|
||||
Zahlentypen — nachgestellte Nullen entfallen —, damit `PRINT` über alle
|
||||
Typen einheitlich wirkt. Wer feste Nachkommastellen braucht, nutzt
|
||||
`PRINT USING`.
|
||||
- `PRINT` stellt nicht-negativen Zahlen ein Leerzeichen, negativen ein
|
||||
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
|
||||
Leerzeichen/`-`, kein nachgestelltes.
|
||||
@@ -266,6 +298,9 @@ als Vergleichs-/Sprungkette abgesenkt.
|
||||
## Offene Punkte
|
||||
|
||||
Keine — die Punkte aus Phase 0 (Opcode-Satz, Konvertierungsmatrix,
|
||||
`STOP`/`CONT` im Runner) sind oben ausgearbeitet; verbleibende
|
||||
Detailunsicherheiten sind als `TODO verify` direkt an den betroffenen
|
||||
Zellen markiert und über den Testkorpus abgesichert.
|
||||
`STOP`/`CONT` im Runner) sind oben ausgearbeitet. Die vier Stellen, zu
|
||||
denen die Original-Hilfe schweigt, sind am 2026-09-04 als
|
||||
*Projektentscheidung* festgelegt, an Ort und Stelle begründet und über
|
||||
`tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine
|
||||
belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie —
|
||||
die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild.
|
||||
|
||||
2
openspec/changes/phase-3-isam/.openspec.yaml
Normal file
2
openspec/changes/phase-3-isam/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-03
|
||||
209
openspec/changes/phase-3-isam/design.md
Normal file
209
openspec/changes/phase-3-isam/design.md
Normal file
@@ -0,0 +1,209 @@
|
||||
## Context
|
||||
|
||||
Siehe proposal.md — Why. Technisch relevant ist der Zustand nach
|
||||
`phase-3-runtime-bildschirm`:
|
||||
|
||||
- Die Dateitabelle, die Recordpuffer und die UTF-32-Serialisierung fester
|
||||
Textfelder existieren (Capability `datei-eio`) und sind die Grundlage,
|
||||
auf der ISAM die Sätze ablegt.
|
||||
- Die Laufzeitfehler 81–89 sind in `tb_runtime::errors` bereits
|
||||
vorhanden; dieser Change liefert ihre Auslöser.
|
||||
- Vom ISAM-Sprachumfang existiert heute nur die Parser-Klausel
|
||||
`OPEN … FOR ISAM` (`parser.rs`). Die ISAM-Anweisungen haben weder
|
||||
Grammatik noch Signatur — sie enden in „unbekannter Bezeichner".
|
||||
- Das Sprachinventar (`docs/inventar.md`) führt die ISAM-Elemente mit
|
||||
Status `offen` und ist der maßgebliche Umfangsnachweis.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Der volle dokumentierte ISAM-Sprachumfang läuft, nicht eine Auswahl.
|
||||
- Die Cursor-Semantik des Vorbilds ist exakt abgebildet — sie ist der
|
||||
Teil, den keine Fremdbibliothek liefert.
|
||||
- Speicher, Indizes und Transaktionen kommen aus einer erprobten
|
||||
Bibliothek statt aus Eigenbau.
|
||||
- Phase 3 ist mit diesem Change abgeschlossen.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Leistungsoptimierung des Satzzugriffs — ISAM-Programme des Vorbilds
|
||||
arbeiten auf Datenmengen, die jede heutige Speicherschicht mühelos
|
||||
trägt.
|
||||
- Nebenläufiger Zugriff mehrerer Prozesse auf dieselbe Datenbankdatei
|
||||
über die Satzsperren aus `datei-eio` hinaus.
|
||||
- Lesen von Datenbankdateien des Vorbilds.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: `redb` als Speicherschicht
|
||||
|
||||
Die Datenbankdatei ist eine `redb`-Datei. Sätze und Indexeinträge liegen
|
||||
darin als Key-Value-Paare; `redb` liefert B-Baum, Transaktionen und
|
||||
Crash-Sicherheit.
|
||||
|
||||
*Warum nicht Eigenbau:* Seitenverwaltung, B-Baum und crash-sichere
|
||||
Transaktionen sind gelöste Probleme mit langen Fehlerkatalogen. Der
|
||||
projektspezifische Teil ist die ISAM-Semantik, nicht der Baum darunter.
|
||||
|
||||
*Warum nicht SQLite:* Zöge eine C-Bibliothek in jedes von `tbc build
|
||||
--exe` erzeugte Binary. Der eigentliche Grund ist aber semantisch: ISAM
|
||||
hat einen *stabilen, benannten Cursor*, der Änderungen überlebt und sich
|
||||
vorwärts wie rückwärts bewegt — ein SQL-Cursor ist ein
|
||||
Ergebnismengen-Iterator. Die Abbildung wäre aufwendiger als der Gewinn.
|
||||
Die für Stufe 2 geplante SQL-Erweiterung bleibt davon unberührt; sie ist
|
||||
eine eigene Sprachebene und kann später ihren eigenen Speicher wählen.
|
||||
|
||||
*Was `redb` nicht liefert und wir bauen:* Sekundärindizes,
|
||||
Savepoints innerhalb einer Transaktion, die Cursor-Semantik.
|
||||
|
||||
### D2: Datenmodell — Sätze und Indizes als getrennte Tabellen
|
||||
|
||||
Je Tabelle eine Satztabelle `satz-id → Satzbytes` und je Index eine
|
||||
Tabelle `Schlüsselbytes → satz-id`. Satz-IDs sind monoton vergebene
|
||||
Zahlen und werden nie wiederverwendet.
|
||||
|
||||
*Warum getrennt:* `SETINDEX` wechselt die Durchlaufordnung zur Laufzeit;
|
||||
mit Indextabellen ist das ein Wechsel der zu lesenden Tabelle, ohne die
|
||||
Sätze anzufassen. `MOVEPREVIOUS` und die `SEEK`-Familie sind dann
|
||||
Bereichsabfragen auf einer geordneten Tabelle.
|
||||
|
||||
*Warum IDs nicht wiederverwendet werden:* Der Cursor merkt sich eine
|
||||
Satz-ID. Würde sie nach `DELETE` neu vergeben, könnte ein Cursor still
|
||||
auf einen fremden Satz zeigen.
|
||||
|
||||
### D3: Schlüsselkodierung muss ordnungserhaltend sein
|
||||
|
||||
Indexschlüssel werden so kodiert, dass die Byte-Reihenfolge der Kodierung
|
||||
der fachlichen Reihenfolge entspricht — Zahlen mit vorzeichenrichtiger
|
||||
Kodierung fester Breite, Texte nach der in D4 festgelegten Ordnung,
|
||||
mehrspaltige Schlüssel als Verkettung mit eindeutigem Trenner.
|
||||
Absteigende Spalten werden durch Invertierung der Bytes abgebildet.
|
||||
|
||||
*Warum das die kritische Stelle ist:* `redb` ordnet nach Bytes. Jede
|
||||
Ordnungsverletzung in der Kodierung äußert sich als falsche
|
||||
Satzreihenfolge — ein Fehler, der erst bei bestimmten Datenwerten
|
||||
sichtbar wird. Ein Eigenschaftstest über zufällige Wertepaare
|
||||
(Kodierungsvergleich = fachlicher Vergleich) gehört deshalb zur
|
||||
Umsetzung.
|
||||
|
||||
*Mehrspaltige Präfixsuche* fällt aus dieser Kodierung ab: Suche mit
|
||||
weniger Werten als Spalten ist eine Bereichsabfrage über das
|
||||
Schlüsselpräfix.
|
||||
|
||||
### D4: Sortierordnung explizit festlegen
|
||||
|
||||
Texte werden nach Unicode-Codepoints geordnet, Groß- und Kleinschreibung
|
||||
unterschieden. Es ist dieselbe Ordnung, die der Vergleichsoperator des
|
||||
Dialekts auf Strings anwendet.
|
||||
|
||||
`TEXTCOMP` gibt es in der Original-Hilfe nicht (Befund aus Aufgabe 1.1,
|
||||
2026-09-03); die Ordnung braucht deshalb keine eigene Vergleichsfunktion,
|
||||
sondern muss nur mit dem Stringvergleich der Sprache übereinstimmen.
|
||||
|
||||
*Warum nicht sprachabhängige Sortierung:* Sie machte die Reihenfolge
|
||||
plattform- und locale-abhängig und damit die Korpustests unreproduzierbar.
|
||||
Das Vorbild ordnete nach Zeichencodes einer festen Codepage; Codepoints
|
||||
sind die direkte Entsprechung unter der bereits getroffenen
|
||||
Unicode-Entscheidung. Die Ordnung wird in docs/sprachreferenz.md
|
||||
festgehalten, weil sie beobachtbar ist.
|
||||
|
||||
### D5: Cursor als Wert, nicht als Bibliotheks-Iterator
|
||||
|
||||
Der Cursor je Dateinummer ist ein eigener Zustand: aktiver Index,
|
||||
zuletzt besuchte Satz-ID und ein Kennzeichen „unpositioniert". Jede
|
||||
Bewegung ist eine frische Bereichsabfrage ab dem gemerkten Schlüssel.
|
||||
|
||||
*Warum keine gehaltenen Iteratoren:* Der ISAM-Cursor überlebt
|
||||
Satzänderungen und Transaktionsgrenzen; ein gehaltener Iterator über eine
|
||||
Transaktion tut das nicht. Ein Cursor als Wert bildet außerdem
|
||||
„unpositioniert" (Fehler 85) direkt ab, statt es aus einem
|
||||
Iteratorzustand ableiten zu müssen.
|
||||
|
||||
*Preis:* Jede Bewegung kostet eine Suche statt eines Schritts. Bei den
|
||||
Datenmengen dieser Programme irrelevant; siehe Non-Goals.
|
||||
|
||||
### D6: Savepoints als Rücknahmeprotokoll
|
||||
|
||||
`BEGINTRANS` öffnet eine Schreibtransaktion. Innerhalb davon führt die
|
||||
Laufzeit ein Protokoll der Änderungen; die Funktion `SAVEPOINT` merkt
|
||||
sich eine Position darin und liefert deren Kennung, `ROLLBACK` mit dieser
|
||||
Kennung spielt das Protokoll bis dorthin rückwärts ab. `ROLLBACK ALL` und
|
||||
`COMMITTRANS` nutzen die Transaktion der Bibliothek direkt.
|
||||
|
||||
*Warum ein eigenes Protokoll:* Verschachtelte Rücknahmepunkte gehören
|
||||
nicht zum Funktionsumfang von `redb`. Das Protokoll ist der kleinste
|
||||
Weg dorthin und bleibt auf den Umfang einer laufenden Transaktion
|
||||
begrenzt.
|
||||
|
||||
### D7: Fehler 89 und `SETMEM` bekommen eine echte Grenze
|
||||
|
||||
`SETMEM` ist eine Funktion: sie ändert die Obergrenze des ISAM-Puffers um
|
||||
den übergebenen Betrag und liefert die danach verfügbare Größe.
|
||||
Überschreiten der Grenze löst Fehler 89 aus. Ohne Grenze wäre der Code nie
|
||||
erreichbar und die Anweisung eine Attrappe.
|
||||
|
||||
*Abweichung:* Die Bedeutung in Bytes entspricht nicht dem
|
||||
DOS-Speichermodell des Vorbilds und wird in docs/sprachreferenz.md
|
||||
dokumentiert.
|
||||
|
||||
### D8: Reihenfolge der Umsetzung
|
||||
|
||||
```
|
||||
1. Frontend: Grammatik + Signaturen -> Elemente werden benannt
|
||||
2. Speicher: Datei, Satztabelle, Schluesselkodierung (+ Eigenschaftstest)
|
||||
3. Indizes: CREATEINDEX/SETINDEX/GETINDEX$/DELETEINDEX
|
||||
4. Cursor: MOVE-Familie, SEEK-Familie, EOF
|
||||
5. Saetze: INSERT/RETRIEVE/UPDATE/DELETE, Eindeutigkeit
|
||||
6. Transaktionen: BEGINTRANS/COMMITTRANS/ROLLBACK[ ALL]/SAVEPOINT
|
||||
7. Rest: SETMEM, DELETETABLE, BOF, Formatversion
|
||||
```
|
||||
|
||||
Schritt 1 zuerst, weil er allein schon den Guiding-Principle-Verstoß
|
||||
behebt: ISAM-Programme werden ab da namentlich abgewiesen statt mit
|
||||
„unbekannter Bezeichner". Schritt 2 vor allem anderen, weil die
|
||||
Schlüsselkodierung jede spätere Ordnung bestimmt.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Fehlerhafte Schlüsselkodierung fällt erst bei bestimmten Daten auf**
|
||||
→ Eigenschaftstest über zufällige Wertepaare je Spaltentyp:
|
||||
Byte-Vergleich der Kodierung MUSS dem fachlichen Vergleich entsprechen.
|
||||
Läuft vor allem Weiteren.
|
||||
- **Der genaue ISAM-Sprachumfang stammt aus der Original-Hilfe und ist
|
||||
bislang nur aus zweiter Hand belegt** → Das Inventar aus
|
||||
`phase-3-runtime-bildschirm` ist die maßgebliche Liste; findet die
|
||||
Erhebung dort weitere Elemente, gehören sie in diesen Change. Die
|
||||
Aufzählung im Proposal ist ein Mindestumfang, keine Obergrenze.
|
||||
- **Cursorverhalten an den Rändern ist die häufigste Fehlerquelle**
|
||||
(`EOF` nach `MOVENEXT` am Ende, Cursor nach `DELETE`, nach `SETINDEX`,
|
||||
nach `ROLLBACK`) → je ein Korpustest pro Randfall, nicht ein
|
||||
Sammeltest.
|
||||
- **Neue Abhängigkeit in `tb-runtime`, die bisher keine hatte** →
|
||||
bewusst; die Alternative wäre ein eigener transaktionaler B-Baum.
|
||||
Die Abhängigkeit bleibt auf das ISAM-Modul beschränkt.
|
||||
- **Das Rücknahmeprotokoll kann bei langen Transaktionen wachsen** →
|
||||
begrenzt auf die laufende Transaktion; `COMMITTRANS` beendet sie. Falls
|
||||
eine Grenze nötig wird, ist Fehler 89 der dafür vorgesehene Ausgang.
|
||||
- **Eigenes Dateiformat heißt: keine Vorbild-Datenbanken lesbar** →
|
||||
bewusst entschieden, dieselbe Linie wie UTF-32-Records; steht in
|
||||
docs/dateiformate.md und in der Sprachreferenz.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Rein additiv — es gibt heute keine ISAM-Funktionalität und keine
|
||||
Datenbankdateien im Feld. Die einzige sichtbare Änderung an bestehendem
|
||||
Verhalten: `OPEN … FOR ISAM` löst nicht mehr Laufzeitfehler 73 aus,
|
||||
sondern arbeitet.
|
||||
|
||||
Die Datenbankdatei trägt von der ersten Fassung an eine Formatversion,
|
||||
damit spätere Änderungen abgewiesen statt fehlinterpretiert werden.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Genaue Argumentformen einzelner ISAM-Anweisungen (etwa die Schreibweise
|
||||
der Spaltenliste bei `CREATEINDEX`): bei der Umsetzung aus der
|
||||
Original-Hilfe zu übernehmen. Berührt weder Datenmodell noch
|
||||
Aufgabenschnitt.
|
||||
- Konkreter Vorgabewert der `SETMEM`-Grenze: bei der Umsetzung zu wählen
|
||||
und zu dokumentieren.
|
||||
104
openspec/changes/phase-3-isam/proposal.md
Normal file
104
openspec/changes/phase-3-isam/proposal.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Phase 3 (Abschluss) — ISAM-Datenbankunterstützung
|
||||
|
||||
## Why
|
||||
|
||||
Die Leitplanke „Vollständigkeit ist das Soll" und die Entscheidung vom
|
||||
2026-09-02 legen fest: ISAM wird implementiert, es ist kein Non-Feature.
|
||||
Heute existiert davon nur eine Parser-Klausel — `OPEN … FOR ISAM typname
|
||||
tabellenname AS #n` wird erkannt und auf den „nicht verfügbar"-Marker
|
||||
abgesenkt. Die eigentlichen ISAM-Anweisungen und -Funktionen
|
||||
(`CREATEINDEX`, `SETINDEX`, `INSERT`, `RETRIEVE`, `UPDATE`, `DELETE`, die
|
||||
`MOVE`- und `SEEK`-Familien, die Transaktionsanweisungen) besitzen im
|
||||
Frontend **keine Signatur**. Sie enden damit in „unbekannter Bezeichner"
|
||||
bzw. einem generischen Syntaxfehler — genau der Fall, den der Guiding
|
||||
Principle als Bug definiert: ein dokumentiertes Feature wird still und
|
||||
ohne Namensnennung abgewiesen.
|
||||
|
||||
Die Laufzeitfehler 81–89 des ISAM-Bereichs sind bereits als
|
||||
`tb_runtime::errors` implementiert und warten auf ihre Auslöser.
|
||||
|
||||
Dieser Change schließt Phase 3 ab. Der vorausgehende Change
|
||||
`phase-3-runtime-bildschirm` liefert die Datei-E/A-Grundlage
|
||||
(Dateitabelle, Recordpuffer, UTF-32-Festtextfelder), auf der ISAM
|
||||
aufsetzt, sowie das Inventar, in dem die ISAM-Elemente ihren Status
|
||||
führen.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **ISAM-Elemente erhalten Frontend-Signaturen**: Sämtliche in der
|
||||
Original-Hilfe dokumentierten ISAM-Anweisungen und -Funktionen werden
|
||||
in Grammatik und Signaturprüfung aufgenommen, damit ihre Verwendung
|
||||
namentlich diagnostiziert statt als unbekannter Bezeichner behandelt
|
||||
wird. Den maßgeblichen Umfang bestimmt das Sprachinventar aus
|
||||
`phase-3-runtime-bildschirm`; nach heutigem Stand umfasst er
|
||||
mindestens `CREATEINDEX`, `DELETEINDEX`, `SETINDEX`, `GETINDEX$`,
|
||||
`INSERT`, `RETRIEVE`, `UPDATE`, `DELETE`, `DELETETABLE`, `MOVEFIRST`,
|
||||
`MOVELAST`, `MOVENEXT`, `MOVEPREVIOUS`, `SEEKEQ`, `SEEKGT`, `SEEKGE`,
|
||||
`BEGINTRANS`, `COMMITTRANS`, `ROLLBACK`/`ROLLBACK ALL`, `SAVEPOINT`
|
||||
(Funktion), `SETMEM` (Funktion), `BOF` sowie das ISAM-Verhalten von
|
||||
`EOF`, `DELETE`, `LOF` und `LOC`.
|
||||
- **ISAM-Datenbanken und Tabellen**: `OPEN … FOR ISAM typname
|
||||
tabellenname AS #n` bindet eine Tabelle einer Datenbankdatei an eine
|
||||
Dateinummer; das Satzlayout ergibt sich aus dem benannten
|
||||
benutzerdefinierten Typ. Tabellen entstehen beim ersten Öffnen,
|
||||
`DELETETABLE` entfernt sie.
|
||||
- **Indizes**: benannte Indizes über eine oder mehrere Spalten, eindeutig
|
||||
oder mehrdeutig, mit auf- und absteigender Ordnung; `SETINDEX` wählt
|
||||
den aktiven Index und bestimmt damit die Satzreihenfolge, `GETINDEX$`
|
||||
liefert ihn zurück.
|
||||
- **Satzoperationen und Cursor**: `INSERT`, `RETRIEVE`, `UPDATE`,
|
||||
`DELETE` arbeiten auf dem aktuellen Satz; die `MOVE`-Familie bewegt den
|
||||
Cursor in der Ordnung des aktiven Index, die `SEEK`-Familie positioniert
|
||||
über Schlüsselvergleiche (gleich, größer, größer-gleich). `EOF` und
|
||||
`BOF` melden das Überlaufen des Cursors am Ende bzw. am Anfang.
|
||||
- **Transaktionen**: `BEGINTRANS`, `COMMITTRANS`, `ROLLBACK` und
|
||||
`ROLLBACK ALL` mit Rücknahme aller Änderungen seit Transaktionsbeginn
|
||||
bzw. seit einem Sicherungspunkt; `SAVEPOINT` ist eine **Funktion** und
|
||||
liefert die Kennung des gesetzten Sicherungspunkts.
|
||||
- **Vergleichsreihenfolge**: Die Sortierordnung der Indizes wird unter
|
||||
Unicode festgelegt und dokumentiert.
|
||||
- **ISAM-Fehlersemantik**: Die vorhandenen Fehlercodes 81–89 werden von
|
||||
den entsprechenden Situationen ausgelöst (ungültiger Name, Tabelle
|
||||
nicht gefunden, Index nicht gefunden, ungültige Spalte, kein aktueller
|
||||
Satz, Verstoß gegen einen eindeutigen Index, ungültige Operation auf
|
||||
NULL-Index, inkonsistente Datenbank, zu wenig Puffer).
|
||||
- **Eigenes Datenbankdateiformat**: Die Datenbankdatei ist bewusst nicht
|
||||
binärkompatibel zum Vorbild — dieselbe Linie wie bei den UTF-32-
|
||||
Records. Das Format wird in `docs/dateiformate.md` beschrieben.
|
||||
|
||||
**Non-Goals:** Ein Konvertierungswerkzeug für Datenbankdateien des
|
||||
Vorbilds; die eingebettete SQL-Erweiterung samt `WHILE SELECT`
|
||||
(Stufe 2, Ideenspeicher); Mehrprozess-Zugriff über
|
||||
Betriebssystemgrenzen hinweg jenseits der Satzsperren aus `datei-eio`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `isam-datenbank`: indizierter Satzzugriff — Datenbank- und
|
||||
Tabellenbindung an Dateinummern, Indexverwaltung, Cursorbewegung und
|
||||
Schlüsselsuche, Satzoperationen, Transaktionen, Vergleichsreihenfolge
|
||||
und die ISAM-Fehlersemantik (Codes 81–89).
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `sprach-frontend`: Die Grammatik und die Signaturprüfung SHALL die
|
||||
ISAM-Anweisungen und -Funktionen umfassen, sodass ihre Verwendung
|
||||
namentlich diagnostiziert wird statt als unbekannter Bezeichner.
|
||||
|
||||
## Impact
|
||||
|
||||
- `crates/tb-frontend`: neue Anweisungsgrammatik und Signaturen für die
|
||||
ISAM-Elemente; die bisherige `Unsupported`-Absenkung von `OPEN … FOR
|
||||
ISAM` entfällt.
|
||||
- `crates/tb-runtime`: neues ISAM-Modul (Speicher, Indizes, Cursor,
|
||||
Transaktionen) auf der Dateitabelle und den Recordpuffern aus
|
||||
`phase-3-runtime-bildschirm`.
|
||||
- `crates/tb-vm`: neue Builtin-Indizes für die ISAM-Anweisungen.
|
||||
- `tests/compat`: neue Korpusprogramme für Indexordnung, Schlüsselsuche,
|
||||
Eindeutigkeitsverletzung, Cursorenden und Transaktionsrücknahme.
|
||||
- `docs/`: `docs/dateiformate.md` um das Datenbankdateiformat erweitert;
|
||||
`docs/bibliothek.md` um die ISAM-Elemente; `docs/inventar.md` wechselt
|
||||
deren Status auf `implementiert`; `docs/sprachreferenz.md` hält die
|
||||
Sortierordnung und die Inkompatibilität des Dateiformats fest.
|
||||
- PLAN.md: Phase 3 gilt mit diesem Change als abgeschlossen.
|
||||
187
openspec/changes/phase-3-isam/specs/isam-datenbank/spec.md
Normal file
187
openspec/changes/phase-3-isam/specs/isam-datenbank/spec.md
Normal file
@@ -0,0 +1,187 @@
|
||||
## Purpose
|
||||
Die ISAM-Unterstützung bietet indizierten Satzzugriff auf Tabellen einer
|
||||
Datenbankdatei: Sätze werden über benannte Indizes geordnet durchlaufen
|
||||
und über Schlüsselvergleiche gesucht, Änderungen laufen in Transaktionen
|
||||
und Verstöße melden die ISAM-Fehlercodes des Vorbilds.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Datenbank- und Tabellenbindung an eine Dateinummer
|
||||
`OPEN datenbank$ FOR ISAM typname tabellenname AS #n` SHALL eine Tabelle
|
||||
einer Datenbankdatei an eine Dateinummer binden. Das Satzlayout SHALL
|
||||
sich aus dem benannten benutzerdefinierten Typ ergeben; dessen Felder
|
||||
sind die Spalten der Tabelle. Existiert die Tabelle noch nicht, SHALL sie
|
||||
mit diesem Layout angelegt werden; existiert sie mit abweichendem Layout,
|
||||
MUST Laufzeitfehler 88 „ISAM - Database inconsistent" auftreten. Ein
|
||||
ungültiger Tabellen- oder Datenbankname MUST Fehler 81 „ISAM - Invalid
|
||||
name" auslösen. `CLOSE` SHALL die Bindung lösen und ausstehende
|
||||
Änderungen festschreiben. `DELETETABLE` SHALL eine Tabelle samt ihren
|
||||
Indizes entfernen; eine nicht vorhandene Tabelle MUST Fehler 82 „ISAM -
|
||||
Table not found" auslösen.
|
||||
|
||||
#### Scenario: Tabelle wird beim ersten Öffnen angelegt
|
||||
- **WHEN** eine Datenbank ohne die Tabelle `Kunden` mit `OPEN "db" FOR ISAM KundeTyp "Kunden" AS #1` geöffnet wird
|
||||
- **THEN** entsteht die Tabelle mit den Feldern des Typs und die Dateinummer ist nutzbar
|
||||
|
||||
#### Scenario: Abweichendes Satzlayout
|
||||
- **WHEN** eine bestehende Tabelle mit einem Typ geöffnet wird, dessen Felder nicht zu ihren Spalten passen
|
||||
- **THEN** tritt Laufzeitfehler 88 auf
|
||||
|
||||
#### Scenario: Entfernte Tabelle
|
||||
- **WHEN** `DELETETABLE` für eine nicht vorhandene Tabelle aufgerufen wird
|
||||
- **THEN** tritt Laufzeitfehler 82 auf
|
||||
|
||||
### Requirement: Indexverwaltung
|
||||
`CREATEINDEX` SHALL einen benannten Index über eine oder mehrere Spalten
|
||||
anlegen, wahlweise eindeutig oder mehrdeutig und je Spalte auf- oder
|
||||
absteigend geordnet. `DELETEINDEX` SHALL einen Index entfernen.
|
||||
`SETINDEX` SHALL den aktiven Index einer Dateinummer setzen und damit die
|
||||
Reihenfolge bestimmen, in der Sätze durchlaufen werden; `GETINDEX$` SHALL
|
||||
den Namen des aktiven Index liefern. Ein unbekannter Indexname MUST
|
||||
Laufzeitfehler 83 „ISAM - Index not found" auslösen, ein unbekannter
|
||||
Spaltenname Fehler 84 „ISAM - Invalid column". Nach `SETINDEX` MUST der
|
||||
Cursor als unpositioniert gelten.
|
||||
|
||||
#### Scenario: Aktiver Index bestimmt die Reihenfolge
|
||||
- **WHEN** ein Index über die Spalte `Name` gesetzt ist und die Sätze mit `MOVEFIRST` und `MOVENEXT` durchlaufen werden
|
||||
- **THEN** erscheinen sie in aufsteigender Reihenfolge der Spalte `Name`
|
||||
|
||||
#### Scenario: Unbekannter Index
|
||||
- **WHEN** `SETINDEX #1, "GibtsNicht"` ausgeführt wird
|
||||
- **THEN** tritt Laufzeitfehler 83 auf
|
||||
|
||||
#### Scenario: Index über unbekannte Spalte
|
||||
- **WHEN** `CREATEINDEX` eine Spalte nennt, die der Satztyp nicht führt
|
||||
- **THEN** tritt Laufzeitfehler 84 auf
|
||||
|
||||
### Requirement: Cursorbewegung in der Ordnung des aktiven Index
|
||||
`MOVEFIRST`, `MOVELAST`, `MOVENEXT` und `MOVEPREVIOUS` SHALL den Cursor
|
||||
in der Ordnung des aktiven Index bewegen. Läuft der Cursor über das Ende
|
||||
hinaus, SHALL `EOF` für diese Dateinummer wahr werden; läuft er über den
|
||||
Anfang hinaus, SHALL `BOF` wahr werden. In beiden Fällen SHALL der Cursor
|
||||
als unpositioniert gelten. Eine Bewegung ohne aktiven Index SHALL der
|
||||
Einfügereihenfolge folgen.
|
||||
|
||||
#### Scenario: Cursor läuft über das Ende hinaus
|
||||
- **WHEN** nach dem letzten Satz `MOVENEXT` ausgeführt wird
|
||||
- **THEN** liefert `EOF` für diese Dateinummer wahr
|
||||
|
||||
#### Scenario: Rückwärts über den Anfang hinaus
|
||||
- **WHEN** auf dem ersten Satz `MOVEPREVIOUS` ausgeführt wird
|
||||
- **THEN** liefert `BOF` für diese Dateinummer wahr
|
||||
|
||||
### Requirement: Schlüsselsuche
|
||||
`SEEKEQ`, `SEEKGT` und `SEEKGE` SHALL den Cursor auf den ersten Satz
|
||||
setzen, dessen Schlüssel im aktiven Index gleich, größer bzw.
|
||||
größer-gleich dem angegebenen Wert ist. Die Zahl der Schlüsselwerte MUST
|
||||
höchstens der Spaltenzahl des aktiven Index entsprechen; ein
|
||||
mehrspaltiger Index SHALL mit einem Präfix der Spalten durchsucht werden
|
||||
können. Findet die Suche keinen Satz, SHALL `EOF` wahr werden und der
|
||||
Cursor unpositioniert bleiben. Eine Suche ohne aktiven Index MUST
|
||||
Laufzeitfehler 83 auslösen.
|
||||
|
||||
#### Scenario: Suche nach größerem Schlüssel
|
||||
- **WHEN** Sätze mit den Schlüsseln 10, 20 und 30 vorliegen und `SEEKGT #1, 20` ausgeführt wird
|
||||
- **THEN** steht der Cursor auf dem Satz mit Schlüssel 30
|
||||
|
||||
#### Scenario: Erfolglose Suche
|
||||
- **WHEN** `SEEKGT #1, 30` bei größtem Schlüssel 30 ausgeführt wird
|
||||
- **THEN** liefert `EOF` wahr
|
||||
|
||||
#### Scenario: Suche über Präfix eines mehrspaltigen Index
|
||||
- **WHEN** der aktive Index über `Nachname` und `Vorname` geht und mit nur einem Wert gesucht wird
|
||||
- **THEN** positioniert die Suche auf dem ersten Satz mit passendem `Nachname`
|
||||
|
||||
### Requirement: Satzoperationen auf dem aktuellen Satz
|
||||
`INSERT` SHALL einen Satz aus einer Variablen des Tabellentyps einfügen,
|
||||
`RETRIEVE` den aktuellen Satz in eine solche Variable lesen, `UPDATE` ihn
|
||||
aus einer solchen Variablen überschreiben und `DELETE` ihn entfernen.
|
||||
`RETRIEVE`, `UPDATE` und `DELETE` ohne positionierten Cursor MUST
|
||||
Laufzeitfehler 85 „ISAM - No current record" auslösen. Ein `INSERT` oder
|
||||
`UPDATE`, das einen als eindeutig angelegten Index verletzen würde, MUST
|
||||
Laufzeitfehler 86 „ISAM - Duplicate value for unique index" auslösen und
|
||||
den Satzbestand unverändert lassen. Nach `DELETE` SHALL der Cursor als
|
||||
unpositioniert gelten. Alle Indizes der Tabelle SHALL nach jeder
|
||||
Satzoperation die neue Ordnung widerspiegeln.
|
||||
|
||||
#### Scenario: Einfügen und Zurücklesen
|
||||
- **WHEN** ein Satz eingefügt, per `SEEKEQ` gesucht und mit `RETRIEVE` gelesen wird
|
||||
- **THEN** stimmen alle Feldwerte mit den eingefügten überein
|
||||
|
||||
#### Scenario: Operation ohne aktuellen Satz
|
||||
- **WHEN** nach `SETINDEX` ohne Positionierung `RETRIEVE #1, k` ausgeführt wird
|
||||
- **THEN** tritt Laufzeitfehler 85 auf
|
||||
|
||||
#### Scenario: Verstoß gegen eindeutigen Index
|
||||
- **WHEN** ein Satz eingefügt wird, dessen Schlüssel in einem eindeutigen Index bereits vorkommt
|
||||
- **THEN** tritt Laufzeitfehler 86 auf und der Satzbestand bleibt unverändert
|
||||
|
||||
#### Scenario: Index folgt der Änderung
|
||||
- **WHEN** die indizierte Spalte eines Satzes per `UPDATE` geändert wird
|
||||
- **THEN** erscheint der Satz beim nächsten Durchlauf an seiner neuen Position in der Indexordnung
|
||||
|
||||
### Requirement: Transaktionen mit Rücknahme
|
||||
`BEGINTRANS` SHALL eine Transaktion eröffnen, `COMMITTRANS` alle
|
||||
Änderungen seit ihrem Beginn festschreiben und `ROLLBACK` sie
|
||||
zurücknehmen. `SAVEPOINT` SHALL als **Funktion** einen Sicherungspunkt
|
||||
setzen und dessen Kennung liefern; `ROLLBACK` mit einer solchen Kennung
|
||||
SHALL auf diesen Punkt zurücksetzen, ohne die Transaktion aufzugeben.
|
||||
`ROLLBACK ALL` SHALL alle Änderungen seit `BEGINTRANS` zurücknehmen und
|
||||
die Transaktion beenden. Nach jeder Rücknahme MUST der Satzbestand
|
||||
einschließlich aller Indizes dem Zustand zum Bezugspunkt entsprechen und
|
||||
der Cursor als unpositioniert gelten.
|
||||
|
||||
#### Scenario: Rücknahme stellt den Bestand wieder her
|
||||
- **WHEN** nach `BEGINTRANS` Sätze eingefügt und gelöscht werden und dann `ROLLBACK` folgt
|
||||
- **THEN** entspricht der Bestand beim erneuten Durchlauf dem Zustand vor `BEGINTRANS`
|
||||
|
||||
#### Scenario: Rücknahme auf einen Sicherungspunkt
|
||||
- **WHEN** innerhalb einer Transaktion `s = SAVEPOINT` ausgewertet, danach geändert und mit `ROLLBACK s` zurückgesetzt wird
|
||||
- **THEN** bleiben die Änderungen vor dem Sicherungspunkt erhalten und die danach sind zurückgenommen
|
||||
|
||||
#### Scenario: ROLLBACK ALL beendet die Transaktion
|
||||
- **WHEN** nach mehreren Sicherungspunkten `ROLLBACK ALL` ausgeführt wird
|
||||
- **THEN** entspricht der Bestand dem Zustand vor `BEGINTRANS` und die Transaktion ist beendet
|
||||
|
||||
### Requirement: Vergleichsreihenfolge für Indizes
|
||||
Textspalten SHALL in Indizes nach einer festgelegten, dokumentierten
|
||||
Sortierordnung geordnet werden. Diese Ordnung MUST dieselbe sein, die der
|
||||
Vergleichsoperator des Dialekts auf Strings anwendet, damit ein Programm
|
||||
die Satzreihenfolge vorhersagen kann. Die gewählte Ordnung MUST in
|
||||
docs/sprachreferenz.md festgehalten sein, einschließlich der Behandlung
|
||||
von Groß- und Kleinschreibung und von Zeichen jenseits von ASCII.
|
||||
|
||||
#### Scenario: Indexordnung entspricht dem Stringvergleich
|
||||
- **WHEN** zwei Texte als Schlüssel indiziert und dieselben Werte mit `<` verglichen werden
|
||||
- **THEN** entspricht ihre Reihenfolge im Index dem Ergebnis des Vergleichs
|
||||
|
||||
### Requirement: Pufferverwaltung mit benanntem Fehler
|
||||
`SETMEM` SHALL als **Funktion** die für ISAM verfügbare Puffergröße um
|
||||
den übergebenen Betrag ändern und die danach verfügbare Größe liefern.
|
||||
Reicht
|
||||
der verfügbare Puffer für eine Operation nicht aus, MUST Laufzeitfehler 89
|
||||
„ISAM - Insufficient ISAM buffers" auftreten statt eines generischen
|
||||
Fehlers oder eines stillen Abbruchs. Weicht die Bedeutung von `SETMEM`
|
||||
mangels DOS-Speichermodell vom Vorbild ab, MUST die Abweichung in
|
||||
docs/sprachreferenz.md dokumentiert sein.
|
||||
|
||||
#### Scenario: Pufferfehler wird benannt
|
||||
- **WHEN** eine Operation den verfügbaren ISAM-Puffer überschreitet
|
||||
- **THEN** tritt Laufzeitfehler 89 auf
|
||||
|
||||
### Requirement: Eigenes Datenbankdateiformat
|
||||
Die Datenbankdatei SHALL ein eigenes, in docs/dateiformate.md
|
||||
beschriebenes Format verwenden und ist bewusst nicht binärkompatibel zu
|
||||
Datenbankdateien des Vorbilds — dieselbe Linie wie bei den
|
||||
UTF-32-Festtextfeldern. Das Format SHALL eine Formatversion tragen; eine
|
||||
unbekannte Version MUST mit einem benannten Fehler abgewiesen werden
|
||||
statt fehlinterpretiert zu werden. Eine strukturell beschädigte Datei
|
||||
MUST Laufzeitfehler 88 auslösen.
|
||||
|
||||
#### Scenario: Unbekannte Formatversion
|
||||
- **WHEN** eine Datenbankdatei mit einer höheren Formatversion geöffnet wird
|
||||
- **THEN** wird sie mit einem benannten Fehler abgewiesen und nicht gelesen
|
||||
|
||||
#### Scenario: Beschädigte Datei
|
||||
- **WHEN** eine strukturell beschädigte Datenbankdatei geöffnet wird
|
||||
- **THEN** tritt Laufzeitfehler 88 auf
|
||||
27
openspec/changes/phase-3-isam/specs/sprach-frontend/spec.md
Normal file
27
openspec/changes/phase-3-isam/specs/sprach-frontend/spec.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ISAM-Anweisungen und -Funktionen in Grammatik und Signaturprüfung
|
||||
Der Parser SHALL die ISAM-Anweisungen und -Funktionen des Vorbilds als
|
||||
eigene Konstrukte erkennen, und die Semantik SHALL für jede eine Signatur
|
||||
führen (Argumentanzahl und -typen). Der maßgebliche Umfang ergibt sich
|
||||
aus dem Sprachinventar; er umfasst mindestens `CREATEINDEX`,
|
||||
`DELETEINDEX`, `SETINDEX`, `GETINDEX$`, `INSERT`, `RETRIEVE`, `UPDATE`,
|
||||
`DELETE`, `DELETETABLE`, `MOVEFIRST`, `MOVELAST`, `MOVENEXT`,
|
||||
`MOVEPREVIOUS`, `SEEKEQ`, `SEEKGT`, `SEEKGE`, `BEGINTRANS`,
|
||||
`COMMITTRANS`, `ROLLBACK`, `ROLLBACK ALL`, `SAVEPOINT`, `SETMEM` und
|
||||
`BOF`. Eine fehlerhafte Verwendung MUST eine Diagnose erzeugen, die
|
||||
das Element namentlich nennt; ein ISAM-Element MUST NOT als unbekannter
|
||||
Bezeichner oder mit einem generischen Syntaxfehler abgewiesen werden
|
||||
(Guiding Principle).
|
||||
|
||||
#### Scenario: ISAM-Anweisung wird als solche erkannt
|
||||
- **WHEN** ein Modul `SETINDEX #1, "NachName"` enthält
|
||||
- **THEN** entsteht keine Diagnose „unbekannter Bezeichner" und die Anweisung wird als ISAM-Anweisung erfasst
|
||||
|
||||
#### Scenario: Falsche Argumentanzahl wird namentlich gemeldet
|
||||
- **WHEN** ein Modul `SEEKGT #1` ohne Schlüsselwert enthält
|
||||
- **THEN** nennt die Diagnose `SEEKGT` und die erwartete Argumentanzahl
|
||||
|
||||
#### Scenario: Typprüfung des Satzarguments
|
||||
- **WHEN** `RETRIEVE #1, x$` mit einem String statt einer Variablen des Tabellentyps geprüft wird
|
||||
- **THEN** wird „Type mismatch" mit Quelltextposition gemeldet
|
||||
78
openspec/changes/phase-3-isam/tasks.md
Normal file
78
openspec/changes/phase-3-isam/tasks.md
Normal file
@@ -0,0 +1,78 @@
|
||||
## 1. Umfang festlegen
|
||||
|
||||
- [ ] 1.1 ISAM-Abschnitt der Original-Hilfe auswerten und alle Anweisungen, Funktionen und Klauseln gegen die ISAM-Einträge in `docs/inventar.md` abgleichen; verifiziert dadurch, dass jedes gefundene Element im Inventar mit Status `offen` steht und im Change notiert ist, falls es über die Aufzählung im Proposal hinausgeht
|
||||
- [ ] 1.2 Argumentformen je Element aus der Original-Hilfe festhalten (insbesondere die Schreibweise der Spaltenliste bei `CREATEINDEX`); verifiziert durch die Notiz im Change, die jede Signatur mit ihrer Quelle nennt
|
||||
|
||||
## 2. Frontend: Grammatik und Signaturen
|
||||
|
||||
- [ ] 2.1 ISAM-Anweisungen in die Parser-Grammatik aufnehmen; verifiziert durch Frontend-Test, dass `SETINDEX #1, "Name"` diagnose-frei parst
|
||||
- [ ] 2.2 Signaturen (Argumentanzahl und -typen) für alle ISAM-Elemente in der Semantik führen; verifiziert durch Frontend-Test, dass `SEEKGT #1` ohne Schlüsselwert eine Diagnose erzeugt, die `SEEKGT` namentlich nennt
|
||||
- [ ] 2.3 Satzargumente gegen den Tabellentyp prüfen; verifiziert durch Frontend-Test, dass `RETRIEVE #1, x$` „Type mismatch" meldet
|
||||
- [ ] 2.4 `Unsupported`-Absenkung von `OPEN … FOR ISAM` entfernen und durch die reguläre Absenkung ersetzen; verifiziert dadurch, dass ein ISAM-Programm nicht mehr mit Laufzeitfehler 73 endet
|
||||
- [ ] 2.5 Prüfen, dass kein ISAM-Element mehr als unbekannter Bezeichner behandelt wird; verifiziert durch Frontend-Test über ein Programm, das jedes Element einmal verwendet, ohne Diagnose „unbekannter Bezeichner"
|
||||
|
||||
## 3. Speicherschicht
|
||||
|
||||
- [ ] 3.1 `redb` als Abhängigkeit aufnehmen, auf das ISAM-Modul in `tb-runtime` beschränkt; verifiziert dadurch, dass `cargo build -p tb-runtime` durchläuft
|
||||
- [ ] 3.2 Datenbankdatei mit Formatversion anlegen und öffnen; verifiziert durch Test, dass eine Datei mit höherer Formatversion mit benanntem Fehler abgewiesen und eine strukturell beschädigte Datei mit Laufzeitfehler 88 abgelehnt wird
|
||||
- [ ] 3.3 Satztabelle `satz-id → Satzbytes` mit monoton vergebenen, nie wiederverwendeten IDs; verifiziert durch Test, dass eine nach `DELETE` neu vergebene ID nicht vorkommt
|
||||
- [ ] 3.4 Satzserialisierung auf den Recordpuffern und der UTF-32-Festtextkodierung aus `datei-eio` aufsetzen; verifiziert durch Roundtrip-Test über alle Feldtypen des Satztyps
|
||||
- [ ] 3.5 Ordnungserhaltende Schlüsselkodierung je Spaltentyp (Zahlen vorzeichenrichtig fester Breite, Text nach Codepoints, mehrspaltig verkettet mit eindeutigem Trenner, absteigend durch Byte-Invertierung); verifiziert durch Eigenschaftstest über zufällige Wertepaare, dass der Byte-Vergleich der Kodierung dem fachlichen Vergleich entspricht
|
||||
- [ ] 3.6 Sortierordnung (Unicode-Codepoints, Groß-/Kleinschreibung unterschieden) in docs/sprachreferenz.md festhalten; verifiziert durch den Abschnittseintrag
|
||||
- [ ] 3.7 Datenbankdateiformat in docs/dateiformate.md beschreiben inklusive Formatversion und bewusster Inkompatibilität zum Vorbild; verifiziert durch den Abschnittseintrag
|
||||
|
||||
## 4. Tabellen
|
||||
|
||||
- [ ] 4.1 `OPEN … FOR ISAM typname tabellenname AS #n` an die Dateitabelle binden, Satzlayout aus dem benannten Typ ableiten; verifiziert durch Korpustest, dass die Dateinummer danach nutzbar ist
|
||||
- [ ] 4.2 Tabelle beim ersten Öffnen anlegen; verifiziert durch Korpustest gegen eine Datenbank ohne diese Tabelle
|
||||
- [ ] 4.3 Layoutabgleich bei bestehender Tabelle mit Fehler 88 bei Abweichung; verifiziert durch Korpustest mit unpassendem Typ
|
||||
- [ ] 4.4 Ungültige Datenbank- und Tabellennamen mit Fehler 81 abweisen; verifiziert durch Korpustest
|
||||
- [ ] 4.5 `CLOSE` löst die Bindung und schreibt ausstehende Änderungen fest; verifiziert durch Korpustest, dass die Sätze nach erneutem Öffnen vorhanden sind
|
||||
- [ ] 4.6 `DELETETABLE` entfernt Tabelle samt Indizes, Fehler 82 bei fehlender Tabelle; verifiziert durch Korpustest für beide Fälle
|
||||
|
||||
## 5. Indizes
|
||||
|
||||
- [ ] 5.1 Indextabellen `Schlüsselbytes → satz-id` je Index anlegen; verifiziert durch Test, dass die Einträge in Schlüsselreihenfolge stehen
|
||||
- [ ] 5.2 `CREATEINDEX` mit einer und mit mehreren Spalten, eindeutig und mehrdeutig, auf- und absteigend; verifiziert durch Korpustest je Variante
|
||||
- [ ] 5.3 `DELETEINDEX`; verifiziert durch Korpustest, dass der Index danach nicht mehr gesetzt werden kann
|
||||
- [ ] 5.4 `SETINDEX` setzt den aktiven Index und den Cursor auf unpositioniert, `GETINDEX$` liefert ihn zurück; verifiziert durch Korpustest
|
||||
- [ ] 5.5 Fehler 83 bei unbekanntem Index und Fehler 84 bei unbekannter Spalte; verifiziert durch Korpustest für beide Fälle
|
||||
|
||||
## 6. Cursor
|
||||
|
||||
- [ ] 6.1 Cursor je Dateinummer als Wert führen (aktiver Index, letzte Satz-ID, Kennzeichen unpositioniert); verifiziert durch Unit-Test, dass der Cursor eine Satzänderung überlebt
|
||||
- [ ] 6.2 `MOVEFIRST`, `MOVELAST`, `MOVENEXT`, `MOVEPREVIOUS` in der Ordnung des aktiven Index; verifiziert durch Korpustest, dass ein Durchlauf die Sätze in Indexreihenfolge liefert
|
||||
- [ ] 6.3 Bewegung ohne aktiven Index folgt der Einfügereihenfolge; verifiziert durch Korpustest
|
||||
- [ ] 6.4 `EOF` am Ende und `BOF` am Anfang, Cursor danach jeweils unpositioniert; verifiziert durch je einen Korpustest für `MOVENEXT` am Ende und `MOVEPREVIOUS` am Anfang
|
||||
- [ ] 6.5 `SEEKEQ`, `SEEKGT`, `SEEKGE` inklusive Präfixsuche über einen mehrspaltigen Index; verifiziert durch Korpustest mit den Schlüsseln 10, 20, 30 und einer Präfixsuche
|
||||
- [ ] 6.6 Erfolglose Suche setzt `EOF` und lässt den Cursor unpositioniert; verifiziert durch Korpustest
|
||||
- [ ] 6.7 Suche ohne aktiven Index löst Fehler 83 aus; verifiziert durch Korpustest
|
||||
|
||||
## 7. Satzoperationen
|
||||
|
||||
- [ ] 7.1 `INSERT` und `RETRIEVE`; verifiziert durch Korpustest, dass ein eingefügter Satz per `SEEKEQ` gefunden und feldgleich gelesen wird
|
||||
- [ ] 7.2 `UPDATE` und `DELETE`, Cursor nach `DELETE` unpositioniert; verifiziert durch Korpustest
|
||||
- [ ] 7.3 Fehler 85 bei `RETRIEVE`/`UPDATE`/`DELETE` ohne positionierten Cursor; verifiziert durch je einen Korpustest
|
||||
- [ ] 7.4 Eindeutigkeitsprüfung mit Fehler 86 und unverändertem Satzbestand; verifiziert durch Korpustest, der nach dem Fehler den Bestand nachzählt
|
||||
- [ ] 7.5 Alle Indizes nach jeder Satzoperation nachführen; verifiziert durch Korpustest, dass ein Satz nach Änderung der indizierten Spalte an neuer Position im Durchlauf erscheint
|
||||
|
||||
## 8. Transaktionen
|
||||
|
||||
- [ ] 8.1 `BEGINTRANS`/`COMMITTRANS` auf einer Schreibtransaktion; verifiziert durch Korpustest, dass die Änderungen nach erneutem Öffnen vorhanden sind
|
||||
- [ ] 8.2 Rücknahmeprotokoll der Änderungen innerhalb einer Transaktion; verifiziert durch Unit-Test, dass jede Satzoperation einen Protokolleintrag erzeugt
|
||||
- [ ] 8.3 `ROLLBACK ALL` stellt Sätze und Indizes wieder her, beendet die Transaktion und setzt den Cursor auf unpositioniert; verifiziert durch Korpustest mit Einfügen, Löschen und anschließendem Durchlauf
|
||||
- [ ] 8.4 `SAVEPOINT` als Funktion (liefert Kennung) und `ROLLBACK kennung`; verifiziert durch Korpustest, dass Änderungen vor dem Punkt erhalten bleiben
|
||||
|
||||
## 9. Übrige Elemente
|
||||
|
||||
- [ ] 9.2 `SETMEM` als Funktion mit echter Puffergrenze und Fehler 89 beim Überschreiten; verifiziert durch Korpustest, der die Grenze niedrig setzt und den Fehler auslöst
|
||||
- [ ] 9.3 Vorgabewert und Bedeutung von `SETMEM` sowie die Abweichung vom DOS-Speichermodell in docs/sprachreferenz.md festhalten; verifiziert durch den Abschnittseintrag
|
||||
- [ ] 9.4 Indexordnung gegen den Stringvergleich des Dialekts prüfen; verifiziert durch Korpustest, dass Indexreihenfolge und `<`-Vergleich übereinstimmen
|
||||
|
||||
## 10. Abschluss
|
||||
|
||||
- [ ] 10.1 ISAM-Einträge in `docs/inventar.md` auf `implementiert` setzen; verifiziert dadurch, dass der Abgleichstest aus `phase-3-runtime-bildschirm` grün bleibt
|
||||
- [ ] 10.2 `docs/bibliothek.md` um die ISAM-Elemente erweitern; verifiziert dadurch, dass jedes umgesetzte Element dort einen Eintrag hat
|
||||
- [ ] 10.3 Korpus-ISAM-Programme in einem temporären Arbeitsverzeichnis ausführen; verifiziert dadurch, dass nach `cargo test` `git status` sauber ist
|
||||
- [ ] 10.4 Vollständige Suite auf allen Zielplattformen ausführen; verifiziert dadurch, dass `cargo test --workspace` grün ist
|
||||
- [ ] 10.5 PLAN.md Phase 3 als abgeschlossen markieren; verifiziert durch den aktualisierten Planabschnitt
|
||||
2
openspec/changes/phase-3-ortszeit/.openspec.yaml
Normal file
2
openspec/changes/phase-3-ortszeit/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-04
|
||||
180
openspec/changes/phase-3-ortszeit/design.md
Normal file
180
openspec/changes/phase-3-ortszeit/design.md
Normal file
@@ -0,0 +1,180 @@
|
||||
## Context
|
||||
|
||||
Siehe proposal.md — Why. Der Ist-Zustand ist eng umrissen:
|
||||
|
||||
- `tb-runtime::datetime` rechnet vollständig selbst: Kalenderumrechnung
|
||||
(Howard Hinnant), serielle Datumswerte ab dem 30.12.1899, `WEEKDAY`,
|
||||
Textformen. Diese Rechnung ist getestet und bleibt.
|
||||
- Der einzige Punkt, an dem die Zeitzone eine Rolle spielt, ist
|
||||
`jetzt_sekunden()` — Sekunden seit der Unix-Epoche. Alles Weitere
|
||||
(`jetzt_serial`, `DATE$`, `TIME$`, `TIMER`, `NOW`) baut darauf auf.
|
||||
- `tb-runtime` hat bislang eine einzige Abhängigkeit mit fachlichem
|
||||
Gewicht: `unicode-width`.
|
||||
|
||||
Damit ist die Aufgabe klein: **eine Zahl** — der UTC-Versatz zu einem
|
||||
Zeitpunkt — kommt künftig von außen.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Zeitfunktionen liefern das, was die Uhr des Rechners zeigt.
|
||||
- Sommerzeit wird zum jeweiligen Zeitpunkt berücksichtigt.
|
||||
- Die Berührungsfläche zur Bibliothek bleibt eine Funktion, damit sie
|
||||
austauschbar ist und die getestete Kalenderrechnung unberührt bleibt.
|
||||
- Zeitverhalten bleibt automatisiert prüfbar, ohne von der Zone des
|
||||
Testrechners abzuhängen.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Zeitzonenbehaftete Datentypen im Dialekt — das Vorbild kennt nur
|
||||
Wanduhrzeit.
|
||||
- Eine Sprachanbindung an die Bibliothek (Zonennamen, Umrechnung zwischen
|
||||
Zonen). Das ist der Stufe-2-Eintrag im PLAN, nicht dieser Change.
|
||||
- Historische Zonendaten jenseits dessen, was die Systemdatenbank
|
||||
hergibt.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: `jiff` als Zeitzonenbibliothek
|
||||
|
||||
*Gewählt:* `jiff` — sie liest die Zeitzonendatenbank des Systems und
|
||||
liefert den Versatz zu einem Zeitpunkt ohne Einschränkung.
|
||||
|
||||
*Alternativen:*
|
||||
|
||||
| Bibliothek | Bewertung |
|
||||
|---|---|
|
||||
| `jiff` | Für genau diese Frage gebaut; Systemzonendatenbank; keine Sicherheitsauflage. **Gewählt.** |
|
||||
| `chrono` (≥ 0.4.20) | Gleichwertig sicher, seit 0.4.20 ohne `localtime_r` (Zonenermittlung über `iana-time-zone`, Datenbank über einen `tz-rs`-Zweig). Etablierter, bringt aber Serde-/Ökosystem-Anbindung mit, die hier niemand braucht. Tragfähige zweite Wahl. |
|
||||
| `time` | Der lokale Versatz ist auf Unix nur nach ausdrücklichem Opt-in in möglicherweise undefiniertes Verhalten verfügbar (`set_soundness`). **Verworfen** — das ist kein Preis für eine Uhrzeit. |
|
||||
| Eigenbau (`iana-time-zone` + TZif-Parser) | Hieße, die Zonendatenbank selbst zu lesen und dauerhaft nachzupflegen. Genau das, wofür man eine Bibliothek nimmt. **Verworfen.** |
|
||||
|
||||
*Warum die Wahl leicht revidierbar bleibt:* Die Bibliothek wird an genau
|
||||
einer Stelle aufgerufen (D2). Ein Wechsel zu `chrono` wäre ein Austausch
|
||||
dieser einen Funktion.
|
||||
|
||||
### D2: Berührungsfläche ist eine Funktion
|
||||
|
||||
```
|
||||
tb-runtime::datetime
|
||||
|
||||
utc_versatz(zeitpunkt: i64) -> Option<i32> // Sekunden, None = Zone unbekannt
|
||||
└── einziger Aufruf der Bibliothek
|
||||
|
||||
jetzt_sekunden() ─┐
|
||||
jetzt_serial() ├── rechnen weiter selbst
|
||||
civil_from_days() │
|
||||
weekday() … ─┘
|
||||
```
|
||||
|
||||
Die Kalenderrechnung sieht die Bibliothek nie. Sie bekommt nach wie vor
|
||||
Sekunden seit der Epoche — nur eben um den Versatz verschobene.
|
||||
|
||||
*Warum nicht die Bibliothek durchreichen:* Ihre Datums-/Zeittypen wären
|
||||
ein zweites Kalendermodell neben dem des Dialekts (der bei 1899 zählt,
|
||||
`WEEKDAY` = 1 für Sonntag, CURRENCY-nahe Serienwerte). Zwei Modelle
|
||||
nebeneinander wären eine dauerhafte Fehlerquelle; die vorhandene Rechnung
|
||||
ist getestet und bleibt maßgeblich.
|
||||
|
||||
### D3: Der Versatz gilt je Zeitpunkt, nicht je Programmlauf
|
||||
|
||||
`utc_versatz` bekommt den Zeitpunkt als Argument. Ein Programm, das über
|
||||
eine Zeitumstellung hinweg läuft, folgt ihr; ein Programm, das mit
|
||||
`DATE$ =` in einen anderen Teil des Jahres springt, bekommt den dort
|
||||
gültigen Versatz.
|
||||
|
||||
*Preis:* Eine Zonenabfrage je Zeitabfrage. Bei den Aufrufmengen eines
|
||||
Terminalprogramms belanglos; die Bibliothek hält die Zonendaten selbst
|
||||
vor.
|
||||
|
||||
### D4: Zonenquelle ist umschaltbar — damit Tests nicht von der Maschine abhängen
|
||||
|
||||
```rust
|
||||
pub enum Zeitzone {
|
||||
/// Zone des Rechners (Vorgabe).
|
||||
System,
|
||||
/// Fester Versatz in Sekunden — für Tests und den Korpus.
|
||||
Fest(i32),
|
||||
/// Zone nicht ermittelbar; es gilt UTC.
|
||||
Unbekannt,
|
||||
}
|
||||
```
|
||||
|
||||
`RtState` hält eine `Zeitzone`. Die Vorgabe ist `System`; schlägt die
|
||||
Ermittlung fehl, wird daraus `Unbekannt`. Tests und Korpusprogramme
|
||||
setzen `Fest(n)`.
|
||||
|
||||
*Warum nicht `TZ` in den Tests setzen:* Umgebungsvariablen sind
|
||||
prozessweit, Tests laufen nebenläufig, und `set_var` ist seit Rust 2024
|
||||
`unsafe`. Ein Zustand im `RtState` ist der offensichtliche Weg — er ist
|
||||
genau so weit sichtbar wie nötig.
|
||||
|
||||
*Korpus:* Das Harness bekommt eine Kopfzeilen-Direktive analog zu
|
||||
`' tb-screen:` und `' tb-tempdir`:
|
||||
|
||||
```text
|
||||
' tb-tz: +02:00
|
||||
```
|
||||
|
||||
Damit bleibt die Sollausgabe eines Zeitprogramms unabhängig davon, wo der
|
||||
Testrechner steht.
|
||||
|
||||
### D5: Rückfallebene ist sichtbar, nicht still
|
||||
|
||||
Ohne ermittelbare Zone gilt UTC — ein Abbruch wäre unverhältnismäßig, ein
|
||||
stiller Wechsel aber ein Verstoß gegen den Guiding Principle. Die Laufzeit
|
||||
führt den Zustand mit, und der Zustand ist abfragbar; die Sprachreferenz
|
||||
beschreibt ihn.
|
||||
|
||||
*Wie ein Programm es erfährt:* Über die vorhandene Systemabfrage
|
||||
`ENVIRON$` wäre es geraten. Stattdessen bekommt die Laufzeit einen
|
||||
benannten Zustand, den der Runner beim Start auf die Fehlerausgabe
|
||||
schreibt, wenn er `Unbekannt` ist — einmal, nicht je Abfrage. Ein
|
||||
Programm, das es programmatisch braucht, bekommt es in Stufe 2 über die
|
||||
Bibliotheksanbindung.
|
||||
|
||||
### D6: Reihenfolge der Umsetzung
|
||||
|
||||
```
|
||||
1. Abhaengigkeit + utc_versatz() -> isoliert testbar
|
||||
2. jetzt_sekunden/jetzt_serial umstellen -> DATE$/TIME$/TIMER/NOW folgen
|
||||
3. Zeitzone im RtState + Korpusdirektive -> Tests werden maschinenunabhaengig
|
||||
4. Doku: Abweichung zurueckziehen, PLAN Stufe 2
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Erste schwere Abhängigkeit der Laufzeit** → bewusst entschieden
|
||||
(2026-09-04). Die Berührungsfläche ist eine Funktion (D2); ein Wechsel
|
||||
der Bibliothek bleibt ein lokaler Eingriff.
|
||||
- **Zeitabhängige Tests werden flatterig** → deshalb D4: kein Test und
|
||||
kein Korpusprogramm fragt die echte Zone ab. Was die Systemzone
|
||||
betrifft, wird über `Fest(n)` und `Unbekannt` geprüft.
|
||||
- **Zeitumstellung erzeugt mehrdeutige Wanduhrzeiten** (Rückstellung) und
|
||||
nicht existierende (Vorstellung) → betrifft nur die Richtung
|
||||
Wanduhr → Zeitpunkt. Die Zeitfunktionen dieses Dialekts gehen die
|
||||
andere Richtung (Zeitpunkt → Wanduhr) und sind davon nicht betroffen.
|
||||
`DATEVALUE`/`TIMEVALUE` lesen Wanduhrzeit und rechnen gar nicht um.
|
||||
`DATE$ =`/`TIME$ =` setzen einen Versatz gegen die aktuelle Wanduhrzeit
|
||||
— auch das ist eindeutig.
|
||||
- **Die Systemzonendatenbank kann fehlen** (schlanke Container) → D5.
|
||||
- **Bauzeit und Binärgröße wachsen** → für ein Werkzeug, dessen Programme
|
||||
Uhrzeiten anzeigen, ist eine falsche Uhrzeit der größere Preis.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Rein verhaltensändernd, ohne Formatbruch: `.tbc`-Dateien, Korpus-Golden-
|
||||
Files und Schnittstellen bleiben unberührt. Das einzige beobachtbare
|
||||
Verhalten, das sich ändert, ist der Wert von `DATE$`, `TIME$`, `TIMER`
|
||||
und `NOW` — und zwar zum Richtigen hin.
|
||||
|
||||
Rücknahme: Ein Zurücksetzen auf UTC wäre `Zeitzone::Unbekannt` als
|
||||
Vorgabe; die Abhängigkeit bliebe ungenutzt.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Ob die Bibliotheksanbindung in Stufe 2 (Zonennamen, Umrechnung zwischen
|
||||
Zonen als BASIC-Funktionen) auf derselben Bibliothek aufsetzt oder eine
|
||||
eigene Wahl trifft, entscheidet Stufe 2. Für diesen Change ohne
|
||||
Wirkung — hier wird nur der Versatz gebraucht.
|
||||
76
openspec/changes/phase-3-ortszeit/proposal.md
Normal file
76
openspec/changes/phase-3-ortszeit/proposal.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Ortszeit statt UTC
|
||||
|
||||
## Why
|
||||
|
||||
`DATE$`, `TIME$`, `TIMER` und `NOW` liefern heute koordinierte Weltzeit.
|
||||
Das ist keine Rekonstruktion des Vorbilds, sondern eine Notlösung: die
|
||||
Standardbibliothek von Rust kennt keine Zeitzonen, und in
|
||||
`phase-3-runtime-bildschirm` sollte die erste schwere Abhängigkeit der
|
||||
Laufzeit nicht nebenbei entstehen. Die Abweichung steht in
|
||||
docs/sprachreferenz.md.
|
||||
|
||||
Für ein BASIC zum Bauen von Terminal-Werkzeugen ist das ein echter
|
||||
Mangel: Ein Programm, das eine Uhr anzeigt, ein Protokoll schreibt oder
|
||||
einen Termin prüft, zeigt schlicht die falsche Zeit — je nach Zone um
|
||||
Stunden daneben. Das Vorbild lief auf der Ortszeit des Rechners; jedes
|
||||
übernommene Programm rechnet damit.
|
||||
|
||||
Der Projektinhaber hat am 2026-09-04 entschieden, dafür eine Bibliothek
|
||||
in die Laufzeit aufzunehmen. Dieser Change zieht die Abweichung zurück.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Zeitfunktionen liefern Ortszeit**: `DATE$`, `TIME$`, `TIMER` und
|
||||
`NOW` rechnen in der Zeitzone des Rechners statt in UTC. `TIMER` zählt
|
||||
Sekunden seit der **lokalen** Mitternacht.
|
||||
- **Sommerzeit wird berücksichtigt**: Der Versatz wird für den jeweiligen
|
||||
Zeitpunkt bestimmt, nicht einmalig beim Programmstart — ein Programm,
|
||||
das über einen Zeitumstellungspunkt hinweg läuft, folgt ihm.
|
||||
- **Neue Abhängigkeit in `tb-runtime`**: eine Zeitzonenbibliothek, deren
|
||||
Berührungsfläche bewusst auf eine einzige Frage beschränkt bleibt —
|
||||
„welcher UTC-Versatz gilt zu diesem Zeitpunkt?". Die Kalenderrechnung
|
||||
(serielle Datumswerte, `WEEKDAY`, Zerlegung) bleibt die vorhandene,
|
||||
getestete Eigenimplementierung in `tb-runtime::datetime`.
|
||||
- **Rückfallebene**: Lässt sich die Zeitzone des Rechners nicht
|
||||
ermitteln, gilt UTC. Das Programm läuft weiter; die Rückfallebene ist
|
||||
über eine Funktion der Laufzeit erkennbar und dokumentiert.
|
||||
- **`DATE$ =` / `TIME$ =` bleiben ein Programmversatz** (sie stellen
|
||||
nicht die Systemuhr) — sie wirken künftig auf die Ortszeit.
|
||||
- **Abweichung zurückziehen**: Der Eintrag „Uhr läuft in UTC" entfällt
|
||||
aus docs/sprachreferenz.md; an seine Stelle tritt die Beschreibung der
|
||||
Zonenermittlung samt Rückfallebene.
|
||||
- **Stufe 2 im PLAN**: Die Zeitzonenbibliothek wird in den Ideenspeicher
|
||||
„Runtime-Bibliotheken als BASIC-Bibliotheken" aufgenommen — neben
|
||||
crossterm —, damit BASIC-Programme später direkt auf Zonen zugreifen
|
||||
können (Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung zwischen
|
||||
Zonen).
|
||||
|
||||
**Non-Goals:** Zeitzonenbehaftete Datentypen im Dialekt (das Vorbild
|
||||
kennt nur lokale Wanduhrzeit); eine Sprachanbindung an die Bibliothek
|
||||
(das ist der Stufe-2-Eintrag, nicht dieser Change); Änderungen an der
|
||||
Zählweise serieller Datumswerte oder an `DATEVALUE`/`TIMEVALUE`, die
|
||||
Wanduhrzeit lesen und daher keine Umrechnung brauchen.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `konsolen-basisbibliothek`: Die Datums- und Zeitfunktionen SHALL die
|
||||
Ortszeit des Rechners liefern statt UTC, einschließlich Sommerzeit und
|
||||
einer dokumentierten Rückfallebene.
|
||||
|
||||
## Impact
|
||||
|
||||
- `crates/tb-runtime/src/datetime.rs`: Versatzermittlung über die neue
|
||||
Bibliothek; `jetzt_serial`/`jetzt_sekunden` liefern Ortszeit. Die
|
||||
Kalenderrechnung bleibt unverändert.
|
||||
- `crates/tb-runtime/src/builtins.rs`: `DATE$`, `TIME$`, `TIMER`, `NOW`
|
||||
rechnen über die neue Funktion.
|
||||
- `Cargo.toml` (Workspace) und `crates/tb-runtime/Cargo.toml`: neue
|
||||
Abhängigkeit.
|
||||
- `tests/compat`: Korpusprogramm für die Zeitfunktionen, das ohne
|
||||
Abhängigkeit von der Zone des Testrechners prüfbar bleibt.
|
||||
- `docs/sprachreferenz.md`: Abweichung „Uhr läuft in UTC" entfällt;
|
||||
Zonenermittlung und Rückfallebene beschrieben.
|
||||
- `docs/bibliothek.md`: Abschnitt Datum/Zeit fortgeschrieben.
|
||||
- `PLAN.md`: Stufe-2-Eintrag um die Bibliothek erweitert.
|
||||
@@ -0,0 +1,58 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Zeitfunktionen liefern Ortszeit
|
||||
`DATE$`, `TIME$`, `TIMER` und `NOW` SHALL den Zeitpunkt in der Zeitzone
|
||||
des Rechners liefern, nicht in koordinierter Weltzeit. `TIMER` SHALL die
|
||||
Sekunden seit der **lokalen** Mitternacht zählen. Der maßgebliche Versatz
|
||||
SHALL für den jeweils abgefragten Zeitpunkt bestimmt werden, nicht einmal
|
||||
beim Programmstart — ein Programm, das über eine Zeitumstellung hinweg
|
||||
läuft, folgt ihr. Die Zerlegungsfunktionen (`DAY`, `MONTH`, `YEAR`,
|
||||
`WEEKDAY`, `HOUR`, `MINUTE`, `SECOND`) arbeiten unverändert auf seriellen
|
||||
Werten und rechnen selbst nicht um.
|
||||
|
||||
#### Scenario: Datum und Uhrzeit stimmen mit der Systemuhr überein
|
||||
- **WHEN** ein Programm `DATE$` und `TIME$` abfragt
|
||||
- **THEN** entsprechen sie der Ortszeit des Rechners zum Abfragezeitpunkt
|
||||
|
||||
#### Scenario: TIMER zählt ab lokaler Mitternacht
|
||||
- **WHEN** `TIMER` abgefragt wird
|
||||
- **THEN** entspricht der Wert den Sekunden seit der letzten lokalen Mitternacht, nicht seit der UTC-Mitternacht
|
||||
|
||||
#### Scenario: NOW und die Zerlegungsfunktionen passen zusammen
|
||||
- **WHEN** `n# = NOW` gebildet und `HOUR(n#)` ausgewertet wird
|
||||
- **THEN** liefert `HOUR(n#)` dieselbe Stunde, die auch `TIME$` nennt
|
||||
|
||||
#### Scenario: Sommerzeit wirkt zum jeweiligen Zeitpunkt
|
||||
- **WHEN** in einer Zone mit Sommerzeit ein Zeitpunkt vor und einer nach der Umstellung abgefragt wird
|
||||
- **THEN** unterscheiden sich die beiden Versätze um die Umstellungsspanne
|
||||
|
||||
### Requirement: Zonenermittlung mit Rückfallebene
|
||||
Die Laufzeit SHALL die Zeitzone des Rechners über die
|
||||
Betriebssystemeinstellung bestimmen. Lässt sie sich nicht ermitteln,
|
||||
SHALL UTC gelten und das Programm ohne Fehler weiterlaufen. Ob die
|
||||
Rückfallebene greift, MUST für ein Programm erkennbar sein und in
|
||||
docs/sprachreferenz.md beschrieben stehen; ein stiller Wechsel auf UTC
|
||||
MUST NOT vorkommen.
|
||||
|
||||
#### Scenario: Zone nicht ermittelbar
|
||||
- **WHEN** die Zeitzone des Rechners nicht bestimmt werden kann
|
||||
- **THEN** rechnen die Zeitfunktionen in UTC und das Programm läuft ohne Fehler weiter
|
||||
|
||||
#### Scenario: Rückfallebene ist erkennbar
|
||||
- **WHEN** ein Programm den Zonenzustand abfragt
|
||||
- **THEN** unterscheidet die Antwort erkennbar zwischen ermittelter Zone und Rückfall auf UTC
|
||||
|
||||
### Requirement: Programmversatz wirkt auf die Ortszeit
|
||||
`DATE$ = text$` und `TIME$ = text$` SHALL weiterhin nicht die Systemuhr
|
||||
stellen, sondern einen programmeigenen Versatz setzen. Dieser Versatz
|
||||
SHALL auf die **Ortszeit** wirken: nach `TIME$ = "10:20:30"` nennt
|
||||
`TIME$` diese Uhrzeit, und `NOW`, `HOUR` und `TIMER` stimmen damit
|
||||
überein.
|
||||
|
||||
#### Scenario: Gesetzte Uhrzeit wird zurückgelesen
|
||||
- **WHEN** `TIME$ = "10:20:30"` ausgeführt und danach `TIME$` abgefragt wird
|
||||
- **THEN** nennt `TIME$` `10:20:30`
|
||||
|
||||
#### Scenario: Versatz wirkt auf alle Zeitfunktionen
|
||||
- **WHEN** `DATE$ = "01-02-1993"` gesetzt und danach `YEAR(NOW)` ausgewertet wird
|
||||
- **THEN** liefert `YEAR(NOW)` 1993
|
||||
35
openspec/changes/phase-3-ortszeit/tasks.md
Normal file
35
openspec/changes/phase-3-ortszeit/tasks.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## 1. Versatzermittlung
|
||||
|
||||
- [ ] 1.1 `jiff` als Workspace-Abhängigkeit aufnehmen und in `tb-runtime` einbinden; verifiziert dadurch, dass `cargo build -p tb-runtime` durchläuft und `cargo tree -p tb-runtime` die Bibliothek genau einmal zeigt
|
||||
- [ ] 1.2 `datetime::utc_versatz(zeitpunkt: i64) -> Option<i32>` als einzigen Aufrufpunkt der Bibliothek schreiben (Sekunden Versatz, `None` = Zone nicht ermittelbar); verifiziert durch Unit-Test, dass der Versatz für einen festen Zeitpunkt im gültigen Bereich −50400…50400 liegt
|
||||
- [ ] 1.3 Sommerzeit prüfen: für dieselbe Zone einen Zeitpunkt im Januar und einen im Juli abfragen; verifiziert durch Unit-Test, dass sich die Versätze in einer Zone mit Sommerzeit unterscheiden und in einer ohne gleich bleiben
|
||||
- [ ] 1.4 Sicherstellen, dass die Kalenderrechnung unberührt bleibt; verifiziert dadurch, dass `datetime.rs` außerhalb von `utc_versatz` keinen Bibliotheksaufruf enthält und die bestehenden `datetime`-Tests unverändert grün sind
|
||||
|
||||
## 2. Zonenzustand in der Laufzeit
|
||||
|
||||
- [ ] 2.1 `Zeitzone { System, Fest(i32), Unbekannt }` einführen und in `RtState` halten; Vorgabe `System`, bei fehlgeschlagener Ermittlung `Unbekannt`; verifiziert durch Unit-Test je Variante
|
||||
- [ ] 2.2 `jetzt_sekunden`/`jetzt_serial` auf den Zonenzustand umstellen; verifiziert durch Unit-Test, dass `Fest(3600)` genau eine Stunde gegenüber `Unbekannt` verschiebt
|
||||
- [ ] 2.3 `DATE$`, `TIME$`, `TIMER` und `NOW` über den Zonenzustand rechnen; verifiziert durch Test, dass `HOUR(NOW)` und die Stunde aus `TIME$` bei `Fest(n)` übereinstimmen
|
||||
- [ ] 2.4 `TIMER` zählt ab lokaler Mitternacht; verifiziert durch Test, dass sich `TIMER` bei `Fest(3600)` gegenüber `Unbekannt` um 3600 verschiebt (modulo Tageslänge)
|
||||
- [ ] 2.5 `DATE$ =` / `TIME$ =` wirken auf die Ortszeit; verifiziert durch Test, dass `TIME$` nach `TIME$ = "10:20:30"` genau diesen Wert nennt und `HOUR(NOW)` 10 liefert
|
||||
|
||||
## 3. Rückfallebene
|
||||
|
||||
- [ ] 3.1 Bei nicht ermittelbarer Zone auf UTC zurückfallen, ohne Fehler; verifiziert durch Test mit `Zeitzone::Unbekannt`, dass alle Zeitfunktionen Werte liefern
|
||||
- [ ] 3.2 Den Rückfall beim Programmstart einmalig auf die Fehlerausgabe melden (nicht je Abfrage); verifiziert dadurch, dass `tbc run` bei `Unbekannt` genau eine Meldung schreibt und die Standardausgabe unberührt bleibt
|
||||
- [ ] 3.3 Rückfallebene in docs/sprachreferenz.md beschreiben; verifiziert durch den Abschnittseintrag
|
||||
|
||||
## 4. Prüfbarkeit ohne Abhängigkeit von der Testmaschine
|
||||
|
||||
- [ ] 4.1 Kopfzeilen-Direktive `' tb-tz: ±HH:MM` im Korpus-Harness auswerten und als `Zeitzone::Fest` setzen; verifiziert dadurch, dass ein Korpusprogramm ohne die Direktive weiterhin läuft und eines mit ihr den gesetzten Versatz verwendet
|
||||
- [ ] 4.2 Korpusprogramm `zeitzone.bas` mit festem Versatz: `DATE$`/`TIME$`/`TIMER`/`NOW` gegen eine per `DATE$ =`/`TIME$ =` gesetzte Uhrzeit prüfen; verifiziert dadurch, dass die Sollausgabe unabhängig von der Zone des Testrechners stimmt
|
||||
- [ ] 4.3 Bestehendes `datumzeit.bas` auf die Direktive umstellen, damit es auch nach der Umstellung deterministisch bleibt; verifiziert dadurch, dass seine Sollausgabe unverändert bleibt
|
||||
|
||||
## 5. Abschluss
|
||||
|
||||
- [ ] 5.1 Abweichung „Uhr läuft in UTC" aus docs/sprachreferenz.md entfernen und durch die Beschreibung der Zonenermittlung ersetzen; verifiziert dadurch, dass der Abweichungspunkt nicht mehr auftaucht
|
||||
- [ ] 5.2 `docs/bibliothek.md`, Abschnitt Datum/Zeit, fortschreiben; verifiziert durch den geänderten Abschnitt
|
||||
- [ ] 5.3 PLAN.md Stufe 2, Eintrag „Runtime-Bibliotheken als BASIC-Bibliotheken", um die Zeitzonenbibliothek erweitern (Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung zwischen Zonen) — neben crossterm; verifiziert durch den ergänzten Eintrag
|
||||
- [ ] 5.4 PLAN.md Phase 3: den Hinweis auf die UTC-Abweichung durch den Verweis auf diesen Change ersetzen; verifiziert durch den aktualisierten Abschnitt
|
||||
- [ ] 5.5 Inventar unverändert lassen und den Abgleichstest prüfen; verifiziert dadurch, dass `cargo test -p tb-frontend --test inventar` grün bleibt (die Elemente waren bereits implementiert, nur ihr Zeitbezug ändert sich)
|
||||
- [ ] 5.6 Vollständige Suite ausführen; verifiziert dadurch, dass `cargo test --workspace` grün ist
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-03
|
||||
287
openspec/changes/phase-3-runtime-bildschirm/design.md
Normal file
287
openspec/changes/phase-3-runtime-bildschirm/design.md
Normal file
@@ -0,0 +1,287 @@
|
||||
## Context
|
||||
|
||||
Siehe proposal.md — Why. Technisch entscheidend ist der Ist-Zustand
|
||||
dreier Stellen:
|
||||
|
||||
- `tb-ui::screen::TextScreen` (385 Zeilen) trägt die Bildschirmsemantik
|
||||
bereits vollständig: `cls`, `set_color`, `locate`, `csrlin`, `pos`,
|
||||
`view_print`, `cell`, `print`, `scroll_up`, `resize`. Ratatui kommt
|
||||
darin an genau zwei Stellen vor: `basic_color()` (4 Zeilen) und
|
||||
`impl Widget for &TextScreen` (~35 Zeilen).
|
||||
- `tb-runtime::host::Host` ist ein Stream-Trait (`write(&str)`,
|
||||
`read_line()`). `tb-runtime` und `tb-ui` sind beide Blätter im
|
||||
Abhängigkeitsgraphen und treffen sich erst in `tb-cli`/`tb-ide`.
|
||||
- Rund 50 dokumentierte Elemente besitzen im Frontend eine Signatur in
|
||||
`builtin_fn`/`builtin_stmt`, senken aber auf `HStmtKind::Unsupported`
|
||||
bzw. `HExpr::Unsupported` ab → Opcode `0x05` → Laufzeitfehler 73.
|
||||
|
||||
Zwei Entscheidungen vom 2026-09-03 rahmen die Arbeit: Das Windowing wird
|
||||
— wie beim Vorbild — vom erzeugten Programm mitgebracht und nicht von der
|
||||
IDE, sodass Forms und `PRINT` in denselben Zellenpuffer zeichnen. Die
|
||||
Forms-Engine selbst bleibt dabei am von PLAN.md vorgesehenen Ort
|
||||
`tb-ui::forms`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Der Zellenpuffer liegt dort, wo sowohl die Laufzeitbibliothek als auch
|
||||
die spätere Forms-Engine in `tb-ui` ihn erreichen, ohne dass die
|
||||
Ausführungsschicht eine Terminal-Bibliothek zieht.
|
||||
- Bildschirmverhalten ist ohne Terminal automatisiert prüfbar.
|
||||
- Die Abdeckung misst sich selbst statt gepflegt zu werden.
|
||||
- Die Umstellung der acht bestehenden Golden Files geschieht in einem
|
||||
nachvollziehbaren, einmaligen Schritt.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Optimierung der Bildschirmausgabe (Diff-Minimierung, Teilaktualisierung)
|
||||
— erst wenn Messungen sie verlangen.
|
||||
- Ein Rendering-Backend jenseits des Terminals.
|
||||
- Die Forms-Engine selbst (Phase 4, `tb-ui::forms`); dieser Change
|
||||
liefert ihr nur die Zeichenfläche.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Zellenpuffer nach `tb-runtime::screen`, ratatui-frei
|
||||
|
||||
`Cell` und `TextScreen` ziehen unverändert nach `tb-runtime::screen`.
|
||||
`basic_color()` und `impl Widget` bleiben in `tb-ui` und arbeiten künftig
|
||||
auf dem Runtime-Typ.
|
||||
|
||||
*Warum:* `PRINT`, `LOCATE`, `CLS`, `COLOR`, `CSRLIN`, `POS` und die
|
||||
Funktionsform von `SCREEN` sind Elemente der Laufzeitbibliothek. Sie
|
||||
müssen den Puffer erreichen, ohne dass `tb-runtime` eine
|
||||
Terminal-Bibliothek zieht — sonst braucht jeder VM- und Bibliothekstest
|
||||
ein Terminal. Das trägt die Entscheidung allein.
|
||||
|
||||
*Verhältnis zur Forms-Engine:* Forms bleibt laut PLAN.md in
|
||||
`tb-ui::forms` (Entscheidung 2026-09-03). Das passt zur Richtung: `tb-ui`
|
||||
hängt künftig an `tb-runtime` und zeichnet von oben in denselben Puffer
|
||||
wie `PRINT` — ein `MSGBOX` über einer Konsolenausgabe überdeckt deren
|
||||
Zellen und gibt sie beim Schließen zurück. Läge der Puffer weiterhin in
|
||||
`tb-ui`, wäre genau diese gemeinsame Fläche für die Bibliothek
|
||||
unerreichbar. Umgekehrt entsteht kein Zyklus: die Ausführungsschicht
|
||||
erreicht die Forms-Engine über die Host-Schnittstelle (D2), die sie
|
||||
selbst deklariert und `tb-ui` implementiert.
|
||||
|
||||
*Alternativen:*
|
||||
- `tb-runtime` hängt an `tb-ui`: null Doppelung, aber ratatui und
|
||||
crossterm landen in jedem VM-Test. Verworfen.
|
||||
- Semantik bleibt in `tb-ui`, jeder Host führt einen eigenen Puffer:
|
||||
dieselbe Scroll-/Umbruch-/Doppelzellenlogik dreimal. Verworfen.
|
||||
|
||||
*Aufwand:* Der Umzug ist mechanisch — betroffen sind der `use`-Block, die
|
||||
zwei ratatui-Stellen und die Modulzuordnung. Die Semantik selbst wird
|
||||
nicht angefasst; erweitert wird sie nur um die Doppelzellen (D4).
|
||||
|
||||
### D2: `Host` wird Bildschirm- statt Stream-Schnittstelle
|
||||
|
||||
`write`/`read_line` weichen einer Schnittstelle, die den Zellenpuffer zur
|
||||
Anzeige übergibt und Ereignisse (Tastendruck, Größenänderung, Abbruch)
|
||||
zurückliefert. Die Bibliothek schreibt nicht mehr in den Host, sondern in
|
||||
den Puffer; der Host stellt dar.
|
||||
|
||||
*Warum:* `LOCATE 5,10`, `COLOR 14,1`, `CSRLIN` und besonders
|
||||
`SCREEN(r,c)` — das ein Zeichen vom Bildschirm *zurückliest* — sind über
|
||||
einen Zeichenstrom nicht ausdrückbar. Das ist ein Zwang, keine Präferenz.
|
||||
|
||||
*Wann dargestellt wird:* Der Host wird an denselben Tick-Grenzen
|
||||
aufgerufen, an denen die VM heute schon die Abbruchprüfung macht, sowie
|
||||
vor jeder blockierenden Eingabe. Kein zusätzlicher Mechanismus.
|
||||
|
||||
*Blockieren bleibt erlaubt* (Entscheidung D3 aus Phase 2 bleibt gültig):
|
||||
`INPUT` und `INPUT$` blockieren im Host. Erst die Ereignisschleife der
|
||||
Phase 4 bringt kooperative Zustellpunkte.
|
||||
|
||||
*Alternativen:*
|
||||
- Stream-Host behalten und Bildschirmwirkungen als ANSI-Escapes
|
||||
hindurchreichen: `CSRLIN`/`POS`/`SCREEN()` bräuchten trotzdem einen
|
||||
Schattenpuffer, und der Capture-Host müsste ANSI parsen. Verworfen.
|
||||
|
||||
### D3: Korpusvergleich als getrimmter Snapshot
|
||||
|
||||
Die `.out`-Datei hält das Textbild des Puffers bis zur letzten belegten
|
||||
Zeile und Spalte. Eine zweite Ebene mit Farbattributen kommt nur hinzu,
|
||||
wenn das Programm `COLOR` verwendet.
|
||||
|
||||
*Warum getrimmt:* Ein voller 80×25-Snapshot für `hello.bas` wären 2000
|
||||
Zeichen für zwei Zeichen Signal, und jeder Diff zeigte Rauschen. Getrimmt
|
||||
bleiben die bestehenden acht Sollausgaben inhaltlich fast unverändert —
|
||||
`PRINT`-only-Programme ergeben dasselbe Textbild wie bisher der Stream.
|
||||
|
||||
*Warum Attribute optional:* Programme ohne `COLOR` erzeugen sonst eine
|
||||
zweite Ebene aus lauter identischen Werten.
|
||||
|
||||
*Umstellung:* Die acht Golden Files werden einmalig neu erzeugt und der
|
||||
Diff wird geprüft — erwartet wird, dass sich nur Zeilen ändern, die
|
||||
vorher auf `\r`/Umbruchverhalten beruhten. Ein unerwarteter Diff ist ein
|
||||
Befund, kein Anpassungsgrund.
|
||||
|
||||
*Alternativen:*
|
||||
- Zwei Harnesses (Stream für Alt-Tests, Snapshot für neue): dauerhaft
|
||||
doppelte Infrastruktur für einen einmaligen Umstellungsschmerz.
|
||||
Verworfen.
|
||||
- Voller Snapshot: siehe oben. Verworfen.
|
||||
|
||||
### D4: Doppelzellen über Darstellungsbreite
|
||||
|
||||
Breite Zeichen belegen zwei Zellen: die erste trägt das Zeichen, die
|
||||
zweite ist als Fortsetzung markiert. Neue Abhängigkeit `unicode-width`
|
||||
in `tb-runtime`.
|
||||
|
||||
*Warum eine Abhängigkeit:* Die Breitentabelle folgt einem
|
||||
Unicode-Annex und ändert sich mit jeder Unicode-Version; sie
|
||||
nachzubauen hieße, sie dauerhaft nachzupflegen.
|
||||
|
||||
*Konsequenzen, die die Spec festhält:* Cursor rückt um zwei Spalten vor;
|
||||
`POS` zählt die Anfangsspalte; ein breites Zeichen in der letzten Spalte
|
||||
wird vollständig umgebrochen und die Spalte bleibt leer; `LOCATE` auf
|
||||
eine Fortsetzungszelle wirkt auf den Zeichenanfang. Beim Überschreiben
|
||||
einer Hälfte muss die andere geleert werden, sonst entstehen
|
||||
Geisterzeichen.
|
||||
|
||||
### D4a: 80×25 ist nirgends eine Konstante
|
||||
|
||||
Anforderung des Projektinhabers (2026-09-03), Priorität für das ganze
|
||||
Programm: 80×25 ist stets Synonym für „voller Bildschirm", nie eine feste
|
||||
Grenze. Moderne Terminals haben keine feste Größe, und sie ändert sich
|
||||
während der Ausführung.
|
||||
|
||||
*Konsequenz für die Umsetzung:* Kein Zweig darf gegen 80 oder 25
|
||||
vergleichen. Die Abmessungen kommen ausschließlich aus dem Puffer
|
||||
(`cols()`/`rows()`), abgeleitet aus der Darstellungsfläche. Betroffen
|
||||
sind `CLS` (voller aktueller Bildschirm), der voreingestellte
|
||||
Scrollbereich, die Umbruchspalte, die Scrollzeile sowie die
|
||||
Grenzprüfungen von `LOCATE` und der Funktionsform von `SCREEN`.
|
||||
|
||||
*Größenänderung zur Laufzeit* ist damit kein Sonderfall, sondern der
|
||||
Normalfall: Das Größenänderungsereignis kommt über den Host (D2), der
|
||||
Puffer wird angepasst, Inhalt bleibt oben links, Cursor und
|
||||
`VIEW PRINT`-Bereich werden geklemmt. Ein Bereich, der durch die
|
||||
Verkleinerung leer würde, fällt auf den vollen Bildschirm zurück, statt
|
||||
ungültig zu bleiben.
|
||||
|
||||
*Das Mindestmaß bleibt eine Darstellungsschranke:* Ist die Fläche kleiner
|
||||
als 80×25, zeigt die Darstellungsschicht den Hinweis „Terminal zu klein"
|
||||
(bestehende Entscheidung 2026-09-02). Das ist eine untere Schranke der
|
||||
Anzeige, keine Bildschirmgröße für das Programm.
|
||||
|
||||
*Prüfbarkeit:* Der Test-Host bekommt seine Größe je Korpusprogramm
|
||||
explizit gesetzt, ohne stillschweigenden Vorgabewert. Mindestens ein
|
||||
Bildschirmprogramm läuft bei zwei Größen mit je eigener Sollausgabe —
|
||||
eine hartkodierte 80 oder 25 fällt damit sofort auf. Ein weiteres
|
||||
durchläuft eine Größenänderung mitten in der Ausführung.
|
||||
|
||||
### D4b: Das Inventar geht der Umfangsschätzung vor
|
||||
|
||||
Die Erhebung in Aufgabe 1.1 (2026-09-03) hat gezeigt, dass die
|
||||
ursprüngliche Umfangsliste dieses Changes drei ganze Gruppen nicht
|
||||
enthielt: die 13 finanzmathematischen Funktionen, die 14
|
||||
Record-Konvertierungsfunktionen und einen Rest an Dateisystem- und
|
||||
Systemelementen. Sie hat außerdem zwei Elemente widerlegt, die
|
||||
`phase-3-isam` fälschlich führte (`CHECKPOINT`, `TEXTCOMP`).
|
||||
|
||||
*Konsequenz für die Arbeitsweise:* Maßgeblich ist das Inventar, nicht die
|
||||
Aufzählung im Proposal. Wo beide auseinandergehen, wird das Proposal
|
||||
nachgezogen. Aufzählungen in Proposal und Specs sind Mindestumfang.
|
||||
|
||||
*Konsequenz für die Quelle:* `bas7advr.hlp/TOPIC_LIST.html` ist
|
||||
nachweislich unvollständig — `SGN`, `SQR`, `SPACE$`, `STRING$`, `CSRLIN`,
|
||||
`BEEP` und `DATE$` als Funktion fehlen dort. Das Inventar bildet die
|
||||
Vereinigungsmenge aus dieser Liste, `qb45advr.hlp/TOPIC_LIST.html` und
|
||||
`docs/sprachreferenz.md`; jeder Eintrag nennt seine Fundstelle. Die
|
||||
Zählung einer einzelnen Themenliste ist kein Vollständigkeitsnachweis.
|
||||
|
||||
### D5: Inventar als Markdown-Tabelle mit Abgleichstest
|
||||
|
||||
`docs/inventar.md` trägt eine Pipe-Tabelle mit festen Spalten (Name, Art,
|
||||
Gruppe, Status, Fundstelle). Ein Test parst sie und hält sie gegen
|
||||
`builtin_fn`/`builtin_stmt`, die `Unsupported`-Marker und
|
||||
`banned_feature`.
|
||||
|
||||
*Warum Markdown und nicht TOML/YAML:* Der Plan nennt „maschinenlesbar",
|
||||
aber keinen Konsumenten außer der Messbarkeit selbst; Zielpfad ist
|
||||
ausdrücklich `docs/inventar.md`, und das IDE-Hilfesystem (Phase 5)
|
||||
rendert `docs/`-Markdown. Eine Pipe-Tabelle ist mit einem Split
|
||||
maschinenlesbar. Ein zweites Format bräuchte einen Generator, um wieder
|
||||
bei Markdown zu landen.
|
||||
|
||||
*Der Test ist der eigentliche Punkt:* Ohne ihn verrottet der Status. Mit
|
||||
ihm kann das Inventar nicht falsch werden, ohne dass die Suite rot wird.
|
||||
|
||||
### D6: `Unsupported`-Marker bleiben, aber nur noch für Phase 4
|
||||
|
||||
Die Marker verschwinden nicht — Ereignissteuerung, `SETUEVENT`, `MSGBOX`
|
||||
und `INPUTBOX$` behalten sie bis Phase 4. Der Abgleichstest aus D5 stellt
|
||||
sicher, dass genau die im Inventar als `offen` geführten Elemente noch
|
||||
Marker tragen.
|
||||
|
||||
### D7: Reihenfolge der Umsetzung
|
||||
|
||||
```
|
||||
1. Inventar (Sollseite) -> misst alles Folgende
|
||||
2. Puffer-Umzug + Host + Snapshot -> BREAKING, zusammen in einem Schritt
|
||||
3. Bildschirmanweisungen -> braucht 2
|
||||
4. Strings/Format/Datum/Mathematik -> unabhaengig, jederzeit
|
||||
5. Datei-E/A -> unabhaengig von 2/3
|
||||
6. Altlasten Phase 2 -> unabhaengig
|
||||
```
|
||||
|
||||
Schritt 2 ist der einzige Bruch und wird bewusst als eine Einheit
|
||||
umgesetzt: Puffer-Umzug, `Host`-Neuschnitt und Snapshot-Umstellung hängen
|
||||
so eng zusammen, dass ein Zwischenzustand die Suite ohnehin rot ließe.
|
||||
Schritte 4–6 können vorgezogen werden, falls Schritt 2 sich zieht.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Der `Host`-Bruch legt kurzzeitig die gesamte Suite lahm** → Schritt 2
|
||||
als eine Einheit umsetzen, nicht in Teilschritten; erst wenn Puffer,
|
||||
Host und Harness zusammen stehen, wird gemessen.
|
||||
- **Neu erzeugte Golden Files zementieren einen Fehler** → Der Diff der
|
||||
acht Dateien wird einzeln geprüft. Erwartung: `PRINT`-only-Programme
|
||||
ändern sich nicht. Jede Abweichung wird begründet oder ist ein Bug.
|
||||
- **Doppelzellen brechen bestehende Annahmen** (`POS` = Zeichenzahl,
|
||||
Zelle = Zeichen) → Korpustest mit CJK/Emoji inklusive Umbruch am
|
||||
rechten Rand, bevor die übrige Bildschirmarbeit darauf aufsetzt.
|
||||
- **`RND`-Kompatibilität ist Rekonstruktionsarbeit** — die Zahlenfolge
|
||||
des Vorbilds ist nur über die Dokumentation und Beobachtung greifbar →
|
||||
Algorithmus dokumentieren; gelingt die exakte Folge nicht, wird die
|
||||
Abweichung in der Sprachreferenz festgehalten statt still
|
||||
abzuweichen.
|
||||
- **UTF-32-Records machen Dateien binär inkompatibel zum Vorbild** →
|
||||
bewusst entschieden (2026-09-02); Preis ist, dass Originaldateien nicht
|
||||
gelesen werden können. Muss in der Sprachreferenz stehen.
|
||||
- **`SHELL`, `FILES`, Satzsperren und `LPRINT` sind plattformabhängig** →
|
||||
jede Abweichung wird in der Sprachreferenz unter „Abweichungen"
|
||||
benannt; stilles Abweichen ist laut Guiding Principle ein Bug.
|
||||
- **Das Inventar aus der Original-Hilfe zu erheben ist der stillste
|
||||
Aufwandstreiber** → als eigener erster Schritt geführt, damit sein
|
||||
Umfang sichtbar wird, statt sich in die Implementierungsarbeit zu
|
||||
mischen.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Inventar anlegen (rein additiv, kein Bruch).
|
||||
2. Bruchschritt in einem Zug: `TextScreen` nach `tb-runtime::screen`,
|
||||
`tb-ui` auf Adapterrolle, `Host` neu, Harness auf Snapshot, acht
|
||||
Golden Files neu erzeugen und Diff prüfen.
|
||||
3. Alles Weitere additiv — jedes neue Element wechselt im Inventar von
|
||||
`offen` auf `implementiert`, der Abgleichstest hält beides zusammen.
|
||||
|
||||
Rücknahme: Schritt 2 ist ein zusammenhängender Commit und als Ganzes
|
||||
rücknehmbar. Danach gibt es keinen weiteren Bruch.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Umfang der `KEY n`-Funktionstasten-Makros (`KEY LIST`/`ON`/`OFF`): Die
|
||||
Anweisung steht im Scope; welche Makro-Semantik das Vorbild genau
|
||||
zeigt, ist bei der Umsetzung aus der Original-Hilfe zu klären und in
|
||||
docs/sprachreferenz.md festzuhalten. Berührt weder Puffer noch Host
|
||||
noch Aufgabenschnitt.
|
||||
- Ersatzwert für `FRE` auf einer Plattform ohne DOS-Speichermodell: bei
|
||||
der Umsetzung zu wählen und zu dokumentieren.
|
||||
- Konkrete Gestalt der Schnittstelle, über die die Forms-Engine in
|
||||
`tb-ui` später eigene Zeichenbereiche im Puffer belegt und wieder
|
||||
freigibt: erst in Phase 4 zu klären, wenn die Steuerelemente stehen.
|
||||
Ändert weder Puffer noch Host noch Aufgabenschnitt dieses Changes.
|
||||
186
openspec/changes/phase-3-runtime-bildschirm/proposal.md
Normal file
186
openspec/changes/phase-3-runtime-bildschirm/proposal.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Phase 3 (Kern) — Laufzeitbibliothek, Bildschirm und Datei-E/A
|
||||
|
||||
## Why
|
||||
|
||||
Phase 2 hat eine lauffähige VM geliefert, aber nur eine dünne
|
||||
Bibliotheksscheibe: rund 50 dokumentierte Anweisungen und Funktionen sind
|
||||
im Frontend zwar typgeprüft, senken aber auf `HStmtKind::Unsupported`/
|
||||
`HExpr::Unsupported` ab und enden zur Laufzeit in Fehler 73. Betroffen
|
||||
sind alle Bildschirmanweisungen (`CLS`, `LOCATE`, `COLOR`, `VIEW PRINT`,
|
||||
`CSRLIN`, `POS`, `INKEY$`), die komplette Datei-E/A, `PRINT USING`,
|
||||
die Datums-/Zeitfunktionen und die System-Anweisungen. Klassische
|
||||
Konsolenprogramme des Vorbilds laufen damit nicht.
|
||||
|
||||
Zusätzlich fehlt das Messinstrument: die Leitplanke „Vollständigkeit ist
|
||||
das Soll" fordert ein Inventar aller Anweisungen/Funktionen mit
|
||||
Abdeckungsstatus als Abnahmekriterium für Phase 6 — bislang existiert nur
|
||||
die Implementierungsseite (die `Unsupported`-Marker), nicht die Sollseite
|
||||
aus der Original-Hilfe.
|
||||
|
||||
Dieser Change löst den Plan-Meilenstein „klassische Konsolenprogramme
|
||||
laufen unverändert" ein. ISAM ist als eigener Change `phase-3-isam`
|
||||
ausgegliedert und schließt Phase 3 ab.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Vollständigkeits-Inventar** (`docs/inventar.md`): Tabelle aller
|
||||
Anweisungen, Funktionen und Metabefehle des Vorbilds aus der
|
||||
Original-Hilfe mit Status `implementiert` / `offen` / `Non-Feature`
|
||||
und Fundstelle. Ein automatischer Test gleicht die Tabelle gegen
|
||||
`builtin_fn`/`builtin_stmt` und die `Unsupported`-Marker im Code ab;
|
||||
Divergenz schlägt fehl. Damit misst sich die Abdeckung selbst.
|
||||
- **Bildschirmpuffer wandert nach `tb-runtime`** (Entscheidung
|
||||
2026-09-03, Explore-Session): `Cell`/`TextScreen` mit der kompletten
|
||||
Bildschirmsemantik (Cursor, Farbattribute, Scrollen, `VIEW PRINT`,
|
||||
`resize`) ziehen ratatui-frei von `tb-ui::screen` nach
|
||||
`tb-runtime::screen`. In `tb-ui` bleiben nur die Ratatui-Anbindung
|
||||
(`basic_color`, `impl Widget`) und künftig der Terminal-Adapter.
|
||||
Begründung: `PRINT`, `LOCATE`, `CLS` und `SCREEN()` sind Elemente der
|
||||
Laufzeitbibliothek und müssen den Puffer erreichen; zugleich bleiben
|
||||
`tb-vm` und `tb-runtime` damit frei von einer Terminal-Abhängigkeit,
|
||||
sodass jedes Bildschirmverhalten ohne Terminal prüfbar ist. Die
|
||||
Forms-Engine bleibt laut PLAN.md in `tb-ui::forms` (Entscheidung
|
||||
2026-09-03) und zeichnet von dort in denselben Puffer — `tb-ui` hängt
|
||||
künftig an `tb-runtime`, nicht umgekehrt.
|
||||
- **BREAKING: `Host`-Trait neu geschnitten**: statt `write(&str)` /
|
||||
`read_line()` künftig Präsentation eines Zellenpuffers und
|
||||
Ereignisabfrage. `write`/`read_line` können den Bildschirmzustand
|
||||
nicht transportieren (`LOCATE`, `COLOR`, `CSRLIN`, `SCREEN(r,c)` liest
|
||||
vom Bildschirm zurück). Betrifft `ConsoleHost`, `CaptureHost` und
|
||||
`tbc run`.
|
||||
- **BREAKING: Korpusvergleich auf Bildschirm-Snapshot** (getrimmt):
|
||||
`.out`-Dateien halten künftig das Textbild des Puffers bis zur letzten
|
||||
belegten Zelle statt eines Byte-Streams; eine Attributebene kommt nur
|
||||
hinzu, wenn das Programm `COLOR` verwendet. Die acht bestehenden
|
||||
Golden Files werden neu erzeugt. Ein Byte-Stream kann `LOCATE`/`COLOR`
|
||||
nicht abbilden.
|
||||
- **Bildschirmanweisungen und -funktionen**: `CLS`, `COLOR`, `LOCATE`,
|
||||
`WIDTH`, `SCREEN` (Anweisung und Funktion), `VIEW PRINT`, `CSRLIN`,
|
||||
`POS`, `INKEY$`, `INPUT$`, `TAB`, `SPC`, `KEY` (inkl. Klärung des
|
||||
Umfangs der Funktionstasten-Makros `KEY LIST`/`ON`/`OFF`).
|
||||
- **80×25 ist nirgends eine feste Grenze** (Vorgabe des Projektinhabers
|
||||
2026-09-03, Priorität für das ganze Programm): 80×25 steht stets als
|
||||
Synonym für „voller Bildschirm". Kein beobachtbares Verhalten darf eine
|
||||
feste Spalten- oder Zeilenzahl voraussetzen — `CLS`, der
|
||||
voreingestellte Scrollbereich, Umbruch- und Scrollgrenzen sowie die
|
||||
Bereichsprüfungen von `LOCATE` und `SCREEN()` folgen den aktuellen
|
||||
Abmessungen. Größenänderungen während der Ausführung werden als
|
||||
Ereignis zugestellt und angepasst (Inhalt oben links erhalten, Cursor
|
||||
und `VIEW PRINT`-Bereich geklemmt). Das Mindestmaß 80×25 bleibt
|
||||
ausschließlich eine untere Schranke der Darstellung.
|
||||
- **Breite Unicode-Zeichen belegen zwei Zellen** (Entscheidung
|
||||
2026-09-02): Zellenmodell, Cursorfortschritt, `POS`/`LOCATE` und
|
||||
Umbruch am rechten Rand berücksichtigen die Darstellungsbreite
|
||||
(unicode-width). Ersetzt die bisherige Regel „1 Zeichen = 1 Zelle".
|
||||
- **Vollständige String- und Zahlenbibliothek**: die restlichen
|
||||
dokumentierten Stringfunktionen sowie `PRINT USING` und `FORMAT$`
|
||||
vollständig, inklusive Klärung des `%`-Präfixes bei Feldüberlauf.
|
||||
- **Datums- und Zeitfunktionen**: `NOW`, `DATESERIAL`, `TIMESERIAL`,
|
||||
`DATEVALUE`, `TIMEVALUE`, `DAY`, `MONTH`, `YEAR`, `WEEKDAY`, `HOUR`,
|
||||
`MINUTE`, `SECOND` sowie `DATE$`/`TIME$` als Anweisungsform.
|
||||
- **Finanzmathematik** (Befund aus Aufgabe 1.1, 2026-09-03 — im
|
||||
ursprünglichen Umfang übersehen): `FV#`, `PV#`, `Pmt#`, `IPmt#`,
|
||||
`PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`, `SYD#`,
|
||||
`DDB#`.
|
||||
- **Record-Konvertierungsfunktionen** (ebenfalls Befund 1.1):
|
||||
`MKI$`/`MKL$`/`MKS$`/`MKD$`/`MKC$` und `CVI`/`CVL`/`CVS`/`CVD`/`CVC`
|
||||
sowie die MBF-Varianten `MKSMBF$`/`MKDMBF$`/`CVSMBF`/`CVDMBF` — sie
|
||||
gehören zum `FIELD`-basierten wahlfreien Zugriff.
|
||||
- **Weitere Befunde aus 1.1**: `CURDIR$`, `DIR$`, `CHDRIVE`,
|
||||
`SHELL` als Funktion, `LPOS`, `STACK` (Anweisung und Funktion),
|
||||
`SYSTEM`, `ERDEV`/`ERDEV$`, `SetFormatCC`.
|
||||
- **Signaturen für alle Inventarelemente**: Die Semantik führt für jedes
|
||||
im Inventar geführte Element eine Signatur, auch wenn das
|
||||
Laufzeitverhalten noch aussteht — sonst enden dokumentierte Elemente
|
||||
in „unbekannter Bezeichner" statt in einer namentlichen Ablehnung.
|
||||
- **Mathematik vervollständigen**: kompatibler `RND`/`RANDOMIZE`-PRNG
|
||||
(gleiche Zahlenfolge wie das Vorbild bei gleichem Startwert),
|
||||
Rundungsverhalten, Integer-Überlauf → Fehler 6.
|
||||
- **Datei-E/A**: `OPEN` in beiden Syntaxen (sequenziell, random, binär),
|
||||
`CLOSE`, `RESET`, `PRINT #`, `WRITE #`, `INPUT #`, `LINE INPUT #`,
|
||||
`GET`, `PUT`, `FIELD`, `LSET`, `RSET`, `SEEK` (Anweisung und
|
||||
Funktion), `EOF`, `LOF`, `LOC`, `FREEFILE`, `FILEATTR`, `LOCK`/
|
||||
`UNLOCK`, `NAME … AS`, `LPRINT`. Feste Strings in Records als UTF-32
|
||||
(Entscheidung 2026-09-02, 4 Bytes/Zeichen, feste Recordlänge).
|
||||
- **System-Anweisungen**: `FILES`, `KILL`, `CHDIR`, `MKDIR`, `RMDIR`,
|
||||
`SHELL`, `ENVIRON`/`ENVIRON$`, `CLEAR`, `FRE`, `TRON`/`TROFF`,
|
||||
plattformübergreifende Pfadsemantik.
|
||||
- **Drei Altlasten aus Phase 2**:
|
||||
- `ON ERROR GOTO`-Scoping: ein Handler-Label auf Modulebene ist auch
|
||||
aus Prozeduren heraus ansprechbar; heute wird nicht-lokales
|
||||
`ON ERROR` in einer Prozedur fälschlich wie `ON LOCAL ERROR`
|
||||
behandelt (`sema.rs`, `lower_stmt`/`OnError`).
|
||||
- DATA-Rohtext: unquotierte `DATA`-Elemente verlieren die Groß-/
|
||||
Kleinschreibung, weil der Lexer Bezeichner normalisiert.
|
||||
- `TODO verify`-Zellen der Konvertierungsmatrix in
|
||||
`docs/tbvm-design.md` gegen die Original-Hilfe auflösen
|
||||
(CURRENCY-Promotion mit Gleitkomma, `^`-Ergebnistyp,
|
||||
Exponentialform-Schwellen, CURRENCY-Ausgabeformat).
|
||||
|
||||
**Non-Goals:** ISAM (eigener Change `phase-3-isam`, schließt Phase 3 ab);
|
||||
Forms-Engine, Ereignisschleife und Ereignissteuerung (`TIMER/KEY(n) ON|
|
||||
OFF|STOP`, `ON SIGNAL`/`SIGNAL`, `SETUEVENT`, `MSGBOX`, `INPUTBOX$` —
|
||||
Phase 4); IDE und
|
||||
Debugger (Phase 5); `tbc build --exe` (Phase 6). Die Forms-Engine bleibt
|
||||
laut PLAN.md in `tb-ui::forms` (Entscheidung 2026-09-03); dieser Change
|
||||
schafft ihr mit dem Puffer in `tb-runtime` nur die Zeichenfläche.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `datei-eio`: Dateiverwaltung und Datenübertragung — `OPEN`/`CLOSE`,
|
||||
die drei Zugriffsarten (sequenziell, random, binär), Record-Layout mit
|
||||
UTF-32-Festtextfeldern, `FIELD`/`LSET`/`RSET`, Positions- und
|
||||
Statusfunktionen (`EOF`/`LOF`/`LOC`/`SEEK`/`FREEFILE`/`FILEATTR`),
|
||||
Sperren, Dateisystem-Anweisungen und plattformübergreifende
|
||||
Pfadsemantik.
|
||||
- `sprachinventar`: das Vollständigkeits-Inventar als prüfbares
|
||||
Artefakt — Aufbau der Tabelle, Statusvokabular, Pflicht zur
|
||||
Vollständigkeit gegenüber der Original-Hilfe und der automatische
|
||||
Abgleich gegen den Code.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `textbildschirm`: Der Zellenpuffer SHALL in `tb-runtime` liegen und
|
||||
ratatui-frei sein; das Unicode-Zellenmodell wechselt von „1 Zeichen =
|
||||
1 Zelle" auf Darstellungsbreite (breite Zeichen belegen zwei Zellen);
|
||||
neue Anforderung für die Bildschirmanweisungen des Dialekts auf dem
|
||||
Puffer.
|
||||
- `konsolen-basisbibliothek`: Der `Host`-Trait wechselt vom Stream- auf
|
||||
ein Bildschirmmodell (Präsentation + Ereignisabfrage); die
|
||||
Funktionsliste wird von der Korpusscheibe auf die vollständige
|
||||
String-, Zahlen-, Formatierungs- (`PRINT USING`) und Datums-/
|
||||
Zeitbibliothek erweitert; `RND` erhält eine Kompatibilitätsanforderung.
|
||||
- `kompat-testkorpus`: Der Laufzeitvergleich wechselt vom Byte-Stream
|
||||
auf einen getrimmten Bildschirm-Snapshot; neue Abdeckungsanforderung
|
||||
für Bildschirm-, Formatierungs- und Datei-E/A-Semantik.
|
||||
- `sprach-frontend`: `ON ERROR GOTO` auf Modulebene SHALL aus
|
||||
Prozeduren heraus ansprechbar sein (bisher fälschlich lokal);
|
||||
unquotierte `DATA`-Elemente SHALL ihren Rohtext behalten.
|
||||
|
||||
## Impact
|
||||
|
||||
- `crates/tb-ui`: `screen.rs` verliert Puffer und Semantik an
|
||||
`tb-runtime`; zurück bleiben `basic_color` und `impl Widget` als
|
||||
Adapter auf den Runtime-Puffer. Neue Abhängigkeit `tb-runtime`
|
||||
(bisher waren beide Blätter im Abhängigkeitsgraphen); `forms.rs`
|
||||
bleibt als Platzhalter für Phase 4 unberührt.
|
||||
- `crates/tb-runtime`: neues `screen`-Modul; `host.rs` neu geschnitten
|
||||
(**BREAKING**); `strings`, `format`, `math`, `datetime`, `fileio`,
|
||||
`builtins` von Stub/Scheibe auf Vollimplementierung; neue
|
||||
Abhängigkeit `unicode-width`.
|
||||
- `crates/tb-frontend`: rund 50 `Unsupported`-Marker weichen echter
|
||||
Absenkung; rund 30 bislang signaturlose Elemente (Finanzmathematik,
|
||||
`MK…$`/`CV…`, `CURDIR$`, `DIR$`, `CHDRIVE`, `LPOS`, `STACK`, `SYSTEM`,
|
||||
`ERDEV`/`ERDEV$`, `SetFormatCC`) erhalten Signaturen; `sema.rs` (`ON ERROR`-Scoping), `lexer.rs` (DATA-Rohtext).
|
||||
- `crates/tb-vm`: neue Builtin-Indizes; `Instr::Unsupported` bleibt für
|
||||
die verbleibenden Phase-4-Marker bestehen.
|
||||
- `crates/tb-cli`: `tbc run` auf den neuen `Host`; Harness auf
|
||||
Snapshot-Vergleich (**BREAKING** für die acht bestehenden `.out`).
|
||||
- `tests/compat`: acht Golden Files neu erzeugt, neue Korpusdateien für
|
||||
Bildschirm, `PRINT USING`, Datei-E/A, Datum/Zeit, breite Zeichen.
|
||||
- `docs/`: neu `docs/inventar.md` und `docs/bibliothek.md`;
|
||||
fortgeschrieben `docs/sprachreferenz.md` (Doppelzellen, `KEY n`,
|
||||
`PRINT USING`-Überlauf) und `docs/tbvm-design.md`
|
||||
(Konvertierungsmatrix). PLAN.md-Haken folgen bei Abschluss.
|
||||
@@ -0,0 +1,295 @@
|
||||
# Rohliste — Themen der Original-Hilfe (Aufgabe 1.1)
|
||||
|
||||
**Quelle:** `bas7advr.hlp` (BASIC 7 Advisor, Sprachreferenz der
|
||||
Professional Edition), Themenliste unter
|
||||
https://dos-help.soulsphere.org/bas7advr.hlp/TOPIC_LIST.html
|
||||
(abgerufen 2026-09-03).
|
||||
|
||||
**Nicht verwendet:** `vbdpss.hlp` ist eine Knowledge-Base mit
|
||||
Artikelnummern und Support-Adressen, keine Sprachreferenz — geprüft und
|
||||
verworfen.
|
||||
|
||||
Die Liste ist wortgetreu übernommen. Sie enthält neben Sprachthemen auch
|
||||
Unterseiten (`SCREEN Mode …`, `DRAW Statement Macro Language`,
|
||||
`PLAY Statement (Music) — …`) und einen Anker (`@L8000`); diese sind
|
||||
keine eigenen Sprachelemente und gehen nicht als Inventareintrag ein.
|
||||
|
||||
## Themen (wortgetreu)
|
||||
|
||||
$INCLUDE Metacommand
|
||||
$STATIC and $DYNAMIC Metacommands
|
||||
@L8000
|
||||
ABS Function
|
||||
ASC Function
|
||||
ATN Function
|
||||
Absolute Routine
|
||||
BEGINTRANS Statement
|
||||
BLOAD Statement
|
||||
BOF Function
|
||||
BSAVE Statement
|
||||
CALL Statement (BASIC Procedures)
|
||||
CALL, CALLS Statements (Non-BASIC Procedures)
|
||||
CCUR Function
|
||||
CDBL Function
|
||||
CHAIN Statement
|
||||
CHDIR Statement
|
||||
CHDRIVE Statement
|
||||
CHR$ Function
|
||||
CINT Function
|
||||
CIRCLE Statement
|
||||
CLEAR Statement
|
||||
CLNG Function
|
||||
CLOSE Statement
|
||||
CLS Statement
|
||||
COLOR Statement
|
||||
COM Statements
|
||||
COMMAND$ Function
|
||||
COMMITTRANS Statement
|
||||
COMMON Statement
|
||||
CONST Statement
|
||||
COS Function
|
||||
CREATEINDEX Statement
|
||||
CSNG Function
|
||||
CURDIR$ Function
|
||||
CVI, CVL, CVS, CVD, and CVC Functions
|
||||
CVSMBF, CVDMBF Function
|
||||
DATA Statement
|
||||
DATE$ Statement
|
||||
DDB# Function
|
||||
DECLARE Statement (BASIC Procedures)
|
||||
DECLARE Statement (Non-BASIC Procedures)
|
||||
DEF FN Statement
|
||||
DEF SEG Statement
|
||||
DEFtype Statement
|
||||
DELETE Statement
|
||||
DELETEINDEX Statement
|
||||
DELETETABLE Statement
|
||||
DIM Statement
|
||||
DIR$ Function
|
||||
DO...LOOP Statement
|
||||
DRAW Statement - Differences from BASICA
|
||||
DRAW Statement Macro Language
|
||||
DateSerial# Function
|
||||
DateValue# Function
|
||||
Day& Function
|
||||
END Statement
|
||||
ENVIRON Statement
|
||||
ENVIRON$ Function
|
||||
EOF Function
|
||||
ERASE Statement
|
||||
ERDEV, ERDEV$ Function
|
||||
ERR Statement
|
||||
ERR, ERL Functions
|
||||
ERROR Statement
|
||||
EVENT Statements
|
||||
EXIT Statement
|
||||
EXP Function
|
||||
FIELD Statement
|
||||
FILEATTR Function
|
||||
FILES Statement
|
||||
FIX Function
|
||||
FOR...NEXT Statement
|
||||
FRE Function
|
||||
FREEFILE Function
|
||||
FUNCTION Statement
|
||||
FV# Function
|
||||
FormatX$ Functions
|
||||
GET Statement (File I/O)
|
||||
GET Statement (Graphics)
|
||||
GETINDEX$ Function
|
||||
GOSUB...RETURN Statement
|
||||
GOTO Statement
|
||||
HEX$ Function
|
||||
Hour& Function
|
||||
IF...THEN...ELSE Statement
|
||||
INKEY$ Function
|
||||
INP Function
|
||||
INPUT # Statement
|
||||
INPUT Statement
|
||||
INPUT$ Function
|
||||
INSERT Statement
|
||||
INSTR Function
|
||||
INT Function
|
||||
IOCTL Statement
|
||||
IOCTL$ Function
|
||||
IPmt# Function
|
||||
IRR# Function
|
||||
Interrupt, InterruptX Routines
|
||||
KEY Statements (Assignment)
|
||||
KEY Statements (Event Trapping)
|
||||
KILL Statement
|
||||
LBOUND Function
|
||||
LCASE$ Function
|
||||
LEFT$ Function
|
||||
LEN Function
|
||||
LET Statement
|
||||
LINE INPUT # Statement
|
||||
LINE INPUT Statement
|
||||
LINE Statement
|
||||
LOC Function
|
||||
LOCATE Statement
|
||||
LOCK...UNLOCK Statement
|
||||
LOF Function
|
||||
LOG Function
|
||||
LPOS Function
|
||||
LPRINT Statement
|
||||
LPRINT USING Statement
|
||||
LSET Statement
|
||||
LTRIM$ Function
|
||||
MID$ Function
|
||||
MID$ Statement
|
||||
MIRR# Function
|
||||
MKDIR Statement
|
||||
MKI$, MKL$, MKS$, MKD$, and MKC$ Functions
|
||||
MKSMBF$, MKDMBF$ Functions
|
||||
MOVEFIRST, MOVELAST, MOVENEXT, MOVEPREVIOUS Statements
|
||||
Minute& Function
|
||||
Month& Function
|
||||
NAME Statement
|
||||
NPV# Function
|
||||
NPer# Function
|
||||
Now# Function
|
||||
OCT$ Function
|
||||
ON COM Statement
|
||||
ON ERROR Statement
|
||||
ON KEY Statement
|
||||
ON PEN Statement
|
||||
ON PLAY Statement
|
||||
ON SIGNAL Statement
|
||||
ON STRIG Statement
|
||||
ON TIMER Statement
|
||||
ON UEVENT Statement
|
||||
ON...GOSUB and ON...GOTO Statement
|
||||
OPEN COM Statement
|
||||
OPEN Statement (File I/O)
|
||||
OPTION BASE Statement
|
||||
OUT Statement
|
||||
PAINT Statement
|
||||
PALETTE, PALETTE USING Statements
|
||||
PCOPY Statement
|
||||
PEEK Function
|
||||
PEN Function
|
||||
PEN Statements
|
||||
PLAY Statement (Music)
|
||||
PLAY Statements (Event Trapping)
|
||||
PMAP Function
|
||||
POINT Function
|
||||
POKE Statement
|
||||
POS Function
|
||||
PPmt# Function
|
||||
PRESET Statement
|
||||
PRINT # Statement
|
||||
PRINT Statement
|
||||
PRINT USING Statement
|
||||
PSET Statement
|
||||
PUT Statement (File I/O)
|
||||
PUT Statement (Graphics)
|
||||
PV# Function
|
||||
Pmt# Function
|
||||
RANDOMIZE Statement
|
||||
READ Statement
|
||||
REDIM Statement
|
||||
REM Statement
|
||||
RESTORE Statement
|
||||
RESUME Statement
|
||||
RETRIEVE Statement
|
||||
RETURN Statement
|
||||
RIGHT$ Function
|
||||
RMDIR Statement
|
||||
RND Function
|
||||
ROLLBACK, ROLLBACK ALL Statements
|
||||
RSET Statement
|
||||
RTRIM$ Function
|
||||
RUN Statement
|
||||
Rate# Function
|
||||
SADD Function
|
||||
SAVEPOINT Function
|
||||
SCREEN Function
|
||||
SCREEN Statement
|
||||
SEEK Function
|
||||
SEEK Statement
|
||||
SEEKGT, SEEKGE, SEEKEQ Statements
|
||||
SELECT CASE Statement
|
||||
SETINDEX Statement
|
||||
SETMEM Function
|
||||
SHARED Statement
|
||||
SHELL Function
|
||||
SHELL Statement
|
||||
SIGNAL Statements
|
||||
SIN Function
|
||||
SLEEP Statement
|
||||
SLN# Function
|
||||
SOUND Statement
|
||||
SPC Function
|
||||
SSEG Function
|
||||
SSEGADD Function
|
||||
STACK Function
|
||||
STACK Statement
|
||||
STATIC Statement
|
||||
STICK Function
|
||||
STOP Statement
|
||||
STR$ Function
|
||||
STRIG Function
|
||||
STRIG Statements
|
||||
SUB Statement
|
||||
SWAP Statement
|
||||
SYD# Function
|
||||
SYSTEM Statement
|
||||
Second& Function
|
||||
SetFormatCC Routine
|
||||
SetUEvent Routine
|
||||
StringAddress Routine
|
||||
StringAssign Routine
|
||||
StringLength Routine
|
||||
StringRelease Routine
|
||||
TAB Function
|
||||
TAN Function
|
||||
TIME$ Function
|
||||
TIME$ Statement
|
||||
TIMER Function
|
||||
TIMER Statements
|
||||
TRON/TROFF Statements
|
||||
TYPE Statement
|
||||
TimeSerial# Function
|
||||
TimeValue# Function
|
||||
UBOUND Function
|
||||
UCASE$ Function
|
||||
UEVENT Statements
|
||||
UNLOCK Statement
|
||||
UPDATE Statement
|
||||
VAL Function
|
||||
VARPTR$ Function
|
||||
VARPTR, VARSEG Functions
|
||||
VIEW PRINT Statement
|
||||
VIEW Statement (Graphics)
|
||||
WAIT Statement
|
||||
WHILE...WEND Statement
|
||||
WIDTH Statements
|
||||
WINDOW Statement
|
||||
WRITE # Statement
|
||||
WRITE Statement
|
||||
Weekday& Function
|
||||
Year& Function
|
||||
|
||||
## Gegenprobe (Aufgabe 1.1, Befund C)
|
||||
|
||||
`bas7advr.hlp/TOPIC_LIST.html` ist **nicht vollständig**. Die Gegenprobe
|
||||
gegen `qb45advr.hlp/TOPIC_LIST.html` (abgerufen 2026-09-03) belegt das:
|
||||
diese Liste führt interne Ankernamen statt Klarnamen, enthält aber
|
||||
Einträge, zu denen `bas7advr` kein Thema führt.
|
||||
|
||||
| Anker in `qb45advr` | Element | in `bas7advr`-Themenliste |
|
||||
|---|---|---|
|
||||
| `.sgnr` / `.sgnx` | `SGN` | fehlt |
|
||||
| `.sqrr` / `.sqrx` | `SQR` | fehlt |
|
||||
| `.sp$r` / `.sp$x` | `SPACE$` | fehlt |
|
||||
| `.str$$$r` / `.str$$$x` | `STRING$` | fehlt |
|
||||
| `.csrlinr` / `.csrlinx` | `CSRLIN` | fehlt |
|
||||
| `.beepr` / `.beepx` | `BEEP` | fehlt |
|
||||
| `.datef` | `DATE$` (Funktion) | nur `DATE$ Statement` |
|
||||
|
||||
**Folge für das Inventar:** Maßgeblich ist die Vereinigungsmenge aus
|
||||
beiden Themenlisten und `docs/sprachreferenz.md`. Jeder Inventareintrag
|
||||
führt seine Fundstelle; Einträge, die nur über die Gegenprobe belegt
|
||||
sind, nennen `qb45advr`. Die Zählung einer einzelnen Themenliste ist
|
||||
kein hinreichender Vollständigkeitsnachweis.
|
||||
@@ -0,0 +1,173 @@
|
||||
## Purpose
|
||||
Die Datei-E/A stellt die Dateiverwaltung des Dialekts bereit: sequenzieller,
|
||||
wahlfreier und binärer Zugriff über Dateinummern, Record-Puffer mit
|
||||
Festtextfeldern, Positions- und Statusfunktionen sowie die
|
||||
Dateisystem-Anweisungen — plattformübergreifend und mit den
|
||||
Laufzeitfehlern des Vorbilds.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Dateinummern und Öffnen in drei Zugriffsarten
|
||||
`OPEN` SHALL in beiden Syntaxen des Vorbilds (Klausel- und Kurzform) die
|
||||
Zugriffsarten sequenziell (`INPUT`, `OUTPUT`, `APPEND`), wahlfrei
|
||||
(`RANDOM`) und binär (`BINARY`) unterstützen und die Datei an eine
|
||||
Dateinummer binden. `FREEFILE` SHALL die kleinste freie Dateinummer
|
||||
liefern. `CLOSE` ohne Argument SHALL alle offenen Dateien schließen,
|
||||
`RESET` ebenso. Eine bereits belegte Dateinummer MUST Laufzeitfehler 55
|
||||
„File already open" auslösen, eine nicht geöffnete Nummer Fehler 52
|
||||
„Bad file name or number", eine fehlende Datei bei `INPUT` Fehler 53
|
||||
„File not found".
|
||||
|
||||
#### Scenario: Sequenzielles Schreiben und Lesen
|
||||
- **WHEN** eine Datei mit `OPEN "d.txt" FOR OUTPUT AS #1` beschrieben, geschlossen und mit `FOR INPUT` wieder geöffnet wird
|
||||
- **THEN** liefert das Lesen genau die geschriebenen Zeilen
|
||||
|
||||
#### Scenario: Doppeltes Öffnen derselben Nummer
|
||||
- **WHEN** `OPEN` eine bereits offene Dateinummer verwendet
|
||||
- **THEN** tritt Laufzeitfehler 55 auf
|
||||
|
||||
#### Scenario: Freie Dateinummer
|
||||
- **WHEN** `#1` offen ist und `FREEFILE` aufgerufen wird
|
||||
- **THEN** liefert `FREEFILE` eine Nummer, die noch nicht belegt ist
|
||||
|
||||
### Requirement: Sequenzielle Datenübertragung
|
||||
`PRINT #` SHALL wie `PRINT` formatieren (Druckzonen, Zahlendarstellung mit
|
||||
Vorzeichenspalte), `WRITE #` SHALL Elemente komma-getrennt mit Strings in
|
||||
Anführungszeichen schreiben. `INPUT #` SHALL komma-getrennte Felder lesen
|
||||
und dabei Anführungszeichen auswerten, `LINE INPUT #` die ganze Zeile ohne
|
||||
Zeilenende. `EOF` SHALL das Erreichen des Dateiendes melden; Lesen über das
|
||||
Dateiende hinaus MUST Laufzeitfehler 62 „Input past end of file" auslösen.
|
||||
|
||||
#### Scenario: WRITE # und INPUT # sind gegenläufig
|
||||
- **WHEN** `WRITE #1, "a,b", 5` geschrieben und die Zeile mit `INPUT #1, s$, n` gelesen wird
|
||||
- **THEN** enthält `s$` den Text `a,b` und `n` den Wert 5
|
||||
|
||||
#### Scenario: Lesen über das Dateiende
|
||||
- **WHEN** `INPUT #1, x` nach dem letzten Datensatz ausgeführt wird
|
||||
- **THEN** tritt Laufzeitfehler 62 auf
|
||||
|
||||
### Requirement: Wahlfreier Zugriff mit Record-Puffern
|
||||
`OPEN … FOR RANDOM … LEN = n` SHALL Datensätze fester Länge bereitstellen.
|
||||
`GET` und `PUT` SHALL einen Datensatz an einer 1-basierten Recordnummer
|
||||
lesen bzw. schreiben; ohne Nummer gilt der Satz nach der letzten Operation.
|
||||
`FIELD` SHALL den Recordpuffer in benannte Festtextfelder aufteilen,
|
||||
`LSET`/`RSET` SHALL links- bzw. rechtsbündig in ein Feld schreiben und
|
||||
überlange Werte abschneiden. Alternativ SHALL `GET`/`PUT` mit einer
|
||||
Variablen eines benutzerdefinierten Typs arbeiten.
|
||||
|
||||
#### Scenario: Datensatz schreiben und an gleicher Stelle lesen
|
||||
- **WHEN** ein Record an Position 3 geschrieben und anschließend von Position 3 gelesen wird
|
||||
- **THEN** stimmen alle Feldwerte mit den geschriebenen überein
|
||||
|
||||
#### Scenario: RSET schneidet ab
|
||||
- **WHEN** in ein Feld der Länge 5 mit `RSET` der Text `ABCDEFG` geschrieben wird
|
||||
- **THEN** enthält das Feld genau 5 Zeichen
|
||||
|
||||
### Requirement: Record-Konvertierungsfunktionen
|
||||
Die Bibliothek SHALL die Konvertierungsfunktionen für Recordpuffer
|
||||
bereitstellen: `MKI$`, `MKL$`, `MKS$`, `MKD$` und `MKC$` wandeln einen
|
||||
numerischen Wert in seine Byte-Darstellung als String, `CVI`, `CVL`,
|
||||
`CVS`, `CVD` und `CVC` wandeln zurück. Jedes Paar MUST gegenläufig sein.
|
||||
Ein Argument, dessen Länge nicht zur Zielbreite passt, MUST
|
||||
Laufzeitfehler 5 „Illegal function call" auslösen. `MKSMBF$`, `MKDMBF$`,
|
||||
`CVSMBF` und `CVDMBF` SHALL dieselbe Rolle für das
|
||||
Microsoft-Binärformat älterer BASIC-Versionen erfüllen; ist dieses
|
||||
Format ein deklariertes Non-Feature, MUST die Ablehnung namentlich
|
||||
erfolgen und in der Sprachreferenz unter „Abweichungen" stehen.
|
||||
|
||||
#### Scenario: MK und CV sind gegenläufig
|
||||
- **WHEN** `CVL(MKL$(123456))` ausgewertet wird
|
||||
- **THEN** ist das Ergebnis 123456
|
||||
|
||||
#### Scenario: Falsche Argumentlänge
|
||||
- **WHEN** `CVI("x")` mit einem ein Zeichen langen String ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: Festtextfelder als UTF-32 mit fester Recordlänge
|
||||
Feste Strings (`STRING * n`) in Recordpuffern SHALL als UTF-32 gespeichert
|
||||
werden — 4 Bytes je Zeichen, damit `STRING * n` stets genau `4 * n` Bytes
|
||||
belegt und die Recordlänge unabhängig vom Zeicheninhalt konstant bleibt.
|
||||
Die daraus folgende Binär-Inkompatibilität zu Dateien des Vorbilds ist
|
||||
beschlossen (2026-09-02) und MUST in der Sprachreferenz unter
|
||||
„Abweichungen" dokumentiert sein.
|
||||
|
||||
#### Scenario: Recordlänge unabhängig vom Inhalt
|
||||
- **WHEN** in ein Feld `STRING * 10` einmal `abc` und einmal `äöüßÄÖÜ` geschrieben wird
|
||||
- **THEN** belegt der Datensatz in beiden Fällen dieselbe Byteanzahl und der Folgesatz beginnt an derselben Position
|
||||
|
||||
### Requirement: Binärzugriff auf Byteebene
|
||||
`OPEN … FOR BINARY` SHALL byteweise Positionierung erlauben. `GET`/`PUT`
|
||||
SHALL an einer 1-basierten Byteposition lesen bzw. schreiben, wobei die
|
||||
übertragene Länge der Größe der Zielvariablen entspricht. `SEEK` als
|
||||
Anweisung SHALL die Position setzen, `SEEK` als Funktion sie liefern.
|
||||
|
||||
#### Scenario: Positioniertes Byte überschreiben
|
||||
- **WHEN** in einer Binärdatei an Position 4 ein Byte geschrieben und anschließend gelesen wird
|
||||
- **THEN** liefert das Lesen den geschriebenen Wert und die übrigen Bytes bleiben unverändert
|
||||
|
||||
### Requirement: Positions- und Statusfunktionen
|
||||
`LOF` SHALL die Dateigröße in Bytes liefern, `LOC` die aktuelle Position
|
||||
in der Einheit der Zugriffsart (Datensatz bei `RANDOM`, Byte bei
|
||||
`BINARY`), `SEEK` die nächste Lese-/Schreibposition und `FILEATTR` den
|
||||
Modus bzw. das Betriebssystem-Handle. Alle Funktionen MUST bei einer nicht
|
||||
geöffneten Dateinummer Laufzeitfehler 52 auslösen.
|
||||
|
||||
#### Scenario: Größe nach dem Schreiben
|
||||
- **WHEN** 100 Bytes geschrieben wurden und `LOF(1)` aufgerufen wird
|
||||
- **THEN** liefert `LOF(1)` den Wert 100
|
||||
|
||||
#### Scenario: Statusfunktion auf geschlossener Datei
|
||||
- **WHEN** `LOF(9)` ohne offene Datei 9 aufgerufen wird
|
||||
- **THEN** tritt Laufzeitfehler 52 auf
|
||||
|
||||
### Requirement: Satzsperren
|
||||
`LOCK` und `UNLOCK` SHALL einen Datensatz-, Byte- oder den gesamten
|
||||
Dateibereich sperren und wieder freigeben. Auf Plattformen ohne
|
||||
entsprechende Sperrmechanik MUST das Verhalten in der Sprachreferenz unter
|
||||
„Abweichungen" dokumentiert sein statt still abzuweichen.
|
||||
|
||||
#### Scenario: Sperren und Freigeben
|
||||
- **WHEN** ein Datensatzbereich gesperrt und anschließend mit denselben Grenzen freigegeben wird
|
||||
- **THEN** läuft die Folgeoperation auf diesem Bereich ohne Fehler
|
||||
|
||||
### Requirement: Dateisystem-Anweisungen mit plattformübergreifender Pfadsemantik
|
||||
`KILL`, `NAME … AS`, `FILES`, `CHDIR`, `CHDRIVE`, `MKDIR`, `RMDIR`,
|
||||
`CURDIR$`, `DIR$` sowie `SHELL` als Anweisung **und** als Funktion SHALL
|
||||
verfügbar sein. `CURDIR$` SHALL das aktuelle Verzeichnis liefern, `DIR$`
|
||||
den ersten bzw. — bei leerem Argument — den nächsten Treffer eines
|
||||
Suchmusters und den leeren String, wenn keiner mehr folgt. Die
|
||||
`SHELL`-Funktion SHALL den Beendigungsstatus des aufgerufenen Prozesses
|
||||
liefern. `CHDRIVE` SHALL auf Plattformen ohne Laufwerksbuchstaben
|
||||
namentlich abgewiesen oder als Non-Feature dokumentiert werden. Pfade SHALL sowohl `\` als auch `/` als Trennzeichen
|
||||
akzeptieren und auf die Konvention der laufenden Plattform abgebildet
|
||||
werden; Laufwerksbuchstaben SHALL auf Nicht-Windows-Plattformen als
|
||||
gewöhnlicher Namensbestandteil behandelt oder mit benannter Ablehnung
|
||||
abgewiesen werden. Fehlende Dateien MUST Fehler 53 auslösen, fehlende
|
||||
Pfade Fehler 76 „Path not found".
|
||||
|
||||
#### Scenario: Rückwärts-Schrägstrich auf Unix
|
||||
- **WHEN** `OPEN "unter\d.txt" FOR OUTPUT AS #1` auf einer Unix-Plattform ausgeführt wird
|
||||
- **THEN** entsteht die Datei `d.txt` im Verzeichnis `unter`
|
||||
|
||||
#### Scenario: Verzeichnis durchlaufen
|
||||
- **WHEN** `DIR$("*.txt")` und danach `DIR$("")` wiederholt ausgewertet werden
|
||||
- **THEN** liefern sie nacheinander alle passenden Namen und zuletzt den leeren String
|
||||
|
||||
#### Scenario: Fehlender Pfad
|
||||
- **WHEN** `MKDIR` in einem nicht existierenden Elternverzeichnis aufgerufen wird
|
||||
- **THEN** tritt Laufzeitfehler 76 auf
|
||||
|
||||
### Requirement: LPRINT als Druckausgabe
|
||||
`LPRINT` und `LPRINT USING` SHALL wie `PRINT` formatieren und in einen
|
||||
Druckerkanal schreiben. `LPOS` SHALL die aktuelle Spalte im
|
||||
Druckerpuffer liefern. Existiert auf der Plattform kein Druckerkanal,
|
||||
MUST das Ziel (Datei, Standardausgabe oder Ablehnung) in der
|
||||
Sprachreferenz unter „Abweichungen" dokumentiert sein.
|
||||
|
||||
#### Scenario: LPRINT formatiert wie PRINT
|
||||
- **WHEN** `LPRINT 1; 2` ausgeführt wird
|
||||
- **THEN** entspricht der erzeugte Text der Ausgabe von `PRINT 1; 2`
|
||||
|
||||
#### Scenario: Druckerspalte
|
||||
- **WHEN** nach `LPRINT "abc";` die Funktion `LPOS(0)` ausgewertet wird
|
||||
- **THEN** liefert sie die Spalte hinter dem ausgegebenen Text
|
||||
@@ -0,0 +1,85 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Korpusabdeckung der Phase-3-Semantik
|
||||
Der Korpus SHALL um Referenzprogramme mit dokumentierter Sollausgabe für
|
||||
die Bildschirmsteuerung (`LOCATE`, `COLOR`, `CLS`, `VIEW PRINT`-Scrollen,
|
||||
`CSRLIN`/`POS`, Zurücklesen per `SCREEN`), die Formatierung
|
||||
(`PRINT USING` inklusive Feldüberlauf), breite Unicode-Zeichen
|
||||
(Doppelzellen, Umbruch am rechten Rand), Datum und Zeit sowie die
|
||||
Datei-E/A in allen drei Zugriffsarten erweitert werden. Dateiprogramme
|
||||
MUST in einem temporären Arbeitsverzeichnis laufen und dürfen keine
|
||||
Artefakte im Projektbaum hinterlassen.
|
||||
|
||||
#### Scenario: Bildschirmsteuerung als Korpustest
|
||||
- **WHEN** die Testsuite läuft
|
||||
- **THEN** existiert ein Korpusprogramm, das mit `LOCATE` und `COLOR` an definierten Positionen ausgibt, und sein Snapshot entspricht der Sollausgabe
|
||||
|
||||
#### Scenario: Datei-Korpustest hinterlässt nichts
|
||||
- **WHEN** ein Datei-E/A-Korpusprogramm gelaufen ist
|
||||
- **THEN** ist das Arbeitsverzeichnis wieder entfernt und der Projektbaum unverändert
|
||||
|
||||
### Requirement: Nachweis der Größenunabhängigkeit
|
||||
Die Bildschirmgröße des Test-Hosts SHALL je Korpusprogramm explizit
|
||||
festgelegt und in der Sollausgabe vermerkt sein; ein Vorgabewert MUST NOT
|
||||
stillschweigend gelten. Mindestens ein Korpusprogramm SHALL bei zwei
|
||||
verschiedenen Bildschirmgrößen laufen und je Größe eine eigene
|
||||
Sollausgabe besitzen, um nachzuweisen, dass Löschen, Umbruch, Scrollen
|
||||
und Cursorgrenzen der jeweiligen Größe folgen. Mindestens ein
|
||||
Korpusprogramm SHALL eine Größenänderung während der Ausführung
|
||||
durchlaufen und danach die neuen Grenzen ausnutzen.
|
||||
|
||||
#### Scenario: Gleiches Programm bei zwei Größen
|
||||
- **WHEN** dasselbe Bildschirm-Korpusprogramm bei 80×25 und bei 120×40 läuft
|
||||
- **THEN** stimmt jede Ausgabe mit der Sollausgabe ihrer Größe überein und die Sollausgaben unterscheiden sich in Umbruch- und Scrollverhalten
|
||||
|
||||
#### Scenario: Größenänderung mitten im Programm
|
||||
- **WHEN** der Test-Host während der Ausführung von 80×25 auf 120×40 wechselt
|
||||
- **THEN** bleibt der bisherige Inhalt oben links erhalten und die anschließende Ausgabe nutzt die neuen Grenzen
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Korpusdateien mit byte-genauer Sollausgabe
|
||||
Jedes Korpusprogramm `tests/compat/<name>.bas` SHALL eine `<name>.out`
|
||||
mit dem exakten Sollzustand des Bildschirms besitzen (UTF-8,
|
||||
LF-Zeilenenden). Die Sollausgabe SHALL den Zellenpuffer als Textbild
|
||||
abbilden, getrimmt bis zur letzten belegten Zeile und Spalte; nachgestellte
|
||||
Leerzeichen innerhalb einer Zeile sind signifikant — `PRINT` gibt Zahlen
|
||||
mit führendem Vorzeichen-/Leerzeichen und nachgestelltem Leerzeichen aus.
|
||||
Verwendet ein Programm `COLOR`, SHALL die Sollausgabe zusätzlich eine
|
||||
Attributebene gleicher Abmessung enthalten; ohne `COLOR` MUST sie
|
||||
entfallen. `.gitattributes` MUST die `.out`-Dateien vor
|
||||
Zeilenenden-Konvertierung schützen.
|
||||
|
||||
#### Scenario: Zahlformatierung in der Sollausgabe
|
||||
- **WHEN** ein Korpusprogramm `PRINT 1; 2; 3` enthält
|
||||
- **THEN** lautet die Sollzeile ` 1 2 3 ` (mit nachgestelltem Leerzeichen)
|
||||
|
||||
#### Scenario: Getrimmter Snapshot
|
||||
- **WHEN** ein Korpusprogramm nur zwei Zeilen ausgibt
|
||||
- **THEN** umfasst die Sollausgabe genau diese zwei Zeilen und keine leeren Folgezeilen
|
||||
|
||||
#### Scenario: Attributebene nur bei COLOR
|
||||
- **WHEN** ein Korpusprogramm ohne `COLOR` läuft
|
||||
- **THEN** enthält seine `.out` keine Attributebene
|
||||
|
||||
### Requirement: Laufzeitvergleich über den Korpus
|
||||
Ein automatischer Test SHALL jede Korpusdatei `tests/compat/*.bas`
|
||||
kompilieren, über die VM mit einem Host ohne Terminal ausführen und den
|
||||
resultierenden Bildschirmzustand gegen die zugehörige `.out`-Datei
|
||||
vergleichen — getrimmt, mit signifikanten Leerzeichen innerhalb der
|
||||
Zeilen und, sofern vorhanden, einschließlich der Attributebene. Bei
|
||||
Abweichung MUST der Test Datei, erste abweichende Zeile sowie Soll und
|
||||
Ist nennen; weicht nur die Attributebene ab, MUST er Zeile, Spalte, Soll-
|
||||
und Ist-Attribut nennen.
|
||||
|
||||
#### Scenario: Korpus läuft mit korrekter Ausgabe
|
||||
- **WHEN** die Laufzeit-Testsuite läuft
|
||||
- **THEN** stimmt der Bildschirmzustand jeder Korpusdatei mit ihrer `.out` überein
|
||||
|
||||
#### Scenario: Abweichung wird benannt
|
||||
- **WHEN** ein Korpusprogramm eine abweichende Ausgabe erzeugt
|
||||
- **THEN** schlägt der Test fehl und nennt Datei, Zeilennummer, Soll- und Ist-Zeile
|
||||
|
||||
#### Scenario: Abweichendes Farbattribut
|
||||
- **WHEN** ein Korpusprogramm dasselbe Textbild, aber ein abweichendes Attribut erzeugt
|
||||
- **THEN** schlägt der Test fehl und nennt Zeile, Spalte, Soll- und Ist-Attribut
|
||||
@@ -0,0 +1,208 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Vollständige Stringbibliothek
|
||||
Die Bibliothek SHALL sämtliche in der Original-Hilfe dokumentierten
|
||||
Stringfunktionen und -anweisungen mit der Semantik des Vorbilds
|
||||
bereitstellen — darunter `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion und
|
||||
Anweisung), `INSTR`, `UCASE$`, `LCASE$`, `LTRIM$`, `RTRIM$`, `STRING$`,
|
||||
`SPACE$`, `CHR$`, `ASC`, `STR$`, `VAL`, `HEX$`, `OCT$` und `SWAP` für
|
||||
Strings. Zeichen sind Unicode-Codepoints (dokumentierte Abweichung).
|
||||
Randfälle folgen dem Vorbild: `VAL` liest das führende Zahlenpräfix und
|
||||
überspringt Leerraum, `STR$` stellt nicht-negativen Zahlen ein Leerzeichen
|
||||
voran, `ASC("")` löst Laufzeitfehler 5 aus, eine Startposition kleiner 1
|
||||
bei `MID$`/`INSTR` ebenso, `LEFT$`/`RIGHT$` mit Länge über der Stringlänge
|
||||
liefern den ganzen String. Welche Elemente die Liste umfasst, MUST das
|
||||
Sprachinventar bestimmen; ein dort als `implementiert` geführtes Element
|
||||
MUST verfügbar sein.
|
||||
|
||||
#### Scenario: VAL mit Restzeichen
|
||||
- **WHEN** `VAL(" 12.5abc")` ausgewertet wird
|
||||
- **THEN** ist das Ergebnis 12.5
|
||||
|
||||
#### Scenario: Länge über Stringende
|
||||
- **WHEN** `LEFT$("ab", 10)` ausgewertet wird
|
||||
- **THEN** ist das Ergebnis `ab`
|
||||
|
||||
#### Scenario: Ungültige Startposition
|
||||
- **WHEN** `MID$("abc", 0)` ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: PRINT USING und FORMAT$
|
||||
`PRINT USING`, `LPRINT USING` und `FORMAT$` SHALL die Formatzeichen des
|
||||
Vorbilds unterstützen: numerische Platzhalter (`#`), Dezimalpunkt,
|
||||
Tausendertrennung (`,`), Vorzeichenführung (`+`, `-`), Währungs- und
|
||||
Füllzeichen (`$$`, `**`, `**$`), Exponentialform (`^^^^`) sowie die
|
||||
String-Platzhalter `&`, `!` und `\ \`. Literale Zeichen SHALL unverändert
|
||||
übernommen werden, `_` SHALL das Folgezeichen literal machen. Passt ein
|
||||
Wert nicht in das Feld, SHALL die Ausgabe mit einem `%`-Präfix erfolgen
|
||||
und der Wert vollständig dargestellt werden; dieses Überlaufverhalten MUST
|
||||
in der Sprachreferenz festgehalten sein. Eine Formatzeichenkette ohne
|
||||
Platzhalter bei vorhandenen Argumenten MUST Laufzeitfehler 5 auslösen.
|
||||
`SetFormatCC` SHALL das für `$$` verwendete Währungszeichen und die
|
||||
zugehörige Stellung setzen; ohne Aufruf gilt ein dokumentierter
|
||||
Vorgabewert.
|
||||
|
||||
#### Scenario: Numerisches Feld mit Nachkommastellen
|
||||
- **WHEN** `PRINT USING "###.##"; 3.14159` ausgeführt wird
|
||||
- **THEN** lautet die Ausgabe ` 3.14`
|
||||
|
||||
#### Scenario: Feldüberlauf
|
||||
- **WHEN** `PRINT USING "##"; 12345` ausgeführt wird
|
||||
- **THEN** beginnt die Ausgabe mit `%` und enthält den vollständigen Wert
|
||||
|
||||
#### Scenario: Formatzeichenkette wird wiederverwendet
|
||||
- **WHEN** `PRINT USING "## "; 1; 2; 3` ausgeführt wird
|
||||
- **THEN** wird die Formatzeichenkette für jeden Wert erneut angewandt
|
||||
|
||||
### Requirement: Datums- und Zeitbibliothek
|
||||
Die Bibliothek SHALL die Datums- und Zeitfunktionen des Vorbilds
|
||||
bereitstellen: `DATE$` und `TIME$` als Funktion und als Anweisung, `TIMER`,
|
||||
`NOW`, `DATESERIAL`, `TIMESERIAL`, `DATEVALUE`, `TIMEVALUE`, `DAY`,
|
||||
`MONTH`, `YEAR`, `WEEKDAY`, `HOUR`, `MINUTE`, `SECOND`. Serielle Datumswerte
|
||||
SHALL der Zählweise des Vorbilds folgen (ganzzahliger Anteil = Tag,
|
||||
Nachkommaanteil = Tageszeit). `WEEKDAY` SHALL 1 für Sonntag liefern.
|
||||
Ungültige Datums- oder Zeitangaben MUST Laufzeitfehler 5 auslösen.
|
||||
|
||||
#### Scenario: Serienwert und Rückumwandlung
|
||||
- **WHEN** `d = DATESERIAL(1992, 3, 15)` gebildet und `YEAR(d)`, `MONTH(d)`, `DAY(d)` ausgewertet werden
|
||||
- **THEN** liefern sie 1992, 3 und 15
|
||||
|
||||
#### Scenario: Wochentagszählung
|
||||
- **WHEN** `WEEKDAY` auf einen Sonntag angewandt wird
|
||||
- **THEN** ist das Ergebnis 1
|
||||
|
||||
#### Scenario: Ungültiges Datum
|
||||
- **WHEN** `DATESERIAL(1992, 13, 1)` ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: Kompatibler Zufallszahlengenerator
|
||||
`RND` und `RANDOMIZE` SHALL die Zahlenfolge des Vorbilds reproduzieren:
|
||||
gleicher Startwert MUST dieselbe Folge liefern. `RND` ohne Argument und
|
||||
mit positivem Argument SHALL die nächste Zahl liefern, mit Argument 0 die
|
||||
zuletzt gelieferte wiederholen, mit negativem Argument die Folge auf einen
|
||||
aus dem Argument abgeleiteten Startwert setzen. `RANDOMIZE` ohne Argument
|
||||
SHALL einen Startwert erfragen, `RANDOMIZE TIMER` einen zeitabhängigen
|
||||
setzen. Der verwendete Algorithmus MUST in der Sprachreferenz dokumentiert
|
||||
sein.
|
||||
|
||||
#### Scenario: Reproduzierbare Folge
|
||||
- **WHEN** zweimal `RANDOMIZE 42` gefolgt von drei `RND`-Aufrufen ausgeführt wird
|
||||
- **THEN** sind beide Dreierfolgen identisch
|
||||
|
||||
#### Scenario: RND(0) wiederholt
|
||||
- **WHEN** nach einem `RND`-Aufruf `RND(0)` ausgewertet wird
|
||||
- **THEN** liefert es denselben Wert wie der vorangegangene Aufruf
|
||||
|
||||
### Requirement: Mathematikbibliothek mit Überlaufverhalten
|
||||
Die Bibliothek SHALL die numerischen Funktionen des Vorbilds
|
||||
bereitstellen (`ABS`, `SGN`, `INT`, `FIX`, `SQR`, `EXP`, `LOG`, `SIN`,
|
||||
`COS`, `TAN`, `ATN`, `CINT`, `CLNG`, `CSNG`, `CDBL`, `CCUR`). Rundung
|
||||
SHALL kaufmännisch-symmetrisch zur geraden Zahl erfolgen (Banker's
|
||||
Rounding). Überschreitet ein Ergebnis den Wertebereich des Zieltyps, MUST
|
||||
Laufzeitfehler 6 „Overflow" auftreten; `SQR` und `LOG` mit unzulässigem
|
||||
Argument MUST Laufzeitfehler 5 auslösen, Division durch null Fehler 11.
|
||||
|
||||
#### Scenario: Rundung zur geraden Zahl
|
||||
- **WHEN** `CINT(0.5)` und `CINT(1.5)` ausgewertet werden
|
||||
- **THEN** liefern sie 0 und 2
|
||||
|
||||
#### Scenario: Überlauf bei Konvertierung
|
||||
- **WHEN** `CINT(40000)` ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 6 auf
|
||||
|
||||
#### Scenario: Wurzel aus negativer Zahl
|
||||
- **WHEN** `SQR(-1)` ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: Finanzmathematische Funktionen
|
||||
Die Bibliothek SHALL die finanzmathematischen Funktionen des Vorbilds
|
||||
bereitstellen: `FV#` (Endwert), `PV#` (Barwert), `Pmt#` (Zahlung),
|
||||
`IPmt#` (Zinsanteil), `PPmt#` (Tilgungsanteil), `NPer#` (Anzahl
|
||||
Zahlungsperioden), `Rate#` (Zinssatz), `NPV#` (Kapitalwert), `IRR#`
|
||||
(interner Zinsfuß), `MIRR#` (modifizierter interner Zinsfuß), `SLN#`
|
||||
(lineare Abschreibung), `SYD#` (arithmetisch-degressive Abschreibung) und
|
||||
`DDB#` (geometrisch-degressive Abschreibung). Alle SHALL in DOUBLE
|
||||
rechnen. Das Argument für die Fälligkeit SHALL zwischen nachschüssiger
|
||||
und vorschüssiger Zahlung unterscheiden. Ungültige Argumente — etwa ein
|
||||
Zinssatz von null bei `NPer#` oder eine Periode außerhalb der Laufzeit
|
||||
bei `IPmt#`/`PPmt#` — MUST Laufzeitfehler 5 auslösen. `Rate#` und `IRR#`
|
||||
nähern sich iterativ; findet die Näherung keine Lösung, MUST
|
||||
Laufzeitfehler 5 auftreten statt eines stillen Ersatzwerts. Die
|
||||
Abbruchbedingung der Näherung MUST in docs/bibliothek.md dokumentiert
|
||||
sein.
|
||||
|
||||
#### Scenario: Endwert einer Rentenzahlung
|
||||
- **WHEN** `FV#` für 10 Perioden, 5 % Zins und eine Zahlung von 100 nachschüssig ausgewertet wird
|
||||
- **THEN** entspricht das Ergebnis dem dokumentierten Sollwert der Bibliotheksreferenz
|
||||
|
||||
#### Scenario: Zins- und Tilgungsanteil ergänzen sich
|
||||
- **WHEN** `IPmt#` und `PPmt#` für dieselbe Periode desselben Darlehens ausgewertet werden
|
||||
- **THEN** ist ihre Summe gleich `Pmt#` für dieses Darlehen
|
||||
|
||||
#### Scenario: Näherung ohne Lösung
|
||||
- **WHEN** `IRR#` mit einer Zahlungsreihe ohne Vorzeichenwechsel ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: System- und Umgebungsanweisungen
|
||||
`ENVIRON$` SHALL Umgebungsvariablen nach Namen oder Position liefern,
|
||||
`ENVIRON` sie setzen. `COMMAND$` SHALL die Aufrufargumente ohne
|
||||
Programmnamen liefern. `FRE` SHALL einen Speicherwert liefern, `CLEAR`
|
||||
Variablen zurücksetzen, `TRON`/`TROFF` die Ablaufverfolgung schalten.
|
||||
`STACK` SHALL als Anweisung die Stapelgröße setzen und als Funktion die
|
||||
verfügbare Stapelgröße liefern. `SYSTEM` SHALL das Programm beenden und
|
||||
die Kontrolle an das Betriebssystem zurückgeben. `ERDEV` und `ERDEV$`
|
||||
SHALL den Fehlercode bzw. den Namen des zuletzt meldenden Geräts
|
||||
liefern. Nicht abbildbare Größen (Speicher- und Stapelwerte, Gerätefehler)
|
||||
MUST einen definierten, dokumentierten Ersatzwert liefern statt still zu
|
||||
variieren; der Ersatzwert MUST in docs/sprachreferenz.md unter
|
||||
„Abweichungen" stehen.
|
||||
|
||||
#### Scenario: Umgebungsvariable lesen
|
||||
- **WHEN** eine Umgebungsvariable gesetzt ist und `ENVIRON$("NAME")` ausgewertet wird
|
||||
- **THEN** liefert es deren Wert
|
||||
|
||||
#### Scenario: SYSTEM beendet das Programm
|
||||
- **WHEN** ein Programm `SYSTEM` ausführt
|
||||
- **THEN** endet es mit demselben Beendigungsstatus wie bei `END`
|
||||
|
||||
#### Scenario: Unbekannte Umgebungsvariable
|
||||
- **WHEN** `ENVIRON$` für einen nicht gesetzten Namen ausgewertet wird
|
||||
- **THEN** ist das Ergebnis der leere String
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Host-Abstraktion für Konsolen-E/A
|
||||
Alle Konsolenwirkungen der Ausführungsschicht SHALL über eine
|
||||
austauschbare Host-Schnittstelle laufen, die einen Bildschirmzustand
|
||||
darstellt statt eines Zeichenstroms: Übergabe des Zellenpuffers zur
|
||||
Anzeige, Abholen von Tastatur- und Größenänderungsereignissen sowie die
|
||||
Abbruchprüfung (Strg+Untbr). Host-Aufrufe dürfen blockieren.
|
||||
`tbc run` SHALL einen Konsolen-Host verwenden, der den Puffer im Terminal
|
||||
darstellt und Tastendrücke liefert. Tests MUST einen Host verwenden
|
||||
können, der ohne Terminal arbeitet, den Bildschirmzustand nach jedem
|
||||
Schritt festhält und vorbereitete Tastendrücke einspeist. Eine
|
||||
Bildschirmwirkung, die nur über einen Zeichenstrom ausdrückbar wäre, MUST
|
||||
NOT vorkommen: `LOCATE`, `COLOR`, `CSRLIN`, `POS` und die Funktionsform
|
||||
von `SCREEN` MUST über diese Schnittstelle vollständig abgebildet sein.
|
||||
|
||||
#### Scenario: Capture-Host im Test
|
||||
- **WHEN** ein Korpusprogramm im Test-Harness läuft
|
||||
- **THEN** entsteht der vollständige Bildschirmzustand im Capture-Host und ist vergleichbar, ohne dass ein Terminal vorhanden ist
|
||||
|
||||
#### Scenario: Cursorbewegung über die Schnittstelle
|
||||
- **WHEN** ein Programm `LOCATE 5, 10 : PRINT "x"` ausführt
|
||||
- **THEN** meldet der Host-Bildschirmzustand das Zeichen `x` an Position (5, 10)
|
||||
|
||||
#### Scenario: Tastendruck aus dem Host
|
||||
- **WHEN** der Test-Host einen vorbereiteten Tastendruck liefert und das Programm `INKEY$` auswertet
|
||||
- **THEN** erhält das Programm diese Taste
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Korpus-Stringfunktionen
|
||||
**Reason**: Die auf den Phase-2-Korpus zugeschnittene Teilliste wird durch
|
||||
die Anforderung „Vollständige Stringbibliothek" ersetzt, die den
|
||||
vollständigen dokumentierten Funktionsumfang fordert und ihren Umfang aus
|
||||
dem Sprachinventar bezieht.
|
||||
**Migration**: Alle bisher geforderten Funktionen und Randfälle sind in
|
||||
„Vollständige Stringbibliothek" enthalten; kein Verhalten entfällt.
|
||||
@@ -0,0 +1,54 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Jedes dokumentierte Element besitzt eine Signatur
|
||||
Die Semantik SHALL für jedes im Sprachinventar geführte Element eine
|
||||
Signatur besitzen (Argumentanzahl und -typen) — auch für Elemente, deren
|
||||
Laufzeitverhalten noch aussteht. Ein im Inventar geführtes Element MUST
|
||||
NOT als unbekannter Bezeichner oder mit einem generischen Syntaxfehler
|
||||
abgewiesen werden; die Diagnose bzw. der Laufzeitfehler MUST es
|
||||
namentlich nennen (Guiding Principle). Das gilt insbesondere für die
|
||||
Gruppen, die bisher gar keine Signatur haben: die finanzmathematischen
|
||||
Funktionen, die Record-Konvertierungsfunktionen (`MK…$`/`CV…`), die
|
||||
Dateisystem-Funktionen `CURDIR$`, `DIR$`, `CHDRIVE`, sowie `LPOS`,
|
||||
`STACK`, `SYSTEM`, `ERDEV`/`ERDEV$` und `SetFormatCC`.
|
||||
|
||||
#### Scenario: Element ohne Laufzeitverhalten wird namentlich abgewiesen
|
||||
- **WHEN** ein Modul eine im Inventar als `offen` geführte Funktion verwendet
|
||||
- **THEN** nennt die Ablehnung deren Namen und nicht „unbekannter Bezeichner"
|
||||
|
||||
#### Scenario: Argumentprüfung ohne Laufzeitverhalten
|
||||
- **WHEN** ein Modul `MKL$()` ohne Argument enthält
|
||||
- **THEN** meldet die Semantik eine Argumentanzahl-Diagnose, die `MKL$` nennt
|
||||
|
||||
### Requirement: Modulweites ON ERROR ist aus Prozeduren ansprechbar
|
||||
`ON ERROR GOTO label` ohne `LOCAL` SHALL einen modulweiten Fehlerhandler
|
||||
setzen, dessen Sprungziel auch dann aufgelöst wird, wenn die Anweisung in
|
||||
einer Prozedur steht und das Label auf Modulebene liegt. Nur
|
||||
`ON LOCAL ERROR GOTO label` SHALL prozedurlokale Handler-Auflösung
|
||||
verlangen. Ein modulweites `ON ERROR` in einer Prozedur MUST NOT wie
|
||||
`ON LOCAL ERROR` behandelt werden, und ein auf Modulebene vorhandenes
|
||||
Label MUST NOT als „Label not defined" gemeldet werden.
|
||||
|
||||
#### Scenario: Handler auf Modulebene aus einer Prozedur
|
||||
- **WHEN** ein Modul das Label `Fehler:` auf Modulebene führt und eine `SUB` die Anweisung `ON ERROR GOTO Fehler` enthält
|
||||
- **THEN** wird das Label aufgelöst und es entsteht keine Diagnose
|
||||
|
||||
#### Scenario: ON LOCAL ERROR bleibt prozedurlokal
|
||||
- **WHEN** eine `SUB` die Anweisung `ON LOCAL ERROR GOTO Fehler` enthält und `Fehler:` nur auf Modulebene existiert
|
||||
- **THEN** wird „Label not defined" gemeldet
|
||||
|
||||
### Requirement: DATA-Elemente behalten ihren Rohtext
|
||||
Unquotierte Elemente einer `DATA`-Anweisung SHALL exakt so erhalten
|
||||
bleiben, wie sie im Quelltext stehen — insbesondere in ihrer Groß- und
|
||||
Kleinschreibung. Die Normalisierung von Bezeichnern MUST NOT auf
|
||||
`DATA`-Elemente wirken. Führender und nachgestellter Leerraum eines
|
||||
unquotierten Elements SHALL entfernt werden, innenliegender erhalten
|
||||
bleiben.
|
||||
|
||||
#### Scenario: Gemischte Schreibweise bleibt erhalten
|
||||
- **WHEN** ein Modul `DATA Hallo Welt` enthält und `READ s$` ausgeführt wird
|
||||
- **THEN** enthält `s$` den Text `Hallo Welt` mit unveränderter Groß- und Kleinschreibung
|
||||
|
||||
#### Scenario: Leerraum an den Rändern
|
||||
- **WHEN** ein Modul `DATA ab cd ` enthält und gelesen wird
|
||||
- **THEN** lautet der Wert `ab cd`
|
||||
@@ -0,0 +1,74 @@
|
||||
## Purpose
|
||||
Das Sprachinventar ist das Messinstrument für die Leitplanke
|
||||
„Vollständigkeit ist das Soll": eine gepflegte Liste aller in der
|
||||
Original-Hilfe des Vorbilds dokumentierten Anweisungen, Funktionen und
|
||||
Metabefehle mit Abdeckungsstatus, gegen die sich der Implementierungsstand
|
||||
jederzeit prüfen lässt.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Vollständiges Inventar der Original-Hilfe
|
||||
`docs/inventar.md` SHALL jede in der Original-Hilfe des Vorbilds
|
||||
dokumentierte Anweisung, Funktion, Metabefehl, Operator-Schlüsselwort und
|
||||
Klausel als eigenen Eintrag führen. Ein dokumentiertes Element MUST NOT
|
||||
fehlen; Vollständigkeit gegenüber der Quelle geht der Kürze vor. Jeder
|
||||
Eintrag SHALL Name, Art (Anweisung/Funktion/Metabefehl), Themengruppe,
|
||||
Status und Fundstelle tragen.
|
||||
|
||||
#### Scenario: Dokumentiertes Element ist erfasst
|
||||
- **WHEN** die Original-Hilfe die Anweisung `VIEW PRINT` dokumentiert
|
||||
- **THEN** enthält das Inventar einen Eintrag `VIEW PRINT` mit Art, Gruppe, Status und Fundstelle
|
||||
|
||||
#### Scenario: Non-Feature ist erfasst, nicht weggelassen
|
||||
- **WHEN** `PEEK` als Non-Feature deklariert ist
|
||||
- **THEN** steht `PEEK` mit Status `Non-Feature` im Inventar und die Fundstelle verweist auf den Abschnitt „Abweichungen" der Sprachreferenz
|
||||
|
||||
### Requirement: Festes Statusvokabular
|
||||
Der Status eines Eintrags SHALL genau einen von drei Werten tragen:
|
||||
`implementiert` (Frontend senkt ab und die Laufzeit führt aus),
|
||||
`offen` (dokumentiert, noch nicht verfügbar) oder `Non-Feature`
|
||||
(vom Projektinhaber abgelehnt und in der Sprachreferenz unter
|
||||
„Abweichungen" gelistet). Ein Eintrag mit Status `Non-Feature` MUST eine
|
||||
Fundstelle in der Sprachreferenz nennen.
|
||||
|
||||
#### Scenario: Unzulässiger Status
|
||||
- **WHEN** ein Eintrag den Status `teilweise` trägt
|
||||
- **THEN** schlägt die Inventarprüfung mit Nennung des Eintrags fehl
|
||||
|
||||
#### Scenario: Non-Feature ohne Fundstelle
|
||||
- **WHEN** ein Eintrag den Status `Non-Feature` trägt, aber keine Fundstelle nennt
|
||||
- **THEN** schlägt die Inventarprüfung mit Nennung des Eintrags fehl
|
||||
|
||||
### Requirement: Automatischer Abgleich Inventar gegen Implementierung
|
||||
Ein automatischer Test SHALL das Inventar gegen den tatsächlichen
|
||||
Implementierungsstand prüfen und bei jeder Divergenz fehlschlagen. Als
|
||||
`implementiert` markierte Elemente MUST im Frontend eine Signatur besitzen
|
||||
und MUST NOT auf einen „nicht verfügbar"-Marker absenken; als `offen`
|
||||
markierte Elemente MUST auf einen solchen Marker absenken oder ohne
|
||||
Signatur sein; als `Non-Feature` markierte Elemente MUST zur Compile-Zeit
|
||||
mit „Feature unavailable" abgewiesen werden. Der Test MUST das
|
||||
divergierende Element, seinen Inventarstatus und den vorgefundenen
|
||||
Zustand benennen.
|
||||
|
||||
#### Scenario: Inventar hinkt der Implementierung hinterher
|
||||
- **WHEN** `LOCATE` implementiert wurde, im Inventar aber noch `offen` steht
|
||||
- **THEN** schlägt der Abgleich fehl und nennt `LOCATE`, Status `offen` und den Befund „implementiert"
|
||||
|
||||
#### Scenario: Inventar behauptet zu viel
|
||||
- **WHEN** ein Eintrag `implementiert` trägt, das Element aber weiterhin auf den „nicht verfügbar"-Marker absenkt
|
||||
- **THEN** schlägt der Abgleich fehl und nennt das Element
|
||||
|
||||
#### Scenario: Abdeckungsstand ist abrufbar
|
||||
- **WHEN** der Abgleich läuft
|
||||
- **THEN** nennt er die Zahl der Einträge je Status als Abdeckungsstand
|
||||
|
||||
### Requirement: Keine stillen Lücken bei dokumentierten Elementen
|
||||
Ein Element mit Status `offen` MUST bei Verwendung eine benannte
|
||||
Ablehnung erzeugen — entweder eine Compile-Diagnose oder den Laufzeitfehler
|
||||
für nicht verfügbare Funktionen —, die das Element namentlich nennt. Ein
|
||||
generischer Syntaxfehler oder „unbekannter Bezeichner" für ein
|
||||
dokumentiertes Element MUST NOT auftreten (Guiding Principle).
|
||||
|
||||
#### Scenario: Offenes Element wird namentlich abgewiesen
|
||||
- **WHEN** ein Programm eine im Inventar als `offen` geführte Anweisung verwendet
|
||||
- **THEN** nennt die Ablehnung den Namen der Anweisung, nicht nur „Syntaxfehler"
|
||||
@@ -0,0 +1,173 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Zellenpuffer ohne Terminalabhängigkeit
|
||||
Der Zellenpuffer mit der vollständigen Bildschirmsemantik (Cursor,
|
||||
Farbattribute, Umbruch, Scrollen, Scrollbereich, Größenänderung) SHALL
|
||||
ohne Terminal instanziierbar, veränderbar und auslesbar sein. Die
|
||||
Anbindung an ein konkretes Terminal SHALL ausschließlich in der
|
||||
Darstellungsschicht liegen. Damit MUST jedes Bildschirmverhalten in
|
||||
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
|
||||
MUST NOT von einer Terminal-Bibliothek abhängen.
|
||||
|
||||
#### Scenario: Bildschirmverhalten im Test ohne Terminal
|
||||
- **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt
|
||||
- **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal
|
||||
|
||||
### Requirement: Bildschirmanweisungen des Dialekts
|
||||
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
|
||||
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich
|
||||
löschen und den Cursor an dessen Anfang setzen; `CLS 2` SHALL nur den
|
||||
Textbereich löschen. `COLOR` SHALL Vordergrund und Hintergrund für
|
||||
nachfolgende Ausgaben setzen, ausgelassene Argumente lassen den bisherigen
|
||||
Wert unverändert. `WIDTH` SHALL die Spalten- und Zeilenzahl setzen, soweit
|
||||
die Darstellungsfläche es zulässt. `VIEW PRINT oben TO unten` SHALL den
|
||||
Scrollbereich begrenzen, `VIEW PRINT` ohne Argumente ihn auf den ganzen
|
||||
Bildschirm zurücksetzen.
|
||||
|
||||
#### Scenario: CLS setzt Cursor zurück
|
||||
- **WHEN** nach Ausgaben in Zeile 10 `CLS` ausgeführt wird
|
||||
- **THEN** ist der Puffer leer und `CSRLIN` liefert 1, `POS(0)` liefert 1
|
||||
|
||||
#### Scenario: COLOR wirkt nur auf Folgeausgaben
|
||||
- **WHEN** `PRINT "a" : COLOR 14, 1 : PRINT "b"` ausgeführt wird
|
||||
- **THEN** trägt die Zelle mit `a` das vorherige Attribut und die Zelle mit `b` Vordergrund 14 auf Hintergrund 1
|
||||
|
||||
#### Scenario: Ausgelassenes COLOR-Argument
|
||||
- **WHEN** nach `COLOR 14, 1` die Anweisung `COLOR , 4` ausgeführt wird
|
||||
- **THEN** bleibt der Vordergrund 14 und der Hintergrund wird 4
|
||||
|
||||
### Requirement: Bildschirm-Abfragefunktionen
|
||||
`CSRLIN` SHALL die aktuelle Cursorzeile liefern, `POS(0)` die aktuelle
|
||||
Cursorspalte, beide 1-basiert. Die Funktionsform `SCREEN(zeile, spalte
|
||||
[, farbe])` SHALL das Zeichen an der genannten Position als Codepoint
|
||||
liefern, bei gesetztem dritten Argument stattdessen dessen Farbattribut.
|
||||
Positionen außerhalb des Bildschirms MUST Laufzeitfehler 5 „Illegal
|
||||
function call" auslösen.
|
||||
|
||||
#### Scenario: Zeichen zurücklesen
|
||||
- **WHEN** `LOCATE 3, 7 : PRINT "Q";` ausgeführt und danach `SCREEN(3, 7)` ausgewertet wird
|
||||
- **THEN** liefert `SCREEN(3, 7)` den Codepoint von `Q`
|
||||
|
||||
#### Scenario: Abfrage außerhalb des Bildschirms
|
||||
- **WHEN** bei 80×25 `SCREEN(30, 1)` ausgewertet wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
### Requirement: Tastatureingabe ohne Zeilenmodell
|
||||
`INKEY$` SHALL ohne zu blockieren die nächste anstehende Taste liefern:
|
||||
den leeren String bei leerem Puffer, ein Zeichen bei einer
|
||||
Zeichentaste, eine zwei Zeichen lange Folge mit führendem Nullzeichen bei
|
||||
einer Sondertaste. `INPUT$(n [, #dateinummer])` SHALL genau `n` Zeichen
|
||||
lesen und dabei blockieren, ohne sie am Bildschirm zu wiederholen.
|
||||
|
||||
#### Scenario: INKEY$ bei leerem Tastaturpuffer
|
||||
- **WHEN** `INKEY$` ohne anstehende Taste ausgewertet wird
|
||||
- **THEN** liefert es den leeren String und blockiert nicht
|
||||
|
||||
#### Scenario: Sondertaste als zwei Zeichen
|
||||
- **WHEN** F1 gedrückt wurde und `INKEY$` ausgewertet wird
|
||||
- **THEN** hat das Ergebnis die Länge 2 und beginnt mit dem Nullzeichen
|
||||
|
||||
### Requirement: Keine feste Bildschirmgröße im Verhalten
|
||||
Kein beobachtbares Verhalten SHALL eine feste Spalten- oder Zeilenzahl
|
||||
voraussetzen. Wo die Referenz des Vorbilds von 80×25 spricht, ist stets
|
||||
der volle aktuelle Bildschirm gemeint. Insbesondere SHALL `CLS` den
|
||||
vollen aktuellen Bildschirm löschen, der Scrollbereich ohne eigene
|
||||
`VIEW PRINT`-Einstellung den vollen aktuellen Bildschirm umfassen, der
|
||||
Zeilenumbruch an der aktuell letzten Spalte erfolgen, das Scrollen an
|
||||
der aktuell letzten Zeile des Bereichs auslösen und die Grenzprüfung von
|
||||
`LOCATE` und der Funktionsform von `SCREEN` gegen die aktuellen
|
||||
Abmessungen prüfen. Die Werte 80 und 25 MUST NOT als Grenze in
|
||||
beobachtbarem Verhalten auftreten.
|
||||
|
||||
#### Scenario: Löschen und Scrollen auf großem Bildschirm
|
||||
- **WHEN** der Bildschirm 120×40 misst, in Zeile 40 ein Umbruch erfolgt und danach `CLS` ausgeführt wird
|
||||
- **THEN** scrollt der Bildschirm erst an Zeile 40 und `CLS` löscht alle 40 Zeilen
|
||||
|
||||
#### Scenario: Adressierbarkeit jenseits von 80×25
|
||||
- **WHEN** der Bildschirm 120×40 misst und `LOCATE 40, 120` ausgeführt wird
|
||||
- **THEN** entsteht kein Fehler und `CSRLIN` liefert 40, `POS(0)` liefert 120
|
||||
|
||||
### Requirement: Größenänderung zur Laufzeit
|
||||
Ändert sich die Größe der Darstellungsfläche während ein Programm läuft,
|
||||
SHALL die Größenänderung als Ereignis bis zum Bildschirmzustand
|
||||
durchgereicht und der Zellenpuffer angepasst werden. Der Inhalt SHALL
|
||||
oben links erhalten bleiben; der Cursor SHALL in die neuen Grenzen
|
||||
geklemmt werden; ein `VIEW PRINT`-Bereich, der nicht mehr vollständig in
|
||||
den Bildschirm passt, SHALL auf die neuen Grenzen geklemmt und, falls er
|
||||
dadurch leer würde, auf den vollen Bildschirm zurückgesetzt werden. Alle
|
||||
programmseitig sichtbaren Größen — `CSRLIN`, `POS`, die Grenzen von
|
||||
`LOCATE` und der Funktionsform von `SCREEN` — MUST unmittelbar nach der
|
||||
Änderung die neuen Abmessungen widerspiegeln.
|
||||
|
||||
#### Scenario: Vergrößerung während der Ausführung
|
||||
- **WHEN** ein laufendes Programm bei 80×25 ausgibt und die Fläche auf 120×40 wächst
|
||||
- **THEN** bleibt der bisherige Inhalt oben links stehen und `LOCATE 40, 120` ist danach zulässig
|
||||
|
||||
#### Scenario: Verkleinerung klemmt den Cursor
|
||||
- **WHEN** der Cursor auf Zeile 40 steht und die Fläche auf 80×25 schrumpft
|
||||
- **THEN** liegt der Cursor danach innerhalb der neuen Grenzen
|
||||
|
||||
#### Scenario: Scrollbereich überlebt die Verkleinerung
|
||||
- **WHEN** `VIEW PRINT 30 TO 38` gesetzt ist und die Fläche auf 25 Zeilen schrumpft
|
||||
- **THEN** ist der Scrollbereich danach gültig und liegt vollständig innerhalb des Bildschirms
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Dynamische Terminalgröße mit Mindestmaß
|
||||
Der Bildschirm SHALL der Terminalgröße folgen (Puffer per `resize`
|
||||
anpassbar; Inhalt bleibt oben links erhalten, Cursor wird geklemmt).
|
||||
Die Größe SHALL zu jedem Zeitpunkt aus der Darstellungsfläche abgeleitet
|
||||
und nicht fest vorgegeben werden. Die Mindestgröße ist 80×25: kleinere
|
||||
Werte werden auf 80×25 geklemmt, und ist die Render-Fläche kleiner als
|
||||
80×25, SHALL nur ein Hinweis „Terminal zu klein" mit Ist- und
|
||||
Mindestgröße gerendert werden. Dieses Mindestmaß ist ausschließlich eine
|
||||
untere Schranke der Darstellung; es MUST NOT als Bildschirmgröße im
|
||||
Verhalten des Programms auftreten, solange die Fläche größer ist.
|
||||
|
||||
#### Scenario: Vergrößertes Terminal
|
||||
- **WHEN** der Bildschirm auf 120×40 gesetzt wird
|
||||
- **THEN** sind alle 120 Spalten und 40 Zeilen adressierbar (`LOCATE 40, 120`)
|
||||
|
||||
#### Scenario: Zu kleines Terminal
|
||||
- **WHEN** die Render-Fläche 60×20 misst
|
||||
- **THEN** erscheint statt des Puffers der Hinweis mit Minimum 80×25
|
||||
|
||||
### Requirement: Unicode-Zellenmodell
|
||||
Der Puffer SHALL Unicode-Zeichen speichern (keine CP437-Emulation).
|
||||
Zeichen mit Darstellungsbreite 2 (u. a. Emoji, CJK) SHALL zwei
|
||||
nebeneinanderliegende Zellen belegen (Entscheidung 2026-09-02): die erste
|
||||
trägt das Zeichen, die zweite ist als Fortsetzung markiert und MUST NOT
|
||||
eigenständig beschrieben werden. Der Cursor SHALL nach der Ausgabe eines
|
||||
breiten Zeichens um zwei Spalten vorrücken; `POS` SHALL die Spalte des
|
||||
Zeichenanfangs zählen. Passt ein breites Zeichen nicht mehr in die letzte
|
||||
Spalte, SHALL es vollständig in die nächste Zeile umgebrochen werden und
|
||||
die letzte Spalte leer bleiben. `LOCATE` auf die Fortsetzungszelle SHALL
|
||||
auf den Zeichenanfang wirken.
|
||||
|
||||
#### Scenario: Umlaute und Symbole
|
||||
- **WHEN** `Ä☃` ausgegeben wird
|
||||
- **THEN** belegen `Ä` und `☃` je genau eine Zelle
|
||||
|
||||
#### Scenario: Breites Zeichen belegt zwei Zellen
|
||||
- **WHEN** an Spalte 1 ein CJK-Zeichen ausgegeben wird
|
||||
- **THEN** ist Spalte 2 als Fortsetzung belegt und der Cursor steht auf Spalte 3
|
||||
|
||||
#### Scenario: Breites Zeichen am rechten Rand
|
||||
- **WHEN** bei 80 Spalten der Cursor auf Spalte 80 steht und ein breites Zeichen ausgegeben wird
|
||||
- **THEN** bleibt Spalte 80 leer und das Zeichen steht in Spalte 1 der Folgezeile
|
||||
|
||||
### Requirement: 1-basierte Cursor-API mit Bereichsprüfung
|
||||
`LOCATE`, `CSRLIN` und `POS` SHALL 1-basiert arbeiten. `LOCATE`
|
||||
außerhalb der aktuellen Bildschirmgrenzen SHALL Laufzeitfehler 5
|
||||
„Illegal function call" auslösen. Ausgelassene Argumente von `LOCATE`
|
||||
SHALL den jeweiligen Wert unverändert lassen; die Argumente für
|
||||
Cursorsichtbarkeit und Cursorform SHALL entgegengenommen und, soweit das
|
||||
Terminal sie nicht abbilden kann, folgenlos bleiben.
|
||||
|
||||
#### Scenario: Grenzprüfung
|
||||
- **WHEN** bei 80×25 `LOCATE 26, 1` aufgerufen wird
|
||||
- **THEN** tritt Laufzeitfehler 5 auf
|
||||
|
||||
#### Scenario: Ausgelassenes LOCATE-Argument
|
||||
- **WHEN** der Cursor auf (5, 9) steht und `LOCATE , 3` ausgeführt wird
|
||||
- **THEN** steht der Cursor auf (5, 3)
|
||||
106
openspec/changes/phase-3-runtime-bildschirm/tasks.md
Normal file
106
openspec/changes/phase-3-runtime-bildschirm/tasks.md
Normal file
@@ -0,0 +1,106 @@
|
||||
## 1. Vollständigkeits-Inventar
|
||||
|
||||
- [x] 1.1 Anweisungs-, Funktions- und Metabefehlsliste aus der Original-Hilfe erheben (dos-help.soulsphere.org, Topic-Listen, README der Professional Edition) und als Rohliste ablegen; verifiziert dadurch, dass die Zahl der erfassten Themen gegen die Topic-Übersicht der Quelle abgeglichen und im Change notiert ist
|
||||
- [x] 1.2 `docs/inventar.md` als Pipe-Tabelle mit den Spalten Name, Art, Gruppe, Status, Fundstelle anlegen und alle Einträge aus 1.1 mit Erststatus füllen; verifiziert dadurch, dass jede Zeile sechs Spalten hat (Name, Art, Gruppe, Status, Fundstelle, Quelle) und das Statusvokabular nur `implementiert`/`offen`/`Non-Feature` enthält
|
||||
- [x] 1.3 Abgleichstest schreiben, der die Tabelle parst und gegen `builtin_fn`, `builtin_stmt`, `banned_feature` sowie die `Unsupported`-Marker hält; verifiziert dadurch, dass der Test auf dem Ist-Stand grün ist und nach einer künstlich falsch gesetzten Statuszeile mit Nennung des Elements fehlschlägt
|
||||
- [x] 1.4 Prüfung ergänzen, dass `Non-Feature`-Einträge eine Fundstelle in docs/sprachreferenz.md nennen und dokumentierte `offen`-Elemente namentlich abgewiesen werden (kein generischer Syntaxfehler); verifiziert durch je einen Testfall pro Regel
|
||||
- [x] 1.5 Abdeckungsstand (Anzahl je Status) im Testausgabetext ausweisen; verifiziert dadurch, dass `cargo test` die drei Zahlen nennt
|
||||
- [x] 1.6 Vom Inventar aufgedeckte Abweichung von der Phase-1-Spec schließen: Non-Features, die der Compiler nicht abwies, ergänzen — `CALLS`, `SSEG`, `SSEGADD`, `StringAddress`/`Assign`/`Length`/`Release`, `POINT`, `VIEW`, `COM`, `PEN`, `STRIG`, `STICK` in `banned_feature`; `LINE` ohne `INPUT` und `OPEN "COMn:"` als „Feature unavailable"; verifiziert durch `dokumentierte_elemente_werden_namentlich_abgewiesen`
|
||||
- [x] 1.7 Signaturen in `builtin_fn`/`builtin_stmt` für alle bislang signaturlosen Inventarelemente ergänzen (Finanzmathematik, `MK…$`/`CV…`, `CURDIR$`, `DIR$`, `CHDRIVE`, `LPOS`, `STACK`, `SYSTEM`, `ERDEV`/`ERDEV$`, `SetFormatCC`); verifiziert durch Frontend-Test über ein Programm, das jedes davon einmal verwendet, ohne Diagnose „unbekannter Bezeichner"
|
||||
- [x] 1.8 Sicherstellen, dass jedes als `offen` geführte Element namentlich abgewiesen wird (Diagnose oder Laufzeitfehler nennt den Namen); verifiziert durch Test, dass `MKL$()` ohne Argument eine Diagnose mit dem Namen `MKL$` erzeugt
|
||||
|
||||
## 2. Bruchschritt: Puffer, Host, Snapshot-Harness
|
||||
|
||||
- [x] 2.1 `Cell`/`TextScreen` samt Tests von `tb-ui::screen` nach `tb-runtime::screen` verschieben, ratatui-Importe entfernen; verifiziert dadurch, dass `cargo test -p tb-runtime` grün ist und `tb-runtime/Cargo.toml` weiterhin weder ratatui noch crossterm führt
|
||||
- [x] 2.2 `tb-runtime` als Abhängigkeit in `tb-ui` aufnehmen; `basic_color()` und `impl Widget` in `tb-ui` belassen und auf den Runtime-Typ umstellen; verifiziert dadurch, dass `cargo run -p tb-ui --example spike` unverändert rendert
|
||||
- [x] 2.3 `Host`-Trait neu schneiden: Übergabe des Zellenpuffers zur Anzeige, Ereignisabholung (Taste, Größenänderung), Abbruchprüfung; verifiziert dadurch, dass `tb-runtime` kompiliert und der Trait keine Stream-Methoden mehr trägt
|
||||
- [x] 2.4 `ConsoleHost` auf Darstellung des Puffers im Terminal und Tastaturereignisse umstellen; verifiziert dadurch, dass `tbc run tests/compat/hello.bas` im Terminal die erwartete Ausgabe zeigt
|
||||
- [x] 2.5 `CaptureHost` auf Bildschirmzustand plus vorbereitete Tastendrücke umstellen; verifiziert durch einen Unit-Test, der Zeichen und Attribut einer Zelle nach `LOCATE`/`COLOR`/`PRINT` prüft
|
||||
- [x] 2.6 Darstellungsaufrufe an die bestehenden Tick-Grenzen und vor blockierende Eingaben hängen; verifiziert dadurch, dass ein Programm mit Ausgabe vor `INPUT` den Text vor der Eingabeaufforderung zeigt
|
||||
- [x] 2.7 Snapshot-Serialisierung schreiben: Textbild getrimmt bis zur letzten belegten Zeile/Spalte, Attributebene nur bei verwendetem `COLOR`; verifiziert durch Unit-Tests für getrimmte Ausgabe, signifikante Zeilen-Innenleerzeichen und Weglassen der Attributebene
|
||||
- [x] 2.8 Harness `crates/tb-cli/tests/compat.rs` auf Snapshot-Vergleich umstellen, inklusive Fehlermeldung mit Datei, erster abweichender Zeile sowie Zeile/Spalte/Soll/Ist bei Attributabweichung; verifiziert durch je einen absichtlich abweichenden Testfall
|
||||
- [x] 2.9 Die acht bestehenden `.out`-Dateien neu erzeugen und jeden Diff einzeln prüfen und begründen; verifiziert dadurch, dass `PRINT`-only-Programme unverändert bleiben und jede Abweichung im Change begründet notiert ist
|
||||
- [x] 2.10 `.gitattributes` auf die neuen Snapshot-Dateien prüfen; verifiziert dadurch, dass `.out`-Dateien weiterhin vor Zeilenenden-Konvertierung geschützt sind
|
||||
|
||||
## 3. Größenunabhängigkeit und Laufzeit-Größenänderung
|
||||
|
||||
- [x] 3.1 Alle Bildschirmpfade auf die Abmessungen des Puffers umstellen (`CLS`, voreingestellter Scrollbereich, Umbruchspalte, Scrollzeile, Grenzprüfungen von `LOCATE` und `SCREEN()`); verifiziert dadurch, dass eine Suche über `tb-runtime` und `tb-ui` keine Vergleiche gegen die Literale 80 oder 25 außerhalb der Mindestmaß-Prüfung der Darstellungsschicht mehr findet
|
||||
- [x] 3.2 Größenänderungsereignis vom Host bis zum Puffer durchreichen; verifiziert durch Test, dass ein vom Test-Host ausgelöster Wechsel die Abmessungen des Puffers ändert
|
||||
- [x] 3.3 Anpassung bei Größenänderung: Inhalt oben links erhalten, Cursor klemmen, `VIEW PRINT`-Bereich klemmen und bei leerem Ergebnis auf den vollen Bildschirm zurücksetzen; verifiziert durch je einen Unit-Test für Vergrößerung, Verkleinerung und ungültig gewordenen Scrollbereich
|
||||
- [x] 3.4 Test-Host mit explizit gesetzter Bildschirmgröße je Korpusprogramm, ohne stillschweigenden Vorgabewert; verifiziert dadurch, dass ein Korpusprogramm ohne Größenangabe vom Harness abgewiesen wird
|
||||
- [x] 3.5 Bildschirm-Korpusprogramm bei 80×25 und 120×40 mit je eigener Sollausgabe; verifiziert dadurch, dass sich die Sollausgaben in Umbruch- und Scrollverhalten unterscheiden und beide Läufe grün sind
|
||||
- [x] 3.6 Korpusprogramm mit Größenänderung während der Ausführung; verifiziert dadurch, dass der Snapshot den erhaltenen Inhalt und die Ausgabe in den neuen Grenzen zeigt
|
||||
|
||||
## 4. Unicode-Doppelzellen
|
||||
|
||||
- [x] 4.1 `unicode-width` als Abhängigkeit aufnehmen und Zellenmodell um Fortsetzungszellen erweitern; verifiziert durch Unit-Test, dass ein CJK-Zeichen an Spalte 1 die Spalte 2 als Fortsetzung belegt
|
||||
- [x] 4.2 Cursorfortschritt, `POS` und Umbruch am rechten Rand auf Darstellungsbreite umstellen; verifiziert durch Unit-Tests für Vorrücken um zwei Spalten und vollständigen Umbruch bei belegter letzter Spalte
|
||||
- [x] 4.3 Überschreiben einer Zellenhälfte leert die andere; verifiziert durch Unit-Test, dass keine Geisterzeichen zurückbleiben
|
||||
- [x] 4.4 `LOCATE` auf eine Fortsetzungszelle auf den Zeichenanfang abbilden; verifiziert durch Unit-Test
|
||||
- [x] 4.5 Korpusprogramm mit CJK und Emoji inklusive Randumbruch ergänzen; verifiziert dadurch, dass der Snapshot der dokumentierten Sollausgabe entspricht
|
||||
- [x] 4.6 Abweichung in docs/sprachreferenz.md festhalten; verifiziert durch den Abschnittseintrag
|
||||
|
||||
## 5. Bildschirmanweisungen und -funktionen
|
||||
|
||||
- [x] 5.1 `CLS` (inkl. `CLS 2`), `COLOR` mit ausgelassenen Argumenten, `LOCATE` mit ausgelassenen Argumenten und Fehler 5 bei Grenzüberschreitung; verifiziert durch Korpustest mit Snapshot und je einem Fehlerfall
|
||||
- [x] 5.2 `WIDTH`, `VIEW PRINT` (setzen und zurücksetzen), Scrollen im begrenzten Bereich; verifiziert durch Korpustest, dass Zeilen außerhalb des Bereichs unverändert bleiben
|
||||
- [x] 5.3 `CSRLIN`, `POS(0)` und die Funktionsform `SCREEN(zeile, spalte [, farbe])` inklusive Fehler 5 außerhalb des Bildschirms; verifiziert durch Korpustest mit Zurücklesen eines geschriebenen Zeichens
|
||||
- [x] 5.4 Anweisungsform `SCREEN` entgegennehmen und dokumentiertes Verhalten festlegen; verifiziert dadurch, dass keine `Unsupported`-Absenkung mehr erfolgt und das Verhalten in der Sprachreferenz steht
|
||||
- [x] 5.5 `INKEY$` nicht blockierend (leerer String, Zeichen, Sondertaste als zwei Zeichen mit führendem Nullzeichen); verifiziert durch Test mit vorbereiteten Tastendrücken im Capture-Host
|
||||
- [x] 5.6 `INPUT$(n)` blockierend ohne Echo; verifiziert durch Test, dass genau `n` Zeichen gelesen werden und der Bildschirm unverändert bleibt
|
||||
- [x] 5.7 `TAB` und `SPC` in der `PRINT`-Familie; verifiziert durch Korpustest mit Spaltenpositionen
|
||||
- [x] 5.8 `KEY`-Umfang (`KEY LIST`/`ON`/`OFF`, Funktionstasten-Makros) aus der Original-Hilfe klären, umsetzen und in docs/sprachreferenz.md festhalten; verifiziert durch Korpustest und Dokumentationsabschnitt
|
||||
|
||||
## 6. String-, Zahlen- und Formatbibliothek
|
||||
|
||||
- [x] 6.1 Alle im Inventar geführten Stringfunktionen vervollständigen inklusive Randfälle (`VAL`-Präfix, `STR$`-Leerzeichen, Fehler 5 bei Startposition < 1, Länge über Stringende); verifiziert durch Korpustest je Randfall
|
||||
- [x] 6.2 `PRINT USING`/`LPRINT USING`/`FORMAT$` mit numerischen Platzhaltern, Tausendertrennung, Vorzeichenführung, `$$`/`**`/`**$`, Exponentialform und String-Platzhaltern; verifiziert durch Korpustest je Formatzeichenklasse
|
||||
- [x] 6.3 Feldüberlauf mit `%`-Präfix umsetzen und in docs/sprachreferenz.md festhalten; verifiziert durch Korpustest und Dokumentationsabschnitt
|
||||
- [x] 6.4 Wiederverwendung der Formatzeichenkette bei mehreren Werten; verifiziert durch Korpustest
|
||||
- [x] 6.5 Mathematikfunktionen vervollständigen inklusive Banker's Rounding, Fehler 6 bei Überlauf, Fehler 5 bei `SQR`/`LOG`, Fehler 11 bei Division durch null; verifiziert durch Korpustest je Fehlerfall
|
||||
- [x] 6.6 Kompatiblen `RND`/`RANDOMIZE`-PRNG umsetzen (`RND(0)` wiederholt, negatives Argument setzt Startwert) und den Algorithmus dokumentieren; verifiziert durch Korpustest mit reproduzierbarer Folge nach `RANDOMIZE 42`
|
||||
- [x] 6.7 Datums- und Zeitfunktionen inklusive Serienwert-Zählweise, `WEEKDAY` = 1 für Sonntag und Fehler 5 bei ungültigen Angaben; verifiziert durch Korpustest mit festem Datum
|
||||
- [x] 6.8 `DATE$`/`TIME$` als Anweisung; verifiziert durch Test, dass die Funktionsform danach den gesetzten Wert liefert
|
||||
- [x] 6.9 Finanzmathematische Funktionen `FV#`, `PV#`, `Pmt#`, `IPmt#`, `PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`, `SYD#`, `DDB#` in DOUBLE mit Fälligkeitsargument; verifiziert durch Korpustest je Funktion gegen dokumentierte Sollwerte
|
||||
- [x] 6.10 Fehler 5 bei ungültigen Argumenten der Finanzfunktionen und bei erfolgloser Näherung von `Rate#`/`IRR#`; verifiziert durch Korpustest je Fall
|
||||
- [x] 6.11 Abbruchbedingung der Näherung in docs/bibliothek.md dokumentieren; verifiziert durch den Abschnittseintrag
|
||||
- [x] 6.12 `SetFormatCC` für Währungszeichen und -stellung von `$$`; verifiziert durch Korpustest, dass `PRINT USING "$$#.##"` das gesetzte Zeichen verwendet
|
||||
- [x] 6.13 `docs/bibliothek.md` anlegen und mit dem umgesetzten Stand füllen; verifiziert dadurch, dass jede in Gruppe 6 umgesetzte Funktion dort einen Eintrag hat
|
||||
|
||||
## 7. Datei-E/A
|
||||
|
||||
- [x] 7.1 Dateitabelle und `OPEN` in beiden Syntaxen für sequenziell, `RANDOM` und `BINARY`, `CLOSE` (mit und ohne Argument), `RESET`, `FREEFILE`; verifiziert durch Korpustest inklusive Fehler 55, 52 und 53
|
||||
- [x] 7.2 Sequenzielle Übertragung: `PRINT #`, `WRITE #`, `INPUT #`, `LINE INPUT #`, `EOF`, Fehler 62 beim Lesen über das Dateiende; verifiziert durch Korpustest, dass `WRITE #` und `INPUT #` gegenläufig sind
|
||||
- [x] 7.3 Recordpuffer mit `FIELD`, `LSET`, `RSET` inklusive Abschneiden überlanger Werte; verifiziert durch Korpustest
|
||||
- [x] 7.4 Festtextfelder als UTF-32 mit konstanter Recordlänge; verifiziert durch Korpustest, dass ein Record mit ASCII und einer mit Nicht-ASCII dieselbe Byteanzahl belegen
|
||||
- [x] 7.5 `GET`/`PUT` für `RANDOM` mit Recordnummer und mit UDT-Variablen; verifiziert durch Korpustest mit Schreiben und Lesen an Position 3
|
||||
- [x] 7.6 Binärzugriff mit byteweiser Positionierung, `SEEK` als Anweisung und Funktion; verifiziert durch Korpustest, dass ein positioniert überschriebenes Byte zurückgelesen wird und die Nachbarbytes unverändert bleiben
|
||||
- [x] 7.7 `LOF`, `LOC`, `FILEATTR` inklusive Fehler 52 auf geschlossener Datei; verifiziert durch Korpustest
|
||||
- [x] 7.8 `LOCK`/`UNLOCK` umsetzen; verifiziert durch Korpustest für Sperren und Freigeben, plattformbedingte Abweichungen in docs/sprachreferenz.md notiert
|
||||
- [x] 7.9 Dateisystem-Anweisungen `KILL`, `NAME … AS`, `FILES`, `CHDIR`, `MKDIR`, `RMDIR`, `SHELL` mit plattformübergreifender Pfadsemantik (`\` und `/`, Laufwerksbuchstaben) inklusive Fehler 53 und 76; verifiziert durch Korpustest mit Rückwärts-Schrägstrich
|
||||
- [x] 7.10 `LPRINT`/`LPRINT USING` mit dokumentiertem Ausgabeziel sowie `LPOS`; verifiziert durch Test, dass die Formatierung `PRINT` entspricht, `LPOS` die Druckerspalte liefert, und durch den Abschnitt in der Sprachreferenz
|
||||
- [x] 7.11 Record-Konvertierungsfunktionen `MKI$`/`MKL$`/`MKS$`/`MKD$`/`MKC$` und `CVI`/`CVL`/`CVS`/`CVD`/`CVC`; verifiziert durch Korpustest, dass jedes Paar gegenläufig ist und falsche Argumentlänge Fehler 5 auslöst
|
||||
- [x] 7.12 MBF-Varianten `MKSMBF$`/`MKDMBF$`/`CVSMBF`/`CVDMBF` umsetzen oder als Non-Feature namentlich abweisen und in docs/sprachreferenz.md listen; verifiziert durch Korpustest bzw. den Abschnittseintrag
|
||||
- [x] 7.13 `CURDIR$`, `DIR$` (erster und Folgetreffer, leerer String am Ende), `CHDRIVE` und `SHELL` als Funktion; verifiziert durch Korpustest, der ein Verzeichnis mit `DIR$` vollständig durchläuft
|
||||
- [x] 7.14 Korpus-Dateiprogramme in einem temporären Arbeitsverzeichnis ausführen; verifiziert dadurch, dass nach `cargo test` `git status` sauber ist
|
||||
|
||||
## 8. System- und Umgebungsanweisungen
|
||||
|
||||
- [x] 8.1 `ENVIRON$` (nach Name und Position), `ENVIRON`, `COMMAND$`; verifiziert durch Korpustest inklusive leerem String für unbekannte Namen
|
||||
- [x] 8.2 `FRE`, `CLEAR`, `TRON`, `TROFF` mit definiertem, dokumentiertem Verhalten; verifiziert dadurch, dass keine `Unsupported`-Absenkung mehr erfolgt und der Ersatzwert für `FRE` in der Sprachreferenz steht
|
||||
- [x] 8.3 `STACK` als Anweisung und Funktion, `SYSTEM`, `ERDEV`/`ERDEV$` mit definierten Ersatzwerten; verifiziert durch Korpustest, dass `SYSTEM` mit demselben Status endet wie `END`, und durch die Ersatzwert-Einträge in docs/sprachreferenz.md
|
||||
|
||||
## 9. Altlasten aus Phase 2
|
||||
|
||||
- [x] 9.1 `ON ERROR GOTO` auf Modulebene aus Prozeduren auflösbar machen (`sema.rs`, `lower_stmt`/`OnError`), `ON LOCAL ERROR` bleibt prozedurlokal; verifiziert durch je einen Frontend-Test für beide Fälle
|
||||
- [x] 9.2 DATA-Rohtext im Lexer erhalten (keine Bezeichner-Normalisierung, Randleerraum entfernt, Innenleerraum erhalten); verifiziert durch Korpustest mit gemischter Schreibweise
|
||||
- [x] 9.3 `TODO verify`-Zellen der Konvertierungsmatrix in docs/tbvm-design.md gegen die Original-Hilfe auflösen (CURRENCY-Promotion mit Gleitkomma, `^`-Ergebnistyp, Exponentialform-Schwellen, CURRENCY-Ausgabeformat); verifiziert dadurch, dass keine `TODO verify`-Zelle mehr im Dokument steht
|
||||
- [x] 9.4 Korpustest `konvertierung.bas` um die in 9.3 geklärten Randfälle schärfen; verifiziert durch die erweiterte Sollausgabe
|
||||
|
||||
## 10. Abschluss
|
||||
|
||||
- [x] 10.1 Inventarstatus für alle in diesem Change umgesetzten Elemente auf `implementiert` setzen; verifiziert dadurch, dass der Abgleichstest aus 1.3 grün bleibt
|
||||
- [x] 10.2 docs/sprachreferenz.md fortschreiben (Doppelzellen, `KEY n`, `PRINT USING`-Überlauf, UTF-32-Records, plattformbedingte Abweichungen); verifiziert durch die vorhandenen Abschnitte
|
||||
- [x] 10.3 Vollständige Suite ausführen; verifiziert dadurch, dass `cargo test --workspace` grün ist. **Nur macOS geprüft** — Windows und Linux stehen aus und gehören zu den Plattformtests in Phase 6 (PLAN.md); die Laufzeit enthält keinen plattformspezifischen Code außer der Pfadnormierung und `SHELL`.
|
||||
- [x] 10.4 Meilenstein prüfen: klassische Konsolenprogramme des Korpus laufen unverändert; verifiziert dadurch, dass jedes Korpusprogramm ohne Laufzeitfehler 73 durchläuft
|
||||
- [x] 10.5 PLAN.md Phase 3 abhaken, soweit dieser Change reicht, und den offenen ISAM-Teil auf `phase-3-isam` verweisen; verifiziert durch den aktualisierten Planabschnitt
|
||||
24
tests/compat/bildschirm.bas
Normal file
24
tests/compat/bildschirm.bas
Normal file
@@ -0,0 +1,24 @@
|
||||
' tb-screen: 80x25
|
||||
' Bildschirmsteuerung (Spec textbildschirm): LOCATE mit ausgelassenen
|
||||
' Argumenten, COLOR, CLS, VIEW PRINT-Scrollen, CSRLIN/POS und das
|
||||
' Zuruecklesen per SCREEN.
|
||||
LOCATE 3, 5
|
||||
PRINT "Position";
|
||||
PRINT " "; CSRLIN; POS(0)
|
||||
LOCATE , 1
|
||||
PRINT "Spalte 1 in Zeile 4"
|
||||
COLOR 14, 1
|
||||
PRINT "gelb auf blau"
|
||||
COLOR 7, 0
|
||||
LOCATE 12, 1
|
||||
PRINT "gelesen: "; CHR$(SCREEN(3, 5)); CHR$(SCREEN(3, 6));
|
||||
PRINT " Attribut "; SCREEN(5, 1, 1)
|
||||
LOCATE 8, 1
|
||||
VIEW PRINT 8 TO 10
|
||||
LOCATE 10, 1
|
||||
PRINT "a"
|
||||
PRINT "b"
|
||||
VIEW PRINT
|
||||
LOCATE 14, 1
|
||||
PRINT "fertig"
|
||||
END
|
||||
29
tests/compat/bildschirm.out
Normal file
29
tests/compat/bildschirm.out
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
Position 3 17
|
||||
Spalte 1 in Zeile 4
|
||||
gelb auf blau
|
||||
|
||||
|
||||
a
|
||||
b
|
||||
|
||||
|
||||
gelesen: Po Attribut 30
|
||||
|
||||
fertig
|
||||
--- Attribute (Vordergrund/Hintergrund je Zelle, hex) ---
|
||||
|
||||
|
||||
7070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070
|
||||
e1e1e1e1e1e1e1e1e1e1e1e1e1
|
||||
|
||||
|
||||
70
|
||||
70
|
||||
|
||||
|
||||
70707070707070707070707070707070707070707070707070
|
||||
|
||||
707070707070
|
||||
10
tests/compat/breitezeichen.bas
Normal file
10
tests/compat/breitezeichen.bas
Normal file
@@ -0,0 +1,10 @@
|
||||
' tb-screen: 80x25
|
||||
' Breite Zeichen (CJK, Emoji) belegen zwei Zellen (Entscheidung 2026-09-02):
|
||||
' der Cursor rückt um zwei Spalten vor, POS zählt die Anfangsspalte, und ein
|
||||
' breites Zeichen am rechten Rand wandert als Ganzes in die nächste Zeile.
|
||||
PRINT "AB"; "中文"; "CD"
|
||||
PRINT LEN("中文")
|
||||
' 79 schmale Zeichen, dann ein breites: die letzte Spalte bleibt leer.
|
||||
PRINT STRING$(79, "x"); "字"; "E"
|
||||
PRINT "Ende"
|
||||
END
|
||||
5
tests/compat/breitezeichen.out
Normal file
5
tests/compat/breitezeichen.out
Normal file
@@ -0,0 +1,5 @@
|
||||
AB中文CD
|
||||
2
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
字E
|
||||
Ende
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' BYREF wirkt auf das Argument zurueck; Klammern erzwingen
|
||||
' Wertuebergabe (BYVAL); STATIC behaelt Werte; Rekursion ueber den
|
||||
' Funktionsnamen (Spec vm-ausfuehrung).
|
||||
|
||||
18
tests/compat/datarohtext.bas
Normal file
18
tests/compat/datarohtext.bas
Normal file
@@ -0,0 +1,18 @@
|
||||
' tb-screen: 80x25
|
||||
' DATA-Rohtext (Spec sprach-frontend): unquotierte Elemente behalten ihre
|
||||
' Gross-/Kleinschreibung und ihren inneren Leerraum; Leerraum an den
|
||||
' Raendern faellt weg. Quotierte Elemente bleiben unveraendert, auch mit
|
||||
' Komma. Ein Doppelpunkt beendet die Anweisung.
|
||||
DATA Hallo Welt, ab cd , x
|
||||
READ a$, b$, c$
|
||||
PRINT "["; a$; "]["; b$; "]["; c$; "]"
|
||||
DATA "a,b", 42, -3.5, MiXeD Text
|
||||
READ s$, n%, d!, t$
|
||||
PRINT "["; s$; "]"; n%; d!; "["; t$; "]"
|
||||
DATA eins : PRINT "nach Doppelpunkt"
|
||||
READ e$
|
||||
PRINT "["; e$; "]"
|
||||
RESTORE
|
||||
READ a$
|
||||
PRINT "nach RESTORE: ["; a$; "]"
|
||||
END
|
||||
5
tests/compat/datarohtext.out
Normal file
5
tests/compat/datarohtext.out
Normal file
@@ -0,0 +1,5 @@
|
||||
[Hallo Welt][ab cd][x]
|
||||
[a,b] 42 -3.5 [MiXeD Text]
|
||||
nach Doppelpunkt
|
||||
[eins]
|
||||
nach RESTORE: [Hallo Welt]
|
||||
119
tests/compat/dateien.bas
Normal file
119
tests/compat/dateien.bas
Normal file
@@ -0,0 +1,119 @@
|
||||
' tb-screen: 80x25
|
||||
' tb-tempdir
|
||||
' Datei-E/A (Spec datei-eio): sequenziell schreiben und lesen, wahlfreier
|
||||
' Zugriff mit UDT-Records und mit FIELD/LSET/RSET, Binaerzugriff sowie die
|
||||
' Status- und Dateisystemfunktionen. Laeuft in einem temporaeren Verzeichnis.
|
||||
TYPE Kunde
|
||||
Nr AS INTEGER
|
||||
Name AS STRING * 8
|
||||
Saldo AS DOUBLE
|
||||
END TYPE
|
||||
DIM k AS Kunde, g AS Kunde
|
||||
|
||||
' --- sequenziell ---
|
||||
OPEN "seq.txt" FOR OUTPUT AS #1
|
||||
PRINT #1, "Zeile eins"
|
||||
PRINT #1, 1; 2; 3
|
||||
WRITE #1, "a,b", 5
|
||||
CLOSE #1
|
||||
OPEN "seq.txt" FOR INPUT AS #1
|
||||
PRINT "Groesse:"; LOF(1)
|
||||
LINE INPUT #1, z$
|
||||
PRINT "["; z$; "]"
|
||||
LINE INPUT #1, z$
|
||||
PRINT "["; z$; "]"
|
||||
INPUT #1, s$, n%
|
||||
PRINT "["; s$; "]"; n%; "EOF:"; EOF(1)
|
||||
CLOSE #1
|
||||
|
||||
' --- Anhaengen ---
|
||||
OPEN "seq.txt" FOR APPEND AS #1
|
||||
PRINT #1, "angehaengt"
|
||||
CLOSE #1
|
||||
OPEN "seq.txt" FOR INPUT AS #1
|
||||
anzahl% = 0
|
||||
DO WHILE NOT EOF(1)
|
||||
LINE INPUT #1, z$
|
||||
anzahl% = anzahl% + 1
|
||||
LOOP
|
||||
PRINT "Zeilen:"; anzahl%; "letzte: ["; z$; "]"
|
||||
CLOSE #1
|
||||
|
||||
' --- wahlfrei mit UDT ---
|
||||
OPEN "rec.dat" FOR RANDOM AS #2 LEN = 42
|
||||
k.Nr = 7
|
||||
k.Name = "Mueller"
|
||||
k.Saldo = 100
|
||||
PUT #2, 3, k
|
||||
GET #2, 3, g
|
||||
PRINT g.Nr; "["; g.Name; "]"; g.Saldo; "LOF:"; LOF(2)
|
||||
CLOSE #2
|
||||
|
||||
' --- wahlfrei mit FIELD ---
|
||||
OPEN "fld.dat" FOR RANDOM AS #3 LEN = 80
|
||||
FIELD #3, 10 AS nm$, 10 AS ort$
|
||||
LSET nm$ = "Meier"
|
||||
RSET ort$ = "Berlin"
|
||||
PUT #3, 1
|
||||
LSET nm$ = "Schulz"
|
||||
LSET ort$ = "Hamburg"
|
||||
PUT #3, 2
|
||||
GET #3, 1
|
||||
PRINT "["; nm$; "]["; ort$; "]"
|
||||
GET #3, 2
|
||||
PRINT "["; nm$; "]["; ort$; "]"
|
||||
CLOSE #3
|
||||
|
||||
' --- binaer ---
|
||||
OPEN "bin.dat" FOR BINARY AS #4
|
||||
w% = 4711
|
||||
PUT #4, 4, w%
|
||||
w% = 0
|
||||
GET #4, 4, w%
|
||||
PRINT "binaer:"; w%; "SEEK:"; SEEK(4)
|
||||
CLOSE #4
|
||||
|
||||
' --- Konvertierungsfunktionen ---
|
||||
PRINT CVI(MKI$(-5)); CVL(MKL$(123456)); CVD(MKD$(1.5))
|
||||
PRINT LEN(MKI$(1)); LEN(MKL$(1)); LEN(MKD$(1))
|
||||
|
||||
' --- Dateisystem ---
|
||||
MKDIR "unter"
|
||||
PRINT "Verzeichnis:"; LEN(CURDIR$) > 0
|
||||
n$ = DIR$("*.dat")
|
||||
anzahl% = 0
|
||||
DO WHILE n$ <> ""
|
||||
anzahl% = anzahl% + 1
|
||||
n$ = DIR$("")
|
||||
LOOP
|
||||
PRINT "dat-Dateien:"; anzahl%
|
||||
NAME "seq.txt" AS "neu.txt"
|
||||
PRINT "umbenannt:"; DIR$("neu.txt") <> ""
|
||||
KILL "neu.txt"
|
||||
KILL "rec.dat"
|
||||
KILL "fld.dat"
|
||||
KILL "bin.dat"
|
||||
RMDIR "unter"
|
||||
|
||||
' --- Fehlerfaelle ---
|
||||
ON ERROR GOTO Fehler
|
||||
OPEN "fehlt.txt" FOR INPUT AS #5
|
||||
W1:
|
||||
OPEN "a.txt" FOR OUTPUT AS #6
|
||||
OPEN "b.txt" FOR OUTPUT AS #6
|
||||
W2:
|
||||
x& = LOF(9)
|
||||
W3:
|
||||
CLOSE #6
|
||||
KILL "a.txt"
|
||||
ON ERROR GOTO 0
|
||||
PRINT "fertig"
|
||||
END
|
||||
Fehler:
|
||||
PRINT "Fehler"; ERR
|
||||
f% = f% + 1
|
||||
SELECT CASE f%
|
||||
CASE 1: RESUME W1
|
||||
CASE 2: RESUME W2
|
||||
CASE ELSE: RESUME W3
|
||||
END SELECT
|
||||
18
tests/compat/dateien.out
Normal file
18
tests/compat/dateien.out
Normal file
@@ -0,0 +1,18 @@
|
||||
Groesse: 29
|
||||
[Zeile eins]
|
||||
[ 1 2 3 ]
|
||||
[a,b] 5 EOF:-1
|
||||
Zeilen: 4 letzte: [angehaengt]
|
||||
7 [Mueller ] 100 LOF: 126
|
||||
[Meier ][ Berlin]
|
||||
[Schulz ][Hamburg ]
|
||||
binaer: 4711 SEEK: 6
|
||||
-5 123456 1.5
|
||||
2 4 8
|
||||
Verzeichnis:-1
|
||||
dat-Dateien: 3
|
||||
umbenannt:-1
|
||||
Fehler 53
|
||||
Fehler 55
|
||||
Fehler 52
|
||||
fertig
|
||||
27
tests/compat/datumzeit.bas
Normal file
27
tests/compat/datumzeit.bas
Normal file
@@ -0,0 +1,27 @@
|
||||
' tb-screen: 80x25
|
||||
' Datum und Zeit (Spec konsolen-basisbibliothek): Serienwert-Zaehlweise,
|
||||
' Rueckumwandlung, WEEKDAY = 1 fuer Sonntag, Textformen und die
|
||||
' programmeigene Uhr ueber DATE$ =/TIME$ =.
|
||||
d# = DATESERIAL(1992, 3, 15)
|
||||
PRINT YEAR(d#); MONTH(d#); DAY(d#); WEEKDAY(d#)
|
||||
PRINT DATESERIAL(1900, 1, 1)
|
||||
t# = TIMESERIAL(13, 45, 30)
|
||||
PRINT HOUR(t#); MINUTE(t#); SECOND(t#)
|
||||
PRINT DATEVALUE("03-15-1992") = d#; TIMEVALUE("13:45:30") = t#
|
||||
DATE$ = "01-02-1993"
|
||||
TIME$ = "10:20:30"
|
||||
PRINT DATE$; " "; TIME$
|
||||
PRINT YEAR(NOW); MONTH(NOW); DAY(NOW)
|
||||
ON ERROR GOTO Fehler
|
||||
x# = DATESERIAL(1992, 13, 1)
|
||||
Weiter:
|
||||
x# = TIMEVALUE("25:00")
|
||||
Ende:
|
||||
ON ERROR GOTO 0
|
||||
PRINT "fertig"
|
||||
END
|
||||
Fehler:
|
||||
PRINT "Fehler"; ERR
|
||||
n% = n% + 1
|
||||
IF n% = 1 THEN RESUME Weiter
|
||||
RESUME Ende
|
||||
9
tests/compat/datumzeit.out
Normal file
9
tests/compat/datumzeit.out
Normal file
@@ -0,0 +1,9 @@
|
||||
1992 3 15 1
|
||||
2
|
||||
13 45 30
|
||||
-1 -1
|
||||
01-02-1993 10:20:30
|
||||
1993 1 2
|
||||
Fehler 5
|
||||
Fehler 5
|
||||
fertig
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' ON ERROR/RESUME NEXT/ERR/ERL (Spec vm-fehlerbehandlung):
|
||||
' Handler-Eintritt setzt ERR und ERL (letzte numerische Zeilennummer);
|
||||
' RESUME NEXT setzt nach der fehlerausloesenden Anweisung fort.
|
||||
|
||||
37
tests/compat/finanzen.bas
Normal file
37
tests/compat/finanzen.bas
Normal file
@@ -0,0 +1,37 @@
|
||||
' tb-screen: 80x25
|
||||
' Finanzmathematik (Spec konsolen-basisbibliothek): Endwert, Zahlung,
|
||||
' Zins-/Tilgungsanteil, Abschreibungen, Kapitalwert und interner Zinsfuss.
|
||||
' Vorzeichen wie beim Vorbild: ausgehende Zahlungen negativ.
|
||||
PRINT USING "########.##"; FV#(0.05, 10, -100, 0, 0)
|
||||
z# = PMT#(0.05, 10, 1000, 0, 0)
|
||||
PRINT USING "########.##"; z#
|
||||
' Zins- und Tilgungsanteil ergaenzen sich zur Zahlung.
|
||||
PRINT IPMT#(0.05, 1, 10, 1000, 0, 0) + PPMT#(0.05, 1, 10, 1000, 0, 0) = z#
|
||||
PRINT USING "#####.## "; SLN#(1000, 100, 10); SYD#(1000, 100, 10, 1); DDB#(1000, 100, 10, 1)
|
||||
DIM q#(1 TO 4)
|
||||
q#(1) = -1000: q#(2) = 400: q#(3) = 400: q#(4) = 400
|
||||
PRINT USING "#.######"; IRR#(q#(), 0.1)
|
||||
PRINT USING "#.######"; MIRR#(q#(), 0.05, 0.06)
|
||||
PRINT USING "#.######"; RATE#(10, z#, 1000, 0, 0, 0.1)
|
||||
PRINT USING "###.##"; NPER#(0.05, z#, 1000, 0, 0)
|
||||
' Fehlerfaelle: ungueltige Argumente und erfolglose Naeherung.
|
||||
ON ERROR GOTO Fehler
|
||||
x# = SLN#(1000, 100, 0)
|
||||
W1:
|
||||
x# = SYD#(1000, 100, 10, 11)
|
||||
W2:
|
||||
DIM p#(1 TO 2)
|
||||
p#(1) = 100: p#(2) = 200
|
||||
x# = IRR#(p#(), 0.1)
|
||||
W3:
|
||||
ON ERROR GOTO 0
|
||||
PRINT "fertig"
|
||||
END
|
||||
Fehler:
|
||||
PRINT "Fehler"; ERR
|
||||
n% = n% + 1
|
||||
SELECT CASE n%
|
||||
CASE 1: RESUME W1
|
||||
CASE 2: RESUME W2
|
||||
CASE ELSE: RESUME W3
|
||||
END SELECT
|
||||
12
tests/compat/finanzen.out
Normal file
12
tests/compat/finanzen.out
Normal file
@@ -0,0 +1,12 @@
|
||||
1257.79
|
||||
-129.50
|
||||
-1
|
||||
90.00 163.64 200.00
|
||||
0.097010
|
||||
0.083909
|
||||
0.050000
|
||||
10.00
|
||||
Fehler 5
|
||||
Fehler 5
|
||||
Fehler 5
|
||||
fertig
|
||||
31
tests/compat/groesse.120x40.out
Normal file
31
tests/compat/groesse.120x40.out
Normal file
@@ -0,0 +1,31 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
z 1
|
||||
z 2
|
||||
z 3
|
||||
z 4
|
||||
z 5
|
||||
z 6
|
||||
z 7
|
||||
z 8
|
||||
z 9
|
||||
z 10
|
||||
z 11
|
||||
z 12
|
||||
z 13
|
||||
z 14
|
||||
z 15
|
||||
z 16
|
||||
z 17
|
||||
z 18
|
||||
z 19
|
||||
z 20
|
||||
z 21
|
||||
z 22
|
||||
z 23
|
||||
z 24
|
||||
z 25
|
||||
z 26
|
||||
z 27
|
||||
z 28
|
||||
z 29
|
||||
z 30
|
||||
24
tests/compat/groesse.80x25.out
Normal file
24
tests/compat/groesse.80x25.out
Normal file
@@ -0,0 +1,24 @@
|
||||
z 7
|
||||
z 8
|
||||
z 9
|
||||
z 10
|
||||
z 11
|
||||
z 12
|
||||
z 13
|
||||
z 14
|
||||
z 15
|
||||
z 16
|
||||
z 17
|
||||
z 18
|
||||
z 19
|
||||
z 20
|
||||
z 21
|
||||
z 22
|
||||
z 23
|
||||
z 24
|
||||
z 25
|
||||
z 26
|
||||
z 27
|
||||
z 28
|
||||
z 29
|
||||
z 30
|
||||
13
tests/compat/groesse.bas
Normal file
13
tests/compat/groesse.bas
Normal file
@@ -0,0 +1,13 @@
|
||||
' tb-screen: 80x25, 120x40
|
||||
' Nachweis der Größenunabhängigkeit (Spec kompat-testkorpus): dasselbe
|
||||
' Programm läuft bei zwei Bildschirmgrößen. Der Umbruch am rechten Rand und
|
||||
' das Scrollen am unteren Rand folgen der jeweiligen Größe — nirgends steckt
|
||||
' eine feste 80 oder 25.
|
||||
FOR i% = 1 TO 100
|
||||
PRINT "x";
|
||||
NEXT i%
|
||||
PRINT
|
||||
FOR z% = 1 TO 30
|
||||
PRINT "z"; z%
|
||||
NEXT z%
|
||||
END
|
||||
11
tests/compat/groessenaenderung.bas
Normal file
11
tests/compat/groessenaenderung.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
' tb-screen: 80x25
|
||||
' tb-resize: nach 1 auf 120x40
|
||||
' Nachweis: eine Größenänderung mitten im Lauf wird zugestellt. Der zuvor
|
||||
' ausgegebene Inhalt bleibt oben links erhalten, die folgende Ausgabe nutzt
|
||||
' die neuen Grenzen (100 Zeichen passen jetzt in eine Zeile).
|
||||
PRINT "vorher"
|
||||
FOR i% = 1 TO 100
|
||||
PRINT "y";
|
||||
NEXT i%
|
||||
PRINT
|
||||
END
|
||||
2
tests/compat/groessenaenderung.out
Normal file
2
tests/compat/groessenaenderung.out
Normal file
@@ -0,0 +1,2 @@
|
||||
vorher
|
||||
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' Minimalfall: PRINT mit String und Zeilenumbruch
|
||||
PRINT "Hallo, Welt!"
|
||||
END
|
||||
11
tests/compat/keymakros.bas
Normal file
11
tests/compat/keymakros.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
' tb-screen: 80x25
|
||||
' Funktionstasten-Makros (Original-Hilfe): KEY n, text$ belegt F1-F10 sowie
|
||||
' 30/31 fuer F11/F12, hoechstens 15 Zeichen. KEY LIST listet alle zwoelf
|
||||
' untereinander, KEY ON blendet die Softkey-Zeile unten ein.
|
||||
KEY 1, "LIST"
|
||||
KEY 2, "RUN"
|
||||
KEY 30, "F11-Makro"
|
||||
KEY 3, "mehr als fuenfzehn Zeichen"
|
||||
KEY LIST
|
||||
PRINT "nach LIST"
|
||||
END
|
||||
13
tests/compat/keymakros.out
Normal file
13
tests/compat/keymakros.out
Normal file
@@ -0,0 +1,13 @@
|
||||
LIST
|
||||
RUN
|
||||
mehr als fuenfz
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
F11-Makro
|
||||
|
||||
nach LIST
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' FOR/NEXT, DO/LOOP, SELECT CASE, GOSUB
|
||||
FOR i% = 1 TO 3
|
||||
PRINT "for"; i%
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' Zahlenkonvertierungs-Matrix (docs/tbvm-design.md, Abschnitt
|
||||
' "Zahlenkonvertierungs-Matrix"): Banker's Rounding (Original-Hilfe:
|
||||
' CINT), Ganzzahldivision rundet Operanden vor, Logik bitweise auf
|
||||
@@ -9,6 +10,17 @@ PRINT 1 / 4; 1 / 3
|
||||
PRINT 2 ^ 10; 2 ^ 0.5
|
||||
PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0; 1.5 AND 1
|
||||
PRINT CLNG(2.5); CLNG(3.5)
|
||||
' --- Die vier Projektentscheidungen vom 2026-09-04 (docs/tbvm-design.md) ---
|
||||
' 1. CURRENCY gemischt mit Gleitkomma ergibt DOUBLE, nicht SINGLE.
|
||||
c@ = 2.5
|
||||
PRINT c@ + 1.5!; c@ + 1.5#; c@ * 2
|
||||
' 2. Ergebnistyp von ^: DOUBLE bei DOUBLE/CURRENCY, sonst SINGLE.
|
||||
PRINT 2 ^ 3; 2.5! ^ 2; 2.5# ^ 2; c@ ^ 2
|
||||
' 3. Exponentialform-Schwelle von SINGLE: Exponent < -7 oder >= 7.
|
||||
PRINT 1E-7!; 1E-8!; 9999999!; 10000000!
|
||||
PRINT .01!; .001!
|
||||
' 4. CURRENCY-Ausgabe: nachgestellte Nullen entfallen.
|
||||
PRINT 1.5@; 2@; 2.5000@; .1234@; 1.10@
|
||||
ON ERROR GOTO Uebergelaufen
|
||||
i% = 32767
|
||||
i% = i% + 1
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
1024 1.414214
|
||||
2 7 5 -1 0
|
||||
2 4
|
||||
4 4 5
|
||||
8 6.25 6.25 6.25
|
||||
.0000001 1E-08 9999999 1E+07
|
||||
.01 .001
|
||||
1.5 2 2.5 .1234 1.1
|
||||
Fehler 6
|
||||
|
||||
40
tests/compat/mathematik.bas
Normal file
40
tests/compat/mathematik.bas
Normal file
@@ -0,0 +1,40 @@
|
||||
' tb-screen: 80x25
|
||||
' Mathematik (Spec konsolen-basisbibliothek): Banker's Rounding, Vorzeichen
|
||||
' von INT/FIX, Konvertierungen und der reproduzierbare PRNG.
|
||||
PRINT CINT(0.5); CINT(1.5); CINT(2.5); CINT(-0.5); CINT(-1.5)
|
||||
PRINT INT(-1.7); FIX(-1.7); INT(1.7); FIX(1.7)
|
||||
PRINT ABS(-3); SGN(-3); SGN(0); SGN(3)
|
||||
PRINT CLNG(2.5); CLNG(3.5)
|
||||
' Fehlerfaelle ueber die Fehlerbehandlung sichtbar machen.
|
||||
ON ERROR GOTO Fehler
|
||||
x = SQR(-1)
|
||||
PRINT "unerreichbar"
|
||||
Weiter1:
|
||||
x = LOG(0)
|
||||
PRINT "unerreichbar"
|
||||
Weiter2:
|
||||
x% = CINT(40000)
|
||||
PRINT "unerreichbar"
|
||||
Weiter3:
|
||||
x = 1 / 0
|
||||
PRINT "unerreichbar"
|
||||
Weiter4:
|
||||
ON ERROR GOTO 0
|
||||
' Reproduzierbarkeit: gleicher Startwert, gleiche Folge.
|
||||
RANDOMIZE 42
|
||||
a1 = RND: a2 = RND
|
||||
RANDOMIZE 42
|
||||
b1 = RND: b2 = RND
|
||||
PRINT "gleich: "; (a1 = b1) AND (a2 = b2)
|
||||
c = RND
|
||||
PRINT "RND(0) wiederholt: "; (RND(0) = c)
|
||||
END
|
||||
Fehler:
|
||||
PRINT "Fehler"; ERR
|
||||
zaehler% = zaehler% + 1
|
||||
SELECT CASE zaehler%
|
||||
CASE 1: RESUME Weiter1
|
||||
CASE 2: RESUME Weiter2
|
||||
CASE 3: RESUME Weiter3
|
||||
CASE ELSE: RESUME Weiter4
|
||||
END SELECT
|
||||
10
tests/compat/mathematik.out
Normal file
10
tests/compat/mathematik.out
Normal file
@@ -0,0 +1,10 @@
|
||||
0 2 2 0 -2
|
||||
-2 -1 1 1
|
||||
3 -1 0 1
|
||||
2 4
|
||||
Fehler 5
|
||||
Fehler 5
|
||||
Fehler 6
|
||||
Fehler 11
|
||||
gleich: -1
|
||||
RND(0) wiederholt: -1
|
||||
22
tests/compat/printusing.bas
Normal file
22
tests/compat/printusing.bas
Normal file
@@ -0,0 +1,22 @@
|
||||
' tb-screen: 80x25
|
||||
' PRINT USING (Spec konsolen-basisbibliothek): numerische Platzhalter,
|
||||
' Tausendertrennung, Vorzeichenfuehrung, Waehrung und Fuellzeichen,
|
||||
' Exponentialform, String-Platzhalter, Wiederverwendung und Feldueberlauf.
|
||||
PRINT USING "###.##"; 3.14159
|
||||
PRINT USING "##.##"; -1.5
|
||||
PRINT USING "+###"; 12
|
||||
PRINT USING "###-"; -12
|
||||
PRINT USING "###-"; 12
|
||||
PRINT USING "##,#####"; 1234567
|
||||
PRINT USING "$$###"; 12
|
||||
PRINT USING "**###"; 12
|
||||
PRINT USING "#.##^^^^"; 1234
|
||||
PRINT USING "!"; "abc"
|
||||
PRINT USING "&"; "abc"
|
||||
PRINT USING "\ \"; "abcdef"
|
||||
PRINT USING "\ \"; "ab"
|
||||
PRINT USING "## "; 1; 2; 3
|
||||
PRINT USING "Wert: ## Stueck"; 7
|
||||
PRINT USING "##"; 12345
|
||||
PRINT FORMAT$(3.14159, "###.##")
|
||||
PRINT FORMAT$(42)
|
||||
18
tests/compat/printusing.out
Normal file
18
tests/compat/printusing.out
Normal file
@@ -0,0 +1,18 @@
|
||||
3.14
|
||||
-1.50
|
||||
+ 12
|
||||
12-
|
||||
12
|
||||
1,234,567
|
||||
$12
|
||||
***12
|
||||
1.23E+03
|
||||
a
|
||||
abc
|
||||
abcd
|
||||
ab
|
||||
1 2 3
|
||||
Wert: 7 Stueck
|
||||
%12345
|
||||
3.14
|
||||
42
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' PRINT-Formatierung von Zahlen: führendes Leerzeichen (bzw. -),
|
||||
' nachgestelltes Leerzeichen; Semikolon = direkt anschließen.
|
||||
PRINT 1; 2; 3
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' Komma = Sprung zur nächsten 14-Zeichen-Druckzone
|
||||
PRINT "a", "b", "c"
|
||||
PRINT "1234567890123", "x"
|
||||
|
||||
14
tests/compat/stringrandfaelle.bas
Normal file
14
tests/compat/stringrandfaelle.bas
Normal file
@@ -0,0 +1,14 @@
|
||||
' tb-screen: 80x25
|
||||
' Randfaelle der Stringbibliothek (Spec konsolen-basisbibliothek):
|
||||
' Laenge ueber Stringende, VAL-Praefix, STR$-Vorzeichenspalte, STRING$ mit
|
||||
' Code und mit String, INSTR mit und ohne Startposition.
|
||||
PRINT "["; LEFT$("ab", 10); "]"; "["; RIGHT$("ab", 10); "]"
|
||||
PRINT "["; MID$("abc", 2); "]"; "["; MID$("abc", 2, 1); "]"
|
||||
PRINT "["; STR$(5); "]"; "["; STR$(-5); "]"; "["; STR$(1.5); "]"
|
||||
PRINT VAL(" 12.5abc"); VAL("x"); VAL("&HFF"); VAL("-3")
|
||||
PRINT "["; STRING$(3, 65); "]"; "["; STRING$(3, "xy"); "]"; "["; SPACE$(3); "]"
|
||||
PRINT INSTR("abcabc", "b"); INSTR(3, "abcabc", "b"); INSTR("abc", "z")
|
||||
PRINT "["; LTRIM$(" a "); "]"; "["; RTRIM$(" a "); "]"
|
||||
PRINT UCASE$("aÄb"); " "; LCASE$("AÄB"); " "; LEN("aÄb")
|
||||
PRINT HEX$(255); " "; OCT$(8); " "; CHR$(9731); " "; ASC("A")
|
||||
END
|
||||
9
tests/compat/stringrandfaelle.out
Normal file
9
tests/compat/stringrandfaelle.out
Normal file
@@ -0,0 +1,9 @@
|
||||
[ab][ab]
|
||||
[bc][b]
|
||||
[ 5][-5][ 1.5]
|
||||
12.5 0 255 -3
|
||||
[AAA][xxx][ ]
|
||||
2 5 0
|
||||
[a ][ a]
|
||||
AÄB aäb 3
|
||||
FF 10 ☃ 65
|
||||
@@ -1,3 +1,4 @@
|
||||
' tb-screen: 80x25
|
||||
' Stringfunktionen; LEN zählt Zeichen (UTF-8-Abweichung dokumentiert)
|
||||
s$ = "Terminal Basic"
|
||||
PRINT LEN(s$)
|
||||
|
||||
15
tests/compat/system.bas
Normal file
15
tests/compat/system.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
' tb-screen: 80x25
|
||||
' System- und Umgebungsanweisungen (Spec konsolen-basisbibliothek).
|
||||
' Nicht abbildbare Groessen liefern dokumentierte Ersatzwerte.
|
||||
ENVIRON "TB_KORPUS=hallo"
|
||||
PRINT "["; ENVIRON$("TB_KORPUS"); "]"
|
||||
ENVIRON "TB_KORPUS="
|
||||
PRINT "["; ENVIRON$("TB_KORPUS"); "]"
|
||||
PRINT "FRE>0:"; FRE(0) > 0; "STACK>0:"; STACK > 0
|
||||
PRINT "ERDEV:"; ERDEV; "["; ERDEV$; "]"
|
||||
TRON
|
||||
TROFF
|
||||
CLEAR
|
||||
PRINT "COMMAND$ leer:"; COMMAND$ = ""
|
||||
PRINT "fertig"
|
||||
END
|
||||
6
tests/compat/system.out
Normal file
6
tests/compat/system.out
Normal file
@@ -0,0 +1,6 @@
|
||||
[hallo]
|
||||
[]
|
||||
FRE>0:-1 STACK>0:-1
|
||||
ERDEV: 0 []
|
||||
COMMAND$ leer:-1
|
||||
fertig
|
||||
7
tests/compat/tabspc.bas
Normal file
7
tests/compat/tabspc.bas
Normal file
@@ -0,0 +1,7 @@
|
||||
' tb-screen: 80x25
|
||||
' TAB(n) springt zur Spalte n; steht der Cursor schon dahinter, folgt erst
|
||||
' ein Zeilenumbruch. SPC(n) gibt n Leerzeichen aus.
|
||||
PRINT "a"; TAB(10); "b"; TAB(20); "c"
|
||||
PRINT "x"; SPC(5); "y"
|
||||
PRINT "lang genug fuer einen Umbruch"; TAB(5); "danach"
|
||||
END
|
||||
4
tests/compat/tabspc.out
Normal file
4
tests/compat/tabspc.out
Normal file
@@ -0,0 +1,4 @@
|
||||
a b c
|
||||
x y
|
||||
lang genug fuer einen Umbruch
|
||||
danach
|
||||
Reference in New Issue
Block a user