Phase 1 vollstaendig abgeschlossen; neue Vorgaben verankert

Frontend-Vervollstaendigung (keine offenen Punkte mehr in Phase 1):
- MID$-Anweisung, DEF-FN-Blockform, COMMON/SHARED/STATIC,
  LPRINT, VIEW PRINT, NAME AS, LOCK/UNLOCK, Ereignissteuerung
  (TIMER/KEY(n)/UEVENT ON|OFF|STOP), Metabefehle $INCLUDE/$STATIC/$DYNAMIC
- Komplette Datei-E/A-Grammatik: OPEN (beide Syntaxen, inkl. ISAM- und
  ACCESS/LOCK-Klauseln), CLOSE, FIELD, GET/PUT, LSET/RSET, WRITE, SEEK
- Semantik: UDT-Feldtypen, SHARED-Import von Modulvariablen,
  Konstantenfaltung (Invalid constant), OPTION BASE erfasst
- Non-Features konsistent zur Compile-Zeit abgewiesen: Hardware-Naehe,
  CHAIN, Grafik-Anweisungen, SOUND/PLAY ("Feature unavailable")
- 33 Frontend-Tests, Korpus-Meilenstein weiter gruen

Neue Vorgaben:
- Stufe-2-Ideen: SQLite/Embedded-SQL mit Record-Buffern; Runtime-
  Bibliotheken (z.B. crossterm) als BASIC-Bibliotheken
- Erster Kompatibilitaetstest: github.com/cout/vbdos (extern, wird nicht
  einvendort); binaer-.FRM-Konverter als Phase-4-Aufgabe (tbc convert-frm)
- Doku als Definition of Done: docs/ = Ist-Dokumentation, IDE-Hilfe
  rendert Markdown dynamisch zur Fenstergroesse (Phase-5-Aufgabe)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 09:30:44 +02:00
parent 333e794540
commit d77574d9de
6 changed files with 1059 additions and 165 deletions

90
PLAN.md
View File

