//! 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, } 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, 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) { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 [(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 { 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 { 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 [(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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 = 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 { 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 { 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 { 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 { 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 { 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 { self.parse_bin(1) } fn parse_bin(&mut self, min_prec: u8) -> Option { 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 { 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 { // `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") ); } }