Phase 1: Sprach-Frontend (Lexer, AST, Parser, Semantik) + Entscheidungen

Frontend:
- Lexer komplett: Typ-Suffixe, Literal-Typisierung (Entscheidung: > 7
  signifikante Stellen -> DOUBLE), Hex/Oktal, Zeilenfortsetzung mit _,
  Strings mit ""-Escape, case-insensitive Keywords (Bibliotheksnamen
  bleiben Bezeichner)
- AST fuer Module/Prozeduren/Anweisungen/Ausdruecke
- Parser: fehlertolerant, zeilenorientiert; Kern-Anweisungssatz inkl.
  Bloecke, ON [LOCAL] ERROR, DEF FN (einzeilig); Datei-E/A als
  Phase-3-Platzhalter
- Semantik: Symboltabellen, implizite Deklaration, DEFtype, OPTION
  EXPLICIT, Arrays, Builtin-Signaturen, Labelpruefung; Hardware-Features
  (PEEK/POKE/...) werden zur Compile-Zeit abgewiesen
- Meilenstein: Testkorpus parst und wird typgeprueft (corpus.rs); 27 Tests

Entscheidungen eingearbeitet:
- Binaries heissen tb (IDE) und tbc (Compiler)
- Dynamische Terminalgroesse statt 80x25 (Minimum 80x25, btop-artiger
  Hinweis darunter); tb-ui::screen mit resize(), Spike angepasst
- Vollstaendigkeits-Leitplanke: 100% Sprache/Stdlib minus deklarierte
  Non-Features; Original-Doku als Guiding Principle; Inventar-Aufgabe
- CURRENCY als i64-Festkomma; ISAM wird implementiert; breite Zeichen
  belegen 2 Zellen; GET/PUT-Strings als UTF-32; Blink als hell simuliert
- LICENSE: MIT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 09:16:01 +02:00
parent 8002ac2388
commit 333e794540
21 changed files with 3799 additions and 138 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Georg Bauer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
PLAN.md
View File

