Files
TerminalBasic/crates/tb-frontend/src/parser.rs

2483 lines
82 KiB
Rust

//! Parser: baut aus dem Tokenstrom den AST.
//!
//! Zeilenorientiert und fehlertolerant: Bei einem Fehler wird die Diagnose
//! gesammelt und bis zum nächsten Anweisungsende (`:` oder Zeilenende)
//! synchronisiert — wie die zeilenweise Prüfung des IDE-Vorbilds.
use crate::ast::*;
use crate::lexer::{Kw, NumValue, Token, TokenKind};
use crate::{Diagnostic, SourcePos};
pub struct ParseOutput {
/// Ausschließlich fehlende Blockabschlüsse am Dateiende.
pub incomplete: bool,
pub module: Module,
pub diagnostics: Vec<Diagnostic>,
}
pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
let mut p = P {
toks: tokens,
i: 0,
diags: Vec::new(),
at_line_start: true,
incomplete_errors: 0,
};
let mut body = Vec::new();
let mut procs = Vec::new();
loop {
p.skip_seps();
match p.k() {
TokenKind::Eof => break,
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function) => {
if let Some(proc) = p.parse_proc(false) {
procs.push(proc);
}
}
TokenKind::Kw(Kw::Static)
if matches!(
p.k_at(1),
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function)
) =>
{
p.advance();
if let Some(proc) = p.parse_proc(true) {
procs.push(proc);
}
}
_ => {
let before = p.i;
if let Some(s) = p.parse_statement() {
body.push(s);
}
if p.i == before {
p.advance(); // Notbremse gegen Endlosschleifen
}
}
}
}
ParseOutput {
incomplete: !p.diags.is_empty() && p.incomplete_errors == p.diags.len(),
module: Module {
name: module_name.to_string(),
body,
procs,
},
diagnostics: p.diags,
}
}
struct P<'a> {
toks: &'a [Token],
i: usize,
diags: Vec<Diagnostic>,
at_line_start: bool,
incomplete_errors: usize,
}
impl<'a> P<'a> {
fn k(&self) -> TokenKind {
self.toks[self.i].kind.clone()
}
fn k_at(&self, off: usize) -> TokenKind {
let j = (self.i + off).min(self.toks.len() - 1);
self.toks[j].kind.clone()
}
fn pos(&self) -> SourcePos {
self.toks[self.i].pos
}
fn advance(&mut self) {
if self.i < self.toks.len() - 1 {
self.at_line_start = matches!(self.toks[self.i].kind, TokenKind::Eol);
self.i += 1;
}
}
fn eat(&mut self, kind: &TokenKind) -> bool {
if &self.k() == kind {
self.advance();
true
} else {
false
}
}
fn eat_kw(&mut self, kw: Kw) -> bool {
self.eat(&TokenKind::Kw(kw))
}
fn is_kw(&self, kw: Kw) -> bool {
self.k() == TokenKind::Kw(kw)
}
fn err(&mut self, msg: impl Into<String>) {
let pos = self.pos();
let msg = msg.into();
if matches!(self.k(), TokenKind::Eof)
&& matches!(
msg.as_str(),
"Expected: END SUB"
| "Expected: END FUNCTION"
| "Expected: END IF"
| "Expected: END SELECT"
| "Expected: END TYPE"
| "Expected: END DEF"
| "Expected: NEXT"
| "Expected: LOOP"
| "Expected: WEND"
)
{
self.incomplete_errors += 1;
}
self.diags.push(Diagnostic {
file: None,
pos,
message: msg,
});
}
fn expect_kw(&mut self, kw: Kw, what: &str) -> bool {
if self.eat_kw(kw) {
true
} else {
self.err(format!("Expected: {what}"));
false
}
}
/// Bis zum Anweisungsende überspringen (Fehler-Synchronisation).
fn sync(&mut self) {
while !self.at_stmt_end() {
self.advance();
}
}
fn at_stmt_end(&self) -> bool {
matches!(
self.k(),
TokenKind::Colon
| TokenKind::Eol
| TokenKind::Eof
| TokenKind::Kw(Kw::Else)
| TokenKind::Kw(Kw::ElseIf)
)
}
/// Anweisungstrenner (`:`/Zeilenende) überspringen.
fn skip_seps(&mut self) {
while matches!(self.k(), TokenKind::Colon | TokenKind::Eol) {
self.advance();
}
}
// ---- Blöcke -----------------------------------------------------------
fn at_end_pair(&self, kw: Kw) -> bool {
self.is_kw(Kw::End) && self.k_at(1) == TokenKind::Kw(kw)
}
fn parse_stmt_list(&mut self, stop: impl Fn(&P) -> bool) -> Vec<Stmt> {
let mut out = Vec::new();
loop {
self.skip_seps();
if matches!(self.k(), TokenKind::Eof) || stop(self) {
break;
}
let before = self.i;
if let Some(s) = self.parse_statement() {
out.push(s);
}
if self.i == before {
self.advance();
}
}
out
}
/// Anweisungen auf derselben Zeile (einzeiliges IF): bis Zeilenende
/// bzw. ELSE.
fn parse_inline_stmts(&mut self) -> Vec<Stmt> {
let mut out = Vec::new();
loop {
match self.k() {
TokenKind::Eol | TokenKind::Eof | TokenKind::Kw(Kw::Else) => break,
TokenKind::Colon => {
self.advance();
continue;
}
_ => {}
}
let before = self.i;
if let Some(s) = self.parse_statement() {
out.push(s);
}
if self.i == before {
self.advance();
}
}
out
}
// ---- Anweisungen ------------------------------------------------------
fn parse_statement(&mut self) -> Option<Stmt> {
let pos = self.pos();
// Zeilennummern und Labels nur am Zeilenanfang
if self.at_line_start {
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
if n >= 0 {
self.advance();
return Some(Stmt::LineNumber(n as u32, pos));
}
}
if let TokenKind::Num(NumValue::Long(n)) = self.k() {
if n >= 0 {
self.advance();
return Some(Stmt::LineNumber(n as u32, pos));
}
}
if let TokenKind::Ident { name, suffix: None } = self.k() {
if self.k_at(1) == TokenKind::Colon {
self.advance();
self.advance();
return Some(Stmt::Label(name));
}
}
}
match self.k() {
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::MetaForm => {
self.advance();
Some(Stmt::MetaForm { pos })
}
TokenKind::Kw(Kw::Print) => self.parse_print(pos, false),
TokenKind::Kw(Kw::Input) => {
self.advance();
self.parse_input(false, pos)
}
TokenKind::Kw(Kw::Line) => {
self.advance();
if self.k() == TokenKind::Kw(Kw::Input) {
self.advance();
self.parse_input(true, pos)
} else {
self.parse_graphics_line(pos)
}
}
TokenKind::Kw(Kw::If) => self.parse_if(pos),
TokenKind::Kw(Kw::Select) => self.parse_select(pos),
TokenKind::Kw(Kw::For) => self.parse_for(pos),
TokenKind::Kw(Kw::Do) => self.parse_do(pos),
TokenKind::Kw(Kw::While) => self.parse_while(pos),
TokenKind::Kw(Kw::Goto) => {
self.advance();
let target = self.parse_label_ref()?;
Some(Stmt::Goto { target, pos })
}
TokenKind::Kw(Kw::Gosub) => {
self.advance();
let target = self.parse_label_ref()?;
Some(Stmt::Gosub { target, pos })
}
TokenKind::Kw(Kw::Return) => {
self.advance();
let target = if self.at_stmt_end() {
None
} else {
self.parse_label_ref()
};
Some(Stmt::Return { target, pos })
}
TokenKind::Kw(Kw::On) => self.parse_on(pos),
TokenKind::Kw(Kw::Resume) => {
self.advance();
let kind = if self.eat_kw(Kw::Next) {
ResumeKind::Next
} else if self.at_stmt_end() {
ResumeKind::Retry
} else if self.k() == TokenKind::Num(NumValue::Int(0)) {
self.advance();
ResumeKind::Retry
} else {
ResumeKind::Label(self.parse_label_ref()?)
};
Some(Stmt::Resume { kind, pos })
}
TokenKind::Kw(Kw::Error) => {
self.advance();
let code = self.parse_expr()?;
Some(Stmt::ErrorStmt { code, pos })
}
TokenKind::Kw(Kw::End) => {
if matches!(
self.k_at(1),
TokenKind::Kw(Kw::If)
| TokenKind::Kw(Kw::Select)
| TokenKind::Kw(Kw::Sub)
| TokenKind::Kw(Kw::Function)
| TokenKind::Kw(Kw::Type)
| TokenKind::Kw(Kw::Def)
) {
self.err("Syntax error (END without matching block)");
self.advance();
self.advance();
return None;
}
self.advance();
Some(Stmt::End(pos))
}
TokenKind::Kw(Kw::Stop) => {
self.advance();
Some(Stmt::StopStmt(pos))
}
TokenKind::Kw(Kw::System) => {
self.advance();
Some(Stmt::System(pos))
}
TokenKind::Kw(Kw::Exit) => {
self.advance();
let kind = if self.eat_kw(Kw::For) {
ExitKind::For
} else if self.eat_kw(Kw::Do) {
ExitKind::Do
} else if self.eat_kw(Kw::Sub) {
ExitKind::Sub
} else if self.eat_kw(Kw::Function) {
ExitKind::Function
} else if self.eat_kw(Kw::Def) {
ExitKind::Def
} else {
self.err("Expected: FOR, DO, SUB, FUNCTION or DEF");
self.sync();
return None;
};
Some(Stmt::Exit { kind, pos })
}
TokenKind::Kw(Kw::Dim) => {
self.advance();
self.parse_dim(false, pos)
}
TokenKind::Kw(Kw::ReDim) => {
self.advance();
self.parse_dim(true, pos)
}
TokenKind::Kw(Kw::Erase) => {
self.advance();
let mut names = Vec::new();
while let Some(e) = self.parse_name_only() {
names.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::Erase { names, pos })
}
TokenKind::Kw(Kw::Const) => {
self.advance();
let mut items = Vec::new();
loop {
let (name, suffix) = match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
(name, suffix)
}
_ => {
self.err("Expected: identifier");
self.sync();
break;
}
};
if !self.eat(&TokenKind::Eq) {
self.err("Expected: =");
self.sync();
break;
}
let value = self.parse_expr()?;
items.push((name, suffix, value));
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::ConstDecl { items, pos })
}
TokenKind::Kw(
k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng | Kw::DefDbl | Kw::DefStr | Kw::DefCur),
) => {
self.advance();
let ty = match k {
Kw::DefInt => TypeName::Integer,
Kw::DefLng => TypeName::Long,
Kw::DefSng => TypeName::Single,
Kw::DefDbl => TypeName::Double,
Kw::DefStr => TypeName::Str,
_ => TypeName::Currency,
};
let mut ranges = Vec::new();
loop {
let a = match self.k() {
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
_ => {
self.err("Expected: letter");
self.sync();
break;
}
};
let b = if self.eat(&TokenKind::Minus) {
match self.k() {
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
_ => {
self.err("Expected: letter");
a
}
}
} else {
a
};
ranges.push((a, b));
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::DefType { ty, ranges, pos })
}
TokenKind::Kw(Kw::Option) => {
self.advance();
match self.k() {
TokenKind::Ident { name, suffix: None } if name == "EXPLICIT" => {
self.advance();
Some(Stmt::OptionStmt {
kind: OptionKind::Explicit,
pos,
})
}
TokenKind::Ident { name, suffix: None } if name == "BASE" => {
self.advance();
let base = match self.k() {
TokenKind::Num(NumValue::Int(n @ (0 | 1))) => {
self.advance();
n as u8
}
_ => {
self.err("Expected: 0 or 1");
self.sync();
0
}
};
Some(Stmt::OptionStmt {
kind: OptionKind::Base(base),
pos,
})
}
_ => {
self.err("Expected: BASE or EXPLICIT");
self.sync();
None
}
}
}
TokenKind::Kw(Kw::Type) => self.parse_type_decl(pos),
TokenKind::Kw(Kw::Declare) => {
self.advance();
let sig = self.parse_proc_sig()?;
Some(Stmt::Declare { sig, pos })
}
TokenKind::Kw(Kw::Call) => {
self.advance();
match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
let args = if self.eat(&TokenKind::LParen) {
let a = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen);
a
} else {
Vec::new()
};
Some(Stmt::Call {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: identifier");
self.sync();
None
}
}
}
TokenKind::Kw(Kw::Data) => {
self.advance();
// Der Lexer liefert den Rohtext; hier wird nur an Kommas
// außerhalb von Anführungszeichen getrennt. Unquotierte
// Elemente verlieren nur den Leerraum an den Rändern.
let roh = match self.k() {
TokenKind::DataRaw(t) => {
self.advance();
t
}
_ => String::new(),
};
let mut items = Vec::new();
let mut cur = String::new();
let mut in_quote = false;
let mut quotiert = false;
for c in roh.chars() {
match c {
'"' if !in_quote && cur.trim().is_empty() => {
in_quote = true;
quotiert = true;
cur.clear();
}
'"' if in_quote => in_quote = false,
',' if !in_quote => {
items.push(if quotiert {
std::mem::take(&mut cur)
} else {
std::mem::take(&mut cur).trim().to_string()
});
quotiert = false;
}
_ => cur.push(c),
}
}
items.push(if quotiert {
cur
} else {
cur.trim().to_string()
});
Some(Stmt::Data { items, pos })
}
TokenKind::Kw(Kw::Read) => {
self.advance();
let mut vars = Vec::new();
while let Some(e) = self.parse_name_ref() {
vars.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::ReadStmt { vars, pos })
}
TokenKind::Kw(Kw::Restore) => {
self.advance();
let target = if self.at_stmt_end() {
None
} else {
self.parse_label_ref()
};
Some(Stmt::Restore { target, pos })
}
TokenKind::Kw(Kw::Def) => self.parse_def_fn(pos),
TokenKind::Kw(Kw::Let) => {
self.advance();
self.parse_assign_or_call(pos, true)
}
TokenKind::Kw(Kw::Open) => self.parse_open(pos),
TokenKind::Kw(Kw::Close) => {
self.advance();
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::Field) => {
self.advance();
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(format!(
"Feature unavailable: {}",
if k == Kw::Get { "GET" } else { "PUT" }
));
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 == "VIEW" => self.parse_graphics_view(pos),
TokenKind::Ident {
ref name,
suffix: None,
} if name == "PAINT" => self.parse_graphics_paint(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,
})
}
// ISAM-Anweisungen: `NAME [#]n [, arg …]`. Sie unterscheiden sich
// von einem gewöhnlichen impliziten Aufruf nur durch das
// erlaubte `#` vor der Dateinummer; alles Weitere (Signatur,
// Absenkung) läuft über die Builtin-Anweisungstabelle.
// `=` und `(` schließen die Anweisungsdeutung aus: dann ist der
// Name eine Variable bzw. ein Array (`DELETE = 1`, `DELETE(2) = 1`).
// Keine ISAM-Syntaxform beginnt mit einer Klammer.
TokenKind::Ident {
ref name,
suffix: None,
} if ist_isam_anweisung(name)
&& self.k_at(1) != TokenKind::Eq
&& self.k_at(1) != TokenKind::LParen =>
{
self.parse_isam(name.clone(), pos)
}
TokenKind::Ident { .. } => self.parse_assign_or_call(pos, false),
_ => {
self.err("Syntax error");
self.sync();
None
}
}
}
/// ISAM-Anweisung: `NAME [#]dateinummer [, argument …]`.
/// Syntaxformen siehe `openspec/changes/phase-3-isam/umfang-und-signaturen.md`.
fn parse_isam(&mut self, name: String, pos: SourcePos) -> Option<Stmt> {
self.advance();
// `ROLLBACK ALL` — hier ist `ALL` Schlüsselwort, kein Bezeichner.
// Es senkt auf die Sentinel-Kennung ab, damit `ROLLBACK` eine
// einzige Signatur behält.
if name == "ROLLBACK" {
if let TokenKind::Ident {
name: ref w,
suffix: None,
} = self.k()
{
if w == "ALL" {
self.advance();
let args = vec![Expr::LongLit(ROLLBACK_ALL, pos)];
return Some(Stmt::Call {
name,
suffix: None,
args,
pos,
});
}
}
}
self.eat(&TokenKind::Hash);
let args = if self.at_stmt_end() {
Vec::new()
} else {
self.parse_arg_list_to_stmt_end()
};
Some(Stmt::Call {
name,
suffix: None,
args,
pos,
})
}
fn parse_graphics_point(&mut self) -> Option<(Expr, Expr)> {
if !self.eat(&TokenKind::LParen) {
self.err("Expected: (");
return None;
}
let x = self.parse_expr()?;
if !self.eat(&TokenKind::Comma) {
self.err("Expected: ,");
return None;
}
let y = self.parse_expr()?;
if !self.eat(&TokenKind::RParen) {
self.err("Expected: )");
return None;
}
Some((x, y))
}
fn parse_graphics_line(&mut self, pos: SourcePos) -> Option<Stmt> {
let from = if self.eat(&TokenKind::Minus) {
None
} else {
let point = self.parse_graphics_point()?;
if !self.eat(&TokenKind::Minus) {
self.err("Expected: -");
return None;
}
Some(point)
};
let relative = self.eat_kw(Kw::Step);
let to = self.parse_graphics_point()?;
let mut color = None;
let mut fill = 0;
if self.eat(&TokenKind::Comma) {
if !matches!(
self.k(),
TokenKind::Comma | TokenKind::Colon | TokenKind::Eol | TokenKind::Eof
) {
color = Some(self.parse_expr()?);
}
if self.eat(&TokenKind::Comma) {
if let TokenKind::Ident { name, .. } = self.k() {
fill = match name.as_str() {
"B" => 1,
"BF" => 2,
_ => {
self.err("Expected: B or BF");
return None;
}
};
self.advance();
}
}
}
Some(Stmt::GraphicsLine {
from,
to,
relative,
color,
fill,
pos,
})
}
fn parse_graphics_paint(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance();
let point = self.parse_graphics_point()?;
let paint = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
let border = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
Some(Stmt::GraphicsPaint {
point,
paint,
border,
pos,
})
}
fn parse_graphics_view(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance();
if self.at_stmt_end() {
return Some(Stmt::GraphicsView {
rect: None,
fill: None,
border: None,
pos,
});
}
let (x1, y1) = self.parse_graphics_point()?;
if !self.eat(&TokenKind::Minus) {
self.err("Expected: -");
return None;
}
let (x2, y2) = self.parse_graphics_point()?;
let fill = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
let border = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
Some(Stmt::GraphicsView {
rect: Some((x1, y1, x2, y2)),
fill,
border,
pos,
})
}
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)
} else {
None
};
let using = if self.eat_kw(Kw::Using) {
let f = self.parse_expr()?;
if !self.eat(&TokenKind::Semicolon) {
self.err("Expected: ;");
}
Some(f)
} else {
None
};
let mut items = Vec::new();
loop {
match self.k() {
TokenKind::Comma => {
self.advance();
items.push(PrintItem::Comma);
}
TokenKind::Semicolon => {
self.advance();
items.push(PrintItem::Semicolon);
}
_ if self.at_stmt_end() => break,
_ => match self.parse_expr() {
Some(e) => items.push(PrintItem::Expr(e)),
None => break,
},
}
}
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> {
let keep_cursor = self.eat(&TokenKind::Semicolon);
let file = if self.eat(&TokenKind::Hash) {
let e = self.parse_expr()?;
self.eat(&TokenKind::Comma);
Some(e)
} else {
None
};
let mut prompt = None;
if file.is_none() {
if let TokenKind::Str(s) = self.k() {
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma) {
self.advance();
let with_question = self.k() == TokenKind::Semicolon;
self.advance();
prompt = Some((s, with_question));
}
}
}
let mut vars = Vec::new();
while let Some(e) = self.parse_name_ref() {
vars.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
}
if vars.is_empty() {
self.err("Expected: variable");
}
Some(Stmt::Input {
line,
file,
keep_cursor,
prompt,
vars,
pos,
})
}
fn parse_if(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // IF
let cond = self.parse_expr()?;
if !self.expect_kw(Kw::Then, "THEN") {
self.sync();
return None;
}
if matches!(self.k(), TokenKind::Eol) {
// Blockform
let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
let then_body = self.parse_stmt_list(stop);
let mut elseifs = Vec::new();
while self.is_kw(Kw::ElseIf) {
let elseif_pos = self.pos();
self.advance();
let c = self.parse_expr()?;
self.expect_kw(Kw::Then, "THEN");
let b = self.parse_stmt_list(stop);
elseifs.push((c, b, elseif_pos));
}
let else_body = if self.eat_kw(Kw::Else) {
Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If)))
} else {
None
};
if self.at_end_pair(Kw::If) {
self.advance();
self.advance();
} else {
self.err("Expected: END IF");
}
Some(Stmt::If {
cond,
then_body,
elseifs,
else_body,
pos,
})
} else {
// Einzeilig
let then_body = if let Some(n) = self.line_number_target() {
vec![Stmt::Goto { target: n, pos }]
} else {
self.parse_inline_stmts()
};
let else_body = if self.eat_kw(Kw::Else) {
if let Some(n) = self.line_number_target() {
Some(vec![Stmt::Goto { target: n, pos }])
} else {
Some(self.parse_inline_stmts())
}
} else {
None
};
Some(Stmt::If {
cond,
then_body,
elseifs: Vec::new(),
else_body,
pos,
})
}
}
fn line_number_target(&mut self) -> Option<LabelRef> {
match self.k() {
TokenKind::Num(NumValue::Int(n)) if n >= 0 => {
self.advance();
Some(LabelRef::Line(n as u32))
}
TokenKind::Num(NumValue::Long(n)) if n >= 0 => {
self.advance();
Some(LabelRef::Line(n as u32))
}
_ => None,
}
}
fn parse_select(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // SELECT
self.expect_kw(Kw::Case, "CASE");
let expr = self.parse_expr()?;
self.skip_seps();
let mut arms = Vec::new();
while self.is_kw(Kw::Case) {
let arm_pos = self.pos();
self.advance();
let mut specs = Vec::new();
if !self.eat_kw(Kw::Else) {
loop {
if self.eat_kw(Kw::Is) {
let op = match self.k() {
TokenKind::Eq => BinOp::Eq,
TokenKind::Ne => BinOp::Ne,
TokenKind::Lt => BinOp::Lt,
TokenKind::Le => BinOp::Le,
TokenKind::Gt => BinOp::Gt,
TokenKind::Ge => BinOp::Ge,
_ => {
self.err("Expected: relational operator");
BinOp::Eq
}
};
self.advance();
let e = self.parse_expr()?;
specs.push(CaseSpec::Is(op, e));
} else {
let a = self.parse_expr()?;
if self.eat_kw(Kw::To) {
let b = self.parse_expr()?;
specs.push(CaseSpec::Range(a, b));
} else {
specs.push(CaseSpec::Expr(a));
}
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
}
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select));
arms.push(CaseArm {
specs,
body,
pos: arm_pos,
});
}
if self.at_end_pair(Kw::Select) {
self.advance();
self.advance();
} else {
self.err("Expected: END SELECT");
}
Some(Stmt::Select { expr, arms, pos })
}
fn parse_for(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // FOR
let var = self.parse_name_only()?;
if !self.eat(&TokenKind::Eq) {
self.err("Expected: =");
self.sync();
return None;
}
let from = self.parse_expr()?;
self.expect_kw(Kw::To, "TO");
let to = self.parse_expr()?;
let step = if self.eat_kw(Kw::Step) {
Some(self.parse_expr()?)
} else {
None
};
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next));
let end_pos = self.pos();
if self.eat_kw(Kw::Next) {
// Optional: Zählvariable(n) hinter NEXT
while matches!(self.k(), TokenKind::Ident { .. }) {
self.advance();
if !self.eat(&TokenKind::Comma) {
break;
}
}
} else {
self.err("FOR without NEXT");
}
Some(Stmt::For {
var,
from,
to,
step,
body,
pos,
end_pos,
})
}
fn parse_do(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // DO
let pre = if self.eat_kw(Kw::While) {
Some((false, self.parse_expr()?))
} else if self.eat_kw(Kw::Until) {
Some((true, self.parse_expr()?))
} else {
None
};
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop));
let end_pos = self.pos();
let mut post = None;
if self.eat_kw(Kw::Loop) {
if self.eat_kw(Kw::While) {
post = Some((false, self.parse_expr()?));
} else if self.eat_kw(Kw::Until) {
post = Some((true, self.parse_expr()?));
}
} else {
self.err("DO without LOOP");
}
Some(Stmt::DoLoop {
pre,
post,
body,
pos,
end_pos,
})
}
fn parse_while(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // WHILE
let cond = self.parse_expr()?;
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend));
let end_pos = self.pos();
if !self.eat_kw(Kw::Wend) {
self.err("WHILE without WEND");
}
Some(Stmt::While {
cond,
body,
pos,
end_pos,
})
}
/// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor
/// unberührt, wenn hier kein Trap steht.
fn parse_trap_def(&mut self, pos: SourcePos) -> Option<Stmt> {
let start = self.i;
let TokenKind::Ident { name, suffix: None } = self.k() else {
return None;
};
if !matches!(
name.as_str(),
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "COM" | "PEN" | "PLAY" | "STRIG"
) {
return None;
}
let device = name.clone();
self.advance();
let index = if self.eat(&TokenKind::LParen) {
let e = self.parse_expr();
if e.is_none() || !self.eat(&TokenKind::RParen) {
self.i = start;
return None;
}
e
} else {
None
};
if !self.eat_kw(Kw::Gosub) {
// Doch kein Trap — z. B. `ON TIMER GOTO …` gibt es nicht, aber
// die Diagnose dafür gehört an die allgemeine Stelle.
self.i = start;
return None;
}
let target = self.parse_label_ref()?;
Some(Stmt::TrapDef {
device,
index,
target,
pos,
})
}
fn parse_on(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // ON
let local = self.eat_kw(Kw::Local);
if self.eat_kw(Kw::Error) {
if self.eat_kw(Kw::Goto) {
if self.k() == TokenKind::Num(NumValue::Int(0)) {
self.advance();
return Some(Stmt::OnError {
local,
action: OnErrorAction::Disable,
pos,
});
}
let target = self.parse_label_ref()?;
return Some(Stmt::OnError {
local,
action: OnErrorAction::Goto(target),
pos,
});
}
if self.eat_kw(Kw::Resume) {
self.expect_kw(Kw::Next, "NEXT");
return Some(Stmt::OnError {
local,
action: OnErrorAction::ResumeNext,
pos,
});
}
self.err("Expected: GOTO or RESUME NEXT");
self.sync();
return None;
}
if local {
self.err("Expected: ERROR");
self.sync();
return None;
}
// ON <quelle>[(n)] GOSUB ziel — klassische Ereignis-Traps (§8).
// Muss vor dem berechneten `ON ausdruck GOSUB` stehen, sonst
// verschwindet `ON TIMER(5) GOSUB Marke` still darin.
if let Some(st) = self.parse_trap_def(pos) {
return Some(st);
}
// ON ausdruck GOTO/GOSUB liste
let expr = self.parse_expr()?;
let gosub = if self.eat_kw(Kw::Goto) {
false
} else if self.eat_kw(Kw::Gosub) {
true
} else {
self.err("Expected: GOTO or GOSUB");
self.sync();
return None;
};
let mut targets = Vec::new();
while let Some(t) = self.parse_label_ref() {
targets.push(t);
if !self.eat(&TokenKind::Comma) {
break;
}
}
Some(Stmt::OnGoto {
expr,
targets,
gosub,
pos,
})
}
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();
let (name, suffix) = match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
(name, suffix)
}
_ => {
self.err("Expected: identifier");
self.sync();
break;
}
};
let dims = if self.eat(&TokenKind::LParen) {
let mut ds = Vec::new();
if !self.eat(&TokenKind::RParen) {
while let Some(a) = self.parse_expr() {
if self.eat_kw(Kw::To) {
match self.parse_expr() {
Some(b) => ds.push((Some(a), b)),
None => break,
}
} else {
ds.push((None, a));
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
self.eat(&TokenKind::RParen);
}
Some(ds)
} else {
None
};
let as_type = if self.eat_kw(Kw::As) {
self.parse_type_name()
} else {
None
};
decls.push(VarDecl {
name,
suffix,
dims,
as_type,
pos: dpos,
});
if !self.eat(&TokenKind::Comma) {
break;
}
}
decls
}
fn parse_type_name(&mut self) -> Option<TypeName> {
match self.k() {
TokenKind::Kw(Kw::Integer) => {
self.advance();
Some(TypeName::Integer)
}
TokenKind::Kw(Kw::Long) => {
self.advance();
Some(TypeName::Long)
}
TokenKind::Kw(Kw::Single) => {
self.advance();
Some(TypeName::Single)
}
TokenKind::Kw(Kw::Double) => {
self.advance();
Some(TypeName::Double)
}
TokenKind::Kw(Kw::Currency) => {
self.advance();
Some(TypeName::Currency)
}
TokenKind::Kw(Kw::String) => {
self.advance();
if self.eat(&TokenKind::Star) {
match self.k() {
TokenKind::Num(NumValue::Int(n)) => {
self.advance();
Some(TypeName::FixedStr(n as i64))
}
TokenKind::Num(NumValue::Long(n)) => {
self.advance();
Some(TypeName::FixedStr(n as i64))
}
_ => {
self.err("Expected: constant");
Some(TypeName::Str)
}
}
} else {
Some(TypeName::Str)
}
}
TokenKind::Ident { name, suffix: None } => {
self.advance();
Some(match name.as_str() {
"FORM" => TypeName::Form,
"CONTROL" => TypeName::Control,
_ => TypeName::Udt(name),
})
}
_ => {
self.err("Expected: type");
None
}
}
}
fn parse_type_decl(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // TYPE
let name = match self.k() {
TokenKind::Ident { name, suffix: None } => {
self.advance();
name
}
_ => {
self.err("Expected: identifier");
self.sync();
return None;
}
};
let mut fields = Vec::new();
loop {
self.skip_seps();
if self.at_end_pair(Kw::Type) {
self.advance();
self.advance();
break;
}
if matches!(self.k(), TokenKind::Eof) {
self.err("Expected: END TYPE");
break;
}
match self.k() {
TokenKind::Ident {
name: fname,
suffix: None,
} => {
self.advance();
if self.expect_kw(Kw::As, "AS") {
if let Some(t) = self.parse_type_name() {
fields.push((fname, t));
}
} else {
self.sync();
}
}
_ => {
self.err("Expected: identifier");
self.sync();
}
}
}
Some(Stmt::TypeDecl { name, fields, pos })
}
fn parse_proc_sig(&mut self) -> Option<ProcSig> {
let kind = if self.eat_kw(Kw::Sub) {
ProcKind::Sub
} else if self.eat_kw(Kw::Function) {
ProcKind::Function
} else {
self.err("Expected: SUB or FUNCTION");
self.sync();
return None;
};
let (name, suffix) = match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
(name, suffix)
}
_ => {
self.err("Expected: identifier");
self.sync();
return None;
}
};
let mut params = Vec::new();
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
let array = if self.eat(&TokenKind::LParen) {
self.eat(&TokenKind::RParen);
true
} else {
false
};
let as_type = if self.eat_kw(Kw::As) {
self.parse_type_name()
} else {
None
};
params.push(Param {
name: pname,
suffix: psfx,
array,
as_type,
});
}
_ => {
self.err("Expected: identifier");
break;
}
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
self.eat(&TokenKind::RParen);
}
Some(ProcSig {
kind,
name,
suffix,
params,
})
}
fn parse_proc(&mut self, leading_static: bool) -> Option<Proc> {
let pos = self.pos();
let sig = self.parse_proc_sig()?;
let is_static = leading_static || self.eat_kw(Kw::Static);
let end_kw = match sig.kind {
ProcKind::Sub => Kw::Sub,
ProcKind::Function => Kw::Function,
};
let body = self.parse_stmt_list(|p: &P| p.at_end_pair(end_kw));
let end_pos = self.pos();
if self.at_end_pair(end_kw) {
self.advance();
self.advance();
} else {
self.err(match sig.kind {
ProcKind::Sub => "Expected: END SUB",
ProcKind::Function => "Expected: END FUNCTION",
});
}
Some(Proc {
sig,
is_static,
body,
pos,
end_pos,
})
}
fn parse_def_fn(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // DEF
// `DEF SEG` ist Segmentadressierung und damit deklariertes
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
// `DEF FNname` einleitet.
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
self.err("Feature unavailable: DEF SEG");
self.sync();
return None;
}
let (name, suffix) = match self.k() {
TokenKind::Ident { name, suffix } if name.starts_with("FN") => {
self.advance();
(name, suffix)
}
_ => {
self.err("Expected: FN identifier");
self.sync();
return None;
}
};
let mut params = Vec::new();
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
params.push(Param {
name: pname,
suffix: psfx,
array: false,
as_type: None,
});
}
_ => {
self.err("Expected: identifier");
break;
}
}
if !self.eat(&TokenKind::Comma) {
break;
}
}
self.eat(&TokenKind::RParen);
}
if self.eat(&TokenKind::Eq) {
let body = self.parse_expr()?;
Some(Stmt::DefFn {
name,
suffix,
params,
body,
pos,
})
} else {
// 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::DefFnBlock {
name,
suffix,
params,
body,
pos,
})
}
}
fn parse_assign_or_call(&mut self, pos: SourcePos, force_assign: bool) -> Option<Stmt> {
let target = self.parse_name_ref()?;
if self.eat(&TokenKind::Eq) {
let value = self.parse_expr()?;
return Some(Stmt::Assign { target, value, pos });
}
if force_assign {
self.err("Expected: =");
self.sync();
return None;
}
// Ausgeschlossene Anweisungen besitzen eigene Koordinaten-/Dateisyntax.
// Erst Zuweisungen erkennen, damit gleichnamige Variablen/Arrays gültig bleiben.
if let Expr::Name {
name, suffix: None, ..
} = &target
{
if matches!(
name.as_str(),
"CIRCLE" | "WINDOW" | "IOCTL" | "PSET" | "PRESET" | "PALETTE"
) {
self.err(format!("Feature unavailable: {name}"));
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" | "SIGNAL" | "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
{
// Ohne CALL-Keyword sind Klammern Wert-Klammern (BYVAL), keine
// Argumentlisten-Klammern: `Foo (n%)` übergibt `(n%)`.
let mut call_args: Vec<Expr> = args
.unwrap_or_default()
.into_iter()
.map(|a| match a {
p @ Expr::Paren(_) => p,
other => Expr::Paren(Box::new(other)),
})
.collect();
if !self.at_stmt_end() && (call_args.is_empty() || name.contains('.')) {
call_args.extend(self.parse_arg_list_to_stmt_end());
}
Some(Stmt::Call {
name,
suffix,
args: call_args,
pos,
})
} else {
self.err("Syntax error");
self.sync();
None
}
}
// ---- Namen und Argumente ---------------------------------------------
/// Name mit optionalen Klammer-Argumenten (Variable/Array/Funktion).
fn parse_name_ref(&mut self) -> Option<Expr> {
let pos = self.pos();
match self.k() {
TokenKind::Ident { mut name, suffix } => {
self.advance();
let mut args = if self.eat(&TokenKind::LParen) {
let a = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen);
Some(a)
} else {
None
};
if self.eat(&TokenKind::Dot) {
match self.k() {
TokenKind::Ident {
name: member,
suffix: None,
} => {
name.push('.');
name.push_str(&member);
self.advance();
if self.eat(&TokenKind::LParen) {
let member_args = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen);
args.get_or_insert_with(Vec::new).extend(member_args);
}
}
_ => self.err("Expected: property"),
}
}
Some(Expr::Name {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: variable");
self.sync();
None
}
}
}
/// Nur ein Name ohne Argumente (FOR-Variable, ERASE).
fn parse_name_only(&mut self) -> Option<Expr> {
let pos = self.pos();
match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
Some(Expr::Name {
name,
suffix,
args: None,
pos,
})
}
_ => {
self.err("Expected: variable");
self.sync();
None
}
}
}
fn parse_label_ref(&mut self) -> Option<LabelRef> {
match self.k() {
TokenKind::Num(NumValue::Int(n)) if n >= 0 => {
self.advance();
Some(LabelRef::Line(n as u32))
}
TokenKind::Num(NumValue::Long(n)) if n >= 0 => {
self.advance();
Some(LabelRef::Line(n as u32))
}
TokenKind::Ident { name, suffix: None } => {
self.advance();
Some(LabelRef::Name(name))
}
_ => {
self.err("Expected: label or line number");
self.sync();
None
}
}
}
/// Argumentliste bis `closer` (nicht konsumiert); leere Plätze → Missing.
fn parse_arg_list(&mut self, closer: &TokenKind) -> Vec<Expr> {
let mut args = Vec::new();
loop {
if &self.k() == closer || self.at_stmt_end() {
if !args.is_empty() {
args.push(Expr::Missing);
}
break;
}
if self.k() == TokenKind::Comma {
self.advance();
args.push(Expr::Missing);
continue;
}
self.eat(&TokenKind::Hash);
match self.parse_expr() {
Some(e) => args.push(e),
None => break,
}
if !self.eat(&TokenKind::Comma) {
break;
}
if &self.k() == closer || self.at_stmt_end() {
args.push(Expr::Missing);
break;
}
}
args
}
/// Argumentliste eines impliziten Aufrufs bis zum Anweisungsende.
fn parse_arg_list_to_stmt_end(&mut self) -> Vec<Expr> {
let mut args = Vec::new();
loop {
if self.at_stmt_end() {
break;
}
if self.k() == TokenKind::Comma {
self.advance();
args.push(Expr::Missing);
continue;
}
match self.parse_expr() {
Some(e) => args.push(e),
None => break,
}
if !self.eat(&TokenKind::Comma) {
break;
}
if self.at_stmt_end() {
args.push(Expr::Missing);
break;
}
}
args
}
// ---- Ausdrücke ---------------------------------------------------------
fn parse_expr(&mut self) -> Option<Expr> {
self.parse_bin(1)
}
fn parse_bin(&mut self, min_prec: u8) -> Option<Expr> {
let mut lhs = self.parse_prefix()?;
while let Some((prec, op)) = self.infix_op() {
if prec < min_prec {
break;
}
let pos = self.pos();
self.advance();
let rhs = self.parse_bin(prec + 1)?;
lhs = Expr::Binary {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
pos,
};
}
Some(lhs)
}
/// Prioritäten nach Sprachreferenz §4 (hoch = bindet stärker).
fn infix_op(&self) -> Option<(u8, BinOp)> {
Some(match self.k() {
TokenKind::Kw(Kw::Imp) => (1, BinOp::Imp),
TokenKind::Kw(Kw::Eqv) => (2, BinOp::Eqv),
TokenKind::Kw(Kw::Xor) => (3, BinOp::Xor),
TokenKind::Kw(Kw::Or) => (4, BinOp::Or),
TokenKind::Kw(Kw::And) => (5, BinOp::And),
TokenKind::Eq => (7, BinOp::Eq),
TokenKind::Ne => (7, BinOp::Ne),
TokenKind::Lt => (7, BinOp::Lt),
TokenKind::Le => (7, BinOp::Le),
TokenKind::Gt => (7, BinOp::Gt),
TokenKind::Ge => (7, BinOp::Ge),
TokenKind::Plus => (8, BinOp::Add),
TokenKind::Minus => (8, BinOp::Sub),
TokenKind::Kw(Kw::Mod) => (9, BinOp::Mod),
TokenKind::Backslash => (10, BinOp::IntDiv),
TokenKind::Star => (11, BinOp::Mul),
TokenKind::Slash => (11, BinOp::Div),
TokenKind::Caret => (13, BinOp::Pow),
_ => return None,
})
}
fn parse_prefix(&mut self) -> Option<Expr> {
let pos = self.pos();
match self.k() {
TokenKind::Minus => {
self.advance();
let operand = self.parse_bin(12)?;
Some(Expr::Unary {
op: UnOp::Neg,
operand: Box::new(operand),
pos,
})
}
TokenKind::Plus => {
self.advance();
self.parse_bin(12)
}
TokenKind::Kw(Kw::Not) => {
self.advance();
let operand = self.parse_bin(6)?;
Some(Expr::Unary {
op: UnOp::Not,
operand: Box::new(operand),
pos,
})
}
_ => self.parse_primary(),
}
}
fn parse_primary(&mut self) -> Option<Expr> {
// `SEEK(n)` ist zugleich Funktion und Anweisung; in Ausdrucksposition
// gilt die Funktionsform.
if self.k() == TokenKind::Kw(Kw::Seek) && self.k_at(1) == TokenKind::LParen {
let pos = self.pos();
self.advance(); // SEEK
self.advance(); // (
let arg = self.parse_expr()?;
if !self.eat(&TokenKind::RParen) {
self.err("Expected: )");
}
return Some(Expr::Name {
name: "SEEK".into(),
suffix: None,
args: Some(vec![arg]),
pos,
});
}
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "TYPEOF") {
let pos = self.pos();
self.advance();
let value = self.parse_primary()?;
if !self.eat_kw(Kw::Is) {
self.err("Expected: IS");
return None;
}
let class = match self.k() {
TokenKind::Ident { name, suffix: None } => {
self.advance();
name
}
_ => {
self.err("Expected: control class");
return None;
}
};
return Some(Expr::TypeOf {
value: Box::new(value),
class,
pos,
});
}
match self.k() {
TokenKind::Num(n) => {
let pos = self.pos();
self.advance();
Some(match n {
NumValue::Int(v) => Expr::IntLit(v, pos),
NumValue::Long(v) => Expr::LongLit(v, pos),
NumValue::Single(v) => Expr::SingleLit(v, pos),
NumValue::Double(v) => Expr::DoubleLit(v, pos),
NumValue::Currency(v) => Expr::CurrencyLit(v, pos),
})
}
TokenKind::Str(s) => {
let pos = self.pos();
self.advance();
Some(Expr::StrLit(s, pos))
}
TokenKind::LParen => {
self.advance();
let e = self.parse_expr()?;
if !self.eat(&TokenKind::RParen) {
self.err("Expected: )");
}
Some(Expr::Paren(Box::new(e)))
}
TokenKind::Ident { .. } => self.parse_name_ref(),
_ => {
self.err("Expected: expression");
self.sync();
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
fn parse_ok(src: &str) -> Module {
let l = lex(src);
assert!(l.diagnostics.is_empty(), "Lexer: {:?}", l.diagnostics);
let p = parse("TEST", &l.tokens);
assert!(p.diagnostics.is_empty(), "Parser: {:?}", p.diagnostics);
p.module
}
#[test]
fn zuweisung_und_aufruf() {
let m = parse_ok("a% = 1\nCOLOR 15, 1\nCLS");
assert_eq!(m.body.len(), 3);
assert!(matches!(m.body[0], Stmt::Assign { .. }));
assert!(matches!(&m.body[1], Stmt::Call { name, args, .. }
if name == "COLOR" && args.len() == 2));
assert!(matches!(&m.body[2], Stmt::Call { name, args, .. }
if name == "CLS" && args.is_empty()));
}
#[test]
fn print_varianten() {
let m = parse_ok("PRINT \"a\", \"b\"; 1\nPRINT\nPRINT USING \"##.#\"; 1.5");
assert!(matches!(&m.body[0], Stmt::Print { items, .. } if items.len() == 5));
assert!(matches!(&m.body[1], Stmt::Print { items, .. } if items.is_empty()));
assert!(matches!(&m.body[2], Stmt::Print { using: Some(_), .. }));
}
#[test]
fn if_block_und_einzeilig() {
let m = parse_ok(
"IF a > 1 THEN\nPRINT 1\nELSEIF a > 0 THEN\nPRINT 2\nELSE\nPRINT 3\nEND IF\nIF b THEN PRINT 4 ELSE PRINT 5",
);
match &m.body[0] {
Stmt::If {
elseifs, else_body, ..
} => {
assert_eq!(elseifs.len(), 1);
assert!(else_body.is_some());
}
other => panic!("erwartet IF, war {other:?}"),
}
assert!(matches!(
&m.body[1],
Stmt::If {
else_body: Some(_),
..
}
));
}
#[test]
fn select_case() {
let m = parse_ok(
"SELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3, IS >= 10\nPRINT \"x\"\nCASE ELSE\nPRINT \"y\"\nEND SELECT",
);
match &m.body[0] {
Stmt::Select { arms, .. } => {
assert_eq!(arms.len(), 3);
assert_eq!(arms[1].specs.len(), 2);
assert!(arms[2].specs.is_empty());
}
other => panic!("erwartet SELECT, war {other:?}"),
}
}
#[test]
fn schleifen() {
let m = parse_ok(
"FOR i = 1 TO 10 STEP 2\nNEXT i\nDO WHILE x\nLOOP\nDO\nLOOP UNTIL x\nWHILE x\nWEND",
);
assert!(matches!(&m.body[0], Stmt::For { step: Some(_), .. }));
assert!(matches!(
&m.body[1],
Stmt::DoLoop {
pre: Some((false, _)),
post: None,
..
}
));
assert!(matches!(
&m.body[2],
Stmt::DoLoop {
pre: None,
post: Some((true, _)),
..
}
));
assert!(matches!(&m.body[3], Stmt::While { .. }));
}
#[test]
fn prozeduren() {
let m = parse_ok(
"DECLARE SUB Foo (a%, b$)\nSUB Foo (a%, b$)\nPRINT a%; b$\nEND SUB\nFUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION",
);
assert_eq!(m.procs.len(), 2);
assert_eq!(m.procs[0].sig.params.len(), 2);
assert!(matches!(m.procs[1].sig.kind, ProcKind::Function));
}
#[test]
fn fehlerbehandlung() {
let m = parse_ok(
"ON ERROR GOTO Handler\nON LOCAL ERROR RESUME NEXT\nON ERROR GOTO 0\nHandler:\nRESUME NEXT",
);
assert!(matches!(
&m.body[0],
Stmt::OnError {
local: false,
action: OnErrorAction::Goto(_),
..
}
));
assert!(matches!(
&m.body[1],
Stmt::OnError {
local: true,
action: OnErrorAction::ResumeNext,
..
}
));
assert!(matches!(
&m.body[2],
Stmt::OnError {
action: OnErrorAction::Disable,
..
}
));
}
#[test]
fn operator_prioritaet() {
let m = parse_ok("x = 1 + 2 * 3 ^ 2");
// 1 + (2 * (3 ^ 2))
match &m.body[0] {
Stmt::Assign {
value:
Expr::Binary {
op: BinOp::Add,
rhs,
..
},
..
} => {
assert!(matches!(**rhs, Expr::Binary { op: BinOp::Mul, .. }));
}
other => panic!("unerwartet: {other:?}"),
}
}
#[test]
fn typen_und_deklarationen() {
let m = parse_ok(
"TYPE Kunde\nName AS STRING * 30\nUmsatz AS DOUBLE\nEND TYPE\nDIM SHARED k AS Kunde, a(1 TO 10) AS INTEGER\nCONST PI = 3.14159\nDEFINT A-C, X",
);
assert!(matches!(&m.body[0], Stmt::TypeDecl { fields, .. } if fields.len() == 2));
assert!(matches!(&m.body[1], Stmt::Dim { shared: true, decls, .. } if decls.len() == 2));
assert!(matches!(&m.body[3], Stmt::DefType { ranges, .. } if ranges.len() == 2));
}
#[test]
fn forms_syntax() {
let m = parse_ok(
"'$FORM\nDIM f AS FORM, c AS CONTROL\nIF TYPEOF c IS CommandButton THEN PRINT 1",
);
assert!(matches!(m.body[0], Stmt::MetaForm { .. }));
assert!(matches!(&m.body[1], Stmt::Dim { decls, .. }
if decls[0].as_type == Some(TypeName::Form) && decls[1].as_type == Some(TypeName::Control)));
assert!(
matches!(&m.body[2], Stmt::If { cond: Expr::TypeOf { class, .. }, .. }
if class == "COMMANDBUTTON")
);
}
}