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:
21
LICENSE
Normal file
21
LICENSE
Normal 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
122
PLAN.md
@@ -22,6 +22,20 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit
|
||||
Formatierung), nicht das, was „richtiger" wäre. Wo das Vorbild aber mit der
|
||||
heutigen Plattform kollidiert (Codepages, DOS-Hardware, Segmente), gewinnt
|
||||
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
|
||||
nicht verwendet. Wir sprechen vom „Vorbild" bzw. „dem Dialekt".
|
||||
- **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
|
||||
(Details in docs/tbvm-design.md, Abschnitt „Performance"); Messlatte via
|
||||
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`) —
|
||||
damit bleibt auch der Weg zum verteilbaren Binary im Sekundenbereich.
|
||||
- **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)
|
||||
- [x] Testkorpus-Grundstock gelegt (`tests/compat/` mit dokumentierter
|
||||
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)
|
||||
|
||||
@@ -101,7 +115,7 @@ BASIC-Semantik, serialisierbares Kompilat, gute Fehlerortung (Zeile/Spalte).
|
||||
|
||||
**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
|
||||
die Executables** (`tb`-IDE und von `tbc` 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
|
||||
@@ -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
|
||||
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;
|
||||
kleinere Terminals: Hinweis „Terminal zu klein", größere: zentriert
|
||||
(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.)
|
||||
|
||||
## Phase 1 — Sprach-Frontend (`tb-frontend`)
|
||||
- [ ] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
|
||||
case-insensitive Keywords, Zeilenfortsetzung mit `_`
|
||||
- [ ] Offene Detailfrage aus der Sprachreferenz klären: exakte
|
||||
SINGLE/DOUBLE-Schwelle suffixloser Dezimalpunkt-Literale
|
||||
(Quelle widersprüchlich → per Testkorpus festlegen und in
|
||||
docs/sprachreferenz.md dokumentieren)
|
||||
- [ ] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen
|
||||
- [ ] Parser (zeilenorientiert, fehlertolerant — die IDE prüft pro Zeile)
|
||||
- [ ] Semantik: Symboltabellen, implizite Deklaration, `DEFINT`-Regeln,
|
||||
Typprüfung und -konvertierung, `OPTION EXPLICIT`/`OPTION BASE`,
|
||||
Array-Dimensionierung (statisch/dynamisch, `REDIM`)
|
||||
- [ ] Diagnostik mit exakten Positionen und den Meldungstexten des Vorbilds
|
||||
- [ ] Meilenstein: kompletter Testkorpus parst und wird typgeprüft
|
||||
- [x] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
|
||||
case-insensitive Keywords, Zeilenfortsetzung mit `_`, Hex-/Oktal-
|
||||
Literale, Literal-Typisierung
|
||||
- [x] SINGLE/DOUBLE-Schwelle suffixloser Dezimalpunkt-Literale entschieden
|
||||
(> 7 signifikante Stellen → DOUBLE) und in docs/sprachreferenz.md §1
|
||||
dokumentiert
|
||||
- [x] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen
|
||||
- [~] Parser (zeilenorientiert, fehlertolerant — Fehler pro Anweisung
|
||||
gesammelt, Synchronisation bis Anweisungsende). Kern komplett:
|
||||
Zuweisung, PRINT (inkl. USING/#), INPUT/LINE INPUT, IF (Block +
|
||||
einzeilig), SELECT CASE, FOR/DO/WHILE, GOTO/GOSUB/ON-GOTO,
|
||||
ON [LOCAL] ERROR/RESUME, DIM/REDIM/CONST/DEFtype/OPTION/TYPE/
|
||||
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`)
|
||||
- [ ] 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
|
||||
< 1 s, einzelnes Modul < 50 ms) und VM-Durchsatz (Schleifen/Strings);
|
||||
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`)
|
||||
|
||||
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`,
|
||||
`SPACE$`, `STRING$`, `LTRIM$`/`RTRIM$`, `UCASE$`/`LCASE$` …
|
||||
- [ ] Zahlenformatierung: `PRINT`-Zonen, `PRINT USING` (vollständig;
|
||||
@@ -175,7 +216,16 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
|
||||
Funktionen, Integer-Überlaufverhalten (Fehler 6)
|
||||
- [ ] Datei-E/A: `OPEN` (sequenziell/random/binär), `INPUT#`/`LINE INPUT#`,
|
||||
`PRINT#`/`WRITE#`, `GET`/`PUT` mit Record-Typen, `EOF`/`LOF`/`SEEK`,
|
||||
Pfadsemantik plattformübergreifend
|
||||
Pfadsemantik plattformübergreifend. Record-Layout: feste Strings als
|
||||
**UTF-32** (Entscheidung 2026-09-02 — 4 Bytes/Zeichen, feste
|
||||
Record-Länge; bewusst inkompatibel zu Vorbild-Dateien)
|
||||
- [ ] ISAM-Dateiunterstützung (Entscheidung 2026-09-02: wird implementiert,
|
||||
nicht Non-Feature): Anweisungen/Funktionen der Professional Edition
|
||||
(`OPEN … FOR ISAM`, Tabellen/Indizes, `SEEKGT`-Familie …) — Umfang
|
||||
aus der Original-Hilfe inventarisieren, dann implementieren
|
||||
- [ ] Breite Unicode-Zeichen (Emoji, CJK): belegen **zwei Zellen**
|
||||
(Entscheidung 2026-09-02) — Zellenmodell und `LOCATE`/`POS`-Semantik
|
||||
entsprechend umsetzen (unicode-width), Verhalten dokumentieren
|
||||
- [ ] Bildschirm: `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT`, `INKEY$`,
|
||||
`CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer
|
||||
- [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros
|
||||
@@ -221,7 +271,7 @@ in der Sprachreferenz unter „Abweichungen" dokumentiert.
|
||||
aus Phase 0 übernommen) und Dokumentation bekannter
|
||||
Terminal-Einschränkungen
|
||||
- [ ] 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)
|
||||
- [ ] Dokumentation: Sprachreferenz, Migrationshinweise, Beispielprogramme
|
||||
- [ ] 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
|
||||
- Standardbibliothek: Prozessaufrufe mit Pipes, Umgebungs-/Argument-Handling
|
||||
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
|
||||
Vorbild ihn überhaupt in allen Kontexten unterstützt
|
||||
- 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?
|
||||
- Unicode-Zeichen mit Darstellungsbreite ≠ 1 (Emoji, CJK): das Zellenmodell
|
||||
ist strikt 1 Zeichen = 1 Zelle. Breite Zeichen ablehnen (Fehler 5),
|
||||
ersetzen oder 2 Zellen belegen?
|
||||
- `GET`/`PUT` mit Record-Typen unter UTF-8: feste Strings (`STRING * n`)
|
||||
waren im Vorbild n Bytes — bei uns n Zeichen. Binärlayout der Records
|
||||
festlegen (Vorschlag: UTF-8 mit Padding auf feste Bytelänge? Oder UTF-32?)
|
||||
- Blink-Attribut (`COLOR` mit Vordergrund 16–31): ignorieren, als „hell"
|
||||
darstellen oder echtes Terminal-Blink nutzen (nicht überall unterstützt)?
|
||||
- Lizenzwahl (MIT im Rahmen angenommen — bestätigen)
|
||||
- **CURRENCY (`@`):** voll unterstützt als i64-Festkomma (×10 000); das
|
||||
Vorbild unterstützt den Typ laut Original-Hilfe vollständig.
|
||||
- **ISAM:** wird implementiert (kein Non-Feature) → Aufgabe in Phase 3.
|
||||
- **`PEEK`/`POKE`/`CALL INTERRUPT` u. ä. Hardware-Nähe:** nicht unterstützt;
|
||||
Ablehnung bereits **zur Compile-Zeit** (Meldung „Feature unavailable") —
|
||||
in der Semantik umgesetzt, dokumentiert in der Sprachreferenz.
|
||||
- **Breite Unicode-Zeichen (Emoji, CJK):** belegen **zwei Zellen** →
|
||||
Umsetzung in Phase 3.
|
||||
- **`GET`/`PUT`-Records unter Unicode:** feste Strings als **UTF-32**
|
||||
(4 Bytes/Zeichen, feste Record-Länge); Inkompatibilität der Binärdateien
|
||||
zum Vorbild wird bewusst akzeptiert → Phase 3.
|
||||
- **Blink-Attribut (`COLOR` 16–31):** kein echtes Blinken, Simulation als
|
||||
„hell" — in `tb-ui::screen` umgesetzt.
|
||||
- **Lizenz:** MIT (LICENSE im Repo).
|
||||
|
||||
Neue offene Fragen werden hier gesammelt und mit Datum entschieden.
|
||||
|
||||
19
README.md
19
README.md
@@ -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**:
|
||||
die ereignisgesteuerte Forms-Engine mit allen Steuerelementen, Menüs,
|
||||
Maus- und Tastaturereignissen, nachgebildet auf einem emulierten
|
||||
80×25-Textbildschirm (16 Farben) über Ratatui. Bewusste Modernisierung:
|
||||
durchgängig UTF-8/Unicode statt Codepage 437.
|
||||
Textbildschirm (16 Farben) über Ratatui. Bewusste Modernisierungen:
|
||||
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.
|
||||
- **Compiler + VM**: der Compiler erzeugt Bytecode für eine eigene virtuelle
|
||||
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-runtime` | Laufzeitbibliothek: Strings, Mathematik, Datei-E/A, Datum/Zeit, Fehlercodes |
|
||||
| `tb-ui` | Textbildschirm-Emulation und Forms-Engine auf Ratatui |
|
||||
| `tb-cli` | `tbdosc`: Standalone-Compiler — wandelt Quellen und Projekte in binäre Ergebnisse um |
|
||||
| `tb-ide` | `tbdos`: die integrierte Entwicklungsumgebung (TUI) zum Erstellen von Programmen |
|
||||
| `tb-cli` | `tbc`: Standalone-Compiler — wandelt Quellen und Projekte in binäre Ergebnisse um |
|
||||
| `tb-ide` | `tb`: die integrierte Entwicklungsumgebung (TUI) zum Erstellen von Programmen |
|
||||
|
||||
## Status
|
||||
|
||||
@@ -62,8 +65,12 @@ Projektrahmen. Die Implementierungs- und Explorationsschritte sind in
|
||||
|
||||
```
|
||||
cargo build
|
||||
cargo run -p tb-cli # tbdosc — Standalone-Compiler
|
||||
cargo run -p tb-ide # tbdos — IDE (TUI)
|
||||
cargo run -p tb-cli # tbc — Standalone-Compiler
|
||||
cargo run -p tb-ide # tb — IDE (TUI)
|
||||
```
|
||||
|
||||
Benötigt wird nur eine aktuelle stabile Rust-Toolchain.
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT — siehe [LICENSE](LICENSE).
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "tb-cli"
|
||||
description = "Terminal Basic: Standalone-Kommandozeilen-Compiler (tbdosc)"
|
||||
description = "Terminal Basic: Standalone-Kommandozeilen-Compiler (tbc)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "tbdosc"
|
||||
name = "tbc"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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
|
||||
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle
|
||||
//! (siehe PLAN.md):
|
||||
//! - `tbdosc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
|
||||
//! - `tbdosc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbdosc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("tbdosc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
|
||||
println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,247 @@
|
||||
//! AST-Definitionen: Module, Prozeduren (`SUB`/`FUNCTION`), Anweisungen, Ausdrücke,
|
||||
//! benutzerdefinierte Typen (`TYPE … END TYPE`), Deklarationen.
|
||||
//! AST-Definitionen: Module, Prozeduren, Anweisungen, Ausdrücke,
|
||||
//! 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>,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,557 @@
|
||||
//! Lexer: zerlegt Quelltext in Tokens.
|
||||
//!
|
||||
//! Besonderheiten des BASIC-Dialekts, die hier abgebildet werden müssen:
|
||||
//! - Typ-Suffixe an Bezeichnern und Literalen (`%`, `&`, `!`, `#`, `$`, `@`)
|
||||
//! - Zeilennummern und Labels
|
||||
//! - Keywords sind case-insensitiv, der Editor normalisiert später auf Großschreibung
|
||||
//! - Fortsetzung/Trennung von Anweisungen mit `:` und Kommentare mit `'` und `REM`
|
||||
//! Besonderheiten des Dialekts:
|
||||
//! - Typ-Suffixe an Bezeichnern und Literalen (`% & ! # $ @`)
|
||||
//! - Zeilennummern und Labels (löst der Parser auf)
|
||||
//! - Keywords case-insensitiv; nur echte Sprach-Keywords sind reserviert,
|
||||
//! 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)]
|
||||
pub enum Token {
|
||||
// Platzhalter — wird in Phase 1 ausgearbeitet (siehe PLAN.md)
|
||||
pub enum TokenKind {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,48 @@
|
||||
//!
|
||||
//! Enthält Lexer, Parser, AST-Definitionen und die semantische Analyse
|
||||
//! (Symboltabellen, Typprüfung, implizite Deklarationen, `DEFINT`-Regeln usw.).
|
||||
//! Ausgabe des Frontends ist ein typgeprüfter AST bzw. eine Zwischen-
|
||||
//! repräsentation (IR), die von `tb-vm` in Bytecode übersetzt wird.
|
||||
//! Ausgabe des Frontends ist ein typgeprüfter AST, den `tb-vm` in Bytecode
|
||||
//! übersetzt.
|
||||
|
||||
pub mod ast;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod ast;
|
||||
pub mod sema;
|
||||
|
||||
/// Quelltextposition für Diagnostik (1-basiert, wie im klassischen IDE-Vorbild).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct SourcePos {
|
||||
pub line: 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
1084
crates/tb-frontend/src/sema.rs
Normal file
1084
crates/tb-frontend/src/sema.rs
Normal file
File diff suppressed because it is too large
Load Diff
27
crates/tb-frontend/tests/corpus.rs
Normal file
27
crates/tb-frontend/tests/corpus.rs
Normal 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}");
|
||||
}
|
||||
@@ -7,7 +7,7 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "tbdos"
|
||||
name = "tb"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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
|
||||
//! Syntaxprüfung pro Zeile, Formular-Designer, Direktfenster,
|
||||
@@ -6,6 +6,6 @@
|
||||
//! Siehe PLAN.md, Phase 5.
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("tbdos — Terminal Basic IDE (Projektrahmen, noch ohne Funktion)");
|
||||
println!("tb — Terminal Basic IDE (Projektrahmen, noch ohne Funktion)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -43,9 +43,10 @@ fn testbild(s: &mut TextScreen) {
|
||||
|
||||
fn status(s: &mut TextScreen, text: &str) {
|
||||
let (r, c) = (s.csrlin(), s.pos());
|
||||
let (row, width) = (s.rows(), s.cols());
|
||||
s.set_color(0, 7);
|
||||
s.locate(25, 1).unwrap();
|
||||
s.print(&format!("{text:<80.80}"));
|
||||
s.locate(row, 1).unwrap();
|
||||
s.print(&format!("{text:<width$.width$}"));
|
||||
s.set_color(7, 0);
|
||||
s.locate(r, c).unwrap();
|
||||
}
|
||||
@@ -54,7 +55,8 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut terminal = ratatui::init();
|
||||
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);
|
||||
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),
|
||||
);
|
||||
}
|
||||
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}"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Textbildschirm-Emulation: 80×25-Zellenpuffer (Unicode-Zeichen + Farb-
|
||||
//! attribut), Cursor, Scrollen — gerendert als Ratatui-Widget.
|
||||
//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
|
||||
//! 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
|
||||
//! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md).
|
||||
|
||||
@@ -11,8 +15,9 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
pub const COLS: usize = 80;
|
||||
pub const ROWS: usize = 25;
|
||||
/// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
|
||||
pub const MIN_COLS: usize = 80;
|
||||
pub const MIN_ROWS: usize = 25;
|
||||
|
||||
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -37,12 +42,14 @@ pub fn basic_color(n: u8) -> Color {
|
||||
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 1–25,
|
||||
/// Spalte 1–80), wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
|
||||
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
|
||||
/// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextScreen {
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
cells: Vec<Cell>,
|
||||
/// Cursorposition, 0-basiert intern.
|
||||
cur_row: usize,
|
||||
@@ -54,6 +61,9 @@ pub struct TextScreen {
|
||||
/// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
|
||||
view_top: usize,
|
||||
view_bottom: usize,
|
||||
/// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
|
||||
/// Scrollbereich folgt dann der Bildschirmgröße.
|
||||
view_full: bool,
|
||||
}
|
||||
|
||||
impl Default for TextScreen {
|
||||
@@ -63,16 +73,63 @@ impl Default for TextScreen {
|
||||
}
|
||||
|
||||
impl TextScreen {
|
||||
/// Bildschirm in Mindestgröße (80×25).
|
||||
pub fn new() -> Self {
|
||||
Self::with_size(MIN_COLS, MIN_ROWS)
|
||||
}
|
||||
|
||||
/// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
|
||||
pub fn with_size(cols: usize, rows: usize) -> Self {
|
||||
let cols = cols.max(MIN_COLS);
|
||||
let rows = rows.max(MIN_ROWS);
|
||||
TextScreen {
|
||||
cells: vec![Cell::default(); COLS * ROWS],
|
||||
cols,
|
||||
rows,
|
||||
cells: vec![Cell::default(); cols * rows],
|
||||
cur_row: 0,
|
||||
cur_col: 0,
|
||||
cursor_visible: true,
|
||||
fg: 7,
|
||||
bg: 0,
|
||||
view_top: 0,
|
||||
view_bottom: ROWS - 1,
|
||||
view_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) {
|
||||
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.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
|
||||
}
|
||||
self.cur_row = self.view_top;
|
||||
self.cur_col = 0;
|
||||
}
|
||||
|
||||
/// `COLOR vg, hg` — Werte werden wie im Vorbild maskiert
|
||||
/// (vg 0–31, wir ignorieren Blink → 0–15; hg 0–7).
|
||||
/// `COLOR vg, hg` (vg 0–31, hg 0–7). Blinkende Vordergrundfarben
|
||||
/// (16–31) werden als „hell" simuliert (Entscheidung 2026-09-02):
|
||||
/// echtes Terminal-Blinken ist nicht überall verfügbar.
|
||||
pub fn set_color(&mut self, fg: u8, bg: u8) {
|
||||
let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
|
||||
self.fg = fg & 0x0F;
|
||||
self.bg = bg & 0x07;
|
||||
}
|
||||
|
||||
/// `LOCATE zeile, spalte` (1-basiert). Außerhalb des Bildschirms:
|
||||
/// Fehler 5 beim Aufrufer — hier wird geklemmt geprüft.
|
||||
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
|
||||
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(());
|
||||
}
|
||||
self.cur_row = row - 1;
|
||||
@@ -117,32 +175,32 @@ impl TextScreen {
|
||||
|
||||
/// `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 {
|
||||
if top < 1 || bottom > self.rows || top > bottom {
|
||||
return Err(());
|
||||
}
|
||||
self.view_top = top - 1;
|
||||
self.view_bottom = bottom - 1;
|
||||
self.view_full = top == 1 && bottom == self.rows;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cell(&self, row: usize, col: usize) -> Cell {
|
||||
self.cells[(row - 1) * COLS + (col - 1)]
|
||||
self.cells[(row - 1) * self.cols + (col - 1)]
|
||||
}
|
||||
|
||||
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
|
||||
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
|
||||
/// `\r` setzt an den Zeilenanfang; andere Steuerzeichen werden
|
||||
/// (noch) als normale Zeichen behandelt.
|
||||
/// `\r` setzt an den Zeilenanfang.
|
||||
pub fn print(&mut self, text: &str) {
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'\n' => self.newline(),
|
||||
'\r' => self.cur_col = 0,
|
||||
_ => {
|
||||
self.cells[self.cur_row * COLS + self.cur_col] =
|
||||
self.cells[self.cur_row * self.cols + self.cur_col] =
|
||||
Cell { ch, fg: self.fg, bg: self.bg };
|
||||
self.cur_col += 1;
|
||||
if self.cur_col >= COLS {
|
||||
if self.cur_col >= self.cols {
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
@@ -165,34 +223,34 @@ impl TextScreen {
|
||||
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]);
|
||||
let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
|
||||
a[row * self.cols..].copy_from_slice(&b[..self.cols]);
|
||||
}
|
||||
self.cells[self.view_bottom * 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.
|
||||
/// Ist das Terminal kleiner als 80×25, wird ein Hinweis angezeigt.
|
||||
/// Rendert den Bildschirm oben links in die verfügbare Fläche. Ist das
|
||||
/// Terminal kleiner als die Mindestgröße, erscheint nur ein Hinweis
|
||||
/// (btop-artig); die Anwendung ruft bei Resize `TextScreen::resize` auf,
|
||||
/// damit Puffer und Terminal deckungsgleich bleiben.
|
||||
impl Widget for &TextScreen {
|
||||
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!(
|
||||
"Terminal zu klein: {}x{} — benötigt {}x{}",
|
||||
area.width, area.height, COLS, ROWS
|
||||
"Terminal zu klein: {}x{} — Minimum {}x{}",
|
||||
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));
|
||||
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 cols = self.cols.min(area.width as usize);
|
||||
let rows = self.rows.min(area.height as usize);
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let c = self.cells[row * self.cols + col];
|
||||
if let Some(cell) = buf.cell_mut((area.x + col as u16, area.y + row as u16)) {
|
||||
let mut s = String::new();
|
||||
s.push(c.ch);
|
||||
cell.set_symbol(&s);
|
||||
@@ -204,10 +262,12 @@ impl Widget for &TextScreen {
|
||||
}
|
||||
// Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in
|
||||
// der Forms-/Runtime-Schicht später gezielt gesteuert).
|
||||
if self.cursor_visible {
|
||||
let (cx, cy) = (x0 + self.cur_col as u16, y0 + self.cur_row as u16);
|
||||
if self.cursor_visible && self.cur_row < rows && self.cur_col < cols {
|
||||
let (cx, cy) = (area.x + self.cur_col as u16, area.y + self.cur_row as u16);
|
||||
if 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();
|
||||
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(24, 1).ch, 'u');
|
||||
assert_eq!(s.cell(25, 1).ch, ' ');
|
||||
assert_eq!(s.csrlin(), 25);
|
||||
}
|
||||
@@ -261,7 +321,7 @@ mod tests {
|
||||
s.view_print(3, 5).unwrap();
|
||||
s.locate(5, 1).unwrap();
|
||||
s.print("a\nb"); // scrollt nur Zeilen 3–5
|
||||
assert_eq!(s.cell(1, 1).ch, 'k'); // Kopfzeile unberührt
|
||||
assert_eq!(s.cell(1, 1).ch, 'k');
|
||||
assert_eq!(s.cell(4, 1).ch, 'a');
|
||||
assert_eq!(s.cell(5, 1).ch, 'b');
|
||||
}
|
||||
@@ -274,6 +334,47 @@ mod tests {
|
||||
assert!(s.locate(25, 80).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamische_groesse_und_resize() {
|
||||
let mut s = TextScreen::with_size(120, 40);
|
||||
assert_eq!((s.cols(), s.rows()), (120, 40));
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("!");
|
||||
assert_eq!(s.cell(40, 1).ch, '!');
|
||||
|
||||
// Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
|
||||
s.locate(1, 1).unwrap();
|
||||
s.print("K");
|
||||
s.resize(100, 30);
|
||||
assert_eq!((s.cols(), s.rows()), (100, 30));
|
||||
assert_eq!(s.cell(1, 1).ch, 'K');
|
||||
|
||||
// Unter Minimum wird auf 80×25 geklemmt
|
||||
s.resize(10, 5);
|
||||
assert_eq!((s.cols(), s.rows()), (80, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blink_wird_als_hell_simuliert() {
|
||||
let mut s = TextScreen::new();
|
||||
s.set_color(17, 0); // blinkend Blau → helles Blau
|
||||
assert_eq!(s.fg, 9);
|
||||
s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
|
||||
assert_eq!(s.fg, 15);
|
||||
s.set_color(7, 0);
|
||||
assert_eq!(s.fg, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_folgt_vollem_scrollbereich() {
|
||||
let mut s = TextScreen::new();
|
||||
s.resize(90, 40);
|
||||
s.locate(40, 1).unwrap();
|
||||
s.print("a\nb"); // Scroll am neuen unteren Rand
|
||||
assert_eq!(s.cell(39, 1).ch, 'a');
|
||||
assert_eq!(s.cell(40, 1).ch, 'b');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn farbabbildung() {
|
||||
assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau
|
||||
|
||||
@@ -61,8 +61,16 @@ Reine Textdatei ohne Kopfzeile, **eine Projektdatei pro Zeile**
|
||||
(`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic
|
||||
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)
|
||||
|
||||
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`).
|
||||
|
||||
@@ -139,8 +139,9 @@ DragOver-State 0 Enter · 1 Leave · 2 Over. Bei Steuerelement-Arrays steht
|
||||
## SCREEN-Objekt
|
||||
|
||||
Eigenschaften: ActiveControl, ActiveForm, AutoRedraw, ControlPanel,
|
||||
Height, Width (Zeichen, read-only), MousePointer. Methoden: HIDE, SHOW
|
||||
(alle sichtbaren Formulare ein-/ausblenden). `SCREEN.ControlPanel(0–17)`
|
||||
Height, Width (Zeichen, read-only — bei uns die **aktuelle Terminalgröße**,
|
||||
Abweichung: das Vorbild lieferte fest 25/43/50 × 80), MousePointer.
|
||||
Methoden: HIDE, SHOW (alle sichtbaren Formulare ein-/ausblenden). `SCREEN.ControlPanel(0–17)`
|
||||
konfiguriert Systemfarben/-effekte (AccessKey-/Desktop-/Menü-/Titel-
|
||||
Farben, Schatten, 3D-Effekte, Desktop-Füllzeichen) — Konstanten in der
|
||||
Konstanten-Include-Datei.
|
||||
|
||||
@@ -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).
|
||||
Nur die Umgebung hat: Immediate Window, Watches/Watchpoints, Breakpoints,
|
||||
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
|
||||
Vorbild — **keine** Verhaltensunterschiede zwischen IDE-Lauf und
|
||||
kompiliertem Programm gibt (dokumentierte Abweichung, gewollt).
|
||||
@@ -162,6 +162,9 @@ kompiliertem Programm gibt (dokumentierte Abweichung, gewollt).
|
||||
F12/Shift+F12-Wechsel bleibt erhalten.
|
||||
3. Zeilenweises Syntax-Checking mit Formatierung/Normalisierung ist
|
||||
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
|
||||
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).
|
||||
|
||||
@@ -11,7 +11,11 @@ Details, `ener.hlp` Fehlermeldungen; Vergleich PDS 7.1: `bas7qck.hlp`)
|
||||
sowie das Original-README der Professional Edition. Aussagen ohne
|
||||
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&`,
|
||||
`1.5!`, `1.5#`, `2.5@`). Ohne Suffix gilt: Ganzzahl im INTEGER-Bereich →
|
||||
INTEGER; Hex/Oktal ohne `&`-Suffix → 16 Bit (INTEGER), mit `&` → LONG;
|
||||
`E`-Exponent → SINGLE, `D`-Exponent → DOUBLE; Dezimalpunkt-Wert →
|
||||
SINGLE, bei mehr als 15 Stellen DOUBLE (TODO: exakte Schwelle — Quelle
|
||||
an der Stelle widersprüchlich); Ganzzahl außerhalb des LONG-Bereichs
|
||||
wird Gleitkommawert (kein Fehler).
|
||||
`E`-Exponent → SINGLE, `D`-Exponent → DOUBLE; Dezimalpunkt-Wert:
|
||||
**Entscheidung (2026-09-02):** bis 7 signifikante Stellen → SINGLE,
|
||||
darüber → DOUBLE. (Die Original-Hilfe nennt widersprüchlich eine
|
||||
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.
|
||||
|
||||
## 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:
|
||||
2-Zeichen-Sequenz `CHR$(0)+code` im Vorbild — Abweichung s. u.),
|
||||
`INPUT$(n [,#f])`.
|
||||
- `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`.
|
||||
- `COLOR [vg][,hg]` (vg 0–31: 16–31 = blinkend; hg 0–7), `CLS`,
|
||||
`WIDTH` (80/40 — wir: nur 80), `VIEW PRINT oben TO unten` (Scrollbereich).
|
||||
- `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`
|
||||
— arbeiten auf der tatsächlichen Bildschirmgröße (dynamisch, s. Abweichungen).
|
||||
- `COLOR [vg][,hg]` (vg 0–31: 16–31 = blinkend — bei uns als „hell"
|
||||
simuliert, Entscheidung 2026-09-02; hg 0–7), `CLS`,
|
||||
`WIDTH` (im Vorbild 80/40 — bei uns wirkungslos, s. Abweichungen),
|
||||
`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).
|
||||
@@ -211,6 +220,9 @@ solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant:
|
||||
`LOC(n)`, `LOF(n)`.
|
||||
- Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`,
|
||||
`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.
|
||||
|
||||
## 11. Eingebaute Funktionen (Katalog)
|
||||
@@ -267,9 +279,15 @@ Dialoge:
|
||||
- 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.
|
||||
- Zeichen mit Darstellungsbreite 2 (Emoji, CJK) **belegen zwei Zellen**
|
||||
(Entscheidung 2026-09-02; Umsetzung Phase 3).
|
||||
- `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
|
||||
`CHR$(0) + code`-Sequenzen mit den klassischen Scancodes (F1 = `CHR$(0)+";"`
|
||||
usw.), damit bestehender Code funktioniert. Zusätzliche moderne Tasten:
|
||||
@@ -282,4 +300,8 @@ Dialoge:
|
||||
„Invalid when forms are showing") — wir übernehmen nur den
|
||||
Forms-/Textmodus-Zweig.
|
||||
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).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
Die VM ist in `tb` (IDE) und in von `tbc` erzeugte Executables
|
||||
eingebettet — identischer Code, identisches Verhalten, schneller Turnaround.
|
||||
|
||||
## Werte-Modell
|
||||
@@ -72,7 +72,7 @@ Format bleibt stabil und dokumentiert).
|
||||
|
||||
## 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
|
||||
Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
|
||||
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim
|
||||
|
||||
@@ -9,7 +9,7 @@ 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
|
||||
`tbc 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.
|
||||
|
||||
Reference in New Issue
Block a user