@@ -22,6 +22,20 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit
Formatierung), nicht das, was „richtiger" wäre. Wo das Vorbild aber mit der Formatierung), nicht das, was „richtiger" wäre. Wo das Vorbild aber mit der
heutigen Plattform kollidiert (Codepages, DOS-Hardware, Segmente), gewinnt heutigen Plattform kollidiert (Codepages, DOS-Hardware, Segmente), gewinnt
die Plattform; solche Abweichungen werden in der Sprachreferenz dokumentiert. die Plattform; solche Abweichungen werden in der Sprachreferenz dokumentiert.
- **Vollständigkeit ist das Soll** (2026-09-02): Erwartet wird eine
**100 % kompatible Sprachimplementierung und volle Standardbibliothek**
des Vorbilds — abzüglich ausschließlich der in der Sprachreferenz
explizit benannten Non-Features. Aufzählungen von Anweisungen/Funktionen
in diesem Plan sind **Beispiele, keine Scope-Definition**. Messbar wird
das über ein Vollständigkeits-Inventar (alle Keywords, Anweisungen,
Funktionen, Methoden, Eigenschaften, Ereignisse aus der Original-Hilfe)
mit Abdeckungsstatus → Aufgabe in Phase 3, Abnahmekriterium in Phase 6.
**Guiding Principle — die Original-Dokumentation führt** (2026-09-02):
Alles, was dort dokumentiert ist, wird unterstützt **oder** durch eine
explizite Fehlermeldung abgewiesen. Eine Abweisung setzt voraus, dass das
Feature vorher vom Projektinhaber als Non-Feature deklariert und in der
Sprachreferenz unter „Abweichungen" gelistet wurde. Stilles Weglassen
oder generische Syntaxfehler für dokumentierte Features sind Bugs.
- **Markenrecht:** Der Name des Vorbilds wird in Code, Doku und Artefakten - **Markenrecht:** Der Name des Vorbilds wird in Code, Doku und Artefakten
nicht verwendet. Wir sprechen vom „Vorbild" bzw. „dem Dialekt". nicht verwendet. Wir sprechen vom „Vorbild" bzw. „dem Dialekt".
- **Performance ist Anforderung, nicht Politur** (2026-09-01): - **Performance ist Anforderung, nicht Politur** (2026-09-01):
@@ -37,7 +51,7 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit
- **Schnelle Ausführung:** Die TBVM führt generierten Code zügig aus - **Schnelle Ausführung:** Die TBVM führt generierten Code zügig aus
(Details in docs/tbvm-design.md, Abschnitt „Performance"); Messlatte via (Details in docs/tbvm-design.md, Abschnitt „Performance"); Messlatte via
Benchmarks ab Phase 2. Benchmarks ab Phase 2.
- `tbdosc build --exe` erzeugt Executables **ohne** Compiler-/Linker- - `tbc build --exe` erzeugt Executables **ohne** Compiler-/Linker-
Toolchain beim Anwender (vorkompilierter Runner + angehängtes `.tbc`) — Toolchain beim Anwender (vorkompilierter Runner + angehängtes `.tbc`) —
damit bleibt auch der Weg zum verteilbaren Binary im Sekundenbereich. damit bleibt auch der Weg zum verteilbaren Binary im Sekundenbereich.
- **Jede Phase endet mit lauffähigen Tests** gegen eine wachsende - **Jede Phase endet mit lauffähigen Tests** gegen eine wachsende
@@ -79,7 +93,7 @@ Dokumentation nicht beantwortet.
→ [docs/ide-referenz.md](docs/ide-referenz.md) → [docs/ide-referenz.md](docs/ide-referenz.md)
- [x] Testkorpus-Grundstock gelegt (`tests/compat/` mit dokumentierter - [x] Testkorpus-Grundstock gelegt (`tests/compat/` mit dokumentierter
Sollausgabe). Der Ausbau ist Daueraufgabe jeder Phase; das Sollausgabe). Der Ausbau ist Daueraufgabe jeder Phase; das
Test-Harness (`tbdosc run` + Ausgabevergleich) ist Phase-2-Aufgabe Test-Harness (`tbc run` + Ausgabevergleich) ist Phase-2-Aufgabe
### 0.2 VM-/Runtime-Entscheidung (Aufstellung der Optionen) ### 0.2 VM-/Runtime-Entscheidung (Aufstellung der Optionen)
@@ -101,7 +115,7 @@ BASIC-Semantik, serialisierbares Kompilat, gute Fehlerortung (Zeile/Spalte).
**Entscheidung (2026-09-01, bestätigt):** Eigene Stack-basierte Bytecode-VM **Entscheidung (2026-09-01, bestätigt):** Eigene Stack-basierte Bytecode-VM
(**Option 1, TBVM**), sowohl in der Entwicklungsphase als auch **eingebettet in (**Option 1, TBVM**), sowohl in der Entwicklungsphase als auch **eingebettet in
die Executables** (`tbdos`-IDE und von `tbdosc` erzeugte Programme), damit der die Executables** (`tb`-IDE und von `tbc` erzeugte Programme), damit der
schnelle Edit-Run-Turnaround überall identisch ist. schnelle Edit-Run-Turnaround überall identisch ist.
Die IR/Bytecode-Schicht wird sauber vom Interpreter getrennt, sodass später ein 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 zweites Backend (WASM via Cranelift, Option 3) ergänzt werden kann, ohne das
@@ -122,6 +136,12 @@ durchgängig **UTF-8/Unicode**. Strings sind Unicode-Text, `CHR$`/`ASC`
arbeiten auf Codepoints. Das ist eine bewusste Abweichung vom Vorbild und wird arbeiten auf Codepoints. Das ist eine bewusste Abweichung vom Vorbild und wird
in der Sprachreferenz unter „Abweichungen" dokumentiert. in der Sprachreferenz unter „Abweichungen" dokumentiert.
**Entscheidung (2026-09-02):** **Dynamische Terminalgröße** statt festem
80×25. IDE und erzeugte Programme passen sich der Fenstergröße an;
Mindestgröße ist 80×25, darunter wird nur ein Hinweis gerendert (btop-artig).
`SCREEN.Height`/`Width`, `CSRLIN`/`POS`/`LOCATE` arbeiten auf der
tatsächlichen Größe; `tb-ui::screen` unterstützt `resize()`.
- [x] 80×25-Zellenpuffer (Zeichen + Farbattribut) als eigenes Widget rendern; - [x] 80×25-Zellenpuffer (Zeichen + Farbattribut) als eigenes Widget rendern;
kleinere Terminals: Hinweis „Terminal zu klein", größere: zentriert kleinere Terminals: Hinweis „Terminal zu klein", größere: zentriert
(Letterboxing) → `tb-ui::screen`, Demo: `cargo run -p tb-ui --example spike` (Letterboxing) → `tb-ui::screen`, Demo: `cargo run -p tb-ui --example spike`
@@ -134,19 +154,31 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
VM-Ticks — ist Eingangsaufgabe von Phase 4.) VM-Ticks — ist Eingangsaufgabe von Phase 4.)
## Phase 1 — Sprach-Frontend (`tb-frontend`) ## Phase 1 — Sprach-Frontend (`tb-frontend`)
- [ ] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare, - [x] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
case-insensitive Keywords, Zeilenfortsetzung mit `_` case-insensitive Keywords, Zeilenfortsetzung mit `_`, Hex-/Oktal-
- [ ] Offene Detailfrage aus der Sprachreferenz klären: exakte Literale, Literal-Typisierung
SINGLE/DOUBLE-Schwelle suffixloser Dezimalpunkt-Literale - [x] SINGLE/DOUBLE-Schwelle suffixloser Dezimalpunkt-Literale entschieden
(Quelle widersprüchlich → per Testkorpus festlegen und in (> 7 signifikante Stellen → DOUBLE) und in docs/sprachreferenz.md §1
docs/sprachreferenz.md dokumentieren) dokumentiert
- [ ] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen - [x] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen
- [ ] Parser (zeilenorientiert, fehlertolerant — die IDE prüft pro Zeile) - [~] Parser (zeilenorientiert, fehlertolerant — Fehler pro Anweisung
- [ ] Semantik: Symboltabellen, implizite Deklaration, `DEFINT`-Regeln, gesammelt, Synchronisation bis Anweisungsende). Kern komplett:
Typprüfung und -konvertierung, `OPTION EXPLICIT`/`OPTION BASE`, Zuweisung, PRINT (inkl. USING/#), INPUT/LINE INPUT, IF (Block +
Array-Dimensionierung (statisch/dynamisch, `REDIM`) einzeilig), SELECT CASE, FOR/DO/WHILE, GOTO/GOSUB/ON-GOTO,
- [ ] Diagnostik mit exakten Positionen und den Meldungstexten des Vorbilds ON [LOCAL] ERROR/RESUME, DIM/REDIM/CONST/DEFtype/OPTION/TYPE/
- [ ] Meilenstein: kompletter Testkorpus parst und wird typgeprüft DECLARE/SUB/FUNCTION/CALL, DATA/READ/RESTORE, DEF FN (einzeilig).
Offen: MID$-Anweisung, DEF FN-Blockform, Datei-E/A-Anweisungen
(werden als Platzhalter geparst → Phase 3), `$INCLUDE`-Metabefehl
- [~] Semantik: Symboltabellen, implizite Deklaration, `DEFtype`-Regeln,
Typprüfung, `OPTION EXPLICIT`, Arrays (implizit/DIM/REDIM),
Builtin-Signaturen, Label-Prüfung, Prozedur-Signaturprüfung.
Offen: `COMMON`/`SHARED` über Prozedurgrenzen, UDT-Feldtypen,
`OPTION BASE`-Auswertung, Konstantenfaltung
- [~] Diagnostik mit exakten Positionen; Meldungstexte am Vorbild
orientiert (Type mismatch, Duplicate definition, Label not defined …)
— vollständiger Abgleich mit den Compile-Meldungen des Vorbilds offen
- [x] Meilenstein: kompletter Testkorpus parst und wird typgeprüft
(`crates/tb-frontend/tests/corpus.rs`)
## Phase 2 — Bytecode und VM (`tb-vm`) ## Phase 2 — Bytecode und VM (`tb-vm`)
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** — - [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
@@ -163,9 +195,18 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
- [ ] Benchmarks in `benches/`: Compile-Budget messen (Projekt ≈50k Zeilen - [ ] Benchmarks in `benches/`: Compile-Budget messen (Projekt ≈50k Zeilen
< 1 s, einzelnes Modul < 50 ms) und VM-Durchsatz (Schleifen/Strings); < 1 s, einzelnes Modul < 50 ms) und VM-Durchsatz (Schleifen/Strings);
Ergebnisse in docs/tbvm-design.md festhalten Ergebnisse in docs/tbvm-design.md festhalten
- [ ] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe (`tbdosc run`) - [ ] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe (`tbc run`)
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`) ## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`)
Ziel ist die **vollständige** Standardbibliothek des Vorbilds (siehe
Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
- [ ] Vollständigkeits-Inventar erstellen: maschinenlesbare Liste aller
Anweisungen/Funktionen des Vorbilds aus der Original-Hilfe
(dos-help.soulsphere.org, Topic-Listen) mit Status
implementiert/offen/Non-Feature → `docs/inventar.md`; ab dann
Abdeckung je Phase fortschreiben
- [ ] Strings: `LEFT$`, `MID$` (auch als Anweisung), `INSTR`, `STR$`/`VAL`, - [ ] Strings: `LEFT$`, `MID$` (auch als Anweisung), `INSTR`, `STR$`/`VAL`,
`SPACE$`, `STRING$`, `LTRIM$`/`RTRIM$`, `UCASE$`/`LCASE$` `SPACE$`, `STRING$`, `LTRIM$`/`RTRIM$`, `UCASE$`/`LCASE$`
- [ ] Zahlenformatierung: `PRINT`-Zonen, `PRINT USING` (vollständig; - [ ] Zahlenformatierung: `PRINT`-Zonen, `PRINT USING` (vollständig;
@@ -175,7 +216,16 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
Funktionen, Integer-Überlaufverhalten (Fehler 6) Funktionen, Integer-Überlaufverhalten (Fehler 6)
- [ ] Datei-E/A: `OPEN` (sequenziell/random/binär), `INPUT#`/`LINE INPUT#`, - [ ] Datei-E/A: `OPEN` (sequenziell/random/binär), `INPUT#`/`LINE INPUT#`,
`PRINT#`/`WRITE#`, `GET`/`PUT` mit Record-Typen, `EOF`/`LOF`/`SEEK`, `PRINT#`/`WRITE#`, `GET`/`PUT` mit Record-Typen, `EOF`/`LOF`/`SEEK`,
Pfadsemantik plattformübergreifend Pfadsemantik plattformübergreifend. Record-Layout: feste Strings als
**UTF-32** (Entscheidung 2026-09-02 — 4 Bytes/Zeichen, feste
Record-Länge; bewusst inkompatibel zu Vorbild-Dateien)
- [ ] ISAM-Dateiunterstützung (Entscheidung 2026-09-02: wird implementiert,
nicht Non-Feature): Anweisungen/Funktionen der Professional Edition
(`OPEN … FOR ISAM`, Tabellen/Indizes, `SEEKGT`-Familie …) — Umfang
aus der Original-Hilfe inventarisieren, dann implementieren
- [ ] Breite Unicode-Zeichen (Emoji, CJK): belegen **zwei Zellen**
(Entscheidung 2026-09-02) — Zellenmodell und `LOCATE`/`POS`-Semantik
entsprechend umsetzen (unicode-width), Verhalten dokumentieren
- [ ] Bildschirm: `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT`, `INKEY$`, - [ ] Bildschirm: `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT`, `INKEY$`,
`CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer `CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer
- [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros - [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros
@@ -221,7 +271,7 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
aus Phase 0 übernommen) und Dokumentation bekannter aus Phase 0 übernommen) und Dokumentation bekannter
Terminal-Einschränkungen Terminal-Einschränkungen
- [ ] Performance-Pass über die VM (nur falls nötig) - [ ] Performance-Pass über die VM (nur falls nötig)
- [ ] `tbdosc build` → binäres Ergebnis: `.tbc`-Bytecode bzw. eigenständig - [ ] `tbc build` → binäres Ergebnis: `.tbc`-Bytecode bzw. eigenständig
ausführbares Programm (Bytecode + eingebetteter Runner) ausführbares Programm (Bytecode + eingebetteter Runner)
- [ ] Dokumentation: Sprachreferenz, Migrationshinweise, Beispielprogramme - [ ] Dokumentation: Sprachreferenz, Migrationshinweise, Beispielprogramme
- [ ] CI (GitHub Actions: Build + Tests auf allen drei Plattformen), Releases - [ ] CI (GitHub Actions: Build + Tests auf allen drei Plattformen), Releases
@@ -241,23 +291,25 @@ bleibt gültig. Noch nichts davon ist beschlossen; Sammlung wächst:
Forms-Engine Forms-Engine
- Standardbibliothek: Prozessaufrufe mit Pipes, Umgebungs-/Argument-Handling - Standardbibliothek: Prozessaufrufe mit Pipes, Umgebungs-/Argument-Handling
für CLI-Tools, JSON/CSV, HTTP-Client, Pfad-/Verzeichnisfunktionen für CLI-Tools, JSON/CSV, HTTP-Client, Pfad-/Verzeichnisfunktionen
- Verteilung: `tbdosc build --exe` als Single-File-Tool-Baukasten - Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten
--- ---
## Offene Fragen ## Entschiedene Fragen (2026-09-02, alle offenen Punkte geklärt)
- Umgang mit `CURRENCY` (`@`): als i64-Festkomma (×10 000) — klären, ob das - **CURRENCY (`@`):** voll unterstützt als i64-Festkomma (×10 000); das
Vorbild ihn überhaupt in allen Kontexten unterstützt Vorbild unterstützt den Typ laut Original-Hilfe vollständig.
- ISAM-Dateiunterstützung des Vorbilds: nachbilden oder als Nicht-Ziel erklären? - **ISAM:** wird implementiert (kein Non-Feature) → Aufgabe in Phase 3.
- `PEEK`/`POKE`/`CALL INTERRUPT` u. ä. Hardware-Nähe: sinnvolle Teilmenge - **`PEEK`/`POKE`/`CALL INTERRUPT` u. ä. Hardware-Nähe:** nicht unterstützt;
emulieren (z. B. Bildschirmspeicher B800) oder Laufzeitfehler? Ablehnung bereits **zur Compile-Zeit** (Meldung „Feature unavailable") —
- Unicode-Zeichen mit Darstellungsbreite ≠ 1 (Emoji, CJK): das Zellenmodell in der Semantik umgesetzt, dokumentiert in der Sprachreferenz.
ist strikt 1 Zeichen = 1 Zelle. Breite Zeichen ablehnen (Fehler 5), - **Breite Unicode-Zeichen (Emoji, CJK):** belegen **zwei Zellen**
ersetzen oder 2 Zellen belegen? Umsetzung in Phase 3.
- `GET`/`PUT` mit Record-Typen unter UTF-8: feste Strings (`STRING * n`) - **`GET`/`PUT`-Records unter Unicode:** feste Strings als **UTF-32**
waren im Vorbild n Bytes — bei uns n Zeichen. Binärlayout der Records (4 Bytes/Zeichen, feste Record-Länge); Inkompatibilität der Binärdateien
festlegen (Vorschlag: UTF-8 mit Padding auf feste Bytelänge? Oder UTF-32?) zum Vorbild wird bewusst akzeptiert → Phase 3.
- Blink-Attribut (`COLOR` mit Vordergrund 1631): ignorieren, als „hell" - **Blink-Attribut (`COLOR` 1631):** kein echtes Blinken, Simulation als
darstellen oder echtes Terminal-Blink nutzen (nicht überall unterstützt)? „hell" — in `tb-ui::screen` umgesetzt.
- Lizenzwahl (MIT im Rahmen angenommen — bestätigen) - **Lizenz:** MIT (LICENSE im Repo).
Neue offene Fragen werden hier gesammelt und mit Datum entschieden.

View File

@@ -33,8 +33,11 @@ durchgängig UTF-8 statt Codepage 437 und keine Emulation von DOS-Hardware.
- **Volle Library-Kompatibilität für die DOS-Oberflächenprogrammierung**: - **Volle Library-Kompatibilität für die DOS-Oberflächenprogrammierung**:
die ereignisgesteuerte Forms-Engine mit allen Steuerelementen, Menüs, die ereignisgesteuerte Forms-Engine mit allen Steuerelementen, Menüs,
Maus- und Tastaturereignissen, nachgebildet auf einem emulierten Maus- und Tastaturereignissen, nachgebildet auf einem emulierten
80×25-Textbildschirm (16 Farben) über Ratatui. Bewusste Modernisierung: Textbildschirm (16 Farben) über Ratatui. Bewusste Modernisierungen:
durchgängig UTF-8/Unicode statt Codepage 437. durchgängig UTF-8/Unicode statt Codepage 437, und **dynamische
Terminalgröße** statt festem 80×25 — IDE und erzeugte Programme passen
sich der Fenstergröße an (Mindestgröße 80×25; darunter erscheint nur
ein Hinweis, wie bei btop).
- **Plattformübergreifend**: läuft in Terminals unter Linux, macOS und Windows. - **Plattformübergreifend**: läuft in Terminals unter Linux, macOS und Windows.
- **Compiler + VM**: der Compiler erzeugt Bytecode für eine eigene virtuelle - **Compiler + VM**: der Compiler erzeugt Bytecode für eine eigene virtuelle
Maschine (TBVM); die Abwägung der Runtime-Optionen ist in Maschine (TBVM); die Abwägung der Runtime-Optionen ist in
@@ -50,8 +53,8 @@ durchgängig UTF-8 statt Codepage 437 und keine Emulation von DOS-Hardware.
| `tb-vm` | Bytecode-Format, Codegenerator, virtuelle Maschine (TBVM) | | `tb-vm` | Bytecode-Format, Codegenerator, virtuelle Maschine (TBVM) |
| `tb-runtime` | Laufzeitbibliothek: Strings, Mathematik, Datei-E/A, Datum/Zeit, Fehlercodes | | `tb-runtime` | Laufzeitbibliothek: Strings, Mathematik, Datei-E/A, Datum/Zeit, Fehlercodes |
| `tb-ui` | Textbildschirm-Emulation und Forms-Engine auf Ratatui | | `tb-ui` | Textbildschirm-Emulation und Forms-Engine auf Ratatui |
| `tb-cli` | `tbdosc`: Standalone-Compiler — wandelt Quellen und Projekte in binäre Ergebnisse um | | `tb-cli` | `tbc`: Standalone-Compiler — wandelt Quellen und Projekte in binäre Ergebnisse um |
| `tb-ide` | `tbdos`: die integrierte Entwicklungsumgebung (TUI) zum Erstellen von Programmen | | `tb-ide` | `tb`: die integrierte Entwicklungsumgebung (TUI) zum Erstellen von Programmen |
## Status ## Status
@@ -62,8 +65,12 @@ Projektrahmen. Die Implementierungs- und Explorationsschritte sind in
``` ```
cargo build cargo build
cargo run -p tb-cli # tbdosc — Standalone-Compiler cargo run -p tb-cli # tbc — Standalone-Compiler
cargo run -p tb-ide # tbdos — IDE (TUI) cargo run -p tb-ide # tb — IDE (TUI)
``` ```
Benötigt wird nur eine aktuelle stabile Rust-Toolchain. Benötigt wird nur eine aktuelle stabile Rust-Toolchain.
## Lizenz
MIT — siehe [LICENSE](LICENSE).

View File

@@ -1,13 +1,13 @@
[package] [package]
name = "tb-cli" name = "tb-cli"
description = "Terminal Basic: Standalone-Kommandozeilen-Compiler (tbdosc)" description = "Terminal Basic: Standalone-Kommandozeilen-Compiler (tbc)"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[[bin]] [[bin]]
name = "tbdosc" name = "tbc"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]

View File

@@ -1,13 +1,13 @@
//! `tbdosc` — Standalone-Compiler von Terminal Basic. //! `tbc` — Standalone-Compiler von Terminal Basic.
//! //!
//! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode //! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle //! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle
//! (siehe PLAN.md): //! (siehe PLAN.md):
//! - `tbdosc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis //! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
//! - `tbdosc run <datei.bas>` Kompilieren und sofort ausführen //! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
//! - `tbdosc check <datei.bas>` Nur Syntax-/Semantikprüfung //! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
println!("tbdosc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)"); println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
Ok(()) Ok(())
} }

View File

@@ -1,4 +1,247 @@
//! AST-Definitionen: Module, Prozeduren (`SUB`/`FUNCTION`), Anweisungen, Ausdrücke, //! AST-Definitionen: Module, Prozeduren, Anweisungen, Ausdrücke,
//! benutzerdefinierte Typen (`TYPE … END TYPE`), Deklarationen. //! Deklarationen und benutzerdefinierte Typen.
// Platzhalter — wird in Phase 1 ausgearbeitet (siehe PLAN.md) use crate::lexer::Suffix;
use crate::SourcePos;
/// Typangabe in `AS`-Klauseln.
#[derive(Debug, Clone, PartialEq)]
pub enum TypeName {
Integer,
Long,
Single,
Double,
Currency,
Str,
FixedStr(i64),
Udt(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnOp {
Neg,
Not,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Pow, Mul, Div, IntDiv, Mod, Add, Sub,
Eq, Ne, Lt, Le, Gt, Ge,
And, Or, Xor, Eqv, Imp,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
IntLit(i16),
LongLit(i32),
SingleLit(f32),
DoubleLit(f64),
CurrencyLit(i64),
StrLit(String),
/// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder
/// Konstante — Auflösung erfolgt in der Semantik.
Name {
name: String,
suffix: Option<Suffix>,
args: Option<Vec<Expr>>,
pos: SourcePos,
},
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
/// Ausgelassenes Argument (`LOCATE , 5`).
Missing,
}
impl Expr {
pub fn pos(&self) -> SourcePos {
match self {
Expr::Name { pos, .. }
| Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } => *pos,
_ => SourcePos::default(),
}
}
}
/// Sprungziel: alphanumerisches Label oder Zeilennummer.
#[derive(Debug, Clone, PartialEq)]
pub enum LabelRef {
Name(String),
Line(u32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum PrintItem {
Expr(Expr),
Comma,
Semicolon,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CaseSpec {
Expr(Expr),
Range(Expr, Expr),
Is(BinOp, Expr),
}
#[derive(Debug, Clone, PartialEq)]
pub struct CaseArm {
/// Leer = `CASE ELSE`.
pub specs: Vec<CaseSpec>,
pub body: Vec<Stmt>,
pub pos: SourcePos,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VarDecl {
pub name: String,
pub suffix: Option<Suffix>,
/// `None` = Skalar; sonst Dimensionen `(untergrenze TO obergrenze)`.
pub dims: Option<Vec<(Option<Expr>, Expr)>>,
pub as_type: Option<TypeName>,
pub pos: SourcePos,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitKind {
For,
Do,
Sub,
Function,
Def,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionKind {
Explicit,
Base(u8),
}
#[derive(Debug, Clone, PartialEq)]
pub enum OnErrorAction {
Goto(LabelRef),
ResumeNext,
Disable, // GOTO 0
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResumeKind {
Retry, // RESUME [0]
Next,
Label(LabelRef),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
pub suffix: Option<Suffix>,
pub array: bool,
pub as_type: Option<TypeName>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcKind {
Sub,
Function,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProcSig {
pub kind: ProcKind,
pub name: String,
pub suffix: Option<Suffix>,
pub params: Vec<Param>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Proc {
pub sig: ProcSig,
pub is_static: bool,
pub body: Vec<Stmt>,
pub pos: SourcePos,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
Label(String),
LineNumber(u32),
Assign { target: Expr, value: Expr, pos: SourcePos },
Print {
file: Option<Expr>,
using: Option<Expr>,
items: Vec<PrintItem>,
pos: SourcePos,
},
Input {
line: bool,
file: Option<Expr>,
keep_cursor: bool,
prompt: Option<(String, bool)>, // (Text, mit Fragezeichen)
vars: Vec<Expr>,
pos: SourcePos,
},
If {
cond: Expr,
then_body: Vec<Stmt>,
elseifs: Vec<(Expr, Vec<Stmt>)>,
else_body: Option<Vec<Stmt>>,
pos: SourcePos,
},
Select { expr: Expr, arms: Vec<CaseArm>, pos: SourcePos },
For {
var: Expr,
from: Expr,
to: Expr,
step: Option<Expr>,
body: Vec<Stmt>,
pos: SourcePos,
},
DoLoop {
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
post: Option<(bool, Expr)>,
body: Vec<Stmt>,
pos: SourcePos,
},
While { cond: Expr, body: Vec<Stmt>, pos: SourcePos },
Goto { target: LabelRef, pos: SourcePos },
Gosub { target: LabelRef, pos: SourcePos },
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
Return { target: Option<LabelRef>, pos: SourcePos },
End,
StopStmt,
System,
Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
Erase { names: Vec<Expr>, pos: SourcePos },
ConstDecl { items: Vec<(String, Option<Suffix>, Expr)>, pos: SourcePos },
DefType { ty: TypeName, ranges: Vec<(char, char)>, pos: SourcePos },
OptionStmt { kind: OptionKind, pos: SourcePos },
TypeDecl { name: String, fields: Vec<(String, TypeName)>, pos: SourcePos },
Declare { sig: ProcSig, pos: SourcePos },
/// Expliziter oder impliziter Prozedur-/Builtin-Aufruf als Anweisung.
Call { name: String, suffix: Option<Suffix>, args: Vec<Expr>, pos: SourcePos },
OnError { local: bool, action: OnErrorAction, pos: SourcePos },
Resume { kind: ResumeKind, pos: SourcePos },
ErrorStmt { code: Expr, pos: SourcePos },
Data { items: Vec<String>, pos: SourcePos },
ReadStmt { vars: Vec<Expr>, pos: SourcePos },
Restore { target: Option<LabelRef>, pos: SourcePos },
/// Einzeilige `DEF FNname(...) = ausdruck`-Definition.
DefFn {
name: String,
suffix: Option<Suffix>,
params: Vec<Param>,
body: Expr,
pos: SourcePos,
},
/// Geparst, aber erst in Phase 3 implementiert (Datei-E/A u. ä.);
/// die Tokens der Anweisung wurden übersprungen.
NotYetImplemented { keyword: String, pos: SourcePos },
}
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
pub name: String,
pub body: Vec<Stmt>,
pub procs: Vec<Proc>,
}

View File

@@ -1,13 +1,557 @@
//! Lexer: zerlegt Quelltext in Tokens. //! Lexer: zerlegt Quelltext in Tokens.
//! //!
//! Besonderheiten des BASIC-Dialekts, die hier abgebildet werden müssen: //! Besonderheiten des Dialekts:
//! - Typ-Suffixe an Bezeichnern und Literalen (`%`, `&`, `!`, `#`, `$`, `@`) //! - Typ-Suffixe an Bezeichnern und Literalen (`% & ! # $ @`)
//! - Zeilennummern und Labels //! - Zeilennummern und Labels (löst der Parser auf)
//! - Keywords sind case-insensitiv, der Editor normalisiert später auf Großschreibung //! - Keywords case-insensitiv; nur echte Sprach-Keywords sind reserviert,
//! - Fortsetzung/Trennung von Anweisungen mit `:` und Kommentare mit `'` und `REM` //! Bibliotheksnamen (`CLS`, `LEFT$` …) bleiben Bezeichner und werden in
//! der Semantik als Builtins aufgelöst
//! - `:` trennt Anweisungen, `'` und `REM` leiten Kommentare ein
//! - Zeilenfortsetzung: `_` als letztes Zeichen nach Leerraum
//! - Literal-Typisierung nach Sprachreferenz (docs/sprachreferenz.md §1)
use crate::{Diagnostic, SourcePos};
/// Typ-Suffix eines Bezeichners oder Literals.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Suffix {
Integer, // %
Long, // &
Single, // !
Double, // #
Str, // $
Currency, // @
}
impl Suffix {
pub fn from_char(c: char) -> Option<Suffix> {
match c {
'%' => Some(Suffix::Integer),
'&' => Some(Suffix::Long),
'!' => Some(Suffix::Single),
'#' => Some(Suffix::Double),
'$' => Some(Suffix::Str),
'@' => Some(Suffix::Currency),
_ => None,
}
}
pub fn as_char(self) -> char {
match self {
Suffix::Integer => '%',
Suffix::Long => '&',
Suffix::Single => '!',
Suffix::Double => '#',
Suffix::Str => '$',
Suffix::Currency => '@',
}
}
}
/// Wert eines numerischen Literals, bereits typisiert.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NumValue {
Int(i16),
Long(i32),
Single(f32),
Double(f64),
/// Festkomma ×10 000
Currency(i64),
}
/// Reservierte Sprach-Keywords (bewusst schlank: Bibliotheksfunktionen
/// und -anweisungen sind KEINE Keywords, sondern Builtins der Semantik).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kw {
And, As, Call, Case, Const, Data, Declare, Def, DefCur, DefDbl, DefInt,
DefLng, DefSng, DefStr, Dim, Do, Double, Else, ElseIf, End, Eqv, Erase,
Error, Exit, For, Function, Gosub, Goto, If, Imp, Input, Integer, Is,
Let, Line, Local, Long, Loop, Mod, Next, Not, On, Option, Or, Print,
Read, ReDim, Rem, Restore, Resume, Return, Select, Shared, Single,
Static, Step, Stop, String, Sub, System, Then, To, Type, Until, Using,
Wend, While, Xor, Currency,
// Datei-E/A-Keywords: werden geparst, aber erst in Phase 3 implementiert
Open, Close, Write, Field, Get, Put, Seek, Lset, Rset,
}
fn keyword(upper: &str) -> Option<Kw> {
use Kw::*;
Some(match upper {
"AND" => And, "AS" => As, "CALL" => Call, "CASE" => Case,
"CONST" => Const, "CURRENCY" => Currency, "DATA" => Data,
"DECLARE" => Declare, "DEF" => Def, "DEFCUR" => DefCur,
"DEFDBL" => DefDbl, "DEFINT" => DefInt, "DEFLNG" => DefLng,
"DEFSNG" => DefSng, "DEFSTR" => DefStr, "DIM" => Dim, "DO" => Do,
"DOUBLE" => Double, "ELSE" => Else, "ELSEIF" => ElseIf, "END" => End,
"EQV" => Eqv, "ERASE" => Erase, "ERROR" => Error, "EXIT" => Exit,
"FIELD" => Field, "FOR" => For, "FUNCTION" => Function,
"GET" => Get, "GOSUB" => Gosub, "GOTO" => Goto, "IF" => If,
"IMP" => Imp, "INPUT" => Input, "INTEGER" => Integer, "IS" => Is,
"LET" => Let, "LINE" => Line, "LOCAL" => Local, "LONG" => Long,
"LOOP" => Loop, "LSET" => Lset, "MOD" => Mod, "NEXT" => Next,
"NOT" => Not, "ON" => On, "OPEN" => Open, "OPTION" => Option,
"OR" => Or, "PRINT" => Print, "PUT" => Put, "READ" => Read,
"REDIM" => ReDim, "REM" => Rem, "RESTORE" => Restore,
"RESUME" => Resume, "RETURN" => Return, "RSET" => Rset,
"SEEK" => Seek, "SELECT" => Select, "SHARED" => Shared,
"SINGLE" => Single, "STATIC" => Static, "STEP" => Step,
"STOP" => Stop, "STRING" => String, "SUB" => Sub,
"SYSTEM" => System, "THEN" => Then,
"TO" => To, "TYPE" => Type, "UNTIL" => Until, "USING" => Using,
"WEND" => Wend, "WHILE" => While, "WRITE" => Write, "XOR" => Xor,
"CLOSE" => Close,
_ => return None,
})
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Token { pub enum TokenKind {
// Platzhalter — wird in Phase 1 ausgearbeitet (siehe PLAN.md) /// Bezeichner; `name` ist bereits in Großschreibung normalisiert.
Ident { name: String, suffix: Option<Suffix> },
Kw(Kw),
Num(NumValue),
Str(String),
Plus, Minus, Star, Slash, Backslash, Caret,
Eq, Ne, Lt, Le, Gt, Ge,
LParen, RParen, Comma, Semicolon, Colon, Hash,
/// Ende einer logischen Zeile.
Eol,
Eof, Eof,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub kind: TokenKind,
pub pos: SourcePos,
}
pub struct LexOutput {
pub tokens: Vec<Token>,
pub diagnostics: Vec<Diagnostic>,
}
/// Signifikante Stellen einer Ziffernfolge (führende Nullen zählen nicht).
fn significant_digits(int_part: &str, frac_part: &str) -> usize {
let all: String = int_part.chars().chain(frac_part.chars()).collect();
let trimmed = all.trim_start_matches('0');
trimmed.len()
}
pub fn lex(source: &str) -> LexOutput {
let mut tokens: Vec<Token> = Vec::new();
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut continuation = false;
for (line_idx, raw_line) in source.lines().enumerate() {
let line_no = (line_idx + 1) as u32;
let chars: Vec<char> = raw_line.chars().collect();
let mut i = 0usize;
let mut line_continued = false;
'line: while i < chars.len() {
// Leerraum überspringen
while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
i += 1;
}
if i >= chars.len() {
break;
}
let start = i;
let pos = SourcePos { line: line_no, column: (start + 1) as u32 };
let c = chars[i];
// Zeilenfortsetzung: `_` nach Leerraum, danach nur noch Leerraum
if c == '_'
&& (start == 0
|| chars[start - 1] == ' '
|| chars[start - 1] == '\t')
&& chars[start + 1..].iter().all(|&ch| ch == ' ' || ch == '\t')
{
line_continued = true;
break 'line;
}
match c {
'\'' => break 'line, // Kommentar bis Zeilenende
'"' => {
i += 1;
let mut s = String::new();
let mut closed = false;
while i < chars.len() {
if chars[i] == '"' {
if i + 1 < chars.len() && chars[i + 1] == '"' {
s.push('"');
i += 2;
} else {
i += 1;
closed = true;
break;
}
} else {
s.push(chars[i]);
i += 1;
}
}
if !closed {
// Das Vorbild toleriert fehlende schließende
// Anführungszeichen am Zeilenende.
}
tokens.push(Token { kind: TokenKind::Str(s), pos });
}
'&' if i + 1 < chars.len()
&& matches!(chars[i + 1], 'h' | 'H' | 'o' | 'O') =>
{
let hex = matches!(chars[i + 1], 'h' | 'H');
i += 2;
let digit_start = i;
while i < chars.len()
&& chars[i].is_ascii_alphanumeric()
{
i += 1;
}
let digits: String = chars[digit_start..i].iter().collect();
let long_suffix = i < chars.len() && chars[i] == '&';
if long_suffix {
i += 1;
}
let radix = if hex { 16 } else { 8 };
match u32::from_str_radix(&digits, radix) {
Ok(v) => {
let kind = if long_suffix {
TokenKind::Num(NumValue::Long(v as i32))
} else if v <= 0xFFFF {
TokenKind::Num(NumValue::Int(v as u16 as i16))
} else {
diagnostics.push(Diagnostic {
pos,
message: "Overflow".into(),
});
TokenKind::Num(NumValue::Long(v as i32))
};
tokens.push(Token { kind, pos });
}
Err(_) => diagnostics.push(Diagnostic {
pos,
message: "Syntax error".into(),
}),
}
}
'0'..='9' | '.' if c != '.'
|| (i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
{
let int_start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let int_part: String = chars[int_start..i].iter().collect();
let mut frac_part = String::new();
let mut has_point = false;
if i < chars.len() && chars[i] == '.' {
has_point = true;
i += 1;
let fs = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
frac_part = chars[fs..i].iter().collect();
}
// Exponent E/D
let mut exp_kind: Option<char> = None;
let mut exp_str = String::new();
if i < chars.len()
&& matches!(chars[i], 'e' | 'E' | 'd' | 'D')
{
let save = i;
let k = chars[i].to_ascii_uppercase();
let mut j = i + 1;
let mut e = String::new();
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
e.push(chars[j]);
j += 1;
}
let ds = j;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
if j > ds {
for ch in &chars[ds..j] {
e.push(*ch);
}
exp_kind = Some(k);
exp_str = e;
i = j;
} else {
i = save; // kein Exponent (z. B. Variable `e`)
}
}
// Suffix
let suffix = if i < chars.len() {
Suffix::from_char(chars[i])
} else {
None
};
if suffix.is_some() {
i += 1;
}
let text = format!(
"{}{}{}{}",
int_part,
if has_point { "." } else { "" },
frac_part,
match exp_kind {
Some(_) => format!("e{exp_str}"),
None => String::new(),
}
);
let dval: f64 = text.parse().unwrap_or(0.0);
let value = match (suffix, exp_kind) {
(Some(Suffix::Integer), _) => {
if dval > i16::MAX as f64 || dval < i16::MIN as f64 {
diagnostics.push(Diagnostic {
pos,
message: "Overflow".into(),
});
}
NumValue::Int(dval as i16)
}
(Some(Suffix::Long), _) => {
if dval > i32::MAX as f64 || dval < i32::MIN as f64 {
diagnostics.push(Diagnostic {
pos,
message: "Overflow".into(),
});
}
NumValue::Long(dval as i32)
}
(Some(Suffix::Single), _) => NumValue::Single(dval as f32),
(Some(Suffix::Double), _) => NumValue::Double(dval),
(Some(Suffix::Currency), _) => {
NumValue::Currency((dval * 10_000.0).round() as i64)
}
(Some(Suffix::Str), _) => {
diagnostics.push(Diagnostic {
pos,
message: "Syntax error".into(),
});
NumValue::Double(dval)
}
(None, Some('D')) => NumValue::Double(dval),
(None, Some(_)) => NumValue::Single(dval as f32),
(None, None) => {
if !has_point {
// Ganzzahl: INTEGER → LONG → Gleitkomma
if let Ok(v) = text.parse::<i64>() {
if let Ok(v16) = i16::try_from(v) {
NumValue::Int(v16)
} else if let Ok(v32) = i32::try_from(v) {
NumValue::Long(v32)
} else {
NumValue::Double(dval)
}
} else {
NumValue::Double(dval)
}
} else {
// Entscheidung (2026-09-02, siehe
// Sprachreferenz §1): > 7 signifikante
// Stellen → DOUBLE, sonst SINGLE.
if significant_digits(&int_part, &frac_part) > 7 {
NumValue::Double(dval)
} else {
NumValue::Single(dval as f32)
}
}
}
};
tokens.push(Token { kind: TokenKind::Num(value), pos });
}
c if c.is_alphabetic() => {
i += 1;
while i < chars.len()
&& (chars[i].is_alphanumeric()
|| chars[i] == '.'
|| chars[i] == '_')
{
i += 1;
}
let mut name: String = chars[start..i]
.iter()
.collect::<String>()
.to_uppercase();
let suffix = if i < chars.len() {
Suffix::from_char(chars[i])
} else {
None
};
if suffix.is_some() {
i += 1;
}
if suffix.is_none() {
if let Some(kw) = keyword(&name) {
if kw == Kw::Rem {
break 'line; // REM: Rest ist Kommentar
}
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
continue;
}
}
// Bezeichner dürfen nicht mit '.' enden (a.b. → a.b + .)
while name.ends_with('.') {
name.pop();
i -= 1;
}
tokens.push(Token {
kind: TokenKind::Ident { name, suffix },
pos,
});
}
_ => {
i += 1;
let kind = match c {
'+' => TokenKind::Plus,
'-' => TokenKind::Minus,
'*' => TokenKind::Star,
'/' => TokenKind::Slash,
'\\' => TokenKind::Backslash,
'^' => TokenKind::Caret,
'=' => TokenKind::Eq,
'(' => TokenKind::LParen,
')' => TokenKind::RParen,
',' => TokenKind::Comma,
';' => TokenKind::Semicolon,
':' => TokenKind::Colon,
'#' => TokenKind::Hash,
'?' => TokenKind::Kw(Kw::Print), // Editor-Kurzform
'&' => TokenKind::Kw(Kw::Long), // isoliertes & (selten)
'<' => {
if i < chars.len() && chars[i] == '=' {
i += 1;
TokenKind::Le
} else if i < chars.len() && chars[i] == '>' {
i += 1;
TokenKind::Ne
} else {
TokenKind::Lt
}
}
'>' => {
if i < chars.len() && chars[i] == '=' {
i += 1;
TokenKind::Ge
} else {
TokenKind::Gt
}
}
other => {
diagnostics.push(Diagnostic {
pos,
message: format!("Syntax error ('{other}')"),
});
continue;
}
};
tokens.push(Token { kind, pos });
}
}
}
if line_continued {
continuation = true;
} else {
if !continuation || !tokens.is_empty() {
tokens.push(Token {
kind: TokenKind::Eol,
pos: SourcePos {
line: line_no,
column: (chars.len() + 1) as u32,
},
});
}
continuation = false;
}
}
tokens.push(Token {
kind: TokenKind::Eof,
pos: SourcePos { line: (source.lines().count() + 1) as u32, column: 1 },
});
LexOutput { tokens, diagnostics }
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(src: &str) -> Vec<TokenKind> {
lex(src).tokens.into_iter().map(|t| t.kind).collect()
}
#[test]
fn keywords_case_insensitiv() {
let k = kinds("print If tHeN");
assert_eq!(k[0], TokenKind::Kw(Kw::Print));
assert_eq!(k[1], TokenKind::Kw(Kw::If));
assert_eq!(k[2], TokenKind::Kw(Kw::Then));
}
#[test]
fn suffix_macht_keyword_zum_bezeichner() {
// STRING$ ist die Bibliotheksfunktion, STRING das Typ-Keyword
let k = kinds("STRING$ STRING");
assert_eq!(
k[0],
TokenKind::Ident { name: "STRING".into(), suffix: Some(Suffix::Str) }
);
assert_eq!(k[1], TokenKind::Kw(Kw::String));
}
#[test]
fn literal_typisierung() {
assert_eq!(kinds("42")[0], TokenKind::Num(NumValue::Int(42)));
assert_eq!(kinds("40000")[0], TokenKind::Num(NumValue::Long(40000)));
assert_eq!(kinds("1.5")[0], TokenKind::Num(NumValue::Single(1.5)));
assert_eq!(
kinds("3.14159265")[0],
TokenKind::Num(NumValue::Double(3.14159265))
);
assert_eq!(kinds("1E3")[0], TokenKind::Num(NumValue::Single(1000.0)));
assert_eq!(kinds("1D3")[0], TokenKind::Num(NumValue::Double(1000.0)));
assert_eq!(kinds("2.5@")[0], TokenKind::Num(NumValue::Currency(25000)));
assert_eq!(kinds("&HFF")[0], TokenKind::Num(NumValue::Int(255)));
assert_eq!(kinds("&HFFFF")[0], TokenKind::Num(NumValue::Int(-1)));
assert_eq!(kinds("&HFFFF&")[0], TokenKind::Num(NumValue::Long(0xFFFF)));
assert_eq!(kinds("&O777")[0], TokenKind::Num(NumValue::Int(511)));
}
#[test]
fn strings_mit_doppelten_anfuehrungszeichen() {
assert_eq!(kinds("\"a\"\"b\"")[0], TokenKind::Str("a\"b".into()));
}
#[test]
fn kommentare_und_rem() {
let k = kinds("PRINT 1 ' Kommentar\nREM ganze Zeile\nPRINT 2");
// PRINT 1 EOL EOL PRINT 2 EOL EOF
assert_eq!(k.len(), 8);
assert_eq!(k[2], TokenKind::Eol);
assert_eq!(k[3], TokenKind::Eol);
}
#[test]
fn zeilenfortsetzung() {
let k = kinds("PRINT 1, _\n 2");
// Kein Eol zwischen 1, und 2
assert!(matches!(k[3], TokenKind::Num(NumValue::Int(2))));
}
#[test]
fn bezeichner_mit_punkt_und_unterstrich() {
let k = kinds("kunde.name_2$");
assert_eq!(
k[0],
TokenKind::Ident {
name: "KUNDE.NAME_2".into(),
suffix: Some(Suffix::Str)
}
);
}
}

View File

@@ -2,16 +2,48 @@
//! //!
//! Enthält Lexer, Parser, AST-Definitionen und die semantische Analyse //! Enthält Lexer, Parser, AST-Definitionen und die semantische Analyse
//! (Symboltabellen, Typprüfung, implizite Deklarationen, `DEFINT`-Regeln usw.). //! (Symboltabellen, Typprüfung, implizite Deklarationen, `DEFINT`-Regeln usw.).
//! Ausgabe des Frontends ist ein typgeprüfter AST bzw. eine Zwischen- //! Ausgabe des Frontends ist ein typgeprüfter AST, den `tb-vm` in Bytecode
//! repräsentation (IR), die von `tb-vm` in Bytecode übersetzt wird. //! übersetzt.
pub mod ast;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod ast; pub mod sema;
/// Quelltextposition für Diagnostik (1-basiert, wie im klassischen IDE-Vorbild). /// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SourcePos { pub struct SourcePos {
pub line: u32, pub line: u32,
pub column: u32, pub column: u32,
} }
/// Eine Diagnosemeldung mit Position. Die Texte folgen den (englischen)
/// Meldungen des Vorbilds, wo es eine Entsprechung gibt.
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub pos: SourcePos,
pub message: String,
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
}
}
/// Ergebnis der Frontend-Pipeline für ein Modul.
pub struct Analysis {
pub module: ast::Module,
pub diagnostics: Vec<Diagnostic>,
}
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung.
pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
let lexed = lexer::lex(source);
let mut diagnostics = lexed.diagnostics;
let parsed = parser::parse(module_name, &lexed.tokens);
diagnostics.extend(parsed.diagnostics);
diagnostics.extend(sema::check(&parsed.module));
diagnostics.sort_by_key(|d| (d.pos.line, d.pos.column));
Analysis { module: parsed.module, diagnostics }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
//! Phase-1-Meilenstein: der komplette Kompatibilitäts-Testkorpus
//! (tests/compat/*.bas) muss fehlerfrei parsen und typgeprüft werden.
use std::fs;
use std::path::Path;
#[test]
fn korpus_parst_und_wird_typgeprueft() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat");
let mut checked = 0;
for entry in fs::read_dir(&dir).expect("tests/compat fehlt") {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) != Some("bas") {
continue;
}
let name = path.file_name().unwrap().to_string_lossy().to_string();
let src = fs::read_to_string(&path).unwrap();
let analysis = tb_frontend::analyze_source(&name, &src);
assert!(
analysis.diagnostics.is_empty(),
"{name}: {:?}",
analysis.diagnostics
);
checked += 1;
}
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
}

View File

@@ -7,7 +7,7 @@ license.workspace = true
authors.workspace = true authors.workspace = true
[[bin]] [[bin]]
name = "tbdos" name = "tb"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]

View File

@@ -1,4 +1,4 @@
//! `tbdos` — die integrierte Entwicklungsumgebung von Terminal Basic (TUI). //! `tb` — die integrierte Entwicklungsumgebung von Terminal Basic (TUI).
//! //!
//! Nachbildung der klassischen DOS-IDE: Menüleiste, Editor mit //! Nachbildung der klassischen DOS-IDE: Menüleiste, Editor mit
//! Syntaxprüfung pro Zeile, Formular-Designer, Direktfenster, //! Syntaxprüfung pro Zeile, Formular-Designer, Direktfenster,
@@ -6,6 +6,6 @@
//! Siehe PLAN.md, Phase 5. //! Siehe PLAN.md, Phase 5.
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
println!("tbdos — Terminal Basic IDE (Projektrahmen, noch ohne Funktion)"); println!("tb — Terminal Basic IDE (Projektrahmen, noch ohne Funktion)");
Ok(()) Ok(())
} }

