Phase 1 vollstaendig abgeschlossen; neue Vorgaben verankert

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

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

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

View File

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