Phase 4: Formularmodell und Objektsprache

This commit is contained in:
2026-09-04 17:30:32 +02:00
parent 5cf5a6582c
commit 54488b5b67
35 changed files with 7195 additions and 627 deletions

View File

@@ -14,7 +14,12 @@ pub struct ParseOutput {
}
pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
let mut p = P { toks: tokens, i: 0, diags: Vec::new(), at_line_start: true };
let mut p = P {
toks: tokens,
i: 0,
diags: Vec::new(),
at_line_start: true,
};
let mut body = Vec::new();
let mut procs = Vec::new();
@@ -40,7 +45,11 @@ pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
}
ParseOutput {
module: Module { name: module_name.to_string(), body, procs },
module: Module {
name: module_name.to_string(),
body,
procs,
},
diagnostics: p.diags,
}
}
@@ -85,7 +94,10 @@ impl<'a> P<'a> {
}
fn err(&mut self, msg: impl Into<String>) {
let pos = self.pos();
self.diags.push(Diagnostic { pos, message: msg.into() });
self.diags.push(Diagnostic {
pos,
message: msg.into(),
});
}
fn expect_kw(&mut self, kw: Kw, what: &str) -> bool {
if self.eat_kw(kw) {
@@ -205,11 +217,21 @@ impl<'a> P<'a> {
}
TokenKind::MetaStatic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: true, pos })
Some(Stmt::MetaArrays {
static_arrays: true,
pos,
})
}
TokenKind::MetaDynamic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: false, pos })
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) => {
@@ -368,8 +390,9 @@ impl<'a> P<'a> {
}
Some(Stmt::ConstDecl { items, pos })
}
TokenKind::Kw(k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng
| Kw::DefDbl | Kw::DefStr | Kw::DefCur)) => {
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,
@@ -382,9 +405,7 @@ impl<'a> P<'a> {
let mut ranges = Vec::new();
loop {
let a = match self.k() {
TokenKind::Ident { name, suffix: None }
if name.len() == 1 =>
{
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
@@ -396,9 +417,7 @@ impl<'a> P<'a> {
};
let b = if self.eat(&TokenKind::Minus) {
match self.k() {
TokenKind::Ident { name, suffix: None }
if name.len() == 1 =>
{
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
@@ -420,11 +439,12 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Option) => {
self.advance();
match self.k() {
TokenKind::Ident { name, suffix: None }
if name == "EXPLICIT" =>
{
TokenKind::Ident { name, suffix: None } if name == "EXPLICIT" => {
self.advance();
Some(Stmt::OptionStmt { kind: OptionKind::Explicit, pos })
Some(Stmt::OptionStmt {
kind: OptionKind::Explicit,
pos,
})
}
TokenKind::Ident { name, suffix: None } if name == "BASE" => {
self.advance();
@@ -439,7 +459,10 @@ impl<'a> P<'a> {
0
}
};
Some(Stmt::OptionStmt { kind: OptionKind::Base(base), pos })
Some(Stmt::OptionStmt {
kind: OptionKind::Base(base),
pos,
})
}
_ => {
self.err("Expected: BASE or EXPLICIT");
@@ -466,7 +489,12 @@ impl<'a> P<'a> {
} else {
Vec::new()
};
Some(Stmt::Call { name, suffix, args, pos })
Some(Stmt::Call {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: identifier");
@@ -510,7 +538,11 @@ impl<'a> P<'a> {
_ => cur.push(c),
}
}
items.push(if quotiert { cur } else { cur.trim().to_string() });
items.push(if quotiert {
cur
} else {
cur.trim().to_string()
});
Some(Stmt::Data { items, pos })
}
TokenKind::Kw(Kw::Read) => {
@@ -594,7 +626,13 @@ impl<'a> P<'a> {
var = Some(self.parse_name_ref()?);
}
}
Some(Stmt::GetPut { put: k == Kw::Put, file, recnum, var, pos })
Some(Stmt::GetPut {
put: k == Kw::Put,
file,
recnum,
var,
pos,
})
}
TokenKind::Kw(k @ (Kw::Lset | Kw::Rset)) => {
self.advance();
@@ -605,7 +643,12 @@ impl<'a> P<'a> {
return None;
}
let value = self.parse_expr()?;
Some(Stmt::LsetRset { rset: k == Kw::Rset, target, value, pos })
Some(Stmt::LsetRset {
rset: k == Kw::Rset,
target,
value,
pos,
})
}
TokenKind::Kw(Kw::Write) => {
self.advance();
@@ -636,7 +679,11 @@ impl<'a> P<'a> {
self.err("Expected: ,");
}
let position = self.parse_expr()?;
Some(Stmt::SeekStmt { file, position, pos })
Some(Stmt::SeekStmt {
file,
position,
pos,
})
}
TokenKind::Kw(Kw::Common) => {
self.advance();
@@ -654,7 +701,12 @@ impl<'a> P<'a> {
}
}
let decls = self.parse_var_decls();
Some(Stmt::CommonDecl { shared, block, decls, pos })
Some(Stmt::CommonDecl {
shared,
block,
decls,
pos,
})
}
TokenKind::Kw(Kw::Shared) => {
self.advance();
@@ -681,19 +733,20 @@ impl<'a> P<'a> {
}
Some(Stmt::ViewPrint { top, bottom, pos })
}
TokenKind::Ident { ref name, suffix: None }
if name == "NAME" && self.k_at(1) != TokenKind::Eq =>
{
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 =>
{
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);
@@ -707,7 +760,13 @@ impl<'a> P<'a> {
to = Some(self.parse_expr()?);
}
}
Some(Stmt::LockStmt { unlock, file, from, to, pos })
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
@@ -716,10 +775,12 @@ impl<'a> P<'a> {
// `=` 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 =>
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)
}
@@ -740,11 +801,20 @@ impl<'a> P<'a> {
// 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 let TokenKind::Ident {
name: ref w,
suffix: None,
} = self.k()
{
if w == "ALL" {
self.advance();
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
return Some(Stmt::Call { name, suffix: None, args, pos });
return Some(Stmt::Call {
name,
suffix: None,
args,
pos,
});
}
}
}
@@ -754,7 +824,12 @@ impl<'a> P<'a> {
} else {
self.parse_arg_list_to_stmt_end()
};
Some(Stmt::Call { name, suffix: None, args, pos })
Some(Stmt::Call {
name,
suffix: None,
args,
pos,
})
}
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
@@ -793,7 +868,13 @@ impl<'a> P<'a> {
},
}
}
Some(Stmt::Print { printer, file, using, items, pos })
Some(Stmt::Print {
printer,
file,
using,
items,
pos,
})
}
/// `OPEN` in beiden Syntaxformen (FOR-Klausel und Kurzform).
@@ -813,7 +894,13 @@ impl<'a> P<'a> {
} else {
None
};
return Some(Stmt::OpenLegacy { mode: first, number, file, len, pos });
return Some(Stmt::OpenLegacy {
mode: first,
number,
file,
len,
pos,
});
}
let mut mode = None;
let mut isam = None;
@@ -823,7 +910,10 @@ impl<'a> P<'a> {
self.advance();
OpenMode::Input
}
TokenKind::Ident { ref name, suffix: None } => {
TokenKind::Ident {
ref name,
suffix: None,
} => {
let m = match name.as_str() {
"OUTPUT" => OpenMode::Output,
"APPEND" => OpenMode::Append,
@@ -873,7 +963,11 @@ impl<'a> P<'a> {
}
}
let mut access = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "ACCESS" {
self.advance();
let read = self.eat_kw(Kw::Read);
@@ -892,7 +986,11 @@ impl<'a> P<'a> {
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() {
} else if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "LOCK" {
self.advance();
let read = self.eat_kw(Kw::Read);
@@ -912,7 +1010,11 @@ impl<'a> P<'a> {
self.eat(&TokenKind::Hash);
let number = self.parse_expr()?;
let mut len = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "LEN" {
self.advance();
if !self.eat(&TokenKind::Eq) {
@@ -921,7 +1023,16 @@ impl<'a> P<'a> {
len = Some(self.parse_expr()?);
}
}
Some(Stmt::Open { file: first, mode, isam, access, lock, number, len, pos })
Some(Stmt::Open {
file: first,
mode,
isam,
access,
lock,
number,
len,
pos,
})
}
fn parse_input(&mut self, line: bool, pos: SourcePos) -> Option<Stmt> {
@@ -936,8 +1047,7 @@ impl<'a> P<'a> {
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)
{
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma) {
self.advance();
let with_question = self.k() == TokenKind::Semicolon;
self.advance();
@@ -959,7 +1069,14 @@ impl<'a> P<'a> {
if vars.is_empty() {
self.err("Expected: variable");
}
Some(Stmt::Input { line, file, keep_cursor, prompt, vars, pos })
Some(Stmt::Input {
line,
file,
keep_cursor,
prompt,
vars,
pos,
})
}
fn parse_if(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -991,7 +1108,13 @@ impl<'a> P<'a> {
} else {
self.err("Expected: END IF");
}
Some(Stmt::If { cond, then_body, elseifs, else_body, pos })
Some(Stmt::If {
cond,
then_body,
elseifs,
else_body,
pos,
})
} else {
// Einzeilig
let then_body = if let Some(n) = self.line_number_target() {
@@ -1008,7 +1131,13 @@ impl<'a> P<'a> {
} else {
None
};
Some(Stmt::If { cond, then_body, elseifs: Vec::new(), else_body, pos })
Some(Stmt::If {
cond,
then_body,
elseifs: Vec::new(),
else_body,
pos,
})
}
}
@@ -1112,7 +1241,14 @@ impl<'a> P<'a> {
} else {
self.err("FOR without NEXT");
}
Some(Stmt::For { var, from, to, step, body, pos })
Some(Stmt::For {
var,
from,
to,
step,
body,
pos,
})
}
fn parse_do(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -1135,7 +1271,12 @@ impl<'a> P<'a> {
} else {
self.err("DO without LOOP");
}
Some(Stmt::DoLoop { pre, post, body, pos })
Some(Stmt::DoLoop {
pre,
post,
body,
pos,
})
}
fn parse_while(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -1252,13 +1393,23 @@ impl<'a> P<'a> {
break;
}
}
Some(Stmt::OnGoto { expr, targets, gosub, pos })
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 })
Some(Stmt::Dim {
shared,
redim,
decls,
pos,
})
}
/// Deklarationsliste `name[suffix][(dims)] [AS typ], …` — gemeinsame
@@ -1309,7 +1460,13 @@ impl<'a> P<'a> {
} else {
None
};
decls.push(VarDecl { name, suffix, dims, as_type, pos: dpos });
decls.push(VarDecl {
name,
suffix,
dims,
as_type,
pos: dpos,
});
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -1362,7 +1519,11 @@ impl<'a> P<'a> {
}
TokenKind::Ident { name, suffix: None } => {
self.advance();
Some(TypeName::Udt(name))
Some(match name.as_str() {
"FORM" => TypeName::Form,
"CONTROL" => TypeName::Control,
_ => TypeName::Udt(name),
})
}
_ => {
self.err("Expected: type");
@@ -1397,7 +1558,10 @@ impl<'a> P<'a> {
break;
}
match self.k() {
TokenKind::Ident { name: fname, suffix: None } => {
TokenKind::Ident {
name: fname,
suffix: None,
} => {
self.advance();
if self.expect_kw(Kw::As, "AS") {
if let Some(t) = self.parse_type_name() {
@@ -1442,7 +1606,10 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident { name: pname, suffix: psfx } => {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
let array = if self.eat(&TokenKind::LParen) {
self.eat(&TokenKind::RParen);
@@ -1474,7 +1641,12 @@ impl<'a> P<'a> {
self.eat(&TokenKind::RParen);
}
}
Some(ProcSig { kind, name, suffix, params })
Some(ProcSig {
kind,
name,
suffix,
params,
})
}
fn parse_proc(&mut self) -> Option<Proc> {
@@ -1495,14 +1667,19 @@ impl<'a> P<'a> {
ProcKind::Function => "Expected: END FUNCTION",
});
}
Some(Proc { sig, is_static, body, pos })
Some(Proc {
sig,
is_static,
body,
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.
// `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");
self.sync();
@@ -1524,7 +1701,10 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident { name: pname, suffix: psfx } => {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
params.push(Param {
name: pname,
@@ -1547,7 +1727,13 @@ impl<'a> P<'a> {
}
if self.eat(&TokenKind::Eq) {
let body = self.parse_expr()?;
Some(Stmt::DefFn { name, suffix, params, body, pos })
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));
@@ -1557,7 +1743,13 @@ impl<'a> P<'a> {
} else {
self.err("Expected: END DEF");
}
Some(Stmt::DefFnBlock { name, suffix, params, body, pos })
Some(Stmt::DefFnBlock {
name,
suffix,
params,
body,
pos,
})
}
}
@@ -1612,7 +1804,10 @@ impl<'a> P<'a> {
}
}
// Impliziter Aufruf: `name [arg [, arg …]]`
if let Expr::Name { name, suffix, args, .. } = target {
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
@@ -1626,7 +1821,12 @@ impl<'a> P<'a> {
if !self.at_stmt_end() && call_args.is_empty() {
call_args = self.parse_arg_list_to_stmt_end();
}
Some(Stmt::Call { name, suffix, args: call_args, pos })
Some(Stmt::Call {
name,
suffix,
args: call_args,
pos,
})
} else {
self.err("Syntax error");
self.sync();
@@ -1640,7 +1840,7 @@ impl<'a> P<'a> {
fn parse_name_ref(&mut self) -> Option<Expr> {
let pos = self.pos();
match self.k() {
TokenKind::Ident { name, suffix } => {
TokenKind::Ident { mut name, suffix } => {
self.advance();
let args = if self.eat(&TokenKind::LParen) {
let a = self.parse_arg_list(&TokenKind::RParen);
@@ -1649,7 +1849,25 @@ impl<'a> P<'a> {
} else {
None
};
Some(Expr::Name { name, suffix, args, pos })
if self.eat(&TokenKind::Dot) {
match self.k() {
TokenKind::Ident {
name: member,
suffix: None,
} => {
name.push('.');
name.push_str(&member);
self.advance();
}
_ => self.err("Expected: property"),
}
}
Some(Expr::Name {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: variable");
@@ -1665,7 +1883,12 @@ impl<'a> P<'a> {
match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
Some(Expr::Name { name, suffix, args: None, pos })
Some(Expr::Name {
name,
suffix,
args: None,
pos,
})
}
_ => {
self.err("Expected: variable");
@@ -1773,7 +1996,12 @@ impl<'a> P<'a> {
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 };
lhs = Expr::Binary {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
pos,
};
}
Some(lhs)
}
@@ -1809,7 +2037,11 @@ impl<'a> P<'a> {
TokenKind::Minus => {
self.advance();
let operand = self.parse_bin(12)?;
Some(Expr::Unary { op: UnOp::Neg, operand: Box::new(operand), pos })
Some(Expr::Unary {
op: UnOp::Neg,
operand: Box::new(operand),
pos,
})
}
TokenKind::Plus => {
self.advance();
@@ -1818,7 +2050,11 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Not) => {
self.advance();
let operand = self.parse_bin(6)?;
Some(Expr::Unary { op: UnOp::Not, operand: Box::new(operand), pos })
Some(Expr::Unary {
op: UnOp::Not,
operand: Box::new(operand),
pos,
})
}
_ => self.parse_primary(),
}
@@ -1842,6 +2078,30 @@ impl<'a> P<'a> {
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) => {
self.advance();
@@ -1913,13 +2173,21 @@ mod tests {
"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, .. } => {
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(_), .. }));
assert!(matches!(
&m.body[1],
Stmt::If {
else_body: Some(_),
..
}
));
}
#[test]
@@ -1943,8 +2211,22 @@ mod tests {
"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[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 { .. }));
}
@@ -1963,9 +2245,29 @@ mod tests {
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, .. }));
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]
@@ -1973,7 +2275,15 @@ mod tests {
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, .. }, .. } => {
Stmt::Assign {
value:
Expr::Binary {
op: BinOp::Add,
rhs,
..
},
..
} => {
assert!(matches!(**rhs, Expr::Binary { op: BinOp::Mul, .. }));
}
other => panic!("unerwartet: {other:?}"),
@@ -1989,4 +2299,18 @@ mod tests {
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")
);
}
}