View File

@@ -43,9 +43,10 @@ fn testbild(s: &mut TextScreen) {
fn status(s: &mut TextScreen, text: &str) { fn status(s: &mut TextScreen, text: &str) {
let (r, c) = (s.csrlin(), s.pos()); let (r, c) = (s.csrlin(), s.pos());
let (row, width) = (s.rows(), s.cols());
s.set_color(0, 7); s.set_color(0, 7);
s.locate(25, 1).unwrap(); s.locate(row, 1).unwrap();
s.print(&format!("{text:<80.80}")); s.print(&format!("{text:<width$.width$}"));
s.set_color(7, 0); s.set_color(7, 0);
s.locate(r, c).unwrap(); s.locate(r, c).unwrap();
} }
@@ -54,7 +55,8 @@ fn main() -> anyhow::Result<()> {
let mut terminal = ratatui::init(); let mut terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?; execute!(stdout(), EnableMouseCapture)?;
let mut screen = TextScreen::new(); let (tw, th) = crossterm::terminal::size()?;
let mut screen = TextScreen::with_size(tw as usize, th as usize);
testbild(&mut screen); testbild(&mut screen);
status(&mut screen, "Bereit. Tasten/Maus testen, Esc beendet."); status(&mut screen, "Bereit. Tasten/Maus testen, Esc beendet.");
@@ -79,7 +81,10 @@ fn main() -> anyhow::Result<()> {
&format!("Maus: {:?} bei Spalte {}, Zeile {}", m.kind, m.column + 1, m.row + 1), &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}")), Event::Resize(w, h) => {
screen.resize(w as usize, h as usize);
status(&mut screen, &format!("Resize: {w}x{h}"));
}
_ => {} _ => {}
} }
} }

