diff --git a/PLAN.md b/PLAN.md index 881f942..ada787d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 --- diff --git a/crates/tb-frontend/src/ast.rs b/crates/tb-frontend/src/ast.rs index 7a8804c..6060a4d 100644 --- a/crates/tb-frontend/src/ast.rs +++ b/crates/tb-frontend/src/ast.rs @@ -96,7 +96,7 @@ pub struct CaseArm { pub struct VarDecl { pub name: String, pub suffix: Option, - /// `None` = Skalar; sonst Dimensionen `(untergrenze TO obergrenze)`. + /// `None` = Skalar; leerer Vec = Array ohne Dimensionsangabe (`a()`). pub dims: Option, Expr)>>, pub as_type: Option, 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, using: Option, items: Vec, @@ -212,6 +247,17 @@ pub enum Stmt { System, Exit { kind: ExitKind, pos: SourcePos }, Dim { shared: bool, redim: bool, decls: Vec, pos: SourcePos }, + /// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen). + SharedDecl { decls: Vec, pos: SourcePos }, + /// `STATIC`-Anweisung in einer Prozedur. + StaticDecl { decls: Vec, pos: SourcePos }, + /// `COMMON [SHARED] [/block/] liste`. + CommonDecl { + shared: bool, + block: Option, + decls: Vec, + pos: SourcePos, + }, Erase { names: Vec, pos: SourcePos }, ConstDecl { items: Vec<(String, Option, 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, + params: Vec, + body: Vec, + pos: SourcePos, + }, + // ---- Datei-E/A (Laufzeit in Phase 3, Grammatik vollständig) ---- + Open { + file: Expr, + mode: Option, + /// Bei `FOR ISAM typname tabellenname`. + isam: Option<(String, String)>, + access: Option, + lock: Option, + number: Expr, + len: Option, + pos: SourcePos, + }, + /// Alte Syntax `OPEN modus$, [#]n, datei$ [, reclen]`. + OpenLegacy { + mode: Expr, + number: Expr, + file: Expr, + len: Option, + pos: SourcePos, + }, + CloseStmt { files: Vec, pos: SourcePos }, + FieldStmt { file: Expr, fields: Vec<(Expr, Expr)>, pos: SourcePos }, + GetPut { + put: bool, + file: Expr, + recnum: Option, + var: Option, + pos: SourcePos, + }, + LsetRset { rset: bool, target: Expr, value: Expr, pos: SourcePos }, + WriteStmt { file: Option, items: Vec, pos: SourcePos }, + SeekStmt { file: Expr, position: Expr, pos: SourcePos }, + LockStmt { + unlock: bool, + file: Expr, + from: Option, + to: Option, + pos: SourcePos, + }, + NameStmt { old: Expr, new: Expr, pos: SourcePos }, + // ---- Bildschirm/Ereignisse ---- + /// `VIEW PRINT [oben TO unten]`. + ViewPrint { top: Option, bottom: Option, pos: SourcePos }, + /// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … + EventControl { + device: String, + index: Option, + 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)] diff --git a/crates/tb-frontend/src/lexer.rs b/crates/tb-frontend/src/lexer.rs index d9bb9aa..19f2410 100644 --- a/crates/tb-frontend/src/lexer.rs +++ b/crates/tb-frontend/src/lexer.rs @@ -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 { 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 { + 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; diff --git a/crates/tb-frontend/src/parser.rs b/crates/tb-frontend/src/parser.rs index 274854b..96cbd41 100644 --- a/crates/tb-frontend/src/parser.rs +++ b/crates/tb-frontend/src/parser.rs @@ -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 { - self.advance(); // PRINT - let file = if self.eat(&TokenKind::Hash) { + fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option { + 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 { + 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 { @@ -857,6 +1158,13 @@ impl<'a> P<'a> { fn parse_dim(&mut self, redim: bool, pos: SourcePos) -> Option { 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 { 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 { @@ -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(); diff --git a/crates/tb-frontend/src/sema.rs b/crates/tb-frontend/src/sema.rs index 1ce6765..b0d4d59 100644 --- a/crates/tb-frontend/src/sema.rs +++ b/crates/tb-frontend/src/sema.rs @@ -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 { 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; 26], explicit: bool, + /// `OPTION BASE` (0 oder 1) — relevant für die Codegenerierung. + option_base: u8, procs: HashMap, - udts: HashSet, - consts: HashMap, + /// Benutzerdefinierte Typen mit Feldtypen. + udts: HashMap>, + /// Konstanten: Typ und gefalteter Wert. + consts: HashMap)>, + /// Modulvariablen nach dem Modulrumpf-Pass (für SHARED in Prozeduren). + module_vars: HashMap, } 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 { + 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, 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, 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())); + } } diff --git a/docs/ide-referenz.md b/docs/ide-referenz.md index c162820..31f5b6f 100644 --- a/docs/ide-referenz.md +++ b/docs/ide-referenz.md @@ -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)