@@ -55,7 +55,24 @@ Konvention: `[ ]` offen · `[x]` erledigt · `[~]` in Arbeit
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
Kompatibilitäts-Testsuite (Verzeichnis `tests/compat`, geplant).
Kompatibilitäts-Testsuite (Verzeichnis `tests/compat`).
- **Erster Kompatibilitätstest — externes Programmkorpus** (2026-09-02):
Die Programme aus https://github.com/cout/vbdos (insb. der Ordner
`microsoft/`) müssen — soweit sie keine deklarierten Non-Features
(PEEK/POKE/CALL INTERRUPT …) nutzen — **erfolgreich kompilieren und
nutzbar sein**. Das Repo wird nicht einvendort (Lizenzlage), sondern vom
Test-Harness bei Bedarf geklont. Aufgaben: Frontend-Check ab Phase 1,
Lauffähigkeit Konsole in Phase 3, Forms in Phase 4 (Meilenstein dort);
binäre `.FRM`-Dateien erfordern ein Konvertierungstool → Phase 4.
- **Dokumentation ist Teil der Definition of Done** (2026-09-02): Im
`docs/`-Ordner entsteht die vollständige Dokumentation der Sprache und
der Bibliothek **so wie implementiert** (Ist-Stand): jede Phase
schreibt docs/sprachreferenz.md fort und pflegt eine
Bibliotheksreferenz (`docs/bibliothek.md`, entsteht mit Phase 3) mit
jeder implementierten Anweisung/Funktion. Diese Markdown-Dokumente sind
zugleich der Inhalt des IDE-Hilfesystems (Phase 5): Anzeige unter
„Help" mit Konsolen-Markdown-Rendering, Umbruch dynamisch nach
aktueller Fenstergröße.
---
@@ -154,31 +171,45 @@ tatsächlichen Größe; `tb-ui::screen` unterstützt `resize()`.
VM-Ticks — ist Eingangsaufgabe von Phase 4.)
## Phase 1 — Sprach-Frontend (`tb-frontend`)
**Status: abgeschlossen (2026-09-02).**
- [x] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
case-insensitive Keywords, Zeilenfortsetzung mit `_`, Hex-/Oktal-
Literale, Literal-Typisierung
Literale, Literal-Typisierung, Metabefehle (`$INCLUDE`, `$STATIC`,
`$DYNAMIC`)
- [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] Parser (zeilenorientiert, fehlertolerant — Fehler pro Anweisung
gesammelt, Synchronisation bis Anweisungsende): Zuweisung inkl.
MID$-Anweisung, PRINT/LPRINT (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, COMMON/SHARED/STATIC,
DATA/READ/RESTORE, DEF FN (einzeilig + Blockform), VIEW PRINT,
NAME…AS, Ereignissteuerung (`TIMER/KEY(n)/… ON|OFF|STOP`), komplette
Datei-E/A-Grammatik (OPEN in beiden Syntaxen inkl. ISAM-Klausel,
CLOSE, FIELD, GET/PUT, LSET/RSET, WRITE, SEEK, LOCK/UNLOCK)
- [x] Semantik: Symboltabellen, implizite Deklaration, `DEFtype`-Regeln,
Typprüfung, `OPTION EXPLICIT`/`BASE`, Arrays (implizit/DIM/REDIM),
UDT-Feldtypen (`kunde.name`), `SHARED`-Import von Modulvariablen,
`COMMON`, Konstantenfaltung (`CONST`, „Invalid constant"),
Builtin-Signaturen, Label-Prüfung, Prozedur-Signaturprüfung;
deklarierte Non-Features (Hardware-Nähe, CHAIN, Grafik, SOUND/PLAY)
werden zur Compile-Zeit mit „Feature unavailable" abgewiesen
- [x] Diagnostik mit exakten Positionen; Meldungstexte folgen dem Katalog
des Vorbilds (Type mismatch, Duplicate definition, Label not
defined, Feature unavailable, Invalid constant …). Neue Meldungen
entstehen mit dem jeweiligen Feature (Teil der Definition of Done)
- [x] Meilenstein: kompletter Testkorpus parst und wird typgeprüft
(`crates/tb-frontend/tests/corpus.rs`)
(`crates/tb-frontend/tests/corpus.rs`; 33 Frontend-Tests)
Vollständigkeits-Hinweis: Die Sprachabdeckung wird in Phase 3 per
Inventar systematisch gegen die Original-Hilfe abgeglichen; dort
gefundene Frontend-Lücken sind Bugs im Sinne des Guiding Principle und
werden sofort geschlossen (kein Wiedereröffnen der Phase nötig).
## Phase 2 — Bytecode und VM (`tb-vm`)
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
@@ -247,6 +278,13 @@ Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
- [ ] `.FRM`-Textformat: Serialisierung **definieren** (kein Original-
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
dokumentieren, dann lesen/schreiben implementieren
- [ ] Konvertierungstool binäre `.FRM` → unsere Text-Serialisierung
(Gegenstück zu FT.EXE des Vorbilds; Magic `FC 08 01 00`): als
`tbc convert-frm`. Format per Reverse Engineering aus den
Beispieldateien des Originalpakets und des cout/vbdos-Repos
- [ ] Meilenstein (erster Kompatibilitätstest): die Programme aus
https://github.com/cout/vbdos ohne Non-Features kompilieren und
sind nutzbar (Konsolenprogramme bereits ab Phase 3)
- [ ] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`)
- [ ] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
@@ -261,6 +299,11 @@ Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,
Überwachungsausdrücke
- [ ] Hilfe-System: rendert die Markdown-Dokumentation aus `docs/`
(Sprach- und Bibliotheksreferenz) im Help-Fenster — Konsolen-
Markdown-Rendering mit Umbruch nach aktueller Fenstergröße,
Hyperlink-Navigation wie im Vorbild (Tab/Enter/Alt+F1), F1 =
kontextsensitiver Sprung zum Thema unter dem Cursor
- [ ] Meilenstein: Programm komplett in der IDE schreiben, gestalten,
debuggen und ausführen
@@ -291,6 +334,15 @@ 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
- **SQLite-Integration mit Embedded SQL** (2026-09-02): Spracherweiterung
für eingebettetes SQL — z. B. `WHILE SELECT … / WEND`-Konstrukte —
mit Record-Buffer-Datentypen, auf denen `SELECT`/`UPDATE`/`INSERT`/
`DELETE` arbeiten (natürliche Weiterentwicklung der ISAM-Idee auf
SQLite als Speicher)
- **Runtime-Bibliotheken als BASIC-Bibliotheken** (2026-09-02): in der
Runtime integrierte Rust-Bibliotheken (z. B. crossterm für direkte
Terminalsteuerung) als eigene, importierbare Bibliotheken für
BASIC-Programme bereitstellen
- Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten
---

View File

@@ -96,7 +96,7 @@ pub struct CaseArm {
pub struct VarDecl {
pub name: String,
pub suffix: Option<Suffix>,
/// `None` = Skalar; sonst Dimensionen `(untergrenze TO obergrenze)`.
/// `None` = Skalar; leerer Vec = Array ohne Dimensionsangabe (`a()`).
pub dims: Option<Vec<(Option<Expr>, Expr)>>,
pub as_type: Option<TypeName>,
pub pos: SourcePos,
@@ -161,12 +161,47 @@ pub struct Proc {
pub pos: SourcePos,
}
/// `OPEN … FOR modus`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenMode {
Input,
Output,
Append,
Random,
Binary,
Isam,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessMode {
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockClause {
Shared,
LockRead,
LockWrite,
LockReadWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventAction {
On,
Off,
Stop,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
Label(String),
LineNumber(u32),
Assign { target: Expr, value: Expr, pos: SourcePos },
Print {
/// LPRINT (Druckerausgabe) statt PRINT.
printer: bool,
file: Option<Expr>,
using: Option<Expr>,
items: Vec<PrintItem>,
@@ -212,6 +247,17 @@ pub enum Stmt {
System,
Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).
SharedDecl { decls: Vec<VarDecl>, pos: SourcePos },
/// `STATIC`-Anweisung in einer Prozedur.
StaticDecl { decls: Vec<VarDecl>, pos: SourcePos },
/// `COMMON [SHARED] [/block/] liste`.
CommonDecl {
shared: bool,
block: Option<String>,
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 },
@@ -234,9 +280,69 @@ pub enum Stmt {
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 },
/// Blockform `DEF FNname(...) … END DEF`.
DefFnBlock {
name: String,
suffix: Option<Suffix>,
params: Vec<Param>,
body: Vec<Stmt>,
pos: SourcePos,
},
// ---- Datei-E/A (Laufzeit in Phase 3, Grammatik vollständig) ----
Open {
file: Expr,
mode: Option<OpenMode>,
/// Bei `FOR ISAM typname tabellenname`.
isam: Option<(String, String)>,
access: Option<AccessMode>,
lock: Option<LockClause>,
number: Expr,
len: Option<Expr>,
pos: SourcePos,
},
/// Alte Syntax `OPEN modus$, [#]n, datei$ [, reclen]`.
OpenLegacy {
mode: Expr,
number: Expr,
file: Expr,
len: Option<Expr>,
pos: SourcePos,
},
CloseStmt { files: Vec<Expr>, pos: SourcePos },
FieldStmt { file: Expr, fields: Vec<(Expr, Expr)>, pos: SourcePos },
GetPut {
put: bool,
file: Expr,
recnum: Option<Expr>,
var: Option<Expr>,
pos: SourcePos,
},
LsetRset { rset: bool, target: Expr, value: Expr, pos: SourcePos },
WriteStmt { file: Option<Expr>, items: Vec<Expr>, pos: SourcePos },
SeekStmt { file: Expr, position: Expr, pos: SourcePos },
LockStmt {
unlock: bool,
file: Expr,
from: Option<Expr>,
to: Option<Expr>,
pos: SourcePos,
},
NameStmt { old: Expr, new: Expr, pos: SourcePos },
// ---- Bildschirm/Ereignisse ----
/// `VIEW PRINT [oben TO unten]`.
ViewPrint { top: Option<Expr>, bottom: Option<Expr>, pos: SourcePos },
/// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
EventControl {
device: String,
index: Option<Expr>,
action: EventAction,
pos: SourcePos,
},
// ---- Metabefehle ----
/// `'$INCLUDE: 'datei''` — Auflösung übernimmt der Compile-Treiber.
Include { path: String, pos: SourcePos },
/// `'$STATIC` / `'$DYNAMIC`.
MetaArrays { static_arrays: bool, pos: SourcePos },
}
#[derive(Debug, Clone, PartialEq)]

View File

@@ -62,7 +62,7 @@ pub enum NumValue {
/// 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,
And, As, Call, Case, Common, 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,
@@ -77,7 +77,8 @@ 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,
"COMMON" => Common, "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,
@@ -113,11 +114,41 @@ pub enum TokenKind {
Plus, Minus, Star, Slash, Backslash, Caret,
Eq, Ne, Lt, Le, Gt, Ge,
LParen, RParen, Comma, Semicolon, Colon, Hash,
/// Metabefehl `'$INCLUDE: 'datei''` (Pfad; leer = fehlerhafte Syntax).
MetaInclude(String),
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
MetaStatic,
MetaDynamic,
/// Ende einer logischen Zeile.
Eol,
Eof,
}
/// Prüft einen Kommentar auf Metabefehle (`$INCLUDE`, `$STATIC`, `$DYNAMIC`).
fn meta_token(rest: &[char]) -> Option<TokenKind> {
let s: String = rest.iter().collect();
let t = s.trim_start();
let up = t.to_uppercase();
if up.starts_with("$STATIC") {
return Some(TokenKind::MetaStatic);
}
if up.starts_with("$DYNAMIC") {
return Some(TokenKind::MetaDynamic);
}
if up.starts_with("$INCLUDE") {
if let Some(colon) = t.find(':') {
let after = t[colon + 1..].trim();
if let Some(stripped) = after.strip_prefix('\'') {
if let Some(end) = stripped.find('\'') {
return Some(TokenKind::MetaInclude(stripped[..end].to_string()));
}
}
}
return Some(TokenKind::MetaInclude(String::new()));
}
None
}
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub kind: TokenKind,
@@ -171,7 +202,13 @@ pub fn lex(source: &str) -> LexOutput {
}
match c {
'\'' => break 'line, // Kommentar bis Zeilenende
'\'' => {
// Kommentar bis Zeilenende; ggf. Metabefehl
if let Some(tok) = meta_token(&chars[i + 1..]) {
tokens.push(Token { kind: tok, pos });
}
break 'line;
}
'"' => {
i += 1;
let mut s = String::new();
@@ -388,7 +425,11 @@ pub fn lex(source: &str) -> LexOutput {
if suffix.is_none() {
if let Some(kw) = keyword(&name) {
if kw == Kw::Rem {
break 'line; // REM: Rest ist Kommentar
// REM: Rest ist Kommentar; ggf. Metabefehl
if let Some(tok) = meta_token(&chars[i..]) {
tokens.push(Token { kind: tok, pos });
}
break 'line;
}
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
continue;

View File

@@ -195,7 +195,23 @@ impl<'a> P<'a> {
}
match self.k() {
TokenKind::Kw(Kw::Print) => self.parse_print(pos),
TokenKind::MetaInclude(path) => {
self.advance();
if path.is_empty() {
self.err("Expected: file name");
return None;
}
Some(Stmt::Include { path, pos })
}
TokenKind::MetaStatic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: true, pos })
}
TokenKind::MetaDynamic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: false, pos })
}
TokenKind::Kw(Kw::Print) => self.parse_print(pos, false),
TokenKind::Kw(Kw::Input) => {
self.advance();
self.parse_input(false, pos)
@@ -511,16 +527,173 @@ impl<'a> P<'a> {
self.advance();
self.parse_assign_or_call(pos, true)
}
TokenKind::Kw(k @ (Kw::Open | Kw::Close | Kw::Write | Kw::Field
| Kw::Get | Kw::Put | Kw::Seek | Kw::Lset | Kw::Rset)) => {
TokenKind::Kw(Kw::Open) => self.parse_open(pos),
TokenKind::Kw(Kw::Close) => {
self.advance();
self.sync();
Some(Stmt::NotYetImplemented { keyword: format!("{k:?}").to_uppercase(), pos })
let mut files = Vec::new();
while !self.at_stmt_end() {
self.eat(&TokenKind::Hash);
match self.parse_expr() {
Some(e) => files.push(e),
None => break,
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::CloseStmt { files, pos })
}
TokenKind::Kw(Kw::Static) | TokenKind::Kw(Kw::Shared) => {
// STATIC/SHARED-Deklaration in Prozeduren: wie DIM behandeln
TokenKind::Kw(Kw::Field) => {
self.advance();
self.parse_dim(false, pos)
self.eat(&TokenKind::Hash);
let file = self.parse_expr()?;
let mut fields = Vec::new();
while self.eat(&TokenKind::Comma) {
let width = self.parse_expr()?;
self.expect_kw(Kw::As, "AS");
let var = self.parse_name_ref()?;
fields.push((width, var));
}
if fields.is_empty() {
self.err("Expected: field list");
}
Some(Stmt::FieldStmt { file, fields, pos })
}
TokenKind::Kw(k @ (Kw::Get | Kw::Put)) => {
self.advance();
if self.k() == TokenKind::LParen {
// Grafikform GET (x1,y1)-(x2,y2): deklariertes Non-Feature
self.err("Feature unavailable");
self.sync();
return None;
}
self.eat(&TokenKind::Hash);
let file = self.parse_expr()?;
let mut recnum = None;
let mut var = None;
if self.eat(&TokenKind::Comma) {
if !self.at_stmt_end() && self.k() != TokenKind::Comma {
recnum = Some(self.parse_expr()?);
}
if self.eat(&TokenKind::Comma) {
var = Some(self.parse_name_ref()?);
}
}
Some(Stmt::GetPut { put: k == Kw::Put, file, recnum, var, pos })
}
TokenKind::Kw(k @ (Kw::Lset | Kw::Rset)) => {
self.advance();
let target = self.parse_name_ref()?;
if !self.eat(&TokenKind::Eq) {
self.err("Expected: =");
self.sync();
return None;
}
let value = self.parse_expr()?;
Some(Stmt::LsetRset { rset: k == Kw::Rset, target, value, pos })
}
TokenKind::Kw(Kw::Write) => {
self.advance();
let file = if self.eat(&TokenKind::Hash) {
let e = self.parse_expr()?;
self.eat(&TokenKind::Comma);
Some(e)
} else {
None
};
let mut items = Vec::new();
while !self.at_stmt_end() {
match self.parse_expr() {
Some(e) => items.push(e),
None => break,
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::WriteStmt { file, items, pos })
}
TokenKind::Kw(Kw::Seek) => {
self.advance();
self.eat(&TokenKind::Hash);
let file = self.parse_expr()?;
if !self.eat(&TokenKind::Comma) {
self.err("Expected: ,");
}
let position = self.parse_expr()?;
Some(Stmt::SeekStmt { file, position, pos })
}
TokenKind::Kw(Kw::Common) => {
self.advance();
let shared = self.eat_kw(Kw::Shared);
let mut block = None;
if self.eat(&TokenKind::Slash) {
if let TokenKind::Ident { name, suffix: None } = self.k() {
self.advance();
block = Some(name);
} else {
self.err("Expected: identifier");
}
if !self.eat(&TokenKind::Slash) {
self.err("Expected: /");
}
}
let decls = self.parse_var_decls();
Some(Stmt::CommonDecl { shared, block, decls, pos })
}
TokenKind::Kw(Kw::Shared) => {
self.advance();
let decls = self.parse_var_decls();
Some(Stmt::SharedDecl { decls, pos })
}
TokenKind::Kw(Kw::Static) => {
self.advance();
let decls = self.parse_var_decls();
Some(Stmt::StaticDecl { decls, pos })
}
TokenKind::Ident { ref name, .. } if name == "LPRINT" => {
self.parse_print(pos, true)
}
TokenKind::Ident { ref name, suffix: None }
if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) =>
{
self.advance(); // VIEW
self.advance(); // PRINT
let (mut top, mut bottom) = (None, None);
if !self.at_stmt_end() {
top = Some(self.parse_expr()?);
self.expect_kw(Kw::To, "TO");
bottom = Some(self.parse_expr()?);
}
Some(Stmt::ViewPrint { top, bottom, pos })
}
TokenKind::Ident { ref name, suffix: None }
if name == "NAME" && self.k_at(1) != TokenKind::Eq =>
{
self.advance();
let old = self.parse_expr()?;
self.expect_kw(Kw::As, "AS");
let new = self.parse_expr()?;
Some(Stmt::NameStmt { old, new, pos })
}
TokenKind::Ident { ref name, suffix: None }
if (name == "LOCK" || name == "UNLOCK")
&& self.k_at(1) != TokenKind::Eq =>
{
let unlock = name == "UNLOCK";
self.advance();
self.eat(&TokenKind::Hash);
let file = self.parse_expr()?;
let (mut from, mut to) = (None, None);
if self.eat(&TokenKind::Comma) {
if !self.is_kw(Kw::To) {
from = Some(self.parse_expr()?);
}
if self.eat_kw(Kw::To) {
to = Some(self.parse_expr()?);
}
}
Some(Stmt::LockStmt { unlock, file, from, to, pos })
}
TokenKind::Ident { .. } => self.parse_assign_or_call(pos, false),
_ => {
@@ -531,9 +704,9 @@ impl<'a> P<'a> {
}
}
fn parse_print(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // PRINT
let file = if self.eat(&TokenKind::Hash) {
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
self.advance(); // PRINT bzw. LPRINT
let file = if !printer && self.eat(&TokenKind::Hash) {
let e = self.parse_expr()?;
self.eat(&TokenKind::Comma);
Some(e)
@@ -567,7 +740,135 @@ impl<'a> P<'a> {
},
}
}
Some(Stmt::Print { file, using, items, pos })
Some(Stmt::Print { printer, file, using, items, pos })
}
/// `OPEN` in beiden Syntaxformen (FOR-Klausel und Kurzform).
fn parse_open(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // OPEN
let first = self.parse_expr()?;
if self.eat(&TokenKind::Comma) {
// Alte Syntax: OPEN modus$, [#]n, datei$ [, reclen]
self.eat(&TokenKind::Hash);
let number = self.parse_expr()?;
if !self.eat(&TokenKind::Comma) {
self.err("Expected: ,");
}
let file = self.parse_expr()?;
let len = if self.eat(&TokenKind::Comma) {
Some(self.parse_expr()?)
} else {
None
};
return Some(Stmt::OpenLegacy { mode: first, number, file, len, pos });
}
let mut mode = None;
let mut isam = None;
if self.eat_kw(Kw::For) {
mode = Some(match self.k() {
TokenKind::Kw(Kw::Input) => {
self.advance();
OpenMode::Input
}
TokenKind::Ident { ref name, suffix: None } => {
let m = match name.as_str() {
"OUTPUT" => OpenMode::Output,
"APPEND" => OpenMode::Append,
"RANDOM" => OpenMode::Random,
"BINARY" => OpenMode::Binary,
"ISAM" => OpenMode::Isam,
_ => {
self.err("Expected: file mode");
OpenMode::Random
}
};
self.advance();
m
}
_ => {
self.err("Expected: file mode");
OpenMode::Random
}
});
if mode == Some(OpenMode::Isam) {
// OPEN datenbank$ FOR ISAM typname tabellenname AS #n
let ty = match self.k() {
TokenKind::Ident { name, suffix: None } => {
self.advance();
name
}
_ => {
self.err("Expected: identifier");
String::new()
}
};
let table = match self.k() {
TokenKind::Ident { name, .. } => {
self.advance();
name
}
TokenKind::Str(s) => {
self.advance();
s
}
_ => {
self.err("Expected: table name");
String::new()
}
};
isam = Some((ty, table));
}
}
let mut access = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if name == "ACCESS" {
self.advance();
let read = self.eat_kw(Kw::Read);
let write = self.eat_kw(Kw::Write);
access = Some(match (read, write) {
(true, true) => AccessMode::ReadWrite,
(true, false) => AccessMode::Read,
(false, true) => AccessMode::Write,
(false, false) => {
self.err("Expected: READ or WRITE");
AccessMode::ReadWrite
}
});
}
}
let mut lock = None;
if self.eat_kw(Kw::Shared) {
lock = Some(LockClause::Shared);
} else if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if name == "LOCK" {
self.advance();
let read = self.eat_kw(Kw::Read);
let write = self.eat_kw(Kw::Write);
lock = Some(match (read, write) {
(true, true) => LockClause::LockReadWrite,
(true, false) => LockClause::LockRead,
(false, true) => LockClause::LockWrite,
(false, false) => {
self.err("Expected: READ or WRITE");
LockClause::LockReadWrite
}
});
}
}
self.expect_kw(Kw::As, "AS");
self.eat(&TokenKind::Hash);
let number = self.parse_expr()?;
let mut len = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if name == "LEN" {
self.advance();
if !self.eat(&TokenKind::Eq) {
self.err("Expected: =");
}
len = Some(self.parse_expr()?);
}
}
Some(Stmt::Open { file: first, mode, isam, access, lock, number, len, pos })
}
fn parse_input(&mut self, line: bool, pos: SourcePos) -> Option<Stmt> {
@@ -857,6 +1158,13 @@ impl<'a> P<'a> {
fn parse_dim(&mut self, redim: bool, pos: SourcePos) -> Option<Stmt> {
let shared = self.eat_kw(Kw::Shared);
let decls = self.parse_var_decls();
Some(Stmt::Dim { shared, redim, decls, pos })
}
/// Deklarationsliste `name[suffix][(dims)] [AS typ], …` — gemeinsame
/// Grammatik von DIM/REDIM/COMMON/SHARED/STATIC.
fn parse_var_decls(&mut self) -> Vec<VarDecl> {
let mut decls = Vec::new();
loop {
let dpos = self.pos();
@@ -875,10 +1183,15 @@ impl<'a> P<'a> {
let mut ds = Vec::new();
if !self.eat(&TokenKind::RParen) {
loop {
let a = self.parse_expr()?;
let a = match self.parse_expr() {
Some(e) => e,
None => break,
};
if self.eat_kw(Kw::To) {
let b = self.parse_expr()?;
ds.push((Some(a), b));
match self.parse_expr() {
Some(b) => ds.push((Some(a), b)),
None => break,
}
} else {
ds.push((None, a));
}
@@ -902,7 +1215,7 @@ impl<'a> P<'a> {
break;
}
}
Some(Stmt::Dim { shared, redim, decls, pos })
decls
}
fn parse_type_name(&mut self) -> Option<TypeName> {
@@ -1129,15 +1442,15 @@ impl<'a> P<'a> {
let body = self.parse_expr()?;
Some(Stmt::DefFn { name, suffix, params, body, pos })
} else {
// Blockform DEF FN … END DEF: noch nicht implementiert
self.sync();
let stop = |p: &P| p.at_end_pair(Kw::Def);
let _ = self.parse_stmt_list(stop);
// Blockform: DEF FNname(...) … [EXIT DEF] … END DEF
let body = self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::Def));
if self.at_end_pair(Kw::Def) {
self.advance();
self.advance();
} else {
self.err("Expected: END DEF");
}
Some(Stmt::NotYetImplemented { keyword: "DEF FN (Blockform)".into(), pos })
Some(Stmt::DefFnBlock { name, suffix, params, body, pos })
}
}
@@ -1152,6 +1465,37 @@ impl<'a> P<'a> {
self.sync();
return None;
}
// Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
if let Expr::Name { ref name, suffix: None, ref args, .. } = target {
if matches!(
name.as_str(),
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT"
| "EVENT"
) {
let action = match self.k() {
TokenKind::Kw(Kw::On) => Some(EventAction::On),
TokenKind::Kw(Kw::Stop) => Some(EventAction::Stop),
TokenKind::Ident { ref name, suffix: None }
if name == "OFF" =>
{
Some(EventAction::Off)
}
_ => None,
};
if let Some(action) = action {
self.advance();
let index = args.clone().and_then(|mut a| {
if a.is_empty() { None } else { Some(a.remove(0)) }
});
return Some(Stmt::EventControl {
device: name.clone(),
index,
action,
pos,
});
}
}
}
// Impliziter Aufruf: `name [arg [, arg …]]`
if let Expr::Name { name, suffix, args, .. } = target {
let mut call_args = args.unwrap_or_default();

View File

@@ -1,15 +1,14 @@
//! Semantische Analyse: Symboltabellen, implizite Deklaration,
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung, Auflösung
//! Array-Index vs. Funktionsaufruf, Label-Prüfung, Builtin-Signaturen.
//!
//! Erste Ausbaustufe (Phase 1): bewusst nachsichtig — lieber eine Prüfung
//! auslassen als falsche Fehler melden. `COMMON`, `SHARED`-Sichtbarkeit
//! über Prozedurgrenzen und UDT-Feldtypen folgen mit Phase 2/3.
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl.
//! UDT-Feldtypen, Konstantenfaltung, Auflösung Array-Index vs.
//! Funktionsaufruf, Label-Prüfung, Builtin-Signaturen sowie die
//! Compile-Zeit-Abweisung deklarierter Non-Features.
use crate::ast::*;
use crate::lexer::Suffix;
use crate::{Diagnostic, SourcePos};
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum Ty {
@@ -68,6 +67,13 @@ fn type_name_ty(t: &TypeName) -> Ty {
}
}
/// Gefalteter Konstantenwert (`CONST`).
#[derive(Debug, Clone, PartialEq)]
pub enum ConstVal {
Num(f64),
Str(String),
}
#[derive(Debug, Clone)]
struct VarInfo {
ty: Ty,
@@ -150,6 +156,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
"CSRLIN" => (0, 0, &[], I),
"POS" => (1, 1, &[N], I),
"SCREEN" => (2, 3, &[N, N, N], I),
"TAB" | "SPC" => (1, 1, &[N], St),
"DATE$" | "TIME$" => (0, 0, &[], St),
"TIMER" => (0, 0, &[], Sg),
@@ -165,6 +172,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"EOF" => (1, 1, &[N], I),
"LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
"FREEFILE" => (0, 0, &[], I),
"FILEATTR" => (2, 2, &[N, N], L),
"ENVIRON$" => (1, 1, &[A], St),
"COMMAND$" => (0, 0, &[], St),
"DOEVENTS" => (0, 0, &[], I),
@@ -174,16 +182,23 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
})
}
/// Hardware-nahe Namen des Vorbilds, die Terminal Basic bewusst nicht
/// unterstützt (Entscheidung 2026-09-02): Ablehnung bereits zur
/// Compile-Zeit statt Laufzeitfehler 73.
/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
fn banned_feature(name: &str) -> bool {
matches!(
name,
// Hardware-Nähe
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
| "DEF.SEG" | "FRE.SEG"
// Overlay-Mechanismus
| "CHAIN"
// Grafik
| "PSET" | "PRESET" | "CIRCLE" | "PAINT" | "DRAW" | "PALETTE"
| "PCOPY" | "PMAP" | "WINDOW"
// Klang (außer BEEP)
| "SOUND" | "PLAY"
)
}
@@ -192,19 +207,23 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
use ArgK::*;
Some(match name {
"CLS" => (0, 1, &[N]),
"BEEP" | "DOEVENTS" => (0, 0, &[]),
"BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
"COLOR" => (0, 3, &[N, N, N]),
"LOCATE" => (0, 5, &[N, N, N, N, N]),
"RANDOMIZE" => (0, 1, &[N]),
"SLEEP" => (0, 1, &[N]),
"WIDTH" => (0, 2, &[N, N]),
"SOUND" => (2, 2, &[N, N]),
"SCREEN" => (1, 4, &[N, N, N, N]),
"SWAP" => (2, 2, &[A, A]),
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
"FILES" => (0, 1, &[S]),
"SHELL" => (0, 1, &[S]),
"RUN" => (0, 1, &[A]),
"CLEAR" => (0, 3, &[N, N, N]),
"KEY" => (1, 2, &[A, S]),
"ENVIRON" => (1, 1, &[S]),
"MSGBOX" => (1, 3, &[S, N, S]),
"RESET" => (0, 0, &[]),
"SETUEVENT" => (0, 0, &[]),
_ => return None,
})
}
@@ -216,9 +235,11 @@ pub fn check(module: &Module) -> Vec<Diagnostic> {
diags: Vec::new(),
deftypes: [const { None }; 26],
explicit: false,
option_base: 0,
procs: HashMap::new(),
udts: HashSet::new(),
udts: HashMap::new(),
consts: HashMap::new(),
module_vars: HashMap::new(),
};
s.run(module);
s.diags
@@ -229,9 +250,15 @@ struct Sema {
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
deftypes: [Option<Ty>; 26],
explicit: bool,
/// `OPTION BASE` (0 oder 1) — relevant für die Codegenerierung.
option_base: u8,
procs: HashMap<String, ProcInfo>,
udts: HashSet<String>,
consts: HashMap<String, Ty>,
/// Benutzerdefinierte Typen mit Feldtypen.
udts: HashMap<String, HashMap<String, Ty>>,
/// Konstanten: Typ und gefalteter Wert.
consts: HashMap<String, (Ty, Option<ConstVal>)>,
/// Modulvariablen nach dem Modulrumpf-Pass (für SHARED in Prozeduren).
module_vars: HashMap<String, VarInfo>,
}
impl Sema {
@@ -240,12 +267,24 @@ impl Sema {
}
fn run(&mut self, module: &Module) {
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren (in Reihenfolge,
// damit TYPE-Verweise „definiert vor Verwendung" geprüft werden).
for stmt in &module.body {
match stmt {
Stmt::Declare { sig, .. } => self.register_proc(sig),
Stmt::TypeDecl { name, .. } => {
self.udts.insert(name.clone());
Stmt::TypeDecl { name, fields, pos } => {
let mut map = HashMap::new();
for (fname, ftype) in fields {
if let TypeName::Udt(n) = ftype {
if !self.udts.contains_key(n) {
self.err(*pos, "Type not defined");
}
}
map.insert(fname.clone(), type_name_ty(ftype));
}
if self.udts.insert(name.clone(), map).is_some() {
self.err(*pos, "Duplicate definition");
}
}
_ => {}
}
@@ -257,10 +296,10 @@ impl Sema {
// Pass 2: Modulrumpf
let mut scope = Scope::default();
collect_labels(&module.body, &mut scope);
let body_ref: Vec<&Stmt> = module.body.iter().collect();
for stmt in body_ref {
self.check_stmt(stmt, &mut scope, None);
for stmt in &module.body {
self.check_stmt(stmt, &mut scope);
}
self.module_vars = scope.vars.clone();
// Pass 3: Prozedurrümpfe
for proc in &module.procs {
@@ -269,23 +308,20 @@ impl Sema {
for p in &proc.sig.params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
pscope.vars.insert(
key,
VarInfo { ty, array: p.array, explicit: true },
);
pscope
.vars
.insert(key, VarInfo { ty, array: p.array, explicit: true });
}
// Funktionsname als Rückgabe-Variable
if proc.sig.kind == ProcKind::Function {
let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
pscope.vars.insert(key, VarInfo { ty: ret, array: false, explicit: true });
pscope
.vars
.insert(key, VarInfo { ty: ret, array: false, explicit: true });
}
let fn_name = match proc.sig.kind {
ProcKind::Function => Some(proc.sig.name.clone()),
ProcKind::Sub => None,
};
for stmt in &proc.body {
self.check_stmt(stmt, &mut pscope, fn_name.as_deref());
self.check_stmt(stmt, &mut pscope);
}
}
}
@@ -303,15 +339,9 @@ impl Sema {
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
ProcKind::Sub => Ty::Unknown,
};
let params = sig
.params
.iter()
.map(|p| (self.param_ty(p), p.array))
.collect();
self.procs.insert(
sig.name.clone(),
ProcInfo { kind: sig.kind, ret, params },
);
let params = sig.params.iter().map(|p| (self.param_ty(p), p.array)).collect();
self.procs
.insert(sig.name.clone(), ProcInfo { kind: sig.kind, ret, params });
}
/// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE).
@@ -352,7 +382,32 @@ impl Sema {
}
}
/// Variable nachschlagen; `declare_if_missing` deklariert implizit.
/// Feldtyp eines UDT-Zugriffs `basisvar.feld[.feld …]`.
fn member_ty(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> Ty {
let mut cur = base_ty.clone();
for seg in path {
match &cur {
Ty::Udt(udt_name) => {
match self.udts.get(udt_name).and_then(|f| f.get(*seg)) {
Some(t) => cur = t.clone(),
None => {
self.err(pos, "Element not defined");
return Ty::Unknown;
}
}
}
Ty::Unknown => return Ty::Unknown,
_ => {
self.err(pos, "Type mismatch");
return Ty::Unknown;
}
}
}
cur
}
/// Variable nachschlagen bzw. implizit deklarieren; löst auch
/// UDT-Feldzugriffe (`a.b.c`) auf.
fn resolve_var(
&mut self,
scope: &mut Scope,
@@ -375,12 +430,19 @@ impl Sema {
if let Some(v) = scope.vars.get(&key) {
return v.ty.clone();
}
// UDT-Feldzugriff `a.b`: Basis auflösen
if let Some(base) = name.split('.').next() {
if base != name {
let base_as = format!("{base}\u{1}AS");
if scope.vars.contains_key(&base_as) {
return Ty::Unknown; // Feldtypen: Phase 2
// UDT-Feldzugriff: Basis vor dem ersten Punkt auflösen
if name.contains('.') {
let parts: Vec<&str> = name.split('.').collect();
let base = parts[0];
let base_as = format!("{base}\u{1}AS");
let base_info = scope
.vars
.get(&base_as)
.or_else(|| scope.vars.get(&self.var_key(base, &None, false)))
.cloned();
if let Some(info) = base_info {
if matches!(info.ty, Ty::Udt(_)) {
return self.member_ty(&info.ty, &parts[1..], pos);
}
}
}
@@ -389,22 +451,109 @@ impl Sema {
return self.name_ty(name, suffix);
}
let ty = self.name_ty(name, suffix);
scope.vars.insert(
key,
VarInfo { ty: ty.clone(), array, explicit: false },
);
scope
.vars
.insert(key, VarInfo { ty: ty.clone(), array, explicit: false });
ty
}
/// Konstantenfaltung für `CONST`-Ausdrücke.
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
match e {
Expr::IntLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::LongLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::SingleLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::DoubleLit(v) => Some(ConstVal::Num(*v)),
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
Expr::Name { name, args: None, .. } => {
self.consts.get(name).and_then(|(_, v)| v.clone())
}
Expr::Unary { op: UnOp::Neg, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
ConstVal::Str(_) => None,
}
}
Expr::Unary { op: UnOp::Not, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
ConstVal::Str(_) => None,
}
}
Expr::Binary { op, lhs, rhs, .. } => {
let l = self.fold_const(lhs)?;
let r = self.fold_const(rhs)?;
match (l, r) {
(ConstVal::Num(a), ConstVal::Num(b)) => {
let v = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
BinOp::Pow => a.powf(b),
BinOp::IntDiv => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) / bi) as f64
}
BinOp::Mod => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) % bi) as f64
}
_ => return None,
};
Some(ConstVal::Num(v))
}
(ConstVal::Str(a), ConstVal::Str(b)) if *op == BinOp::Add => {
Some(ConstVal::Str(format!("{a}{b}")))
}
_ => None,
}
}
_ => None,
}
}
// ---- Anweisungen -------------------------------------------------------
fn check_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, fn_name: Option<&str>) {
fn check_stmt(&mut self, stmt: &Stmt, scope: &mut Scope) {
match stmt {
Stmt::Label(_) | Stmt::LineNumber(_) | Stmt::End | Stmt::StopStmt
| Stmt::System | Stmt::Data { .. } | Stmt::Exit { .. }
| Stmt::NotYetImplemented { .. } => {}
| Stmt::Include { .. } | Stmt::MetaArrays { .. } => {}
Stmt::Assign { target, value, pos } => {
// MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck
if let Expr::Name {
name,
suffix: Some(Suffix::Str),
args: Some(args),
..
} = target
{
if name == "MID" {
if args.is_empty() || args.len() > 3 {
self.err(*pos, "Argument-count mismatch");
}
if let Some(sv) = args.first() {
let t = self.lvalue_ty(sv, scope);
if !is_str(&t) {
self.err(sv.pos(), "Type mismatch");
}
}
for a in args.iter().skip(1) {
self.want_num(a, scope);
}
self.want_str(value, scope);
return;
}
}
let tt = self.lvalue_ty(target, scope);
let vt = self.expr_ty(value, scope);
self.check_assign(&tt, &vt, *pos);
@@ -433,21 +582,21 @@ impl Sema {
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
self.want_num(cond, scope);
for s in then_body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
for (c, b) in elseifs {
self.want_num(c, scope);
for s in b {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
if let Some(b) = else_body {
for s in b {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
}
Stmt::Select { expr, arms, pos: _ } => {
Stmt::Select { expr, arms, .. } => {
let st = self.expr_ty(expr, scope);
for arm in arms {
for spec in &arm.specs {
@@ -466,7 +615,7 @@ impl Sema {
}
}
for s in &arm.body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
}
@@ -481,7 +630,7 @@ impl Sema {
self.want_num(s, scope);
}
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::DoLoop { pre, post, body, .. } => {
@@ -492,13 +641,13 @@ impl Sema {
self.want_num(c, scope);
}
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::While { cond, body, .. } => {
self.want_num(cond, scope);
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::Goto { target, pos } | Stmt::Gosub { target, pos } => {
@@ -531,6 +680,36 @@ impl Sema {
self.declare(d, *redim, scope);
}
}
Stmt::StaticDecl { decls, .. } | Stmt::CommonDecl { decls, .. } => {
for d in decls {
self.declare(d, false, scope);
}
}
Stmt::SharedDecl { decls, .. } => {
// Zugriff auf Modulvariablen aus einer Prozedur heraus:
// vorhandene Modulvariable importieren, sonst dort anlegen.
for d in decls {
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
let info = match self.module_vars.get(&key) {
Some(v) => v.clone(),
None => {
let ty = if let Some(t) = &d.as_type {
type_name_ty(t)
} else {
self.name_ty(&d.name, &d.suffix)
};
let v = VarInfo {
ty,
array: d.dims.is_some(),
explicit: true,
};
self.module_vars.insert(key.clone(), v.clone());
v
}
};
scope.vars.insert(key, info);
}
}
Stmt::Erase { names, .. } => {
for n in names {
if let Expr::Name { name, suffix, pos, .. } = n {
@@ -538,10 +717,22 @@ impl Sema {
}
}
}
Stmt::ConstDecl { items, .. } => {
for (name, _suffix, value) in items {
let ty = self.expr_ty(value, scope);
self.consts.insert(name.clone(), ty);
Stmt::ConstDecl { items, pos } => {
for (name, suffix, value) in items {
let ty = match suffix {
Some(s) => suffix_ty(*s),
None => match self.fold_const(value) {
Some(ConstVal::Str(_)) => Ty::Str,
_ => Ty::Dbl,
},
};
let folded = self.fold_const(value);
if folded.is_none() {
self.err(*pos, "Invalid constant");
}
if self.consts.insert(name.clone(), (ty, folded)).is_some() {
self.err(*pos, "Duplicate definition");
}
}
}
Stmt::DefType { ty, ranges, .. } => {
@@ -555,22 +746,12 @@ impl Sema {
}
}
}
Stmt::OptionStmt { kind, .. } => {
if *kind == OptionKind::Explicit {
self.explicit = true;
}
// OPTION BASE: relevant erst für die Array-Semantik (Phase 2)
}
Stmt::TypeDecl { fields, pos, .. } => {
for (_, t) in fields {
if let TypeName::Udt(n) = t {
if !self.udts.contains(n) {
self.err(*pos, "Type not defined");
}
}
}
}
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
Stmt::OptionStmt { kind, .. } => match kind {
OptionKind::Explicit => self.explicit = true,
OptionKind::Base(b) => self.option_base = *b,
},
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
Stmt::Call { name, args, pos, .. } => {
self.check_call(name, args, scope, *pos);
}
@@ -584,33 +765,150 @@ impl Sema {
self.check_label(t, scope, *pos);
}
}
Stmt::DefFn { name, suffix, params, body, pos: _ } => {
let ret = self.name_ty(name, suffix);
let param_infos = params
.iter()
.map(|p| (self.param_ty(p), p.array))
.collect();
self.procs.insert(
name.clone(),
ProcInfo { kind: ProcKind::Function, ret, params: param_infos },
);
// Rumpf in Mini-Scope mit den Parametern prüfen
let mut fscope = Scope::default();
for p in params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, false);
fscope.vars.insert(key, VarInfo { ty, array: false, explicit: true });
}
Stmt::DefFn { name, suffix, params, body, .. } => {
self.register_def_fn(name, suffix, params);
let mut fscope = self.def_fn_scope(name, suffix, params);
self.expr_ty(body, &mut fscope);
}
Stmt::DefFnBlock { name, suffix, params, body, .. } => {
self.register_def_fn(name, suffix, params);
let mut fscope = self.def_fn_scope(name, suffix, params);
collect_labels(body, &mut fscope);
// Modul-Labels bleiben aus DEF FN heraus ansprechbar
fscope.labels.extend(scope.labels.iter().cloned());
fscope.line_labels.extend(scope.line_labels.iter().copied());
for s in body {
self.check_stmt(s, &mut fscope);
}
}
// ---- Datei-E/A ----
Stmt::Open { file, isam, number, len, pos, .. } => {
self.want_str(file, scope);
self.want_num(number, scope);
if let Some(l) = len {
self.want_num(l, scope);
}
if let Some((ty_name, _table)) = isam {
if !ty_name.is_empty() && !self.udts.contains_key(ty_name) {
self.err(*pos, "Type not defined");
}
}
}
Stmt::OpenLegacy { mode, number, file, len, .. } => {
self.want_str(mode, scope);
self.want_num(number, scope);
self.want_str(file, scope);
if let Some(l) = len {
self.want_num(l, scope);
}
}
Stmt::CloseStmt { files, .. } => {
for f in files {
self.want_num(f, scope);
}
}
Stmt::FieldStmt { file, fields, .. } => {
self.want_num(file, scope);
for (width, var) in fields {
self.want_num(width, scope);
let t = self.lvalue_ty(var, scope);
if !is_str(&t) {
self.err(var.pos(), "Type mismatch");
}
}
}
Stmt::GetPut { file, recnum, var, .. } => {
self.want_num(file, scope);
if let Some(r) = recnum {
self.want_num(r, scope);
}
if let Some(v) = var {
self.lvalue_ty(v, scope);
}
}
Stmt::LsetRset { target, value, pos, .. } => {
let tt = self.lvalue_ty(target, scope);
let vt = self.expr_ty(value, scope);
let ok = (is_str(&tt) && is_str(&vt))
|| matches!((&tt, &vt), (Ty::Udt(a), Ty::Udt(b)) if a == b)
|| tt == Ty::Unknown
|| vt == Ty::Unknown;
if !ok {
self.err(*pos, "Type mismatch");
}
}
Stmt::WriteStmt { file, items, .. } => {
if let Some(f) = file {
self.want_num(f, scope);
}
for e in items {
self.expr_ty(e, scope);
}
}
Stmt::SeekStmt { file, position, .. } => {
self.want_num(file, scope);
self.want_num(position, scope);
}
Stmt::LockStmt { file, from, to, .. } => {
self.want_num(file, scope);
if let Some(f) = from {
self.want_num(f, scope);
}
if let Some(t) = to {
self.want_num(t, scope);
}
}
Stmt::NameStmt { old, new, .. } => {
self.want_str(old, scope);
self.want_str(new, scope);
}
// ---- Bildschirm/Ereignisse ----
Stmt::ViewPrint { top, bottom, .. } => {
if let Some(t) = top {
self.want_num(t, scope);
}
if let Some(b) = bottom {
self.want_num(b, scope);
}
}
Stmt::EventControl { device, index, pos, .. } => {
match device.as_str() {
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {}
_ => self.err(*pos, "Feature unavailable"),
}
if let Some(i) = index {
self.want_num(i, scope);
}
}
}
let _ = fn_name; // Rückgabezuweisung läuft über die Scope-Variable
}
fn register_def_fn(&mut self, name: &str, suffix: &Option<Suffix>, params: &[Param]) {
let ret = self.name_ty(name, suffix);
let param_infos = params.iter().map(|p| (self.param_ty(p), p.array)).collect();
self.procs.insert(
name.to_string(),
ProcInfo { kind: ProcKind::Function, ret, params: param_infos },
);
}
fn def_fn_scope(&mut self, name: &str, suffix: &Option<Suffix>, params: &[Param]) -> Scope {
let mut fscope = Scope::default();
for p in params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, false);
fscope.vars.insert(key, VarInfo { ty, array: false, explicit: true });
}
let ret = self.name_ty(name, suffix);
let key = self.var_key(name, suffix, false);
fscope.vars.insert(key, VarInfo { ty: ret, array: false, explicit: true });
fscope
}
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope) {
let ty = if let Some(t) = &d.as_type {
if let TypeName::Udt(n) = t {
if !self.udts.contains(n) {
if !self.udts.contains_key(n) {
self.err(d.pos, "Type not defined");
}
}
@@ -707,13 +1005,13 @@ impl Sema {
self.err(pos, "Argument-count mismatch");
}
// Sonderfall INSTR([start%,] s$, such$)
let insts_with_start = name == "INSTR" && args.len() == 3;
let instr_with_start = name == "INSTR" && args.len() == 3;
for (i, a) in args.iter().enumerate() {
if matches!(a, Expr::Missing) {
continue;
}
let at = self.expr_ty(a, scope);
let kind = if insts_with_start {
let kind = if instr_with_start {
match i {
0 => ArgK::N,
_ => ArgK::S,
@@ -868,7 +1166,7 @@ impl Sema {
};
// 1. Konstante
if let Some(t) = self.consts.get(name).cloned() {
if let Some((t, _)) = self.consts.get(name).cloned() {
if args.is_some() {
self.err(pos, "Syntax error");
}
@@ -895,7 +1193,7 @@ impl Sema {
self.err(pos, "Duplicate definition");
return Ty::Unknown;
}
// 3. Nicht unterstützte Hardware-Features → Compile-Fehler
// 3. Nicht unterstützte Features → Compile-Fehler
if banned_feature(&full_name) {
for a in idx {
self.expr_ty(a, scope);
@@ -908,7 +1206,7 @@ impl Sema {
self.check_builtin_args(&full_name, idx, min, max, spec, scope, pos);
return ret_ty(ret);
}
// 4. FUNCTION / DEF FN
// 5. FUNCTION / DEF FN
if let Some(info) = self.procs.get(name).cloned() {
if info.kind == ProcKind::Function {
if idx.len() != info.params.len() {
@@ -922,7 +1220,7 @@ impl Sema {
self.err(pos, "Duplicate definition");
return Ty::Unknown;
}
// 5. Implizites Array (klassisch: DIM x(10) implizit)
// 6. Implizites Array (klassisch: DIM x(10) implizit)
for a in idx {
self.want_num(a, scope);
}
@@ -932,10 +1230,9 @@ impl Sema {
}
let ty = self.name_ty(name, suffix);
let key = self.var_key(name, suffix, false);
scope.vars.insert(
key,
VarInfo { ty: ty.clone(), array: true, explicit: false },
);
scope
.vars
.insert(key, VarInfo { ty: ty.clone(), array: true, explicit: false });
ty
}
None => {
@@ -945,6 +1242,10 @@ impl Sema {
let declared = scope.vars.contains_key(&key)
|| (suffix.is_none() && scope.vars.contains_key(&as_key));
if !declared {
if banned_feature(&full_name) {
self.err(pos, "Feature unavailable");
return Ty::Unknown;
}
if let Some((0, _, _, ret)) = builtin_fn(&full_name) {
return ret_ty(ret);
}
@@ -1023,7 +1324,6 @@ mod tests {
#[test]
fn deftype_regeln() {
// DEFSTR macht s zum String
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
assert!(diags("DEFINT I\ni = 5").is_empty());
}
@@ -1047,6 +1347,10 @@ mod tests {
assert!(diags("POKE 100, 1").contains(&"Feature unavailable".to_string()));
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable".to_string()));
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable".to_string()));
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable".to_string()));
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable".to_string()));
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable".to_string()));
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable".to_string()));
}
#[test]
@@ -1058,9 +1362,7 @@ mod tests {
#[test]
fn funktionsaufruf_und_rueckgabe() {
let d = diags(
"FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)",
);
let d = diags("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
assert!(d.is_empty(), "{d:?}");
}
@@ -1081,4 +1383,50 @@ mod tests {
assert!(diags("x$ = INKEY$").is_empty());
assert!(diags("t! = TIMER").is_empty());
}
#[test]
fn mid_anweisung() {
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"").is_empty());
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5")
.contains(&"Type mismatch".to_string()));
}
#[test]
fn def_fn_blockform() {
let d = diags(
"DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)",
);
assert!(d.is_empty(), "{d:?}");
}
#[test]
fn udt_feldtypen() {
let src = "TYPE Kunde\nName AS STRING * 30\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Umsatz = 5\nk.Name = \"x\"";
assert!(diags(src).is_empty(), "{:?}", diags(src));
let bad = "TYPE Kunde\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Gibtsnicht = 1";
assert!(diags(bad).contains(&"Element not defined".to_string()));
let bad2 = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM k AS Kunde\nk.Name = 5";
assert!(diags(bad2).contains(&"Type mismatch".to_string()));
}
#[test]
fn shared_und_common() {
let src = "DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("COMMON SHARED /blk/ a%, b$()").is_empty());
}
#[test]
fn konstantenfaltung() {
assert!(diags("CONST PI = 3.14159, ZWEI.PI = PI * 2").is_empty());
assert!(diags("x = 1\nCONST K = x + 1")
.contains(&"Invalid constant".to_string()));
}
#[test]
fn datei_ea_und_events() {
let src = "OPEN \"test.dat\" FOR RANDOM AS #1 LEN = 64\nCLOSE #1\nOPEN \"o\", #2, \"f.txt\"\nCLOSE\nTIMER ON\nKEY(5) OFF";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("PEN ON").contains(&"Feature unavailable".to_string()));
}
}

View File

@@ -128,7 +128,10 @@ Prozeduraufrufe, `ERROR n`; „Set Next Statement" verlegt die Ausführung.
**Shift+F1** Using Help. Hyperlinks als ◄Thema►, Tab/Shift+Tab springt,
Enter folgt, **Alt+F1** zurück (20 Ebenen), Ctrl+F1 nächstes Topic,
Esc schließt. Hilfe im eigenen Fenster; Index, Contents, Keyboard-Guide,
Tutorial. → Terminal Basic: eigenes Hilfeformat, gleiche Navigation.
Tutorial. → Terminal Basic: Hilfeinhalt sind die Markdown-Dokumente aus
`docs/` (Sprach- und Bibliotheksreferenz, Ist-Stand der Implementierung),
gerendert im Help-Fenster mit dynamischem Umbruch nach Fenstergröße;
Navigation wie im Vorbild (F1 kontextsensitiv, Tab/Enter, Alt+F1 zurück).
## 8. Statuszeile (Konfidenz: sicher; Caps/Num: unklar)