2483 lines
82 KiB
Rust
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")
|
|
);
|
|
}
|
|
}
|