From 7e6fa6ea888e55048b5f788966b0942ba313e707 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Tue, 1 Sep 2026 16:36:45 +0200 Subject: [PATCH] Phase 0: Referenzdokumente, Fehlerkatalog, Testkorpus, Ratatui-Spike Entscheidungen festgehalten: TBVM bestaetigt und eingebettet in die Executables; durchgaengig UTF-8 statt CP437 (dokumentierte Abweichung). - docs/: Sprachreferenz, Forms-Referenz, Dateiformate, TBVM-Design - tb-runtime::errors: klassischer Laufzeitfehler-Katalog (implementiert) - tb-ui::screen: 80x25-Unicode-Zellenpuffer mit 16-Farben-Abbildung, Scrollbereich (VIEW PRINT), Letterboxing; Ratatui-Widget + Tests - Spike: cargo run -p tb-ui --example spike (Farben, Unicode, Tasten, Maus) - tests/compat/: erste Referenzprogramme mit byte-genauer Sollausgabe Co-Authored-By: Claude Fable 5 --- .gitattributes | 2 + Cargo.lock | 1 + PLAN.md | 59 +++++-- README.md | 3 +- crates/tb-runtime/src/errors.rs | 163 +++++++++++++++++- crates/tb-ui/Cargo.toml | 3 + crates/tb-ui/examples/spike.rs | 90 ++++++++++ crates/tb-ui/src/lib.rs | 2 +- crates/tb-ui/src/screen.rs | 286 +++++++++++++++++++++++++++++++- docs/dateiformate.md | 55 ++++++ docs/forms-referenz.md | 85 ++++++++++ docs/sprachreferenz.md | 240 +++++++++++++++++++++++++++ docs/tbvm-design.md | 85 ++++++++++ tests/compat/README.md | 20 +++ tests/compat/hello.bas | 3 + tests/compat/hello.out | 1 + tests/compat/kontrollfluss.bas | 26 +++ tests/compat/kontrollfluss.out | 11 ++ tests/compat/printzahlen.bas | 6 + tests/compat/printzahlen.out | 3 + tests/compat/printzonen.bas | 5 + tests/compat/printzonen.out | 3 + tests/compat/strings.bas | 12 ++ tests/compat/strings.out | 9 + 24 files changed, 1148 insertions(+), 25 deletions(-) create mode 100644 .gitattributes create mode 100644 crates/tb-ui/examples/spike.rs create mode 100644 docs/dateiformate.md create mode 100644 docs/forms-referenz.md create mode 100644 docs/sprachreferenz.md create mode 100644 docs/tbvm-design.md create mode 100644 tests/compat/README.md create mode 100644 tests/compat/hello.bas create mode 100644 tests/compat/hello.out create mode 100644 tests/compat/kontrollfluss.bas create mode 100644 tests/compat/kontrollfluss.out create mode 100644 tests/compat/printzahlen.bas create mode 100644 tests/compat/printzahlen.out create mode 100644 tests/compat/printzonen.bas create mode 100644 tests/compat/printzonen.out create mode 100644 tests/compat/strings.bas create mode 100644 tests/compat/strings.out diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..077b328 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Erwartete Ausgaben sind byte-genau (LF, signifikante Leerzeichen am Zeilenende) +tests/compat/*.out -text diff --git a/Cargo.lock b/Cargo.lock index 0ac0a0c..1f54bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,6 +495,7 @@ dependencies = [ name = "tb-ui" version = "0.1.0" dependencies = [ + "anyhow", "crossterm", "log", "ratatui", diff --git a/PLAN.md b/PLAN.md index 3ab6e5c..08dd6a4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -23,17 +23,23 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit ## Phase 0 — Exploration und Grundsatzentscheidungen ### 0.1 Sprach- und Bibliotheksreferenz rekonstruieren -- [ ] Sprachreferenz zusammentragen: Anweisungen, Funktionen, Operatoren, +- [~] Sprachreferenz zusammentragen: Anweisungen, Funktionen, Operatoren, Typsystem (INTEGER, LONG, SINGLE, DOUBLE, STRING, CURRENCY, feste Strings, `TYPE…END TYPE`), Typ-Suffixe (`% & ! # $ @`), `DEFINT`-Familie, Sichtbarkeitsregeln (`SHARED`, `COMMON`, `STATIC`) -- [ ] Forms-/Steuerelemente-Referenz: Steuerelementtypen, Eigenschaften, + → Grundgerüst in [docs/sprachreferenz.md](docs/sprachreferenz.md); + Detailfragen (exaktes Verhalten) werden dort als TODO markiert +- [~] Forms-/Steuerelemente-Referenz: Steuerelementtypen, Eigenschaften, Methoden, Ereignisse, Standardwerte, Fokus- und Z-Reihenfolge -- [ ] Dateiformate dokumentieren: Quelltext (`.BAS`), Formulare (`.FRM`, + → Grundgerüst in [docs/forms-referenz.md](docs/forms-referenz.md) +- [~] Dateiformate dokumentieren: Quelltext (`.BAS`), Formulare (`.FRM`, Textformat mit `VERSION`/`Begin…End`-Blöcken), Projekt (`.MAK`) -- [ ] Laufzeitfehler-Katalog (Codes + Meldungstexte) erfassen -- [ ] Testkorpus aufbauen: kleine Referenzprogramme, deren erwartete Ausgabe - dokumentiert ist (Grundlage der Kompatibilitäts-Testsuite) + → [docs/dateiformate.md](docs/dateiformate.md) +- [x] Laufzeitfehler-Katalog (Codes + Meldungstexte) erfasst und als + `tb_runtime::errors` implementiert (inkl. Tests) +- [~] Testkorpus aufbauen: kleine Referenzprogramme, deren erwartete Ausgabe + dokumentiert ist → erste Programme in `tests/compat/`, Harness folgt + mit Phase 2 (`tbdosc run` + Ausgabevergleich) ### 0.2 VM-/Runtime-Entscheidung (Aufstellung der Optionen) @@ -53,7 +59,10 @@ BASIC-Semantik, serialisierbares Kompilat, gute Fehlerortung (Zeile/Spalte). | **6. Transpilation auf fremde Skript-VM (Lua via mlua/piccolo)** | Ausgereifte VM mit GC geschenkt | Semantik-Mismatch (Zahlentypen, Fehler-/Eventmodell, 1-basierte vs. BASIC-Arrays mit `OPTION BASE`); Fehlerortung und Debugger bilden schlecht ab | Ungeeignet | | **7. Transpilation nach Rust (AOT)** | Native Binaries, keine VM | Kein Interpretermodus → IDE-Kernfeatures (Direktfenster, Start ohne Build-Wartezeit, Debugger) praktisch unmöglich; Rust-Toolchain als Laufzeitvoraussetzung | Ungeeignet als Primärziel | -**Entscheidung (2026-09-01):** Eigene Stack-basierte Bytecode-VM (**Option 1, TBVM**). +**Entscheidung (2026-09-01, bestätigt):** Eigene Stack-basierte Bytecode-VM +(**Option 1, TBVM**), sowohl in der Entwicklungsphase als auch **eingebettet in +die Executables** (`tbdos`-IDE und von `tbdosc` erzeugte Programme), damit der +schnelle Edit-Run-Turnaround überall identisch ist. Die IR/Bytecode-Schicht wird sauber vom Interpreter getrennt, sodass später ein zweites Backend (WASM via Cranelift, Option 3) ergänzt werden kann, ohne das Frontend anzufassen. Für die Ausführungsgeschwindigkeit typischer @@ -61,19 +70,28 @@ Terminal-Programme ist ein Interpreter mehr als ausreichend; entscheidend sind Debugger-Fähigkeit, exakte Fehlersemantik und der schnelle Edit-Run-Zyklus der IDE. - [x] Optionen aufstellen und bewerten (siehe Tabelle) -- [ ] Bytecode-Grobdesign: Stack-Maschine, Konstantenpool, Zeilentabelle +- [~] Bytecode-Grobdesign: Stack-Maschine, Konstantenpool, Zeilentabelle für Diagnostik/Debugger, Aufrufkonventionen (SUB/FUNCTION vs. GOSUB) -- [ ] Speichermodell für Werte: Tagged Enum vs. NaN-Boxing; Refcounting + → Entwurf in [docs/tbvm-design.md](docs/tbvm-design.md) +- [~] Speichermodell für Werte: Tagged Enum (kein NaN-Boxing); Refcounting für Strings/Arrays (kein Zyklenrisiko im Dialekt → Rc genügt) + → Entwurf in [docs/tbvm-design.md](docs/tbvm-design.md) ### 0.3 Ratatui-/Terminal-Spike -- [ ] 80×25-Zellenpuffer (Zeichen + Farbattribut) als eigenes Widget rendern; - Verhalten bei kleineren/größeren Terminals festlegen (Letterboxing?) -- [ ] Codepage-437-Abbildung auf Unicode (Rahmenzeichen, Symbole) prüfen — - Darstellungstest unter Windows Terminal, gnome-terminal, iTerm2 -- [ ] 16-Farben-Palette und Blink/Intensity-Attribut abbilden -- [ ] Maus-Ereignisse (crossterm) und Sondertasten (F1–F12, Alt-Kombis) testen; - bekannte Terminal-Einschränkungen dokumentieren + +**Entscheidung (2026-09-01):** Keine CP437-Emulation — der Textbildschirm ist +durchgängig **UTF-8/Unicode**. Strings sind Unicode-Text, `CHR$`/`ASC` +arbeiten auf Codepoints. Das ist eine bewusste Abweichung vom Vorbild und wird +in der Sprachreferenz unter „Abweichungen" dokumentiert. + +- [x] 80×25-Zellenpuffer (Zeichen + Farbattribut) als eigenes Widget rendern; + kleinere Terminals: Hinweis „Terminal zu klein", größere: zentriert + (Letterboxing) → `tb-ui::screen`, Demo: `cargo run -p tb-ui --example spike` +- [x] 16-Farben-Palette (Vordergrund 0–15, Hintergrund 0–7) auf + ANSI-Indexfarben abgebildet; Blink-Attribut vorerst ignoriert (offen) +- [~] Maus-Ereignisse (crossterm) und Sondertasten (F1–F12, Alt-Kombis) im + Spike sichtbar gemacht; systematischer Test unter Windows Terminal, + gnome-terminal, iTerm2 steht aus - [ ] Ereignisschleifen-Architektur: Terminal-Events → Event-Queue → VM-Ticks ## Phase 1 — Sprach-Frontend (`tb-frontend`) @@ -155,5 +173,12 @@ Debugger-Fähigkeit, exakte Fehlersemantik und der schnelle Edit-Run-Zyklus der - ISAM-Dateiunterstützung des Vorbilds: nachbilden oder als Nicht-Ziel erklären? - `PEEK`/`POKE`/`CALL INTERRUPT` u. ä. Hardware-Nähe: sinnvolle Teilmenge emulieren (z. B. Bildschirmspeicher B800) oder Laufzeitfehler? -- Verhalten bei Terminals < 80×25: Mindestgröße erzwingen oder scrollen? +- Unicode-Zeichen mit Darstellungsbreite ≠ 1 (Emoji, CJK): das Zellenmodell + ist strikt 1 Zeichen = 1 Zelle. Breite Zeichen ablehnen (Fehler 5), + ersetzen oder 2 Zellen belegen? +- `GET`/`PUT` mit Record-Typen unter UTF-8: feste Strings (`STRING * n`) + waren im Vorbild n Bytes — bei uns n Zeichen. Binärlayout der Records + festlegen (Vorschlag: UTF-8 mit Padding auf feste Bytelänge? Oder UTF-32?) +- Blink-Attribut (`COLOR` mit Vordergrund 16–31): ignorieren, als „hell" + darstellen oder echtes Terminal-Blink nutzen (nicht überall unterstützt)? - Lizenzwahl (MIT im Rahmen angenommen — bestätigen) diff --git a/README.md b/README.md index 4a29a9b..a3cdf6a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ mit [Ratatui](https://ratatui.rs) als Terminal-UI-Schicht. - **Volle Library-Kompatibilität für die DOS-Oberflächenprogrammierung**: die ereignisgesteuerte Forms-Engine mit allen Steuerelementen, Menüs, Maus- und Tastaturereignissen, nachgebildet auf einem emulierten - 80×25-Textbildschirm (Codepage 437, 16 Farben) über Ratatui. + 80×25-Textbildschirm (16 Farben) über Ratatui. Bewusste Modernisierung: + durchgängig UTF-8/Unicode statt Codepage 437. - **Plattformübergreifend**: läuft in Terminals unter Linux, macOS und Windows. - **Compiler + VM**: der Compiler erzeugt Bytecode für eine eigene virtuelle Maschine (TBVM); die Abwägung der Runtime-Optionen ist in diff --git a/crates/tb-runtime/src/errors.rs b/crates/tb-runtime/src/errors.rs index 6d8ca68..7aa91be 100644 --- a/crates/tb-runtime/src/errors.rs +++ b/crates/tb-runtime/src/errors.rs @@ -1,4 +1,161 @@ -//! Laufzeitfehler: Codes und Meldungstexte kompatibel zum Vorbild -//! (z. B. 6 = Overflow, 9 = Subscript out of range, 53 = File not found). +//! Laufzeitfehler: Codes und Meldungstexte kompatibel zum Vorbild. +//! +//! Die Codes und (englischen) Meldungstexte entsprechen dem klassischen +//! DOS-BASIC-Katalog; Programme prüfen `ERR` gegen diese Nummern, daher +//! sind sie Teil des Kompatibilitätsvertrags. Nicht belegte Nummern +//! liefern "Unprintable error" (wie im Vorbild bei `ERROR n`). -// Platzhalter — wird in Phase 3 ausgearbeitet (siehe PLAN.md) +/// Ein Laufzeitfehler des Dialekts, identifiziert durch seinen klassischen Code. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RuntimeError(pub u8); + +impl RuntimeError { + pub const NEXT_WITHOUT_FOR: Self = Self(1); + pub const SYNTAX_ERROR: Self = Self(2); + pub const RETURN_WITHOUT_GOSUB: Self = Self(3); + pub const OUT_OF_DATA: Self = Self(4); + pub const ILLEGAL_FUNCTION_CALL: Self = Self(5); + pub const OVERFLOW: Self = Self(6); + pub const OUT_OF_MEMORY: Self = Self(7); + pub const LABEL_NOT_DEFINED: Self = Self(8); + pub const SUBSCRIPT_OUT_OF_RANGE: Self = Self(9); + pub const DUPLICATE_DEFINITION: Self = Self(10); + pub const DIVISION_BY_ZERO: Self = Self(11); + pub const ILLEGAL_IN_DIRECT_MODE: Self = Self(12); + pub const TYPE_MISMATCH: Self = Self(13); + pub const OUT_OF_STRING_SPACE: Self = Self(14); + pub const STRING_TOO_COMPLEX: Self = Self(16); + pub const CANNOT_CONTINUE: Self = Self(17); + pub const FUNCTION_NOT_DEFINED: Self = Self(18); + pub const NO_RESUME: Self = Self(19); + pub const RESUME_WITHOUT_ERROR: Self = Self(20); + pub const DEVICE_TIMEOUT: Self = Self(24); + pub const DEVICE_FAULT: Self = Self(25); + pub const FOR_WITHOUT_NEXT: Self = Self(26); + pub const OUT_OF_PAPER: Self = Self(27); + pub const WHILE_WITHOUT_WEND: Self = Self(29); + pub const WEND_WITHOUT_WHILE: Self = Self(30); + pub const DUPLICATE_LABEL: Self = Self(33); + pub const SUBPROGRAM_NOT_DEFINED: Self = Self(35); + pub const ARGUMENT_COUNT_MISMATCH: Self = Self(37); + pub const ARRAY_NOT_DEFINED: Self = Self(38); + pub const VARIABLE_REQUIRED: Self = Self(40); + pub const FIELD_OVERFLOW: Self = Self(50); + pub const INTERNAL_ERROR: Self = Self(51); + pub const BAD_FILE_NAME_OR_NUMBER: Self = Self(52); + pub const FILE_NOT_FOUND: Self = Self(53); + pub const BAD_FILE_MODE: Self = Self(54); + pub const FILE_ALREADY_OPEN: Self = Self(55); + pub const FIELD_STATEMENT_ACTIVE: Self = Self(56); + pub const DEVICE_IO_ERROR: Self = Self(57); + pub const FILE_ALREADY_EXISTS: Self = Self(58); + pub const BAD_RECORD_LENGTH: Self = Self(59); + pub const DISK_FULL: Self = Self(61); + pub const INPUT_PAST_END_OF_FILE: Self = Self(62); + pub const BAD_RECORD_NUMBER: Self = Self(63); + pub const BAD_FILE_NAME: Self = Self(64); + pub const TOO_MANY_FILES: Self = Self(67); + pub const DEVICE_UNAVAILABLE: Self = Self(68); + pub const COMM_BUFFER_OVERFLOW: Self = Self(69); + pub const PERMISSION_DENIED: Self = Self(70); + pub const DISK_NOT_READY: Self = Self(71); + pub const DISK_MEDIA_ERROR: Self = Self(72); + pub const FEATURE_UNAVAILABLE: Self = Self(73); + pub const RENAME_ACROSS_DISKS: Self = Self(74); + pub const PATH_FILE_ACCESS_ERROR: Self = Self(75); + pub const PATH_NOT_FOUND: Self = Self(76); + + /// Klassischer Fehlercode für `ERR`. + pub fn code(self) -> u8 { + self.0 + } + + /// Meldungstext des Vorbilds; unbelegte Codes: "Unprintable error". + pub fn message(self) -> &'static str { + match self.0 { + 1 => "NEXT without FOR", + 2 => "Syntax error", + 3 => "RETURN without GOSUB", + 4 => "Out of DATA", + 5 => "Illegal function call", + 6 => "Overflow", + 7 => "Out of memory", + 8 => "Label not defined", + 9 => "Subscript out of range", + 10 => "Duplicate definition", + 11 => "Division by zero", + 12 => "Illegal in direct mode", + 13 => "Type mismatch", + 14 => "Out of string space", + 16 => "String formula too complex", + 17 => "Cannot continue", + 18 => "Function not defined", + 19 => "No RESUME", + 20 => "RESUME without error", + 24 => "Device timeout", + 25 => "Device fault", + 26 => "FOR without NEXT", + 27 => "Out of paper", + 29 => "WHILE without WEND", + 30 => "WEND without WHILE", + 33 => "Duplicate label", + 35 => "Subprogram not defined", + 37 => "Argument-count mismatch", + 38 => "Array not defined", + 40 => "Variable required", + 50 => "FIELD overflow", + 51 => "Internal error", + 52 => "Bad file name or number", + 53 => "File not found", + 54 => "Bad file mode", + 55 => "File already open", + 56 => "FIELD statement active", + 57 => "Device I/O error", + 58 => "File already exists", + 59 => "Bad record length", + 61 => "Disk full", + 62 => "Input past end of file", + 63 => "Bad record number", + 64 => "Bad file name", + 67 => "Too many files", + 68 => "Device unavailable", + 69 => "Communication-buffer overflow", + 70 => "Permission denied", + 71 => "Disk not ready", + 72 => "Disk-media error", + 73 => "Feature unavailable", + 74 => "Rename across disks", + 75 => "Path/File access error", + 76 => "Path not found", + _ => "Unprintable error", + } + } +} + +impl std::fmt::Display for RuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl std::error::Error for RuntimeError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bekannte_codes() { + assert_eq!(RuntimeError::OVERFLOW.code(), 6); + assert_eq!(RuntimeError::OVERFLOW.message(), "Overflow"); + assert_eq!(RuntimeError::SUBSCRIPT_OUT_OF_RANGE.code(), 9); + assert_eq!(RuntimeError::FILE_NOT_FOUND.message(), "File not found"); + assert_eq!(RuntimeError::FEATURE_UNAVAILABLE.code(), 73); + } + + #[test] + fn unbelegte_codes_sind_unprintable() { + assert_eq!(RuntimeError(15).message(), "Unprintable error"); + assert_eq!(RuntimeError(200).message(), "Unprintable error"); + } +} diff --git a/crates/tb-ui/Cargo.toml b/crates/tb-ui/Cargo.toml index 31e7f1c..ffad1eb 100644 --- a/crates/tb-ui/Cargo.toml +++ b/crates/tb-ui/Cargo.toml @@ -11,3 +11,6 @@ ratatui.workspace = true crossterm.workspace = true thiserror.workspace = true log.workspace = true + +[dev-dependencies] +anyhow.workspace = true diff --git a/crates/tb-ui/examples/spike.rs b/crates/tb-ui/examples/spike.rs new file mode 100644 index 0000000..e0cea9f --- /dev/null +++ b/crates/tb-ui/examples/spike.rs @@ -0,0 +1,90 @@ +//! Phase-0-Spike: 80×25-Bildschirmpuffer, 16-Farben-Palette, Unicode, +//! Tastatur- und Mausereignisse. +//! +//! Start: `cargo run -p tb-ui --example spike` — Beenden mit Esc. +//! Getippte Zeichen erscheinen im Eingabebereich; Sondertasten und +//! Mausereignisse werden in der Statuszeile angezeigt. + +use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}; +use crossterm::execute; +use std::io::stdout; +use std::time::Duration; +use tb_ui::screen::TextScreen; + +fn testbild(s: &mut TextScreen) { + s.set_color(15, 1); + s.print(&format!("{:^80}", "Terminal Basic — Phase-0-Spike (Esc beendet)")); + + // Farbraster: alle Vordergrundfarben auf allen Hintergründen + for bg in 0u8..8 { + s.set_color(15, 0); + s.locate(3 + bg as usize, 3).unwrap(); + s.print(&format!("HG {bg} ")); + for fg in 0u8..16 { + s.set_color(fg, bg); + s.print(&format!(" {fg:2}")); + } + } + + // Unicode statt CP437: Rahmen, Umlaute, Symbole + s.set_color(14, 0); + s.locate(12, 3).unwrap(); + s.print("┌─ Unicode ────────────────────────────┐"); + s.locate(13, 3).unwrap(); + s.print("│ Äpfel, Öl, Übermaß, ß — ☃ ♥ ♦ ♣ ♠ π λ │"); + s.locate(14, 3).unwrap(); + s.print("└──────────────────────────────────────┘"); + + s.set_color(7, 0); + s.locate(16, 3).unwrap(); + s.print("Eingabe (Zeichen erscheinen hier):"); + s.locate(17, 3).unwrap(); +} + +fn status(s: &mut TextScreen, text: &str) { + let (r, c) = (s.csrlin(), s.pos()); + s.set_color(0, 7); + s.locate(25, 1).unwrap(); + s.print(&format!("{text:<80.80}")); + s.set_color(7, 0); + s.locate(r, c).unwrap(); +} + +fn main() -> anyhow::Result<()> { + let mut terminal = ratatui::init(); + execute!(stdout(), EnableMouseCapture)?; + + let mut screen = TextScreen::new(); + testbild(&mut screen); + status(&mut screen, "Bereit. Tasten/Maus testen, Esc beendet."); + + loop { + terminal.draw(|f| f.render_widget(&screen, f.area()))?; + if !event::poll(Duration::from_millis(100))? { + continue; + } + match event::read()? { + Event::Key(k) if k.kind != KeyEventKind::Release => match k.code { + KeyCode::Esc => break, + KeyCode::Char(c) => { + screen.print(&c.to_string()); + status(&mut screen, &format!("Taste: {:?} Modifier: {:?}", k.code, k.modifiers)); + } + KeyCode::Enter => screen.print("\n"), + other => status(&mut screen, &format!("Sondertaste: {other:?} Modifier: {:?}", k.modifiers)), + }, + Event::Mouse(m) => { + status( + &mut screen, + &format!("Maus: {:?} bei Spalte {}, Zeile {}", m.kind, m.column + 1, m.row + 1), + ); + } + Event::Resize(w, h) => status(&mut screen, &format!("Resize: {w}x{h}")), + _ => {} + } + } + + execute!(stdout(), DisableMouseCapture)?; + ratatui::restore(); + Ok(()) +} diff --git a/crates/tb-ui/src/lib.rs b/crates/tb-ui/src/lib.rs index ea633a4..366ce32 100644 --- a/crates/tb-ui/src/lib.rs +++ b/crates/tb-ui/src/lib.rs @@ -1,7 +1,7 @@ //! Bildschirm-Runtime und Forms-Engine von Terminal Basic, aufbauend auf Ratatui. //! //! Zwei Schichten: -//! 1. `screen`: Emulation des 80×25-Textbildschirms (Codepage 437, 16 Farben, +//! 1. `screen`: Emulation des 80×25-Textbildschirms (Unicode/UTF-8, 16 Farben, //! Cursor) als Zeichenpuffer, gerendert über Ratatui. Darauf setzen die //! klassischen Anweisungen `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT` auf. //! 2. `forms`: ereignisgesteuerte Forms-Engine — Fenster, Steuerelemente diff --git a/crates/tb-ui/src/screen.rs b/crates/tb-ui/src/screen.rs index 98594c4..4c9049e 100644 --- a/crates/tb-ui/src/screen.rs +++ b/crates/tb-ui/src/screen.rs @@ -1,4 +1,284 @@ -//! Textbildschirm-Emulation: Zellenpuffer (Zeichen + Attribut), Cursor, -//! Codepage-437-Abbildung auf Unicode, Rendering über Ratatui. +//! Textbildschirm-Emulation: 80×25-Zellenpuffer (Unicode-Zeichen + Farb- +//! attribut), Cursor, Scrollen — gerendert als Ratatui-Widget. +//! +//! Der Puffer ist die Grundlage für `PRINT`, `LOCATE`, `COLOR`, `CLS` usw. +//! Entscheidung (siehe PLAN.md): durchgängig Unicode, keine CP437-Emulation. +//! Das Zellenmodell ist strikt 1 Zeichen = 1 Zelle; Zeichen mit +//! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md). -// Platzhalter — wird in Phase 0/3 ausgearbeitet (siehe PLAN.md) +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::widgets::Widget; + +pub const COLS: usize = 80; +pub const 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 { + const MAP: [u8; 16] = [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15]; + Color::Indexed(MAP[(n & 0x0F) as usize]) +} + +/// Der emulierte 80×25-Textbildschirm. +/// +/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile 1–25, +/// Spalte 1–80), wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts. +#[derive(Debug, Clone)] +pub struct TextScreen { + cells: Vec, + /// 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, +} + +impl Default for TextScreen { + fn default() -> Self { + Self::new() + } +} + +impl TextScreen { + pub fn new() -> Self { + TextScreen { + 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, + } + } + + /// `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 * COLS..(row + 1) * COLS].fill(blank); + } + self.cur_row = self.view_top; + self.cur_col = 0; + } + + /// `COLOR vg, hg` — Werte werden wie im Vorbild maskiert + /// (vg 0–31, wir ignorieren Blink → 0–15; hg 0–7). + pub fn set_color(&mut self, fg: u8, bg: u8) { + self.fg = fg & 0x0F; + self.bg = bg & 0x07; + } + + /// `LOCATE zeile, spalte` (1-basiert). Außerhalb des Bildschirms: + /// Fehler 5 beim Aufrufer — hier wird geklemmt geprüft. + pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> { + if row < 1 || row > ROWS || col < 1 || col > 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 > ROWS || top > bottom { + return Err(()); + } + self.view_top = top - 1; + self.view_bottom = bottom - 1; + Ok(()) + } + + pub fn cell(&self, row: usize, col: usize) -> Cell { + self.cells[(row - 1) * 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; andere Steuerzeichen werden + /// (noch) als normale Zeichen behandelt. + 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 * COLS + self.cur_col] = + Cell { ch, fg: self.fg, bg: self.bg }; + self.cur_col += 1; + if self.cur_col >= 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) * COLS); + a[row * COLS..].copy_from_slice(&b[..COLS]); + } + self.cells[self.view_bottom * COLS..(self.view_bottom + 1) * COLS].fill(blank); + } +} + +/// Rendert den Bildschirm zentriert (Letterboxing) in die verfügbare Fläche. +/// Ist das Terminal kleiner als 80×25, wird ein Hinweis angezeigt. +impl Widget for &TextScreen { + fn render(self, area: Rect, buf: &mut Buffer) { + if (area.width as usize) < COLS || (area.height as usize) < ROWS { + let msg = format!( + "Terminal zu klein: {}x{} — benötigt {}x{}", + area.width, area.height, COLS, ROWS + ); + if let Some(cell) = buf.cell_mut((area.x, area.y)) { + cell.set_symbol(" "); + } + buf.set_string(area.x, area.y, msg, Style::default().fg(Color::Red)); + return; + } + let x0 = area.x + (area.width - COLS as u16) / 2; + let y0 = area.y + (area.height - ROWS as u16) / 2; + for row in 0..ROWS { + for col in 0..COLS { + let c = self.cells[row * COLS + col]; + if let Some(cell) = buf.cell_mut((x0 + col as u16, y0 + row as u16)) { + let mut s = String::new(); + s.push(c.ch); + cell.set_symbol(&s); + cell.set_style( + Style::default().fg(basic_color(c.fg)).bg(basic_color(c.bg)), + ); + } + } + } + // Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in + // der Forms-/Runtime-Schicht später gezielt gesteuert). + if self.cursor_visible { + let (cx, cy) = (x0 + self.cur_col as u16, y0 + self.cur_row as u16); + if let Some(cell) = buf.cell_mut((cx, cy)) { + cell.set_style(cell.style().add_modifier(ratatui::style::Modifier::REVERSED)); + } + } + } +} + +#[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 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'); // Zeile 25 ist nach 24 gerutscht + 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'); // Kopfzeile unberührt + 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 farbabbildung() { + assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau + assert_eq!(basic_color(4), Color::Indexed(1)); // klassisch Rot + assert_eq!(basic_color(14), Color::Indexed(11)); // Gelb + assert_eq!(basic_color(15), Color::Indexed(15)); // Weiß + } +} diff --git a/docs/dateiformate.md b/docs/dateiformate.md new file mode 100644 index 0000000..0b4bb12 --- /dev/null +++ b/docs/dateiformate.md @@ -0,0 +1,55 @@ +# Dateiformate + +Terminal Basic liest und schreibt die Textformate des Vorbilds, durchgängig +in UTF-8 (Abweichung: das Vorbild nutzte die DOS-Codepage). Binäre +„Fast-Load"-Varianten des Vorbilds sind Nicht-Ziel — nur Textformate. + +## Quelltext: `.BAS` + +Reiner Text, eine Anweisung(sfolge) pro Zeile. Optionale Kopfzeilen der IDE +(`DECLARE`-Prototypen) werden beim Speichern erzeugt/aktualisiert. +Metabefehle in Kommentaren: `'$INCLUDE: 'datei.bi'`, `'$STATIC`, `'$DYNAMIC`. + +## Formular: `.FRM` + +Textformat, zwei Abschnitte: Formular-Beschreibung, dann Code. + +``` +VERSION 1.00 +Begin Form Form1 + Caption = "Beispiel" + Height = 15 + Left = 10 + Top = 4 + Width = 50 + Begin CommandButton cmdOK + Caption = "&OK" + Height = 1 + Left = 18 + Top = 11 + Width = 10 + End +End + +SUB cmdOK_Click () + UNLOAD Form1 +END SUB +``` + +- `VERSION`-Zeile, dann verschachtelte `Begin … End`-Blöcke + mit `Eigenschaft = Wert`-Zeilen (Strings in `"…"`). +- Danach normaler BASIC-Code des Formular-Moduls. +- TODO: exakte Eigenschaftsnamen/-reihenfolge und Einrückung des Vorbilds + an Originaldateien verifizieren (Ziel: Roundtrip-Kompatibilität). + +## Projekt: `.MAK` + +Zeilenweise Liste der Projektdateien (`.BAS`, `.FRM`, `.BI`), TODO: +Optionszeilen des Vorbilds prüfen. Terminal Basic akzeptiert zusätzlich +Kommentarzeilen mit `'`. + +## Kompilat: `.tbc` (neu, eigenes Format) + +Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md). +`tbdosc build` erzeugt wahlweise `.tbc` oder ein eigenständiges Executable +(Runner + eingebettetes `.tbc`). diff --git a/docs/forms-referenz.md b/docs/forms-referenz.md new file mode 100644 index 0000000..b670845 --- /dev/null +++ b/docs/forms-referenz.md @@ -0,0 +1,85 @@ +# Forms-Referenz Terminal Basic + +Rekonstruierte Referenz der Forms-Engine des Vorbilds (textbasierte, +ereignisgesteuerte Oberflächen). Grundlage für `tb-ui::forms` und den +Formular-Designer der IDE. Unklare Punkte sind mit `TODO` markiert. + +## Koordinatenmodell + +Alle Maße in **Textzellen** (Zeilen/Spalten), Ursprung links oben. +Formulare liegen auf dem 80×25-Bildschirm; Steuerelemente relativ zum +Formular-Innenbereich. Eigenschaften `Top`, `Left`, `Height`, `Width`. + +## Steuerelemente (Übersicht) + +| Steuerelement | Zweck | Schlüssel-Ereignisse | +|---|---|---| +| Form | Fenster (mit/ohne Rahmen, Titel, verschiebbar) | Load, Unload, Activate, Deactivate, Resize, Paint, Click, KeyPress | +| Label | statischer Text | Click | +| TextBox | ein-/mehrzeilige Texteingabe | Change, KeyPress, GotFocus, LostFocus | +| CommandButton | Schaltfläche | Click | +| CheckBox | Mehrfachauswahl (Value 0/1/2) | Click | +| OptionButton | Einfachauswahl in Gruppe (Value TRUE/FALSE) | Click | +| Frame | Gruppierungsrahmen (Container) | — | +| ListBox | Liste (List, ListCount, ListIndex, AddItem/RemoveItem) | Click, DblClick | +| ComboBox | Eingabe + Liste (Style 0/1/2) | Change, Click, DblClick | +| HScrollBar / VScrollBar | Bildlauf (Min, Max, Value, SmallChange, LargeChange) | Change, Scroll | +| PictureBox | Text-Zeichenfläche (PRINT/CLS auf Steuerelement) | Click, Paint | +| Timer | unsichtbar; Interval in ms (Vorbild: 55-ms-Ticks) | Timer | +| Menu | Menüleiste/-einträge (Designer-definiert) | Click | + +## Gemeinsame Eigenschaften + +`Name` (Entwurfszeit), `Caption`/`Text`, `Top`/`Left`/`Height`/`Width`, +`Visible`, `Enabled`, `TabIndex`, `TabStop`, `Tag`, `Index` (Steuerelement- +Arrays), `ForeColor`/`BackColor` (0–15 bzw. 0–7), `MousePointer` (TODO). +Zugriff zur Laufzeit: `form.eigenschaft`, `form!element.eigenschaft`; +Standard-Eigenschaft (z. B. `Text` bei TextBox, `Caption` bei Label) bei +Zuweisung ohne Eigenschaftsname. TODO: exakte Standard-Eigenschaften prüfen. + +## Gemeinsame Methoden + +`Move top, left [, height, width]` · `SetFocus` · `Refresh` · `Drag` (Nicht- +Ziel?) · Form zusätzlich: `Show [modal]`, `Hide`, `Cls`, `Print`, `Line` +(Textmodus-Variante? TODO). ListBox/ComboBox: `AddItem text$ [, index]`, +`RemoveItem index`, `Clear` (TODO: hieß es `Clear`?). + +## Ereignismodell + +- Ereignisprozeduren heißen `SUB elementname_Ereignis (argumente)` und liegen + im Formular-Modul. +- Tastatur: `KeyDown(KeyCode%, Shift%)`, `KeyPress(KeyAscii%)`, `KeyUp` — + KeyAscii ist bei uns ein Unicode-Codepoint (Abweichung UTF-8). +- Maus: `MouseDown/MouseMove/MouseUp(Button%, Shift%, X!, Y!)` in Zellen. +- Fokusreihenfolge über `TabIndex`; Tab/Shift+Tab wechseln, Access-Keys per + `&` im Caption (z. B. `"&OK"` → Alt+O). +- Zentrale Schleife: Ereignisse werden nur bei `DOEVENTS`, `SLEEP`, `INPUT`- + artigen Wartezuständen oder nach Ende einer Ereignisprozedur zugestellt + (kooperativ, wie im Vorbild — keine Präemption). + +## Globale Objekte und Funktionen + +- `SCREEN`-Objekt: `SCREEN.ActiveForm`, `SCREEN.ActiveControl`, + `SCREEN.Height`/`Width` (25/80), `SCREEN.MousePointer` (TODO: Umfang). +- `MSGBOX(text$ [, typ% [, titel$]])` als Anweisung und Funktion + (Rückgabe: gedrückte Schaltfläche), `INPUTBOX$(prompt$ [, titel$ + [, standard$ [, spalte%, zeile%]]])`. +- `LOAD form` (Load-Ereignis, unsichtbar), `UNLOAD form` (Unload-Ereignis), + Formular-Referenzen sind statisch (keine Instanziierung wie in späteren + Nachfolgern). + +## Farben und Zeichensatz + +Rahmen, Schatten und Bedienelemente werden mit Unicode-Box-Drawing gezeichnet +(Abweichung: UTF-8 statt CP437, identische Optik). Standard-Farbschema des +Vorbilds (grauer Dialog, schwarze Schrift, weiße Akzente) wird als Default +nachgebildet. TODO: exakte Attributtabelle je Steuerelement-Zustand +(normal/fokussiert/deaktiviert) aus Screenshots/Emulator ableiten. + +## Offene Detailfragen (per Emulator-Session zu klären) + +- Exakte Standardwerte aller Eigenschaften je Steuerelement +- Z-Reihenfolge/Überlappung von Steuerelementen und Formularen +- Verhalten `Show 1` (modal): welche Ereignisse laufen weiter? +- Timer-Auflösung und -Reihenfolge bei mehreren Timern +- Genauer Umfang von PictureBox im Textmodus diff --git a/docs/sprachreferenz.md b/docs/sprachreferenz.md new file mode 100644 index 0000000..3d3d9cc --- /dev/null +++ b/docs/sprachreferenz.md @@ -0,0 +1,240 @@ +# Sprachreferenz Terminal Basic + +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. + +**Bewusste Abweichungen vom Vorbild** stehen am Ende des Dokuments. + +--- + +## 1. Lexik + +- **Zeilenorientiert.** Eine logische Zeile enthält eine oder mehrere + Anweisungen, getrennt durch `:`. Keine Zeilenfortsetzung im Vorbild + (`_` ist eine spätere Erfindung) — TODO: als Erweiterung erlauben? +- **Zeilennummern** sind optional und wirken als Labels. Alphanumerische + **Labels** enden mit `:` am Zeilenanfang (`Fehler:`). +- **Kommentare:** `REM` (ganze Anweisung) und `'` (bis Zeilenende). + `REM`/`'` am Zeilenanfang mit `$STATIC`/`$DYNAMIC`/`$INCLUDE: 'datei'` + sind **Metabefehle**. +- **Bezeichner:** Buchstabe, dann Buchstaben/Ziffern/`.`, max. 40 Zeichen, + case-insensitiv. Optionales Typ-Suffix als letztes Zeichen. +- **Typ-Suffixe:** `%` INTEGER · `&` LONG · `!` SINGLE · `#` DOUBLE · + `$` STRING · `@` CURRENCY. `name`, `name%`, `name$` sind + **verschiedene Variablen**. +- **Keywords** sind reserviert und case-insensitiv; die IDE normalisiert + auf Großschreibung. +- **Numerische Literale:** dezimal (`123`, `1.5`, `1.5E3`, `1D3` für DOUBLE), + hexadezimal `&HFF`, oktal `&O777`; Suffixe wie bei Variablen (`10%`, `10&`, + `1.5#`, `2.5@`). Ohne Suffix: kleinster passender Typ (Ganzzahl → INTEGER, + sonst SINGLE/DOUBLE je nach Präzision; TODO: exakte Regel testen). +- **String-Literale:** `"…"`; doppeltes `""` ergibt ein Anführungszeichen. + +## 2. Typsystem + +| Typ | Suffix | Repräsentation | Wertebereich | +|---|---|---|---| +| INTEGER | `%` | i16 | −32 768 … 32 767 | +| LONG | `&` | i32 | −2 147 483 648 … 2 147 483 647 | +| SINGLE | `!` | f32 | ±3.4E38 | +| DOUBLE | `#` | f64 | ±1.8E308 | +| STRING | `$` | dynamischer Unicode-String | Länge 0 … 32 767 Zeichen | +| STRING * n | — | fester String, n Zeichen | nur in `TYPE`/`DIM` | +| CURRENCY | `@` | i64, Festkomma ×10 000 | ±922 337 203 685 477.5807 | + +- **Benutzerdefinierte Typen:** `TYPE name … END TYPE` mit Elementen fester + Größe (numerische Typen, `STRING * n`, verschachtelte TYPEs). Kein + dynamischer STRING in TYPE. +- **Implizite Deklaration:** Erstverwendung deklariert die Variable. Ohne + Suffix gilt der Standardtyp SINGLE, änderbar per + `DEFINT/DEFLNG/DEFSNG/DEFDBL/DEFSTR/DEFCUR a–z` (buchstabenbereichsweise, + wirkt pro Modul/Prozedur ab Deklaration). +- `OPTION EXPLICIT` gibt es im Vorbild **nicht** — TODO: als opt-in + Erweiterung anbieten? +- **Arrays:** `DIM a(10)`, `DIM a(1 TO 10, 0 TO 5)`. Untergrenze standardmäßig + 0, per `OPTION BASE 1` änderbar. `$STATIC`/`$DYNAMIC` bzw. Kontext bestimmen + statisch/dynamisch; `REDIM` (dynamisch, löscht Inhalt), `ERASE` + (reinitialisiert statisch / gibt dynamisch frei). Max. 8 Dimensionen + (TODO: prüfen). `LBOUND`/`UBOUND` liefern Grenzen. +- **Konvertierung:** implizit zwischen numerischen Typen mit Rundung + (Banker's Rounding bei `CINT`/`CLNG` und Zuweisung an Ganzzahl); Überlauf + → Fehler 6. Keine implizite Konvertierung Zahl ↔ String (Fehler 13, + Type mismatch). + +## 3. Deklarationen und Sichtbarkeit + +- `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). +- `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. +- `CONST name = ausdruck` — Konstanten (konstante Ausdrücke zur Compilezeit). +- `DECLARE SUB/FUNCTION name (parameter)` — Prototyp; die IDE erzeugt sie + automatisch beim Speichern. + +## 4. Operatoren (nach Priorität, hoch → niedrig) + +1. `^` (Potenz) +2. unäres `-` +3. `*`, `/` (Fließkommadivision) +4. `\` (Ganzzahldivision; Operanden werden gerundet auf INTEGER/LONG) +5. `MOD` (Ganzzahlrest, Vorzeichen wie Dividend) +6. `+`, `-` (`+` auch String-Verkettung) +7. Vergleiche `= <> < > <= >=` (Zahlen und Strings; Strings + codepoint-weise — Abweichung, s. u.) +8. `NOT` +9. `AND` +10. `OR` +11. `XOR` +12. `EQV` +13. `IMP` + +Logische Operatoren sind **bitweise** auf Ganzzahlen; Vergleichsergebnis ist +INTEGER −1 (wahr) / 0 (falsch). + +## 5. Kontrollfluss + +- `IF b THEN … [ELSE …]` (einzeilig) und Block-`IF … THEN / ELSEIF / ELSE / + END IF` +- `SELECT CASE ausdruck` mit `CASE wert`, `CASE a TO b`, `CASE IS >= x`, + `CASE ELSE` +- `FOR i = a TO b [STEP s] … NEXT [i]` (Grenzen werden einmal ausgewertet; + Schleifenvariable numerisch) +- `DO [WHILE|UNTIL b] … LOOP [WHILE|UNTIL b]`, `WHILE … WEND` +- `EXIT FOR / EXIT DO / EXIT SUB / EXIT FUNCTION / EXIT DEF` +- `GOTO ziel`, `GOSUB ziel` / `RETURN [ziel]` +- `ON n GOTO liste`, `ON n GOSUB liste` (berechneter Sprung, 1-basiert; + 0 oder > Anzahl: kein Sprung; negativ/>255: Fehler 5) +- `END` (Programmende), `STOP` (Unterbrechung → im IDE-Kontext Debugger), + `SYSTEM` (Programmende, im Vorbild „zurück zu DOS") +- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten — + zentral für Forms) + +## 6. Prozeduren + +- `SUB name (p1 AS t, p2(), …) [STATIC] … END SUB`; Aufruf `CALL name(args)` + oder `name args` (ohne Klammern). +- `FUNCTION name (…) [STATIC] … name = wert … END FUNCTION`; Typ über Suffix + 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?). +- 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`; Aufruf + `FNname(…)`. Modulweit, kein eigener Namensraum. + +## 7. Fehlerbehandlung + +- `ON ERROR GOTO label` (aktiviert Handler), `ON ERROR GOTO 0` (deaktiviert; + in einem aktiven Handler: Fehler weiterreichen → Programmabbruch), + `ON ERROR RESUME NEXT` — TODO: prüfen, ob das Vorbild das kennt + (QB-Familie: nein; Forms-Dialekt: ja?). +- Im Handler: `RESUME` (fehlerauslösende Anweisung wiederholen), + `RESUME NEXT`, `RESUME label`. +- `ERR` (Code), `ERL` (Zeilennummer, nur numerische Zeilennummern!), + `ERROR n` (Fehler auslösen). +- Fehler im aktiven Handler → sofortiger Abbruch. Fehler ohne Handler → + Abbruch mit Meldung „Fehlertext in Zeile n" bzw. Debugger in der IDE. +- Fehlerkatalog: siehe `tb-runtime/src/errors.rs` (implementiert). + +## 8. Ereignis-Traps (klassisch, ohne Forms) + +`ON TIMER(n) GOSUB label` + `TIMER ON/OFF/STOP`; analog `ON KEY(n)`, +`ON PLAY`, `ON COM(n)`, `ON PEN`, `ON STRIG(n)`. Für Terminal Basic relevant: +`TIMER` und `KEY`; Rest: Nicht-Ziel (siehe Abweichungen). + +## 9. Konsolen-E/A + +- `PRINT [#n,] liste` — Trennzeichen `;` (direkt anschließend) und `,` + (nächste 14-Zeichen-Druckzone). Zahlen: führendes Leerzeichen bzw. `-`, + nachgestelltes Leerzeichen. Abschluss ohne `;`/`,` → Zeilenumbruch. +- `PRINT USING "format"; liste` — Formatzeichen: `#` Ziffer, `.` Dezimalpunkt, + `,` Tausendertrennung, `+`/`-` Vorzeichen, `$$` Währung, `**` Füllsterne, + `^^^^` Exponent, `&` String ganz, `!` erstes Zeichen, `\ \` n Zeichen, + `_` Literal-Escape. +- `INPUT ["prompt"{;|,}] var, …` (mit `;` vor Prompt: kein „? "); + `LINE INPUT` (ganze Zeile in String). +- `INKEY$` (nicht blockierend; "" wenn leer; erweiterte Tasten: + 2-Zeichen-Sequenz `CHR$(0)+code` im Vorbild — Abweichung s. u.), + `INPUT$(n [,#f])`. +- `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`. +- `COLOR [vg][,hg]` (vg 0–31: 16–31 = blinkend; hg 0–7), `CLS`, + `WIDTH` (80/40 — wir: nur 80), `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. + +## 10. Datei-E/A + +- `OPEN datei$ [FOR modus] [ACCESS zugriff] [lock] AS [#]n [LEN=reclen]` + Modi: `INPUT`, `OUTPUT`, `APPEND` (sequenziell), `RANDOM` (Standard), + `BINARY`. +- `CLOSE [#n, …]`, `RESET` (alle schließen). +- Sequenziell: `PRINT #`, `PRINT # USING`, `WRITE #` (CSV-artig, Strings in + `"…"`), `INPUT #`, `LINE INPUT #`, `EOF(n)`. +- Random: `FIELD #n, breite AS var$…` (klassisch) **und** `GET/PUT #n + [,satznr] [,var]` mit TYPE-Variablen; `LSET`/`RSET` für Feldpuffer. +- Binary: `GET/PUT #n, [pos], var`, `SEEK #n, pos` / `SEEK(n)`, + `LOC(n)`, `LOF(n)`. +- Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`, + `CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`. +- Pfade: plattformneutral; `/` und `\` werden akzeptiert. + +## 11. Eingebaute Funktionen (Katalog) + +**Strings:** `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion **und** Anweisung), +`INSTR([start,] s$, such$)`, `UCASE$`, `LCASE$`, `LTRIM$`, `RTRIM$`, +`SPACE$`, `STRING$(n, zeichen|code)`, `ASC`, `CHR$`, `STR$`, `VAL`, +`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`. + +**Datum/Zeit:** `DATE$` (Funktion und Anweisung — Setzen: Nicht-Ziel), +`TIME$`, `TIMER` (Sekunden seit Mitternacht, SINGLE). + +**Sonstiges:** `LBOUND`, `UBOUND`, `FRE(…)` (freier Speicher — liefert bei +uns Pseudowerte), `VARPTR`/`VARSEG`/`SADD`/`PEEK`/`POKE` → Nicht-Ziel +(Fehler 73), `ENVIRON$`, `COMMAND$`, `SHELL [cmd$]`. + +**DATA:** `DATA konstanten`, `READ var, …`, `RESTORE [label]`. + +## 12. Forms-Anbindung (Details in forms-referenz.md) + +`form.eigenschaft = wert`, `form!steuerelement.eigenschaft`, +Ereignisprozeduren `SUB name_Ereignis (…)`, `LOAD`/`UNLOAD form`, +`form.SHOW [modal]`, `form.HIDE`, `MSGBOX`/`INPUTBOX$`-Funktionen, +`DOEVENTS`, `SCREEN`-Objekt (aktives Formular/Steuerelement). + +--- + +## Abweichungen vom Vorbild (beschlossen) + +1. **UTF-8/Unicode statt CP437** (2026-09-01). Konsequenzen: + - STRING ist eine Folge von Unicode-Zeichen (Codepoints); `LEN` zählt + Zeichen, nicht Bytes. `ASC`/`CHR$` arbeiten auf Codepoints + (`CHR$(9731)` = „☃"). `ASC("")` bleibt Fehler 5. + - String-Vergleich codepoint-weise (keine CP437-Sortierung). + - Der Bildschirmpuffer speichert Unicode-Zeichen; Rahmen werden mit + Unicode-Box-Drawing gezeichnet. + - Zeichen mit Darstellungsbreite ≠ 1: offen (siehe PLAN.md). +2. **Keine Hardware-Nähe:** `PEEK`/`POKE`/`INP`/`OUT`/`CALL ABSOLUTE`/ + Interrupts lösen Fehler 73 (Feature unavailable) aus. +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. +4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell. +5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `LINE`, `CIRCLE`, `PAINT`, + `DRAW`) sind Nicht-Ziel (Fehler 73) — das Vorbild war im Forms-Modus + ebenfalls textonly. +6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73). +7. **`WIDTH 40`** wird nicht unterstützt (nur 80×25). diff --git a/docs/tbvm-design.md b/docs/tbvm-design.md new file mode 100644 index 0000000..b688866 --- /dev/null +++ b/docs/tbvm-design.md @@ -0,0 +1,85 @@ +# TBVM — Designentwurf + +Eigene Stack-basierte Bytecode-VM (Entscheidung siehe PLAN.md, 0.2). +Die VM ist in `tbdos` (IDE) und in von `tbdosc` erzeugte Executables +eingebettet — identischer Code, identisches Verhalten, schneller Turnaround. + +## Werte-Modell + +Tagged Enum, kein NaN-Boxing (Einfachheit und Debugbarkeit vor Mikro- +performance): + +```rust +enum Value { + Int(i16), // INTEGER + Long(i32), // LONG + Single(f32), // SINGLE + Double(f64), // DOUBLE + Currency(i64), // CURRENCY, Festkomma ×10 000 + Str(Rc), // STRING (immutabel geteilt; Copy-on-Write bei MID$-Anweisung) + // Arrays und TYPE-Instanzen leben im Heap-Bereich der VM, + // Variablen-Slots referenzieren sie per Handle (Rc>). +} +``` + +- Kein GC nötig: der Dialekt kennt keine Zyklen (keine Objektreferenzen in + TYPEs, keine Closures) → `Rc` genügt. +- Feste Strings (`STRING * n`) und TYPE-Records werden als eigene + Speicherobjekte mit fester Zeichen-/Elementstruktur geführt. + +## Bytecode + +- Stack-Maschine, Opcodes 1 Byte + Operanden variabler Länge (u8/u16/u32, + little-endian). +- **Modul-Struktur:** Konstantenpool (Strings, Zahlen), Typtabelle + (TYPE-Layouts), Prozedurtabelle (Signatur, Locals-Anzahl, Code-Offset), + globale Slots, DATA-Segment (für READ/RESTORE), Zeilentabelle. +- **Zeilentabelle:** Abbildung Code-Offset → (Moduldatei, Zeile). Grundlage + für `ERL`, Fehlermeldungen, Breakpoints und Einzelschritt. +- **Aufrufkonventionen:** + - SUB/FUNCTION: eigener Frame (Locals-Slots, Operandenstack-Basis); + 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. + +## Ausführungsmodell / Unterbrechbarkeit + +- Die Interpreterschleife läuft in **Ticks**: nach jeder Anweisung (Grenze + aus der Zeilentabelle) prüft sie ein Flag-Set: Breakpoint? Einzelschritt? + Strg+Untbr? Ereignis-Queue nicht leer und Zustellung erlaubt? +- Ereigniszustellung (Forms, Timer) erfolgt kooperativ: nur an + Zustellpunkten (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer + Ereignisprozedur) — wie im Vorbild. +- Die VM ist eine gewöhnliche zustandsbehaftete Struktur, `step()`-basiert; + die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein + eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt. + +## `.tbc`-Container (Entwurf) + +``` +Magic "TBC\0" · Formatversion u16 · Flags +Abschnittstabelle: [ (Kennung, Offset, Länge) ] +Abschnitte: CONSTS, TYPES, PROCS, CODE, DATA, LINES, FORMS (serialisierte .FRM-Beschreibungen) +``` + +Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig, +Format bleibt stabil und dokumentiert). + +## Eigenständige Executables + +`tbdosc build --exe` kopiert den vorkompilierten Runner (dieselbe +tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als +Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für +alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim +Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender. + +## Offene Punkte + +- Opcode-Satz konkret ausformulieren (mit Phase 2) +- Zahlenkonvertierungs-Matrix (implizite Casts, Rundung, Überlauf) als Tabelle +- Verhalten von `STOP`/`CONT` im Runner (ohne IDE): Abbruch mit Meldung? diff --git a/tests/compat/README.md b/tests/compat/README.md new file mode 100644 index 0000000..2073d24 --- /dev/null +++ b/tests/compat/README.md @@ -0,0 +1,20 @@ +# Kompatibilitäts-Testkorpus + +Kleine Referenzprogramme mit dokumentierter erwarteter Ausgabe. Jede +`name.bas` hat eine `name.out` mit der exakten Konsolenausgabe (UTF-8, +LF-Zeilenenden). **Achtung:** PRINT gibt Zahlen mit führendem Leerzeichen +(bzw. `-`) und nachgestelltem Leerzeichen aus — auch am Zeilenende. Die +`.out`-Dateien enthalten daher signifikante Leerzeichen am Zeilenende; +Editoren dürfen sie nicht wegtrimmen; `.gitattributes` schützt die Dateien +vor Zeilenenden-Konvertierung. + +Ab Phase 2 führt ein Test-Harness (`cargo test`) jede Datei per +`tbdosc run` aus und vergleicht die Ausgabe. Bis dahin dienen die +`.out`-Dateien als festgehaltene Verhaltensspezifikation — wo möglich am +Vorbild (Emulator) verifiziert, sonst nach Referenzlage rekonstruiert +und mit `TODO verify` im Programmkopf markiert. + +Konventionen: +- Nur Konsolen-E/A (keine Forms) bis Phase 4 +- Kein Zufall/Zeit, außer der Test fixiert die Saat (`RANDOMIZE` mit + Konstante) bzw. prüft nur Invarianten diff --git a/tests/compat/hello.bas b/tests/compat/hello.bas new file mode 100644 index 0000000..3fee8ce --- /dev/null +++ b/tests/compat/hello.bas @@ -0,0 +1,3 @@ +' Minimalfall: PRINT mit String und Zeilenumbruch +PRINT "Hallo, Welt!" +END diff --git a/tests/compat/hello.out b/tests/compat/hello.out new file mode 100644 index 0000000..8095a18 --- /dev/null +++ b/tests/compat/hello.out @@ -0,0 +1 @@ +Hallo, Welt! diff --git a/tests/compat/kontrollfluss.bas b/tests/compat/kontrollfluss.bas new file mode 100644 index 0000000..3ba5248 --- /dev/null +++ b/tests/compat/kontrollfluss.bas @@ -0,0 +1,26 @@ +' FOR/NEXT, DO/LOOP, SELECT CASE, GOSUB +FOR i% = 1 TO 3 + PRINT "for"; i% +NEXT i% + +n% = 3 +DO WHILE n% > 0 + PRINT "do"; n% + n% = n% - 1 +LOOP + +FOR i% = 1 TO 3 + SELECT CASE i% + CASE 1: PRINT "eins" + CASE 2 TO 3: PRINT "zwei-drei" + CASE ELSE: PRINT "nie" + END SELECT +NEXT + +GOSUB Unterprogramm +PRINT "ende" +END + +Unterprogramm: +PRINT "gosub" +RETURN diff --git a/tests/compat/kontrollfluss.out b/tests/compat/kontrollfluss.out new file mode 100644 index 0000000..bc60ab3 --- /dev/null +++ b/tests/compat/kontrollfluss.out @@ -0,0 +1,11 @@ +for 1 +for 2 +for 3 +do 3 +do 2 +do 1 +eins +zwei-drei +zwei-drei +gosub +ende diff --git a/tests/compat/printzahlen.bas b/tests/compat/printzahlen.bas new file mode 100644 index 0000000..94731fd --- /dev/null +++ b/tests/compat/printzahlen.bas @@ -0,0 +1,6 @@ +' PRINT-Formatierung von Zahlen: führendes Leerzeichen (bzw. -), +' nachgestelltes Leerzeichen; Semikolon = direkt anschließen. +PRINT 1; 2; 3 +PRINT -1; -2 +PRINT 1.5; -2.5 +END diff --git a/tests/compat/printzahlen.out b/tests/compat/printzahlen.out new file mode 100644 index 0000000..868c9e0 --- /dev/null +++ b/tests/compat/printzahlen.out @@ -0,0 +1,3 @@ + 1 2 3 +-1 -2 + 1.5 -2.5 diff --git a/tests/compat/printzonen.bas b/tests/compat/printzonen.bas new file mode 100644 index 0000000..7a32e1e --- /dev/null +++ b/tests/compat/printzonen.bas @@ -0,0 +1,5 @@ +' Komma = Sprung zur nächsten 14-Zeichen-Druckzone +PRINT "a", "b", "c" +PRINT "1234567890123", "x" +PRINT "12345678901234", "x" +END diff --git a/tests/compat/printzonen.out b/tests/compat/printzonen.out new file mode 100644 index 0000000..7948735 --- /dev/null +++ b/tests/compat/printzonen.out @@ -0,0 +1,3 @@ +a b c +1234567890123 x +12345678901234 x diff --git a/tests/compat/strings.bas b/tests/compat/strings.bas new file mode 100644 index 0000000..a9fd2f4 --- /dev/null +++ b/tests/compat/strings.bas @@ -0,0 +1,12 @@ +' Stringfunktionen; LEN zählt Zeichen (UTF-8-Abweichung dokumentiert) +s$ = "Terminal Basic" +PRINT LEN(s$) +PRINT LEFT$(s$, 8) +PRINT RIGHT$(s$, 5) +PRINT MID$(s$, 10, 3) +PRINT INSTR(s$, "Basic") +PRINT UCASE$("äöü grüße") +PRINT STRING$(5, "*"); SPACE$(3); "|" +PRINT CHR$(65); ASC("A") +PRINT STR$(42); "|"; VAL(" 12.5abc") +END diff --git a/tests/compat/strings.out b/tests/compat/strings.out new file mode 100644 index 0000000..45eb63b --- /dev/null +++ b/tests/compat/strings.out @@ -0,0 +1,9 @@ + 14 +Terminal +Basic +Bas + 10 +ÄÖÜ GRÜSSE +***** | +A 65 + 42| 12.5