CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
767 lines
24 KiB
Rust
767 lines
24 KiB
Rust
//! Recursive-descent parser for the security-relevant subset of Structured Text.
|
|
//!
|
|
//! Tolerant by design: it parses the POUs, variable sections, and statement
|
|
//! bodies it understands, and skips (with statement/POU-level recovery) anything
|
|
//! it does not, so a single odd construct never sinks the whole file.
|
|
|
|
use super::ast::*;
|
|
use super::lexer::{Keyword as K, Tok, Token};
|
|
|
|
pub struct Parser {
|
|
toks: Vec<Token>,
|
|
pos: usize,
|
|
}
|
|
|
|
impl Parser {
|
|
pub fn new(toks: Vec<Token>) -> Self {
|
|
Self { toks, pos: 0 }
|
|
}
|
|
|
|
// ── token helpers ──────────────────────────────────────────────
|
|
fn peek(&self) -> &Tok {
|
|
&self.toks[self.pos.min(self.toks.len() - 1)].kind
|
|
}
|
|
fn line(&self) -> u32 {
|
|
self.toks[self.pos.min(self.toks.len() - 1)].line
|
|
}
|
|
fn at_end(&self) -> bool {
|
|
matches!(self.peek(), Tok::Eof)
|
|
}
|
|
fn advance(&mut self) -> Tok {
|
|
let t = self.toks[self.pos.min(self.toks.len() - 1)].kind.clone();
|
|
if self.pos < self.toks.len() - 1 {
|
|
self.pos += 1;
|
|
}
|
|
t
|
|
}
|
|
fn eat(&mut self, t: &Tok) -> bool {
|
|
if self.peek() == t {
|
|
self.advance();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
fn eat_kw(&mut self, k: K) -> bool {
|
|
if matches!(self.peek(), Tok::Kw(x) if *x == k) {
|
|
self.advance();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
fn at_kw(&self, k: K) -> bool {
|
|
matches!(self.peek(), Tok::Kw(x) if *x == k)
|
|
}
|
|
fn ident(&mut self) -> Option<String> {
|
|
if let Tok::Ident(s) = self.peek() {
|
|
let s = s.clone();
|
|
self.advance();
|
|
Some(s)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// ── top level ──────────────────────────────────────────────────
|
|
/// Parse every POU in the token stream.
|
|
pub fn parse_units(&mut self) -> Vec<Pou> {
|
|
let mut pous = Vec::new();
|
|
while !self.at_end() {
|
|
match self.peek() {
|
|
Tok::Kw(K::Program) => {
|
|
self.advance();
|
|
if let Some(p) = self.parse_pou(PouKind::Program, K::EndProgram) {
|
|
pous.push(p);
|
|
}
|
|
}
|
|
Tok::Kw(K::Function) => {
|
|
self.advance();
|
|
if let Some(p) = self.parse_pou(PouKind::Function, K::EndFunction) {
|
|
pous.push(p);
|
|
}
|
|
}
|
|
Tok::Kw(K::FunctionBlock) => {
|
|
self.advance();
|
|
if let Some(p) = self.parse_pou(PouKind::FunctionBlock, K::EndFunctionBlock) {
|
|
pous.push(p);
|
|
}
|
|
}
|
|
// Skip TYPE...END_TYPE and anything else at top level.
|
|
_ => {
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
pous
|
|
}
|
|
|
|
fn parse_pou(&mut self, kind: PouKind, end: K) -> Option<Pou> {
|
|
let line = self.line();
|
|
let name = self.ident().unwrap_or_else(|| "<anonymous>".to_string());
|
|
// Optional `: return_type` for functions.
|
|
if self.eat(&Tok::Colon) {
|
|
let _ = self.advance(); // return type token
|
|
}
|
|
|
|
let mut vars = Vec::new();
|
|
// Variable sections precede the body.
|
|
while let Some(section) = self.var_section_kw() {
|
|
self.advance();
|
|
let _ = self.eat_kw(K::Constant); // CONSTANT is informational for our rules
|
|
self.parse_var_decls(section, &mut vars);
|
|
}
|
|
|
|
// Body statements until END_<kind>.
|
|
let mut body = Vec::new();
|
|
while !self.at_end() && !self.at_kw(end) {
|
|
if let Some(s) = self.parse_stmt() {
|
|
body.push(s);
|
|
}
|
|
}
|
|
self.eat_kw(end);
|
|
|
|
Some(Pou {
|
|
name,
|
|
kind,
|
|
vars,
|
|
body,
|
|
line,
|
|
})
|
|
}
|
|
|
|
fn var_section_kw(&self) -> Option<VarSection> {
|
|
match self.peek() {
|
|
Tok::Kw(K::Var) => Some(VarSection::Var),
|
|
Tok::Kw(K::VarInput) => Some(VarSection::Input),
|
|
Tok::Kw(K::VarOutput) => Some(VarSection::Output),
|
|
Tok::Kw(K::VarInOut) => Some(VarSection::InOut),
|
|
Tok::Kw(K::VarGlobal) => Some(VarSection::Global),
|
|
Tok::Kw(K::VarTemp) => Some(VarSection::Temp),
|
|
Tok::Kw(K::VarExternal) => Some(VarSection::External),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_var_decls(&mut self, section: VarSection, out: &mut Vec<VarDecl>) {
|
|
while !self.at_end() && !self.at_kw(K::EndVar) {
|
|
let line = self.line();
|
|
// names: a, b, c
|
|
let mut names = Vec::new();
|
|
match self.ident() {
|
|
Some(n) => names.push(n),
|
|
None => {
|
|
// Not a declaration we understand — skip to next ; or END_VAR.
|
|
self.sync_decl();
|
|
continue;
|
|
}
|
|
}
|
|
while self.eat(&Tok::Comma) {
|
|
if let Some(n) = self.ident() {
|
|
names.push(n);
|
|
}
|
|
}
|
|
if !self.eat(&Tok::Colon) {
|
|
self.sync_decl();
|
|
continue;
|
|
}
|
|
let (type_name, array_bounds) = self.parse_type();
|
|
let init = if self.eat(&Tok::Assign) {
|
|
Some(self.parse_expr())
|
|
} else {
|
|
None
|
|
};
|
|
self.eat(&Tok::Semi);
|
|
for n in names {
|
|
out.push(VarDecl {
|
|
name: n,
|
|
section,
|
|
type_name: type_name.clone(),
|
|
array_bounds,
|
|
init: init.clone(),
|
|
line,
|
|
});
|
|
}
|
|
}
|
|
self.eat_kw(K::EndVar);
|
|
}
|
|
|
|
/// Parse a (possibly ARRAY) type, returning its rendered name and literal
|
|
/// bounds when present.
|
|
fn parse_type(&mut self) -> (String, Option<(i64, i64)>) {
|
|
if self.eat_kw(K::Array) {
|
|
let mut bounds = None;
|
|
if self.eat(&Tok::LBrack) {
|
|
let lo = self.int_lit();
|
|
self.eat(&Tok::DotDot);
|
|
let hi = self.int_lit();
|
|
if let (Some(lo), Some(hi)) = (lo, hi) {
|
|
bounds = Some((lo, hi));
|
|
}
|
|
// Skip any further dimensions / tokens to the closing bracket.
|
|
while !self.at_end() && !self.eat(&Tok::RBrack) {
|
|
self.advance();
|
|
}
|
|
}
|
|
self.eat_kw(K::Of);
|
|
let elem = self.type_ident();
|
|
(format!("ARRAY OF {elem}"), bounds)
|
|
} else {
|
|
(self.type_ident(), None)
|
|
}
|
|
}
|
|
|
|
fn type_ident(&mut self) -> String {
|
|
// Types can be qualified idents; keep it simple: one token, plus any
|
|
// string-length suffix like STRING[80].
|
|
let base = match self.advance() {
|
|
Tok::Ident(s) => s,
|
|
Tok::Kw(_) => "TYPE".to_string(),
|
|
other => format!("{other:?}"),
|
|
};
|
|
if self.eat(&Tok::LBrack) {
|
|
while !self.at_end() && !self.eat(&Tok::RBrack) {
|
|
self.advance();
|
|
}
|
|
}
|
|
base
|
|
}
|
|
|
|
fn int_lit(&mut self) -> Option<i64> {
|
|
match self.peek() {
|
|
Tok::Int(n) => {
|
|
let n = *n;
|
|
self.advance();
|
|
Some(n)
|
|
}
|
|
Tok::Minus => {
|
|
self.advance();
|
|
if let Tok::Int(n) = self.peek() {
|
|
let n = -*n;
|
|
self.advance();
|
|
Some(n)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn sync_decl(&mut self) {
|
|
while !self.at_end() && !self.eat(&Tok::Semi) && !self.at_kw(K::EndVar) {
|
|
self.advance();
|
|
}
|
|
}
|
|
fn sync_stmt(&mut self) {
|
|
while !self.at_end() && !self.eat(&Tok::Semi) {
|
|
// Stop at block terminators so recovery doesn't swallow structure.
|
|
if matches!(
|
|
self.peek(),
|
|
Tok::Kw(
|
|
K::EndIf
|
|
| K::EndFor
|
|
| K::EndWhile
|
|
| K::EndCase
|
|
| K::EndRepeat
|
|
| K::EndProgram
|
|
| K::EndFunction
|
|
| K::EndFunctionBlock
|
|
| K::Else
|
|
| K::Elsif
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
self.advance();
|
|
}
|
|
}
|
|
|
|
// ── statements ─────────────────────────────────────────────────
|
|
fn parse_stmt(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
match self.peek().clone() {
|
|
Tok::Semi => {
|
|
self.advance();
|
|
None
|
|
}
|
|
Tok::Kw(K::If) => self.parse_if(),
|
|
Tok::Kw(K::Case) => self.parse_case(),
|
|
Tok::Kw(K::For) => self.parse_for(),
|
|
Tok::Kw(K::While) => self.parse_while(),
|
|
Tok::Kw(K::Repeat) => self.parse_repeat(),
|
|
Tok::Kw(K::Return) => {
|
|
self.advance();
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Return { line })
|
|
}
|
|
Tok::Kw(K::Exit) => {
|
|
self.advance();
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Exit { line })
|
|
}
|
|
Tok::Kw(K::Jmp) => {
|
|
self.advance();
|
|
let label = self.ident().unwrap_or_default();
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Jump { label, line })
|
|
}
|
|
Tok::Ident(name) => {
|
|
// Could be `label:`, `call(...)`, or an assignment.
|
|
// Lookahead: ident ':' (not ':=') → label.
|
|
if matches!(
|
|
self.toks.get(self.pos + 1).map(|t| &t.kind),
|
|
Some(Tok::Colon)
|
|
) && !matches!(self.toks.get(self.pos + 2).map(|t| &t.kind), Some(Tok::Eq))
|
|
{
|
|
self.advance(); // ident
|
|
self.advance(); // ':'
|
|
return Some(Stmt::Label { name, line });
|
|
}
|
|
let lhs = self.parse_expr();
|
|
if self.eat(&Tok::Assign) {
|
|
let value = self.parse_expr();
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Assign {
|
|
target: lhs,
|
|
value,
|
|
line,
|
|
})
|
|
} else if let Expr::Call { callee, args, .. } = lhs {
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Call { callee, args, line })
|
|
} else {
|
|
// Bare expression / FB invocation without args recognized —
|
|
// skip to the terminator.
|
|
self.sync_stmt();
|
|
None
|
|
}
|
|
}
|
|
_ => {
|
|
self.sync_stmt();
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_block_until(&mut self, terms: &[K]) -> Vec<Stmt> {
|
|
let mut body = Vec::new();
|
|
while !self.at_end() && !terms.iter().any(|k| self.at_kw(*k)) {
|
|
if let Some(s) = self.parse_stmt() {
|
|
body.push(s);
|
|
}
|
|
}
|
|
body
|
|
}
|
|
|
|
fn parse_if(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
self.eat_kw(K::If);
|
|
let mut branches = Vec::new();
|
|
let cond = self.parse_expr();
|
|
self.eat_kw(K::Then);
|
|
let body = self.parse_block_until(&[K::Elsif, K::Else, K::EndIf]);
|
|
branches.push((cond, body));
|
|
while self.eat_kw(K::Elsif) {
|
|
let c = self.parse_expr();
|
|
self.eat_kw(K::Then);
|
|
let b = self.parse_block_until(&[K::Elsif, K::Else, K::EndIf]);
|
|
branches.push((c, b));
|
|
}
|
|
let else_body = if self.eat_kw(K::Else) {
|
|
Some(self.parse_block_until(&[K::EndIf]))
|
|
} else {
|
|
None
|
|
};
|
|
self.eat_kw(K::EndIf);
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::If {
|
|
branches,
|
|
else_body,
|
|
line,
|
|
})
|
|
}
|
|
|
|
fn parse_case(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
self.eat_kw(K::Case);
|
|
let selector = self.parse_expr();
|
|
self.eat_kw(K::Of);
|
|
let mut arms = Vec::new();
|
|
let mut else_body = None;
|
|
while !self.at_end() && !self.at_kw(K::EndCase) {
|
|
if self.eat_kw(K::Else) {
|
|
else_body = Some(self.parse_block_until(&[K::EndCase]));
|
|
break;
|
|
}
|
|
// labels: expr {, expr} :
|
|
let mut labels = vec![self.parse_expr()];
|
|
while self.eat(&Tok::Comma) {
|
|
labels.push(self.parse_expr());
|
|
}
|
|
self.eat(&Tok::Colon);
|
|
let body = self.parse_block_until(&[K::EndCase, K::Else]);
|
|
arms.push((labels, body));
|
|
}
|
|
self.eat_kw(K::EndCase);
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Case {
|
|
selector,
|
|
arms,
|
|
else_body,
|
|
line,
|
|
})
|
|
}
|
|
|
|
fn parse_for(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
self.eat_kw(K::For);
|
|
let var = self.ident().unwrap_or_default();
|
|
self.eat(&Tok::Assign);
|
|
let from = self.parse_expr();
|
|
self.eat_kw(K::To);
|
|
let to = self.parse_expr();
|
|
let by = if self.eat_kw(K::By) {
|
|
Some(self.parse_expr())
|
|
} else {
|
|
None
|
|
};
|
|
self.eat_kw(K::Do);
|
|
let body = self.parse_block_until(&[K::EndFor]);
|
|
self.eat_kw(K::EndFor);
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::For {
|
|
var,
|
|
from,
|
|
to,
|
|
by,
|
|
body,
|
|
line,
|
|
})
|
|
}
|
|
|
|
fn parse_while(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
self.eat_kw(K::While);
|
|
let cond = self.parse_expr();
|
|
self.eat_kw(K::Do);
|
|
let body = self.parse_block_until(&[K::EndWhile]);
|
|
self.eat_kw(K::EndWhile);
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::While { cond, body, line })
|
|
}
|
|
|
|
fn parse_repeat(&mut self) -> Option<Stmt> {
|
|
let line = self.line();
|
|
self.eat_kw(K::Repeat);
|
|
let body = self.parse_block_until(&[K::Until, K::EndRepeat]);
|
|
self.eat_kw(K::Until);
|
|
let until = self.parse_expr();
|
|
self.eat_kw(K::EndRepeat);
|
|
self.eat(&Tok::Semi);
|
|
Some(Stmt::Repeat { body, until, line })
|
|
}
|
|
|
|
// ── expressions (precedence climbing) ──────────────────────────
|
|
pub fn parse_expr(&mut self) -> Expr {
|
|
self.parse_or()
|
|
}
|
|
|
|
fn parse_or(&mut self) -> Expr {
|
|
let mut lhs = self.parse_and();
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Kw(K::Or) => BinOp::Or,
|
|
Tok::Kw(K::Xor) => BinOp::Xor,
|
|
_ => break,
|
|
};
|
|
let line = self.line();
|
|
self.advance();
|
|
let rhs = self.parse_and();
|
|
lhs = Expr::Binary {
|
|
op,
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
line,
|
|
};
|
|
}
|
|
lhs
|
|
}
|
|
|
|
fn parse_and(&mut self) -> Expr {
|
|
let mut lhs = self.parse_cmp();
|
|
while matches!(self.peek(), Tok::Kw(K::And) | Tok::Amp) {
|
|
let op = BinOp::And;
|
|
let line = self.line();
|
|
self.advance();
|
|
let rhs = self.parse_cmp();
|
|
lhs = Expr::Binary {
|
|
op,
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
line,
|
|
};
|
|
}
|
|
lhs
|
|
}
|
|
|
|
fn parse_cmp(&mut self) -> Expr {
|
|
let mut lhs = self.parse_add();
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Eq => BinOp::Eq,
|
|
Tok::Ne => BinOp::Ne,
|
|
Tok::Lt => BinOp::Lt,
|
|
Tok::Le => BinOp::Le,
|
|
Tok::Gt => BinOp::Gt,
|
|
Tok::Ge => BinOp::Ge,
|
|
_ => break,
|
|
};
|
|
let line = self.line();
|
|
self.advance();
|
|
let rhs = self.parse_add();
|
|
lhs = Expr::Binary {
|
|
op,
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
line,
|
|
};
|
|
}
|
|
lhs
|
|
}
|
|
|
|
fn parse_add(&mut self) -> Expr {
|
|
let mut lhs = self.parse_mul();
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Plus => BinOp::Add,
|
|
Tok::Minus => BinOp::Sub,
|
|
_ => break,
|
|
};
|
|
let line = self.line();
|
|
self.advance();
|
|
let rhs = self.parse_mul();
|
|
lhs = Expr::Binary {
|
|
op,
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
line,
|
|
};
|
|
}
|
|
lhs
|
|
}
|
|
|
|
fn parse_mul(&mut self) -> Expr {
|
|
let mut lhs = self.parse_unary();
|
|
loop {
|
|
let op = match self.peek() {
|
|
Tok::Star => BinOp::Mul,
|
|
Tok::Slash => BinOp::Div,
|
|
Tok::Kw(K::Mod) => BinOp::Mod,
|
|
Tok::Power => BinOp::Pow,
|
|
_ => break,
|
|
};
|
|
let line = self.line();
|
|
self.advance();
|
|
let rhs = self.parse_unary();
|
|
lhs = Expr::Binary {
|
|
op,
|
|
lhs: Box::new(lhs),
|
|
rhs: Box::new(rhs),
|
|
line,
|
|
};
|
|
}
|
|
lhs
|
|
}
|
|
|
|
fn parse_unary(&mut self) -> Expr {
|
|
let line = self.line();
|
|
match self.peek() {
|
|
Tok::Kw(K::Not) => {
|
|
self.advance();
|
|
Expr::Unary {
|
|
op: UnOp::Not,
|
|
expr: Box::new(self.parse_unary()),
|
|
line,
|
|
}
|
|
}
|
|
Tok::Minus => {
|
|
self.advance();
|
|
Expr::Unary {
|
|
op: UnOp::Neg,
|
|
expr: Box::new(self.parse_unary()),
|
|
line,
|
|
}
|
|
}
|
|
_ => self.parse_postfix(),
|
|
}
|
|
}
|
|
|
|
fn parse_postfix(&mut self) -> Expr {
|
|
let mut e = self.parse_primary();
|
|
loop {
|
|
let line = self.line();
|
|
match self.peek() {
|
|
Tok::LBrack => {
|
|
self.advance();
|
|
let index = self.parse_expr();
|
|
self.eat(&Tok::RBrack);
|
|
e = Expr::Index {
|
|
base: Box::new(e),
|
|
index: Box::new(index),
|
|
line,
|
|
};
|
|
}
|
|
Tok::Dot => {
|
|
self.advance();
|
|
let field = self.ident().unwrap_or_default();
|
|
e = Expr::Member {
|
|
base: Box::new(e),
|
|
field,
|
|
line,
|
|
};
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
e
|
|
}
|
|
|
|
fn parse_primary(&mut self) -> Expr {
|
|
let line = self.line();
|
|
match self.advance() {
|
|
Tok::Int(n) => Expr::Int(n, line),
|
|
Tok::Real(r) => Expr::Real(r, line),
|
|
Tok::Bool(b) => Expr::Bool(b, line),
|
|
Tok::Str(s) => Expr::Str(s, line),
|
|
Tok::Time(t) => Expr::Time(t, line),
|
|
Tok::LParen => {
|
|
let e = self.parse_expr();
|
|
self.eat(&Tok::RParen);
|
|
e
|
|
}
|
|
Tok::Ident(name) => {
|
|
if self.eat(&Tok::LParen) {
|
|
let args = self.parse_call_args();
|
|
Expr::Call {
|
|
callee: name,
|
|
args,
|
|
line,
|
|
}
|
|
} else {
|
|
Expr::Ident(name, line)
|
|
}
|
|
}
|
|
// Unrecognized start of expression — yield a placeholder identifier.
|
|
_ => Expr::Ident(String::new(), line),
|
|
}
|
|
}
|
|
|
|
fn parse_call_args(&mut self) -> Vec<CallArg> {
|
|
let mut args = Vec::new();
|
|
if self.eat(&Tok::RParen) {
|
|
return args;
|
|
}
|
|
loop {
|
|
// Named arg: ident := expr (peek two tokens).
|
|
if let Tok::Ident(name) = self.peek().clone() {
|
|
if matches!(
|
|
self.toks.get(self.pos + 1).map(|t| &t.kind),
|
|
Some(Tok::Assign)
|
|
) {
|
|
self.advance(); // ident
|
|
self.advance(); // :=
|
|
let value = self.parse_expr();
|
|
args.push(CallArg {
|
|
name: Some(name),
|
|
value,
|
|
});
|
|
if self.eat(&Tok::Comma) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
let value = self.parse_expr();
|
|
args.push(CallArg { name: None, value });
|
|
if self.eat(&Tok::Comma) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
self.eat(&Tok::RParen);
|
|
args
|
|
}
|
|
}
|
|
|
|
/// Parse ST source into its POUs.
|
|
pub fn parse(src: &str) -> Vec<Pou> {
|
|
let toks = super::lexer::lex(src);
|
|
Parser::new(toks).parse_units()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const SAMPLE: &str = r#"
|
|
PROGRAM Main
|
|
VAR
|
|
idx : INT;
|
|
pw : STRING := 'admin123';
|
|
buf : ARRAY[0..9] OF INT;
|
|
ok : BOOL := FALSE;
|
|
END_VAR
|
|
// a comment
|
|
IF idx > 0 THEN
|
|
buf[idx] := idx * 2;
|
|
ELSE
|
|
JMP done;
|
|
END_IF;
|
|
Comm(IP := '10.0.0.1', PORT := 502);
|
|
done:
|
|
ok := TRUE;
|
|
END_PROGRAM
|
|
"#;
|
|
|
|
#[test]
|
|
fn parses_program_vars_and_body() {
|
|
let pous = parse(SAMPLE);
|
|
assert_eq!(pous.len(), 1, "one POU");
|
|
let p = &pous[0];
|
|
assert_eq!(p.name, "Main");
|
|
assert_eq!(p.kind, PouKind::Program);
|
|
// vars: idx, pw, buf, ok
|
|
assert_eq!(p.vars.len(), 4);
|
|
let pw = p.vars.iter().find(|v| v.name == "pw").expect("pw");
|
|
assert!(matches!(&pw.init, Some(Expr::Str(s, _)) if s == "admin123"));
|
|
let buf = p.vars.iter().find(|v| v.name == "buf").expect("buf");
|
|
assert_eq!(buf.array_bounds, Some((0, 9)));
|
|
// body has an IF, a Call, a Label, and an Assign
|
|
assert!(p.body.iter().any(|s| matches!(s, Stmt::If { .. })));
|
|
assert!(p
|
|
.body
|
|
.iter()
|
|
.any(|s| matches!(s, Stmt::Call { callee, .. } if callee == "Comm")));
|
|
assert!(p
|
|
.body
|
|
.iter()
|
|
.any(|s| matches!(s, Stmt::Label { name, .. } if name == "done")));
|
|
}
|
|
|
|
#[test]
|
|
fn jmp_inside_if_is_captured() {
|
|
let pous = parse(SAMPLE);
|
|
let p = &pous[0];
|
|
// find the IF, check its else branch has a JMP
|
|
let has_jmp = p.body.iter().any(|s| match s {
|
|
Stmt::If { else_body, .. } => else_body
|
|
.as_ref()
|
|
.map(|b| b.iter().any(|s| matches!(s, Stmt::Jump { .. })))
|
|
.unwrap_or(false),
|
|
_ => false,
|
|
});
|
|
assert!(has_jmp, "JMP should be parsed inside the ELSE branch");
|
|
}
|
|
}
|