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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user