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:
90
PLAN.md
90
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
|
||||
|
||||
---
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user