View File

@@ -1,8 +1,12 @@
//! Textbildschirm-Emulation: 80×25-Zellenpuffer (Unicode-Zeichen + Farb- //! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
//! attribut), Cursor, Scrollen — gerendert als Ratatui-Widget. //! Cursor, Scrollen — gerendert als Ratatui-Widget.
//!
//! Entscheidungen (siehe PLAN.md):
//! - durchgängig Unicode, keine CP437-Emulation
//! - **dynamische Größe**: der Bildschirm folgt der Terminalgröße; unterhalb
//! der Mindestgröße (80×25) wird nur ein Hinweis gerendert (btop-artig),
//! größere Terminals werden voll genutzt.
//! //!
//! 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 //! Das Zellenmodell ist strikt 1 Zeichen = 1 Zelle; Zeichen mit
//! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md). //! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md).
@@ -11,8 +15,9 @@ use ratatui::layout::Rect;
use ratatui::style::{Color, Style}; use ratatui::style::{Color, Style};
use ratatui::widgets::Widget; use ratatui::widgets::Widget;
pub const COLS: usize = 80; /// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
pub const ROWS: usize = 25; pub const MIN_COLS: usize = 80;
pub const MIN_ROWS: usize = 25;
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut. /// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -37,12 +42,14 @@ pub fn basic_color(n: u8) -> Color {
Color::Indexed(MAP[(n & 0x0F) as usize]) Color::Indexed(MAP[(n & 0x0F) as usize])
} }
/// Der emulierte 80×25-Textbildschirm. /// Der emulierte Textbildschirm mit dynamischer Größe.
/// ///
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile 125, /// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
/// Spalte 180), wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts. /// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextScreen { pub struct TextScreen {
cols: usize,
rows: usize,
cells: Vec<Cell>, cells: Vec<Cell>,
/// Cursorposition, 0-basiert intern. /// Cursorposition, 0-basiert intern.
cur_row: usize, cur_row: usize,
@@ -54,6 +61,9 @@ pub struct TextScreen {
/// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv. /// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
view_top: usize, view_top: usize,
view_bottom: usize, view_bottom: usize,
/// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
/// Scrollbereich folgt dann der Bildschirmgröße.
view_full: bool,
} }
impl Default for TextScreen { impl Default for TextScreen {
@@ -63,16 +73,63 @@ impl Default for TextScreen {
} }
impl TextScreen { impl TextScreen {
/// Bildschirm in Mindestgröße (80×25).
pub fn new() -> Self { pub fn new() -> Self {
Self::with_size(MIN_COLS, MIN_ROWS)
}
/// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
pub fn with_size(cols: usize, rows: usize) -> Self {
let cols = cols.max(MIN_COLS);
let rows = rows.max(MIN_ROWS);
TextScreen { TextScreen {
cells: vec![Cell::default(); COLS * ROWS], cols,
rows,
cells: vec![Cell::default(); cols * rows],
cur_row: 0, cur_row: 0,
cur_col: 0, cur_col: 0,
cursor_visible: true, cursor_visible: true,
fg: 7, fg: 7,
bg: 0, bg: 0,
view_top: 0, view_top: 0,
view_bottom: ROWS - 1, view_bottom: rows - 1,
view_full: true,
}
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows
}
/// An neue Terminalgröße anpassen: Inhalt bleibt oben links erhalten,
/// neue Zellen sind leer. Unterhalb der Mindestgröße bleibt der Puffer
/// bei 80×25 (das Widget zeigt dann den Zu-klein-Hinweis).
pub fn resize(&mut self, cols: usize, rows: usize) {
let cols = cols.max(MIN_COLS);
let rows = rows.max(MIN_ROWS);
if cols == self.cols && rows == self.rows {
return;
}
let mut cells = vec![Cell { ch: ' ', fg: self.fg, bg: self.bg }; cols * rows];
for row in 0..self.rows.min(rows) {
for col in 0..self.cols.min(cols) {
cells[row * cols + col] = self.cells[row * self.cols + col];
}
}
self.cells = cells;
self.cols = cols;
self.rows = rows;
self.cur_row = self.cur_row.min(rows - 1);
self.cur_col = self.cur_col.min(cols - 1);
if self.view_full {
self.view_top = 0;
self.view_bottom = rows - 1;
} else {
self.view_top = self.view_top.min(rows - 1);
self.view_bottom = self.view_bottom.min(rows - 1);
} }
} }
@@ -81,23 +138,24 @@ impl TextScreen {
pub fn cls(&mut self) { pub fn cls(&mut self) {
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg }; let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
for row in self.view_top..=self.view_bottom { for row in self.view_top..=self.view_bottom {
self.cells[row * COLS..(row + 1) * COLS].fill(blank); self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
} }
self.cur_row = self.view_top; self.cur_row = self.view_top;
self.cur_col = 0; self.cur_col = 0;
} }
/// `COLOR vg, hg` — Werte werden wie im Vorbild maskiert /// `COLOR vg, hg` (vg 031, hg 07). Blinkende Vordergrundfarben
/// (vg 031, wir ignorieren Blink → 015; hg 07). /// (1631) werden als „hell" simuliert (Entscheidung 2026-09-02):
/// echtes Terminal-Blinken ist nicht überall verfügbar.
pub fn set_color(&mut self, fg: u8, bg: u8) { pub fn set_color(&mut self, fg: u8, bg: u8) {
let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
self.fg = fg & 0x0F; self.fg = fg & 0x0F;
self.bg = bg & 0x07; self.bg = bg & 0x07;
} }
/// `LOCATE zeile, spalte` (1-basiert). Außerhalb des Bildschirms: /// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
/// Fehler 5 beim Aufrufer — hier wird geklemmt geprüft.
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> { pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> {
if row < 1 || row > ROWS || col < 1 || col > COLS { if row < 1 || row > self.rows || col < 1 || col > self.cols {
return Err(()); return Err(());
} }
self.cur_row = row - 1; self.cur_row = row - 1;
@@ -117,32 +175,32 @@ impl TextScreen {
/// `VIEW PRINT oben TO unten` (1-basiert). /// `VIEW PRINT oben TO unten` (1-basiert).
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> { pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> {
if top < 1 || bottom > ROWS || top > bottom { if top < 1 || bottom > self.rows || top > bottom {
return Err(()); return Err(());
} }
self.view_top = top - 1; self.view_top = top - 1;
self.view_bottom = bottom - 1; self.view_bottom = bottom - 1;
self.view_full = top == 1 && bottom == self.rows;
Ok(()) Ok(())
} }
pub fn cell(&self, row: usize, col: usize) -> Cell { pub fn cell(&self, row: usize, col: usize) -> Cell {
self.cells[(row - 1) * COLS + (col - 1)] self.cells[(row - 1) * self.cols + (col - 1)]
} }
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand, /// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um, /// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
/// `\r` setzt an den Zeilenanfang; andere Steuerzeichen werden /// `\r` setzt an den Zeilenanfang.
/// (noch) als normale Zeichen behandelt.
pub fn print(&mut self, text: &str) { pub fn print(&mut self, text: &str) {
for ch in text.chars() { for ch in text.chars() {
match ch { match ch {
'\n' => self.newline(), '\n' => self.newline(),
'\r' => self.cur_col = 0, '\r' => self.cur_col = 0,
_ => { _ => {
self.cells[self.cur_row * COLS + self.cur_col] = self.cells[self.cur_row * self.cols + self.cur_col] =
Cell { ch, fg: self.fg, bg: self.bg }; Cell { ch, fg: self.fg, bg: self.bg };
self.cur_col += 1; self.cur_col += 1;
if self.cur_col >= COLS { if self.cur_col >= self.cols {
self.newline(); self.newline();
} }
} }
@@ -165,34 +223,34 @@ impl TextScreen {
pub fn scroll_up(&mut self) { pub fn scroll_up(&mut self) {
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg }; let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
for row in self.view_top..self.view_bottom { for row in self.view_top..self.view_bottom {
let (a, b) = self.cells.split_at_mut((row + 1) * COLS); let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
a[row * COLS..].copy_from_slice(&b[..COLS]); a[row * self.cols..].copy_from_slice(&b[..self.cols]);
} }
self.cells[self.view_bottom * COLS..(self.view_bottom + 1) * COLS].fill(blank); self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols]
.fill(blank);
} }
} }
/// Rendert den Bildschirm zentriert (Letterboxing) in die verfügbare Fläche. /// Rendert den Bildschirm oben links in die verfügbare Fläche. Ist das
/// Ist das Terminal kleiner als 80×25, wird ein Hinweis angezeigt. /// Terminal kleiner als die Mindestgröße, erscheint nur ein Hinweis
/// (btop-artig); die Anwendung ruft bei Resize `TextScreen::resize` auf,
/// damit Puffer und Terminal deckungsgleich bleiben.
impl Widget for &TextScreen { impl Widget for &TextScreen {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if (area.width as usize) < COLS || (area.height as usize) < ROWS { if (area.width as usize) < MIN_COLS || (area.height as usize) < MIN_ROWS {
let msg = format!( let msg = format!(
"Terminal zu klein: {}x{}benötigt {}x{}", "Terminal zu klein: {}x{}Minimum {}x{}",
area.width, area.height, COLS, ROWS area.width, area.height, MIN_COLS, MIN_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)); buf.set_string(area.x, area.y, msg, Style::default().fg(Color::Red));
return; return;
} }
let x0 = area.x + (area.width - COLS as u16) / 2; let cols = self.cols.min(area.width as usize);
let y0 = area.y + (area.height - ROWS as u16) / 2; let rows = self.rows.min(area.height as usize);
for row in 0..ROWS { for row in 0..rows {
for col in 0..COLS { for col in 0..cols {
let c = self.cells[row * COLS + col]; let c = self.cells[row * self.cols + col];
if let Some(cell) = buf.cell_mut((x0 + col as u16, y0 + row as u16)) { if let Some(cell) = buf.cell_mut((area.x + col as u16, area.y + row as u16)) {
let mut s = String::new(); let mut s = String::new();
s.push(c.ch); s.push(c.ch);
cell.set_symbol(&s); cell.set_symbol(&s);
@@ -204,10 +262,12 @@ impl Widget for &TextScreen {
} }
// Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in // Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in
// der Forms-/Runtime-Schicht später gezielt gesteuert). // der Forms-/Runtime-Schicht später gezielt gesteuert).
if self.cursor_visible { if self.cursor_visible && self.cur_row < rows && self.cur_col < cols {
let (cx, cy) = (x0 + self.cur_col as u16, y0 + self.cur_row as u16); let (cx, cy) = (area.x + self.cur_col as u16, area.y + self.cur_row as u16);
if let Some(cell) = buf.cell_mut((cx, cy)) { if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_style(cell.style().add_modifier(ratatui::style::Modifier::REVERSED)); cell.set_style(
cell.style().add_modifier(ratatui::style::Modifier::REVERSED),
);
} }
} }
} }
@@ -248,7 +308,7 @@ mod tests {
let mut s = TextScreen::new(); let mut s = TextScreen::new();
s.locate(25, 1).unwrap(); s.locate(25, 1).unwrap();
s.print("unten\n"); // erzwingt Scroll s.print("unten\n"); // erzwingt Scroll
assert_eq!(s.cell(24, 1).ch, 'u'); // Zeile 25 ist nach 24 gerutscht assert_eq!(s.cell(24, 1).ch, 'u');
assert_eq!(s.cell(25, 1).ch, ' '); assert_eq!(s.cell(25, 1).ch, ' ');
assert_eq!(s.csrlin(), 25); assert_eq!(s.csrlin(), 25);
} }
@@ -261,7 +321,7 @@ mod tests {
s.view_print(3, 5).unwrap(); s.view_print(3, 5).unwrap();
s.locate(5, 1).unwrap(); s.locate(5, 1).unwrap();
s.print("a\nb"); // scrollt nur Zeilen 35 s.print("a\nb"); // scrollt nur Zeilen 35
assert_eq!(s.cell(1, 1).ch, 'k'); // Kopfzeile unberührt assert_eq!(s.cell(1, 1).ch, 'k');
assert_eq!(s.cell(4, 1).ch, 'a'); assert_eq!(s.cell(4, 1).ch, 'a');
assert_eq!(s.cell(5, 1).ch, 'b'); assert_eq!(s.cell(5, 1).ch, 'b');
} }
@@ -274,6 +334,47 @@ mod tests {
assert!(s.locate(25, 80).is_ok()); assert!(s.locate(25, 80).is_ok());
} }
#[test]
fn dynamische_groesse_und_resize() {
let mut s = TextScreen::with_size(120, 40);
assert_eq!((s.cols(), s.rows()), (120, 40));
s.locate(40, 1).unwrap();
s.print("!");
assert_eq!(s.cell(40, 1).ch, '!');
// Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
s.locate(1, 1).unwrap();
s.print("K");
s.resize(100, 30);
assert_eq!((s.cols(), s.rows()), (100, 30));
assert_eq!(s.cell(1, 1).ch, 'K');
// Unter Minimum wird auf 80×25 geklemmt
s.resize(10, 5);
assert_eq!((s.cols(), s.rows()), (80, 25));
}
#[test]
fn blink_wird_als_hell_simuliert() {
let mut s = TextScreen::new();
s.set_color(17, 0); // blinkend Blau → helles Blau
assert_eq!(s.fg, 9);
s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
assert_eq!(s.fg, 15);
s.set_color(7, 0);
assert_eq!(s.fg, 7);
}
#[test]
fn resize_folgt_vollem_scrollbereich() {
let mut s = TextScreen::new();
s.resize(90, 40);
s.locate(40, 1).unwrap();
s.print("a\nb"); // Scroll am neuen unteren Rand
assert_eq!(s.cell(39, 1).ch, 'a');
assert_eq!(s.cell(40, 1).ch, 'b');
}
#[test] #[test]
fn farbabbildung() { fn farbabbildung() {
assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau

View File

@@ -61,8 +61,16 @@ Reine Textdatei ohne Kopfzeile, **eine Projektdatei pro Zeile**
(`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic (`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic
akzeptiert zusätzlich Kommentarzeilen mit `'`. akzeptiert zusätzlich Kommentarzeilen mit `'`.
## Record-Dateien (`GET`/`PUT`, `OPEN … FOR RANDOM`)
Feste Strings (`STRING * n`) in Records werden als **UTF-32LE** gespeichert
(4 Bytes pro Zeichen → feste Record-Länge bleibt erhalten). Entscheidung
2026-09-02: Die Binärdateien sind damit bewusst inkompatibel zu Dateien
des Vorbilds; numerische Felder behalten ihr klassisches Layout
(INTEGER i16, LONG i32, SINGLE f32, DOUBLE f64, CURRENCY i64, little-endian).
## Kompilat: `.tbc` (neu, eigenes Format) ## Kompilat: `.tbc` (neu, eigenes Format)
Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md). Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md).
`tbdosc build` erzeugt wahlweise `.tbc` oder ein eigenständiges Executable `tbc build` erzeugt wahlweise `.tbc` oder ein eigenständiges Executable
(Runner + eingebettetes `.tbc`). (Runner + eingebettetes `.tbc`).

View File

@@ -139,8 +139,9 @@ DragOver-State 0 Enter · 1 Leave · 2 Over. Bei Steuerelement-Arrays steht
## SCREEN-Objekt ## SCREEN-Objekt
Eigenschaften: ActiveControl, ActiveForm, AutoRedraw, ControlPanel, Eigenschaften: ActiveControl, ActiveForm, AutoRedraw, ControlPanel,
Height, Width (Zeichen, read-only), MousePointer. Methoden: HIDE, SHOW Height, Width (Zeichen, read-only — bei uns die **aktuelle Terminalgröße**,
(alle sichtbaren Formulare ein-/ausblenden). `SCREEN.ControlPanel(017)` Abweichung: das Vorbild lieferte fest 25/43/50 × 80), MousePointer.
Methoden: HIDE, SHOW (alle sichtbaren Formulare ein-/ausblenden). `SCREEN.ControlPanel(017)`
konfiguriert Systemfarben/-effekte (AccessKey-/Desktop-/Menü-/Titel- konfiguriert Systemfarben/-effekte (AccessKey-/Desktop-/Menü-/Titel-
Farben, Schatten, 3D-Effekte, Desktop-Füllzeichen) — Konstanten in der Farben, Schatten, 3D-Effekte, Desktop-Füllzeichen) — Konstanten in der
Konstanten-Include-Datei. Konstanten-Include-Datei.

View File

@@ -149,7 +149,7 @@ Die Umgebung führt P-Code-übersetzte Zeilen sofort aus; der separate
Compiler erzeugt native EXEs (wahlweise standalone oder mit Runtime-Modul). Compiler erzeugt native EXEs (wahlweise standalone oder mit Runtime-Modul).
Nur die Umgebung hat: Immediate Window, Watches/Watchpoints, Breakpoints, Nur die Umgebung hat: Immediate Window, Watches/Watchpoints, Breakpoints,
Stepping, History-Trace, Set Next Statement, automatische Bereichs-/ Stepping, History-Trace, Set Next Statement, automatische Bereichs-/
Überlaufprüfungen. → Terminal Basic: `tbdos` (IDE) und `tbdosc` Überlaufprüfungen. → Terminal Basic: `tb` (IDE) und `tbc`
(Standalone-Compiler) teilen sich dieselbe TBVM, damit es — anders als im (Standalone-Compiler) teilen sich dieselbe TBVM, damit es — anders als im
Vorbild — **keine** Verhaltensunterschiede zwischen IDE-Lauf und Vorbild — **keine** Verhaltensunterschiede zwischen IDE-Lauf und
kompiliertem Programm gibt (dokumentierte Abweichung, gewollt). kompiliertem Programm gibt (dokumentierte Abweichung, gewollt).
@@ -162,6 +162,9 @@ kompiliertem Programm gibt (dokumentierte Abweichung, gewollt).
F12/Shift+F12-Wechsel bleibt erhalten. F12/Shift+F12-Wechsel bleibt erhalten.
3. Zeilenweises Syntax-Checking mit Formatierung/Normalisierung ist 3. Zeilenweises Syntax-Checking mit Formatierung/Normalisierung ist
Editor-Standard (abschaltbar wie im Vorbild). Editor-Standard (abschaltbar wie im Vorbild).
4. IDE-Lauf und `tbdosc`-Kompilat verhalten sich identisch (gleiche TBVM). 4. IDE-Lauf und `tbc`-Kompilat verhalten sich identisch (gleiche TBVM).
5. MS-DOS-spezifische Toolbox-Controls (Dir/Drive/File List) werden als 5. MS-DOS-spezifische Toolbox-Controls (Dir/Drive/File List) werden als
plattformneutrale Datei-Browser-Controls nachgebildet. plattformneutrale Datei-Browser-Controls nachgebildet.
6. **Dynamische Terminalgröße** (2026-09-02): die IDE nutzt das gesamte
Terminal — Desktop, Fensterverwaltung und Formulare passen sich an;
Mindestgröße 80×25, darunter nur ein Hinweis (btop-artig).

View File

@@ -11,7 +11,11 @@ Details, `ener.hlp` Fehlermeldungen; Vergleich PDS 7.1: `bas7qck.hlp`)
sowie das Original-README der Professional Edition. Aussagen ohne sowie das Original-README der Professional Edition. Aussagen ohne
TODO-Markierung sind daraus belegt. TODO-Markierung sind daraus belegt.
**Bewusste Abweichungen vom Vorbild** stehen am Ende des Dokuments. **Guiding Principle:** Die Original-Dokumentation führt. Jedes dort
dokumentierte Sprachelement wird unterstützt oder — nur nach expliziter
Non-Feature-Entscheidung des Projektinhabers — mit einer klaren
Fehlermeldung abgewiesen. **Bewusste Abweichungen und Non-Features**
stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
--- ---
@@ -40,10 +44,12 @@ TODO-Markierung sind daraus belegt.
hexadezimal `&HFF`, oktal `&O777`; Suffixe erzwingen den Typ (`10%`, `10&`, hexadezimal `&HFF`, oktal `&O777`; Suffixe erzwingen den Typ (`10%`, `10&`,
`1.5!`, `1.5#`, `2.5@`). Ohne Suffix gilt: Ganzzahl im INTEGER-Bereich → `1.5!`, `1.5#`, `2.5@`). Ohne Suffix gilt: Ganzzahl im INTEGER-Bereich →
INTEGER; Hex/Oktal ohne `&`-Suffix → 16 Bit (INTEGER), mit `&` → LONG; INTEGER; Hex/Oktal ohne `&`-Suffix → 16 Bit (INTEGER), mit `&` → LONG;
`E`-Exponent → SINGLE, `D`-Exponent → DOUBLE; Dezimalpunkt-Wert `E`-Exponent → SINGLE, `D`-Exponent → DOUBLE; Dezimalpunkt-Wert:
SINGLE, bei mehr als 15 Stellen DOUBLE (TODO: exakte Schwelle — Quelle **Entscheidung (2026-09-02):** bis 7 signifikante Stellen → SINGLE,
an der Stelle widersprüchlich); Ganzzahl außerhalb des LONG-Bereichs darüber → DOUBLE. (Die Original-Hilfe nennt widersprüchlich eine
wird Gleitkommawert (kein Fehler). 15-Stellen-Schwelle, die SINGLE-Präzision verlöre; wir legen die Schwelle
auf die SINGLE-Genauigkeit — präzisionserhaltende Auslegung.)
Ganzzahl außerhalb des LONG-Bereichs wird Gleitkommawert (kein Fehler).
- **String-Literale:** `"…"`; doppeltes `""` ergibt ein Anführungszeichen. - **String-Literale:** `"…"`; doppeltes `""` ergibt ein Anführungszeichen.
## 2. Typsystem ## 2. Typsystem
@@ -188,9 +194,12 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
- `INKEY$` (nicht blockierend; "" wenn leer; erweiterte Tasten: - `INKEY$` (nicht blockierend; "" wenn leer; erweiterte Tasten:
2-Zeichen-Sequenz `CHR$(0)+code` im Vorbild — Abweichung s. u.), 2-Zeichen-Sequenz `CHR$(0)+code` im Vorbild — Abweichung s. u.),
`INPUT$(n [,#f])`. `INPUT$(n [,#f])`.
- `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`. - `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`
- `COLOR [vg][,hg]` (vg 031: 1631 = blinkend; hg 07), `CLS`, — arbeiten auf der tatsächlichen Bildschirmgröße (dynamisch, s. Abweichungen).
`WIDTH` (80/40 — wir: nur 80), `VIEW PRINT oben TO unten` (Scrollbereich). - `COLOR [vg][,hg]` (vg 031: 1631 = blinkend — bei uns als „hell"
simuliert, Entscheidung 2026-09-02; hg 07), `CLS`,
`WIDTH` (im Vorbild 80/40 — bei uns wirkungslos, s. Abweichungen),
`VIEW PRINT oben TO unten` (Scrollbereich).
- `TAB(n)`, `SPC(n)` in PRINT-Listen. - `TAB(n)`, `SPC(n)` in PRINT-Listen.
- `BEEP`, `SOUND freq, dauer` (Terminal-Bell / Nicht-Ziel, s. Abweichungen), - `BEEP`, `SOUND freq, dauer` (Terminal-Bell / Nicht-Ziel, s. Abweichungen),
`PLAY` (Nicht-Ziel). `PLAY` (Nicht-Ziel).
@@ -211,6 +220,9 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
`LOC(n)`, `LOF(n)`. `LOC(n)`, `LOF(n)`.
- Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`, - Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`,
`CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`. `CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`.
- **ISAM** (Professional-Edition des Vorbilds): wird unterstützt
(Entscheidung 2026-09-02) — `OPEN … FOR ISAM`, Tabellen/Indizes,
`SEEKGT`-Familie usw.; Umfang wird mit dem Inventar (Phase 3) erfasst.
- Pfade: plattformneutral; `/` und `\` werden akzeptiert. - Pfade: plattformneutral; `/` und `\` werden akzeptiert.
## 11. Eingebaute Funktionen (Katalog) ## 11. Eingebaute Funktionen (Katalog)
@@ -267,9 +279,15 @@ Dialoge:
- String-Vergleich codepoint-weise (keine CP437-Sortierung). - String-Vergleich codepoint-weise (keine CP437-Sortierung).
- Der Bildschirmpuffer speichert Unicode-Zeichen; Rahmen werden mit - Der Bildschirmpuffer speichert Unicode-Zeichen; Rahmen werden mit
Unicode-Box-Drawing gezeichnet. Unicode-Box-Drawing gezeichnet.
- Zeichen mit Darstellungsbreite ≠ 1: offen (siehe PLAN.md). - Zeichen mit Darstellungsbreite 2 (Emoji, CJK) **belegen zwei Zellen**
2. **Keine Hardware-Nähe:** `PEEK`/`POKE`/`INP`/`OUT`/`CALL ABSOLUTE`/ (Entscheidung 2026-09-02; Umsetzung Phase 3).
Interrupts lösen Fehler 73 (Feature unavailable) aus. - `GET`/`PUT`-Records: feste Strings (`STRING * n`) werden als **UTF-32**
gespeichert (4 Bytes/Zeichen, feste Record-Länge) — Binärdateien sind
dadurch bewusst inkompatibel zum Vorbild (Entscheidung 2026-09-02).
2. **Keine Hardware-Nähe** (bestätigt 2026-09-02): `PEEK`/`POKE`/`INP`/
`OUT`/`WAIT`/`BLOAD`/`BSAVE`/`VARPTR`/`VARSEG`/`SADD`/`CALL ABSOLUTE`/
`CALL INTERRUPT`/`IOCTL` werden bereits **zur Compile-Zeit** mit der
Meldung „Feature unavailable" abgewiesen (kein Laufzeitfehler).
3. **`INKEY$` für erweiterte Tasten** liefert weiterhin 3. **`INKEY$` für erweiterte Tasten** liefert weiterhin
`CHR$(0) + code`-Sequenzen mit den klassischen Scancodes (F1 = `CHR$(0)+";"` `CHR$(0) + code`-Sequenzen mit den klassischen Scancodes (F1 = `CHR$(0)+";"`
usw.), damit bestehender Code funktioniert. Zusätzliche moderne Tasten: usw.), damit bestehender Code funktioniert. Zusätzliche moderne Tasten:
@@ -282,4 +300,8 @@ Dialoge:
„Invalid when forms are showing") — wir übernehmen nur den „Invalid when forms are showing") — wir übernehmen nur den
Forms-/Textmodus-Zweig. Forms-/Textmodus-Zweig.
6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73). 6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73).
7. **`WIDTH 40`** wird nicht unterstützt (nur 80×25). 7. **Dynamische Bildschirmgröße** (2026-09-02): kein festes 80×25 — der
Bildschirm folgt der Terminalgröße (Mindestgröße 80×25, darunter nur
ein Hinweis). `CSRLIN`/`POS`/`LOCATE`/`VIEW PRINT` und das
`SCREEN`-Objekt arbeiten auf der tatsächlichen Größe. `WIDTH 40/80`
wird akzeptiert, ändert aber nichts (die Größe bestimmt das Terminal).

View File

@@ -1,7 +1,7 @@
# TBVM — Designentwurf # TBVM — Designentwurf
Eigene Stack-basierte Bytecode-VM (Entscheidung siehe PLAN.md, 0.2). Eigene Stack-basierte Bytecode-VM (Entscheidung siehe PLAN.md, 0.2).
Die VM ist in `tbdos` (IDE) und in von `tbdosc` erzeugte Executables Die VM ist in `tb` (IDE) und in von `tbc` erzeugte Executables
eingebettet — identischer Code, identisches Verhalten, schneller Turnaround. eingebettet — identischer Code, identisches Verhalten, schneller Turnaround.
## Werte-Modell ## Werte-Modell
@@ -72,7 +72,7 @@ Format bleibt stabil und dokumentiert).
## Eigenständige Executables ## Eigenständige Executables
`tbdosc build --exe` kopiert den vorkompilierten Runner (dieselbe `tbc build --exe` kopiert den vorkompilierten Runner (dieselbe
tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als 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 Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim

View File

@@ -9,7 +9,7 @@ Editoren dürfen sie nicht wegtrimmen; `.gitattributes` schützt die Dateien
vor Zeilenenden-Konvertierung. vor Zeilenenden-Konvertierung.
Ab Phase 2 führt ein Test-Harness (`cargo test`) jede Datei per Ab Phase 2 führt ein Test-Harness (`cargo test`) jede Datei per
`tbdosc run` aus und vergleicht die Ausgabe. Bis dahin dienen die `tbc run` aus und vergleicht die Ausgabe. Bis dahin dienen die
`.out`-Dateien als festgehaltene Verhaltensspezifikation — wo möglich am `.out`-Dateien als festgehaltene Verhaltensspezifikation — wo möglich am
Vorbild (Emulator) verifiziert, sonst nach Referenzlage rekonstruiert Vorbild (Emulator) verifiziert, sonst nach Referenzlage rekonstruiert
und mit `TODO verify` im Programmkopf markiert. und mit `TODO verify` im Programmkopf markiert.