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 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 16:36:45 +02:00
parent 5821ace0cf
commit 7e6fa6ea88
24 changed files with 1148 additions and 25 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Erwartete Ausgaben sind byte-genau (LF, signifikante Leerzeichen am Zeilenende)
tests/compat/*.out -text

1
Cargo.lock generated
View File

@@ -495,6 +495,7 @@ dependencies = [
name = "tb-ui"
version = "0.1.0"
dependencies = [
"anyhow",
"crossterm",
"log",
"ratatui",

59
PLAN.md
View File

@@ -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 (F1F12, 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 015, Hintergrund 07) auf
ANSI-Indexfarben abgebildet; Blink-Attribut vorerst ignoriert (offen)
- [~] Maus-Ereignisse (crossterm) und Sondertasten (F1F12, 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 1631): ignorieren, als „hell"
darstellen oder echtes Terminal-Blink nutzen (nicht überall unterstützt)?
- Lizenzwahl (MIT im Rahmen angenommen — bestätigen)

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -11,3 +11,6 @@ ratatui.workspace = true
crossterm.workspace = true
thiserror.workspace = true
log.workspace = true
[dev-dependencies]
anyhow.workspace = true

View File

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

View File

@@ -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

View File

@@ -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 015 (klassische Palette).
pub fg: u8,
/// Hintergrund 07.
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 125,
/// Spalte 180), wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
#[derive(Debug, Clone)]
pub struct TextScreen {
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,
}
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 031, wir ignorieren Blink → 015; hg 07).
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 35
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ß
}
}

55
docs/dateiformate.md Normal file
View File

@@ -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 <Typ> <Name> … 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`).

85
docs/forms-referenz.md Normal file
View File

@@ -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` (015 bzw. 07), `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

240
docs/sprachreferenz.md Normal file
View File

@@ -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 az` (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 031: 1631 = blinkend; hg 07), `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).

85
docs/tbvm-design.md Normal file
View File

@@ -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<str>), // 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<RefCell<…>>).
}
```
- 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?

20
tests/compat/README.md Normal file
View File

@@ -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

3
tests/compat/hello.bas Normal file
View File

@@ -0,0 +1,3 @@
' Minimalfall: PRINT mit String und Zeilenumbruch
PRINT "Hallo, Welt!"
END

1
tests/compat/hello.out Normal file
View File

@@ -0,0 +1 @@
Hallo, Welt!

View File

@@ -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

View File

@@ -0,0 +1,11 @@
for 1
for 2
for 3
do 3
do 2
do 1
eins
zwei-drei
zwei-drei
gosub
ende

View File

@@ -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

View File

@@ -0,0 +1,3 @@
1 2 3
-1 -2
1.5 -2.5

View File

@@ -0,0 +1,5 @@
' Komma = Sprung zur nächsten 14-Zeichen-Druckzone
PRINT "a", "b", "c"
PRINT "1234567890123", "x"
PRINT "12345678901234", "x"
END

View File

@@ -0,0 +1,3 @@
a b c
1234567890123 x
12345678901234 x

12
tests/compat/strings.bas Normal file
View File

@@ -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

9
tests/compat/strings.out Normal file
View File

@@ -0,0 +1,9 @@
14
Terminal
Basic
Bas
10
ÄÖÜ GRÜSSE
***** |
A 65
42| 12.5