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`) —
|
Toolchain beim Anwender (vorkompilierter Runner + angehängtes `.tbc`) —
|
||||||
damit bleibt auch der Weg zum verteilbaren Binary im Sekundenbereich.
|
damit bleibt auch der Weg zum verteilbaren Binary im Sekundenbereich.
|
||||||
- **Jede Phase endet mit lauffähigen Tests** gegen eine wachsende
|
- **Jede Phase endet mit lauffähigen Tests** gegen eine wachsende
|
||||||
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.)
|
VM-Ticks — ist Eingangsaufgabe von Phase 4.)
|
||||||
|
|
||||||
## Phase 1 — Sprach-Frontend (`tb-frontend`)
|
## Phase 1 — Sprach-Frontend (`tb-frontend`)
|
||||||
|
|
||||||
|
**Status: abgeschlossen (2026-09-02).**
|
||||||
|
|
||||||
- [x] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
|
- [x] Lexer inkl. Typ-Suffixe, Zeilennummern/Labels, `REM`/`'`-Kommentare,
|
||||||
case-insensitive Keywords, Zeilenfortsetzung mit `_`, Hex-/Oktal-
|
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
|
- [x] SINGLE/DOUBLE-Schwelle suffixloser Dezimalpunkt-Literale entschieden
|
||||||
(> 7 signifikante Stellen → DOUBLE) und in docs/sprachreferenz.md §1
|
(> 7 signifikante Stellen → DOUBLE) und in docs/sprachreferenz.md §1
|
||||||
dokumentiert
|
dokumentiert
|
||||||
- [x] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen
|
- [x] AST für Module, Prozeduren, Anweisungen, Ausdrücke, Deklarationen
|
||||||
- [~] Parser (zeilenorientiert, fehlertolerant — Fehler pro Anweisung
|
- [x] Parser (zeilenorientiert, fehlertolerant — Fehler pro Anweisung
|
||||||
gesammelt, Synchronisation bis Anweisungsende). Kern komplett:
|
gesammelt, Synchronisation bis Anweisungsende): Zuweisung inkl.
|
||||||
Zuweisung, PRINT (inkl. USING/#), INPUT/LINE INPUT, IF (Block +
|
MID$-Anweisung, PRINT/LPRINT (inkl. USING/#), INPUT/LINE INPUT,
|
||||||
einzeilig), SELECT CASE, FOR/DO/WHILE, GOTO/GOSUB/ON-GOTO,
|
IF (Block + einzeilig), SELECT CASE, FOR/DO/WHILE,
|
||||||
ON [LOCAL] ERROR/RESUME, DIM/REDIM/CONST/DEFtype/OPTION/TYPE/
|
GOTO/GOSUB/ON-GOTO, ON [LOCAL] ERROR/RESUME, DIM/REDIM/CONST/
|
||||||
DECLARE/SUB/FUNCTION/CALL, DATA/READ/RESTORE, DEF FN (einzeilig).
|
DEFtype/OPTION/TYPE/DECLARE/SUB/FUNCTION/CALL, COMMON/SHARED/STATIC,
|
||||||
Offen: MID$-Anweisung, DEF FN-Blockform, Datei-E/A-Anweisungen
|
DATA/READ/RESTORE, DEF FN (einzeilig + Blockform), VIEW PRINT,
|
||||||
(werden als Platzhalter geparst → Phase 3), `$INCLUDE`-Metabefehl
|
NAME…AS, Ereignissteuerung (`TIMER/KEY(n)/… ON|OFF|STOP`), komplette
|
||||||
- [~] Semantik: Symboltabellen, implizite Deklaration, `DEFtype`-Regeln,
|
Datei-E/A-Grammatik (OPEN in beiden Syntaxen inkl. ISAM-Klausel,
|
||||||
Typprüfung, `OPTION EXPLICIT`, Arrays (implizit/DIM/REDIM),
|
CLOSE, FIELD, GET/PUT, LSET/RSET, WRITE, SEEK, LOCK/UNLOCK)
|
||||||
Builtin-Signaturen, Label-Prüfung, Prozedur-Signaturprüfung.
|
- [x] Semantik: Symboltabellen, implizite Deklaration, `DEFtype`-Regeln,
|
||||||
Offen: `COMMON`/`SHARED` über Prozedurgrenzen, UDT-Feldtypen,
|
Typprüfung, `OPTION EXPLICIT`/`BASE`, Arrays (implizit/DIM/REDIM),
|
||||||
`OPTION BASE`-Auswertung, Konstantenfaltung
|
UDT-Feldtypen (`kunde.name`), `SHARED`-Import von Modulvariablen,
|
||||||
- [~] Diagnostik mit exakten Positionen; Meldungstexte am Vorbild
|
`COMMON`, Konstantenfaltung (`CONST`, „Invalid constant"),
|
||||||
orientiert (Type mismatch, Duplicate definition, Label not defined …)
|
Builtin-Signaturen, Label-Prüfung, Prozedur-Signaturprüfung;
|
||||||
— vollständiger Abgleich mit den Compile-Meldungen des Vorbilds offen
|
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
|
- [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`)
|
## Phase 2 — Bytecode und VM (`tb-vm`)
|
||||||
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
|
- [ ] 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-
|
- [ ] `.FRM`-Textformat: Serialisierung **definieren** (kein Original-
|
||||||
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
|
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
|
||||||
dokumentieren, dann lesen/schreiben implementieren
|
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`)
|
- [ ] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`)
|
||||||
- [ ] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
|
- [ ] 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
|
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart
|
||||||
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,
|
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,
|
||||||
Überwachungsausdrücke
|
Ü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,
|
- [ ] Meilenstein: Programm komplett in der IDE schreiben, gestalten,
|
||||||
debuggen und ausführen
|
debuggen und ausführen
|
||||||
|
|
||||||
@@ -291,6 +334,15 @@ bleibt gültig. Noch nichts davon ist beschlossen; Sammlung wächst:
|
|||||||
Forms-Engine
|
Forms-Engine
|
||||||
- Standardbibliothek: Prozessaufrufe mit Pipes, Umgebungs-/Argument-Handling
|
- Standardbibliothek: Prozessaufrufe mit Pipes, Umgebungs-/Argument-Handling
|
||||||
für CLI-Tools, JSON/CSV, HTTP-Client, Pfad-/Verzeichnisfunktionen
|
für CLI-Tools, JSON/CSV, HTTP-Client, Pfad-/Verzeichnisfunktionen
|
||||||
|
- **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
|
- Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ pub struct CaseArm {
|
|||||||
pub struct VarDecl {
|
pub struct VarDecl {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub suffix: Option<Suffix>,
|
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 dims: Option<Vec<(Option<Expr>, Expr)>>,
|
||||||
pub as_type: Option<TypeName>,
|
pub as_type: Option<TypeName>,
|
||||||
pub pos: SourcePos,
|
pub pos: SourcePos,
|
||||||
@@ -161,12 +161,47 @@ pub struct Proc {
|
|||||||
pub pos: SourcePos,
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Stmt {
|
pub enum Stmt {
|
||||||
Label(String),
|
Label(String),
|
||||||
LineNumber(u32),
|
LineNumber(u32),
|
||||||
Assign { target: Expr, value: Expr, pos: SourcePos },
|
Assign { target: Expr, value: Expr, pos: SourcePos },
|
||||||
Print {
|
Print {
|
||||||
|
/// LPRINT (Druckerausgabe) statt PRINT.
|
||||||
|
printer: bool,
|
||||||
file: Option<Expr>,
|
file: Option<Expr>,
|
||||||
using: Option<Expr>,
|
using: Option<Expr>,
|
||||||
items: Vec<PrintItem>,
|
items: Vec<PrintItem>,
|
||||||
@@ -212,6 +247,17 @@ pub enum Stmt {
|
|||||||
System,
|
System,
|
||||||
Exit { kind: ExitKind, pos: SourcePos },
|
Exit { kind: ExitKind, pos: SourcePos },
|
||||||
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, 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 },
|
Erase { names: Vec<Expr>, pos: SourcePos },
|
||||||
ConstDecl { items: Vec<(String, Option<Suffix>, Expr)>, pos: SourcePos },
|
ConstDecl { items: Vec<(String, Option<Suffix>, Expr)>, pos: SourcePos },
|
||||||
DefType { ty: TypeName, ranges: Vec<(char, char)>, pos: SourcePos },
|
DefType { ty: TypeName, ranges: Vec<(char, char)>, pos: SourcePos },
|
||||||
@@ -234,9 +280,69 @@ pub enum Stmt {
|
|||||||
body: Expr,
|
body: Expr,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
},
|
},
|
||||||
/// Geparst, aber erst in Phase 3 implementiert (Datei-E/A u. ä.);
|
/// Blockform `DEF FNname(...) … END DEF`.
|
||||||
/// die Tokens der Anweisung wurden übersprungen.
|
DefFnBlock {
|
||||||
NotYetImplemented { keyword: String, pos: SourcePos },
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ pub enum NumValue {
|
|||||||
/// und -anweisungen sind KEINE Keywords, sondern Builtins der Semantik).
|
/// und -anweisungen sind KEINE Keywords, sondern Builtins der Semantik).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Kw {
|
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,
|
DefLng, DefSng, DefStr, Dim, Do, Double, Else, ElseIf, End, Eqv, Erase,
|
||||||
Error, Exit, For, Function, Gosub, Goto, If, Imp, Input, Integer, Is,
|
Error, Exit, For, Function, Gosub, Goto, If, Imp, Input, Integer, Is,
|
||||||
Let, Line, Local, Long, Loop, Mod, Next, Not, On, Option, Or, Print,
|
Let, Line, Local, Long, Loop, Mod, Next, Not, On, Option, Or, Print,
|
||||||
@@ -77,7 +77,8 @@ fn keyword(upper: &str) -> Option<Kw> {
|
|||||||
use Kw::*;
|
use Kw::*;
|
||||||
Some(match upper {
|
Some(match upper {
|
||||||
"AND" => And, "AS" => As, "CALL" => Call, "CASE" => Case,
|
"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,
|
"DECLARE" => Declare, "DEF" => Def, "DEFCUR" => DefCur,
|
||||||
"DEFDBL" => DefDbl, "DEFINT" => DefInt, "DEFLNG" => DefLng,
|
"DEFDBL" => DefDbl, "DEFINT" => DefInt, "DEFLNG" => DefLng,
|
||||||
"DEFSNG" => DefSng, "DEFSTR" => DefStr, "DIM" => Dim, "DO" => Do,
|
"DEFSNG" => DefSng, "DEFSTR" => DefStr, "DIM" => Dim, "DO" => Do,
|
||||||
@@ -113,11 +114,41 @@ pub enum TokenKind {
|
|||||||
Plus, Minus, Star, Slash, Backslash, Caret,
|
Plus, Minus, Star, Slash, Backslash, Caret,
|
||||||
Eq, Ne, Lt, Le, Gt, Ge,
|
Eq, Ne, Lt, Le, Gt, Ge,
|
||||||
LParen, RParen, Comma, Semicolon, Colon, Hash,
|
LParen, RParen, Comma, Semicolon, Colon, Hash,
|
||||||
|
/// Metabefehl `'$INCLUDE: 'datei''` (Pfad; leer = fehlerhafte Syntax).
|
||||||
|
MetaInclude(String),
|
||||||
|
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
|
||||||
|
MetaStatic,
|
||||||
|
MetaDynamic,
|
||||||
/// Ende einer logischen Zeile.
|
/// Ende einer logischen Zeile.
|
||||||
Eol,
|
Eol,
|
||||||
Eof,
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub kind: TokenKind,
|
pub kind: TokenKind,
|
||||||
@@ -171,7 +202,13 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match c {
|
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;
|
i += 1;
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
@@ -388,7 +425,11 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
if suffix.is_none() {
|
if suffix.is_none() {
|
||||||
if let Some(kw) = keyword(&name) {
|
if let Some(kw) = keyword(&name) {
|
||||||
if kw == Kw::Rem {
|
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 });
|
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -195,7 +195,23 @@ impl<'a> P<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.k() {
|
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) => {
|
TokenKind::Kw(Kw::Input) => {
|
||||||
self.advance();
|
self.advance();
|
||||||
self.parse_input(false, pos)
|
self.parse_input(false, pos)
|
||||||
@@ -511,16 +527,173 @@ impl<'a> P<'a> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
self.parse_assign_or_call(pos, true)
|
self.parse_assign_or_call(pos, true)
|
||||||
}
|
}
|
||||||
TokenKind::Kw(k @ (Kw::Open | Kw::Close | Kw::Write | Kw::Field
|
TokenKind::Kw(Kw::Open) => self.parse_open(pos),
|
||||||
| Kw::Get | Kw::Put | Kw::Seek | Kw::Lset | Kw::Rset)) => {
|
TokenKind::Kw(Kw::Close) => {
|
||||||
self.advance();
|
self.advance();
|
||||||
self.sync();
|
let mut files = Vec::new();
|
||||||
Some(Stmt::NotYetImplemented { keyword: format!("{k:?}").to_uppercase(), pos })
|
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) => {
|
TokenKind::Kw(Kw::Field) => {
|
||||||
// STATIC/SHARED-Deklaration in Prozeduren: wie DIM behandeln
|
|
||||||
self.advance();
|
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),
|
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> {
|
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
|
||||||
self.advance(); // PRINT
|
self.advance(); // PRINT bzw. LPRINT
|
||||||
let file = if self.eat(&TokenKind::Hash) {
|
let file = if !printer && self.eat(&TokenKind::Hash) {
|
||||||
let e = self.parse_expr()?;
|
let e = self.parse_expr()?;
|
||||||
self.eat(&TokenKind::Comma);
|
self.eat(&TokenKind::Comma);
|
||||||
Some(e)
|
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> {
|
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> {
|
fn parse_dim(&mut self, redim: bool, pos: SourcePos) -> Option<Stmt> {
|
||||||
let shared = self.eat_kw(Kw::Shared);
|
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();
|
let mut decls = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
let dpos = self.pos();
|
let dpos = self.pos();
|
||||||
@@ -875,10 +1183,15 @@ impl<'a> P<'a> {
|
|||||||
let mut ds = Vec::new();
|
let mut ds = Vec::new();
|
||||||
if !self.eat(&TokenKind::RParen) {
|
if !self.eat(&TokenKind::RParen) {
|
||||||
loop {
|
loop {
|
||||||
let a = self.parse_expr()?;
|
let a = match self.parse_expr() {
|
||||||
|
Some(e) => e,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
if self.eat_kw(Kw::To) {
|
if self.eat_kw(Kw::To) {
|
||||||
let b = self.parse_expr()?;
|
match self.parse_expr() {
|
||||||
ds.push((Some(a), b));
|
Some(b) => ds.push((Some(a), b)),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ds.push((None, a));
|
ds.push((None, a));
|
||||||
}
|
}
|
||||||
@@ -902,7 +1215,7 @@ impl<'a> P<'a> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Stmt::Dim { shared, redim, decls, pos })
|
decls
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_type_name(&mut self) -> Option<TypeName> {
|
fn parse_type_name(&mut self) -> Option<TypeName> {
|
||||||
@@ -1129,15 +1442,15 @@ impl<'a> P<'a> {
|
|||||||
let body = self.parse_expr()?;
|
let body = self.parse_expr()?;
|
||||||
Some(Stmt::DefFn { name, suffix, params, body, pos })
|
Some(Stmt::DefFn { name, suffix, params, body, pos })
|
||||||
} else {
|
} else {
|
||||||
// Blockform DEF FN … END DEF: noch nicht implementiert
|
// Blockform: DEF FNname(...) … [EXIT DEF] … END DEF
|
||||||
self.sync();
|
let body = self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::Def));
|
||||||
let stop = |p: &P| p.at_end_pair(Kw::Def);
|
|
||||||
let _ = self.parse_stmt_list(stop);
|
|
||||||
if self.at_end_pair(Kw::Def) {
|
if self.at_end_pair(Kw::Def) {
|
||||||
self.advance();
|
self.advance();
|
||||||
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();
|
self.sync();
|
||||||
return None;
|
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 …]]`
|
// Impliziter Aufruf: `name [arg [, arg …]]`
|
||||||
if let Expr::Name { name, suffix, args, .. } = target {
|
if let Expr::Name { name, suffix, args, .. } = target {
|
||||||
let mut call_args = args.unwrap_or_default();
|
let mut call_args = args.unwrap_or_default();
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
//! Semantische Analyse: Symboltabellen, implizite Deklaration,
|
//! Semantische Analyse: Symboltabellen, implizite Deklaration,
|
||||||
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung, Auflösung
|
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl.
|
||||||
//! Array-Index vs. Funktionsaufruf, Label-Prüfung, Builtin-Signaturen.
|
//! UDT-Feldtypen, Konstantenfaltung, Auflösung Array-Index vs.
|
||||||
//!
|
//! Funktionsaufruf, Label-Prüfung, Builtin-Signaturen sowie die
|
||||||
//! Erste Ausbaustufe (Phase 1): bewusst nachsichtig — lieber eine Prüfung
|
//! Compile-Zeit-Abweisung deklarierter Non-Features.
|
||||||
//! auslassen als falsche Fehler melden. `COMMON`, `SHARED`-Sichtbarkeit
|
|
||||||
//! über Prozedurgrenzen und UDT-Feldtypen folgen mit Phase 2/3.
|
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::lexer::Suffix;
|
use crate::lexer::Suffix;
|
||||||
use crate::{Diagnostic, SourcePos};
|
use crate::{Diagnostic, SourcePos};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Ty {
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
struct VarInfo {
|
struct VarInfo {
|
||||||
ty: Ty,
|
ty: Ty,
|
||||||
@@ -150,6 +156,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
|||||||
"LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
|
"LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
|
||||||
"CSRLIN" => (0, 0, &[], I),
|
"CSRLIN" => (0, 0, &[], I),
|
||||||
"POS" => (1, 1, &[N], I),
|
"POS" => (1, 1, &[N], I),
|
||||||
|
"SCREEN" => (2, 3, &[N, N, N], I),
|
||||||
"TAB" | "SPC" => (1, 1, &[N], St),
|
"TAB" | "SPC" => (1, 1, &[N], St),
|
||||||
"DATE$" | "TIME$" => (0, 0, &[], St),
|
"DATE$" | "TIME$" => (0, 0, &[], St),
|
||||||
"TIMER" => (0, 0, &[], Sg),
|
"TIMER" => (0, 0, &[], Sg),
|
||||||
@@ -165,6 +172,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
|||||||
"EOF" => (1, 1, &[N], I),
|
"EOF" => (1, 1, &[N], I),
|
||||||
"LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
|
"LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
|
||||||
"FREEFILE" => (0, 0, &[], I),
|
"FREEFILE" => (0, 0, &[], I),
|
||||||
|
"FILEATTR" => (2, 2, &[N, N], L),
|
||||||
"ENVIRON$" => (1, 1, &[A], St),
|
"ENVIRON$" => (1, 1, &[A], St),
|
||||||
"COMMAND$" => (0, 0, &[], St),
|
"COMMAND$" => (0, 0, &[], St),
|
||||||
"DOEVENTS" => (0, 0, &[], I),
|
"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
|
/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
|
||||||
/// unterstützt (Entscheidung 2026-09-02): Ablehnung bereits zur
|
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
|
||||||
/// Compile-Zeit statt Laufzeitfehler 73.
|
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
|
||||||
fn banned_feature(name: &str) -> bool {
|
fn banned_feature(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name,
|
name,
|
||||||
|
// Hardware-Nähe
|
||||||
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
|
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
|
||||||
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
|
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
|
||||||
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
|
| "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::*;
|
use ArgK::*;
|
||||||
Some(match name {
|
Some(match name {
|
||||||
"CLS" => (0, 1, &[N]),
|
"CLS" => (0, 1, &[N]),
|
||||||
"BEEP" | "DOEVENTS" => (0, 0, &[]),
|
"BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
|
||||||
"COLOR" => (0, 3, &[N, N, N]),
|
"COLOR" => (0, 3, &[N, N, N]),
|
||||||
"LOCATE" => (0, 5, &[N, N, N, N, N]),
|
"LOCATE" => (0, 5, &[N, N, N, N, N]),
|
||||||
"RANDOMIZE" => (0, 1, &[N]),
|
"RANDOMIZE" => (0, 1, &[N]),
|
||||||
"SLEEP" => (0, 1, &[N]),
|
"SLEEP" => (0, 1, &[N]),
|
||||||
"WIDTH" => (0, 2, &[N, N]),
|
"WIDTH" => (0, 2, &[N, N]),
|
||||||
"SOUND" => (2, 2, &[N, N]),
|
"SCREEN" => (1, 4, &[N, N, N, N]),
|
||||||
"SWAP" => (2, 2, &[A, A]),
|
"SWAP" => (2, 2, &[A, A]),
|
||||||
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
|
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
|
||||||
|
"FILES" => (0, 1, &[S]),
|
||||||
"SHELL" => (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]),
|
"ENVIRON" => (1, 1, &[S]),
|
||||||
"MSGBOX" => (1, 3, &[S, N, S]),
|
"MSGBOX" => (1, 3, &[S, N, S]),
|
||||||
"RESET" => (0, 0, &[]),
|
"SETUEVENT" => (0, 0, &[]),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -216,9 +235,11 @@ pub fn check(module: &Module) -> Vec<Diagnostic> {
|
|||||||
diags: Vec::new(),
|
diags: Vec::new(),
|
||||||
deftypes: [const { None }; 26],
|
deftypes: [const { None }; 26],
|
||||||
explicit: false,
|
explicit: false,
|
||||||
|
option_base: 0,
|
||||||
procs: HashMap::new(),
|
procs: HashMap::new(),
|
||||||
udts: HashSet::new(),
|
udts: HashMap::new(),
|
||||||
consts: HashMap::new(),
|
consts: HashMap::new(),
|
||||||
|
module_vars: HashMap::new(),
|
||||||
};
|
};
|
||||||
s.run(module);
|
s.run(module);
|
||||||
s.diags
|
s.diags
|
||||||
@@ -229,9 +250,15 @@ struct Sema {
|
|||||||
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
|
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
|
||||||
deftypes: [Option<Ty>; 26],
|
deftypes: [Option<Ty>; 26],
|
||||||
explicit: bool,
|
explicit: bool,
|
||||||
|
/// `OPTION BASE` (0 oder 1) — relevant für die Codegenerierung.
|
||||||
|
option_base: u8,
|
||||||
procs: HashMap<String, ProcInfo>,
|
procs: HashMap<String, ProcInfo>,
|
||||||
udts: HashSet<String>,
|
/// Benutzerdefinierte Typen mit Feldtypen.
|
||||||
consts: HashMap<String, Ty>,
|
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 {
|
impl Sema {
|
||||||
@@ -240,12 +267,24 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(&mut self, module: &Module) {
|
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 {
|
for stmt in &module.body {
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Declare { sig, .. } => self.register_proc(sig),
|
Stmt::Declare { sig, .. } => self.register_proc(sig),
|
||||||
Stmt::TypeDecl { name, .. } => {
|
Stmt::TypeDecl { name, fields, pos } => {
|
||||||
self.udts.insert(name.clone());
|
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
|
// Pass 2: Modulrumpf
|
||||||
let mut scope = Scope::default();
|
let mut scope = Scope::default();
|
||||||
collect_labels(&module.body, &mut scope);
|
collect_labels(&module.body, &mut scope);
|
||||||
let body_ref: Vec<&Stmt> = module.body.iter().collect();
|
for stmt in &module.body {
|
||||||
for stmt in body_ref {
|
self.check_stmt(stmt, &mut scope);
|
||||||
self.check_stmt(stmt, &mut scope, None);
|
|
||||||
}
|
}
|
||||||
|
self.module_vars = scope.vars.clone();
|
||||||
|
|
||||||
// Pass 3: Prozedurrümpfe
|
// Pass 3: Prozedurrümpfe
|
||||||
for proc in &module.procs {
|
for proc in &module.procs {
|
||||||
@@ -269,23 +308,20 @@ impl Sema {
|
|||||||
for p in &proc.sig.params {
|
for p in &proc.sig.params {
|
||||||
let ty = self.param_ty(p);
|
let ty = self.param_ty(p);
|
||||||
let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
|
let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
|
||||||
pscope.vars.insert(
|
pscope
|
||||||
key,
|
.vars
|
||||||
VarInfo { ty, array: p.array, explicit: true },
|
.insert(key, VarInfo { ty, array: p.array, explicit: true });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Funktionsname als Rückgabe-Variable
|
// Funktionsname als Rückgabe-Variable
|
||||||
if proc.sig.kind == ProcKind::Function {
|
if proc.sig.kind == ProcKind::Function {
|
||||||
let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
|
let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
|
||||||
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
|
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 {
|
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::Function => self.name_ty(&sig.name, &sig.suffix),
|
||||||
ProcKind::Sub => Ty::Unknown,
|
ProcKind::Sub => Ty::Unknown,
|
||||||
};
|
};
|
||||||
let params = sig
|
let params = sig.params.iter().map(|p| (self.param_ty(p), p.array)).collect();
|
||||||
.params
|
self.procs
|
||||||
.iter()
|
.insert(sig.name.clone(), ProcInfo { kind: sig.kind, ret, params });
|
||||||
.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).
|
/// 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(
|
fn resolve_var(
|
||||||
&mut self,
|
&mut self,
|
||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
@@ -375,12 +430,19 @@ impl Sema {
|
|||||||
if let Some(v) = scope.vars.get(&key) {
|
if let Some(v) = scope.vars.get(&key) {
|
||||||
return v.ty.clone();
|
return v.ty.clone();
|
||||||
}
|
}
|
||||||
// UDT-Feldzugriff `a.b`: Basis auflösen
|
// UDT-Feldzugriff: Basis vor dem ersten Punkt auflösen
|
||||||
if let Some(base) = name.split('.').next() {
|
if name.contains('.') {
|
||||||
if base != name {
|
let parts: Vec<&str> = name.split('.').collect();
|
||||||
let base_as = format!("{base}\u{1}AS");
|
let base = parts[0];
|
||||||
if scope.vars.contains_key(&base_as) {
|
let base_as = format!("{base}\u{1}AS");
|
||||||
return Ty::Unknown; // Feldtypen: Phase 2
|
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);
|
return self.name_ty(name, suffix);
|
||||||
}
|
}
|
||||||
let ty = self.name_ty(name, suffix);
|
let ty = self.name_ty(name, suffix);
|
||||||
scope.vars.insert(
|
scope
|
||||||
key,
|
.vars
|
||||||
VarInfo { ty: ty.clone(), array, explicit: false },
|
.insert(key, VarInfo { ty: ty.clone(), array, explicit: false });
|
||||||
);
|
|
||||||
ty
|
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 -------------------------------------------------------
|
// ---- 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 {
|
match stmt {
|
||||||
Stmt::Label(_) | Stmt::LineNumber(_) | Stmt::End | Stmt::StopStmt
|
Stmt::Label(_) | Stmt::LineNumber(_) | Stmt::End | Stmt::StopStmt
|
||||||
| Stmt::System | Stmt::Data { .. } | Stmt::Exit { .. }
|
| Stmt::System | Stmt::Data { .. } | Stmt::Exit { .. }
|
||||||
| Stmt::NotYetImplemented { .. } => {}
|
| Stmt::Include { .. } | Stmt::MetaArrays { .. } => {}
|
||||||
|
|
||||||
Stmt::Assign { target, value, pos } => {
|
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 tt = self.lvalue_ty(target, scope);
|
||||||
let vt = self.expr_ty(value, scope);
|
let vt = self.expr_ty(value, scope);
|
||||||
self.check_assign(&tt, &vt, *pos);
|
self.check_assign(&tt, &vt, *pos);
|
||||||
@@ -433,21 +582,21 @@ impl Sema {
|
|||||||
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
|
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
|
||||||
self.want_num(cond, scope);
|
self.want_num(cond, scope);
|
||||||
for s in then_body {
|
for s in then_body {
|
||||||
self.check_stmt(s, scope, fn_name);
|
self.check_stmt(s, scope);
|
||||||
}
|
}
|
||||||
for (c, b) in elseifs {
|
for (c, b) in elseifs {
|
||||||
self.want_num(c, scope);
|
self.want_num(c, scope);
|
||||||
for s in b {
|
for s in b {
|
||||||
self.check_stmt(s, scope, fn_name);
|
self.check_stmt(s, scope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(b) = else_body {
|
if let Some(b) = else_body {
|
||||||
for s in b {
|
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);
|
let st = self.expr_ty(expr, scope);
|
||||||
for arm in arms {
|
for arm in arms {
|
||||||
for spec in &arm.specs {
|
for spec in &arm.specs {
|
||||||
@@ -466,7 +615,7 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for s in &arm.body {
|
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);
|
self.want_num(s, scope);
|
||||||
}
|
}
|
||||||
for s in body {
|
for s in body {
|
||||||
self.check_stmt(s, scope, fn_name);
|
self.check_stmt(s, scope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::DoLoop { pre, post, body, .. } => {
|
Stmt::DoLoop { pre, post, body, .. } => {
|
||||||
@@ -492,13 +641,13 @@ impl Sema {
|
|||||||
self.want_num(c, scope);
|
self.want_num(c, scope);
|
||||||
}
|
}
|
||||||
for s in body {
|
for s in body {
|
||||||
self.check_stmt(s, scope, fn_name);
|
self.check_stmt(s, scope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::While { cond, body, .. } => {
|
Stmt::While { cond, body, .. } => {
|
||||||
self.want_num(cond, scope);
|
self.want_num(cond, scope);
|
||||||
for s in body {
|
for s in body {
|
||||||
self.check_stmt(s, scope, fn_name);
|
self.check_stmt(s, scope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::Goto { target, pos } | Stmt::Gosub { target, pos } => {
|
Stmt::Goto { target, pos } | Stmt::Gosub { target, pos } => {
|
||||||
@@ -531,6 +680,36 @@ impl Sema {
|
|||||||
self.declare(d, *redim, scope);
|
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, .. } => {
|
Stmt::Erase { names, .. } => {
|
||||||
for n in names {
|
for n in names {
|
||||||
if let Expr::Name { name, suffix, pos, .. } = n {
|
if let Expr::Name { name, suffix, pos, .. } = n {
|
||||||
@@ -538,10 +717,22 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::ConstDecl { items, .. } => {
|
Stmt::ConstDecl { items, pos } => {
|
||||||
for (name, _suffix, value) in items {
|
for (name, suffix, value) in items {
|
||||||
let ty = self.expr_ty(value, scope);
|
let ty = match suffix {
|
||||||
self.consts.insert(name.clone(), ty);
|
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, .. } => {
|
Stmt::DefType { ty, ranges, .. } => {
|
||||||
@@ -555,22 +746,12 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::OptionStmt { kind, .. } => {
|
Stmt::OptionStmt { kind, .. } => match kind {
|
||||||
if *kind == OptionKind::Explicit {
|
OptionKind::Explicit => self.explicit = true,
|
||||||
self.explicit = true;
|
OptionKind::Base(b) => self.option_base = *b,
|
||||||
}
|
},
|
||||||
// OPTION BASE: relevant erst für die Array-Semantik (Phase 2)
|
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
|
||||||
}
|
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
|
||||||
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::Call { name, args, pos, .. } => {
|
Stmt::Call { name, args, pos, .. } => {
|
||||||
self.check_call(name, args, scope, *pos);
|
self.check_call(name, args, scope, *pos);
|
||||||
}
|
}
|
||||||
@@ -584,33 +765,150 @@ impl Sema {
|
|||||||
self.check_label(t, scope, *pos);
|
self.check_label(t, scope, *pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::DefFn { name, suffix, params, body, pos: _ } => {
|
Stmt::DefFn { name, suffix, params, body, .. } => {
|
||||||
let ret = self.name_ty(name, suffix);
|
self.register_def_fn(name, suffix, params);
|
||||||
let param_infos = params
|
let mut fscope = self.def_fn_scope(name, suffix, 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 });
|
|
||||||
}
|
|
||||||
self.expr_ty(body, &mut fscope);
|
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) {
|
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope) {
|
||||||
let ty = if let Some(t) = &d.as_type {
|
let ty = if let Some(t) = &d.as_type {
|
||||||
if let TypeName::Udt(n) = t {
|
if let TypeName::Udt(n) = t {
|
||||||
if !self.udts.contains(n) {
|
if !self.udts.contains_key(n) {
|
||||||
self.err(d.pos, "Type not defined");
|
self.err(d.pos, "Type not defined");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,13 +1005,13 @@ impl Sema {
|
|||||||
self.err(pos, "Argument-count mismatch");
|
self.err(pos, "Argument-count mismatch");
|
||||||
}
|
}
|
||||||
// Sonderfall INSTR([start%,] s$, such$)
|
// 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() {
|
for (i, a) in args.iter().enumerate() {
|
||||||
if matches!(a, Expr::Missing) {
|
if matches!(a, Expr::Missing) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let at = self.expr_ty(a, scope);
|
let at = self.expr_ty(a, scope);
|
||||||
let kind = if insts_with_start {
|
let kind = if instr_with_start {
|
||||||
match i {
|
match i {
|
||||||
0 => ArgK::N,
|
0 => ArgK::N,
|
||||||
_ => ArgK::S,
|
_ => ArgK::S,
|
||||||
@@ -868,7 +1166,7 @@ impl Sema {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. Konstante
|
// 1. Konstante
|
||||||
if let Some(t) = self.consts.get(name).cloned() {
|
if let Some((t, _)) = self.consts.get(name).cloned() {
|
||||||
if args.is_some() {
|
if args.is_some() {
|
||||||
self.err(pos, "Syntax error");
|
self.err(pos, "Syntax error");
|
||||||
}
|
}
|
||||||
@@ -895,7 +1193,7 @@ impl Sema {
|
|||||||
self.err(pos, "Duplicate definition");
|
self.err(pos, "Duplicate definition");
|
||||||
return Ty::Unknown;
|
return Ty::Unknown;
|
||||||
}
|
}
|
||||||
// 3. Nicht unterstützte Hardware-Features → Compile-Fehler
|
// 3. Nicht unterstützte Features → Compile-Fehler
|
||||||
if banned_feature(&full_name) {
|
if banned_feature(&full_name) {
|
||||||
for a in idx {
|
for a in idx {
|
||||||
self.expr_ty(a, scope);
|
self.expr_ty(a, scope);
|
||||||
@@ -908,7 +1206,7 @@ impl Sema {
|
|||||||
self.check_builtin_args(&full_name, idx, min, max, spec, scope, pos);
|
self.check_builtin_args(&full_name, idx, min, max, spec, scope, pos);
|
||||||
return ret_ty(ret);
|
return ret_ty(ret);
|
||||||
}
|
}
|
||||||
// 4. FUNCTION / DEF FN
|
// 5. FUNCTION / DEF FN
|
||||||
if let Some(info) = self.procs.get(name).cloned() {
|
if let Some(info) = self.procs.get(name).cloned() {
|
||||||
if info.kind == ProcKind::Function {
|
if info.kind == ProcKind::Function {
|
||||||
if idx.len() != info.params.len() {
|
if idx.len() != info.params.len() {
|
||||||
@@ -922,7 +1220,7 @@ impl Sema {
|
|||||||
self.err(pos, "Duplicate definition");
|
self.err(pos, "Duplicate definition");
|
||||||
return Ty::Unknown;
|
return Ty::Unknown;
|
||||||
}
|
}
|
||||||
// 5. Implizites Array (klassisch: DIM x(10) implizit)
|
// 6. Implizites Array (klassisch: DIM x(10) implizit)
|
||||||
for a in idx {
|
for a in idx {
|
||||||
self.want_num(a, scope);
|
self.want_num(a, scope);
|
||||||
}
|
}
|
||||||
@@ -932,10 +1230,9 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
let ty = self.name_ty(name, suffix);
|
let ty = self.name_ty(name, suffix);
|
||||||
let key = self.var_key(name, suffix, false);
|
let key = self.var_key(name, suffix, false);
|
||||||
scope.vars.insert(
|
scope
|
||||||
key,
|
.vars
|
||||||
VarInfo { ty: ty.clone(), array: true, explicit: false },
|
.insert(key, VarInfo { ty: ty.clone(), array: true, explicit: false });
|
||||||
);
|
|
||||||
ty
|
ty
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -945,6 +1242,10 @@ impl Sema {
|
|||||||
let declared = scope.vars.contains_key(&key)
|
let declared = scope.vars.contains_key(&key)
|
||||||
|| (suffix.is_none() && scope.vars.contains_key(&as_key));
|
|| (suffix.is_none() && scope.vars.contains_key(&as_key));
|
||||||
if !declared {
|
if !declared {
|
||||||
|
if banned_feature(&full_name) {
|
||||||
|
self.err(pos, "Feature unavailable");
|
||||||
|
return Ty::Unknown;
|
||||||
|
}
|
||||||
if let Some((0, _, _, ret)) = builtin_fn(&full_name) {
|
if let Some((0, _, _, ret)) = builtin_fn(&full_name) {
|
||||||
return ret_ty(ret);
|
return ret_ty(ret);
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1324,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deftype_regeln() {
|
fn deftype_regeln() {
|
||||||
// DEFSTR macht s zum String
|
|
||||||
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
|
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
|
||||||
assert!(diags("DEFINT I\ni = 5").is_empty());
|
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("POKE 100, 1").contains(&"Feature unavailable".to_string()));
|
||||||
assert!(diags("x = PEEK(100)").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("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]
|
#[test]
|
||||||
@@ -1058,9 +1362,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn funktionsaufruf_und_rueckgabe() {
|
fn funktionsaufruf_und_rueckgabe() {
|
||||||
let d = diags(
|
let d = diags("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
|
||||||
"FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)",
|
|
||||||
);
|
|
||||||
assert!(d.is_empty(), "{d:?}");
|
assert!(d.is_empty(), "{d:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1081,4 +1383,50 @@ mod tests {
|
|||||||
assert!(diags("x$ = INKEY$").is_empty());
|
assert!(diags("x$ = INKEY$").is_empty());
|
||||||
assert!(diags("t! = TIMER").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,
|
**Shift+F1** Using Help. Hyperlinks als ◄Thema►, Tab/Shift+Tab springt,
|
||||||
Enter folgt, **Alt+F1** zurück (20 Ebenen), Ctrl+F1 nächstes Topic,
|
Enter folgt, **Alt+F1** zurück (20 Ebenen), Ctrl+F1 nächstes Topic,
|
||||||
Esc schließt. Hilfe im eigenen Fenster; Index, Contents, Keyboard-Guide,
|
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)
|
## 8. Statuszeile (Konfidenz: sicher; Caps/Num: unklar)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user