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
227 lines
5.1 KiB
Rust
227 lines
5.1 KiB
Rust
//! Abstract syntax tree for IEC 61131-3 Structured Text (ST).
|
|
//!
|
|
//! This is the security-relevant subset: POUs with their variable declarations
|
|
//! and statement bodies, enough to run semantic control-logic rules over. It is
|
|
//! deliberately not a full language model — declarations we don't reason about
|
|
//! (e.g. exotic type definitions) are parsed loosely and kept as raw text.
|
|
|
|
/// A Program Organization Unit: a PROGRAM, FUNCTION, or FUNCTION_BLOCK.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Pou {
|
|
pub name: String,
|
|
pub kind: PouKind,
|
|
/// The declared variables, across all VAR_* sections.
|
|
pub vars: Vec<VarDecl>,
|
|
/// The statement body.
|
|
pub body: Vec<Stmt>,
|
|
/// 1-based line where the POU header appears (in the source that was parsed).
|
|
pub line: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PouKind {
|
|
Program,
|
|
Function,
|
|
FunctionBlock,
|
|
}
|
|
|
|
impl PouKind {
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
PouKind::Program => "PROGRAM",
|
|
PouKind::Function => "FUNCTION",
|
|
PouKind::FunctionBlock => "FUNCTION_BLOCK",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A single declared variable.
|
|
#[derive(Debug, Clone)]
|
|
pub struct VarDecl {
|
|
pub name: String,
|
|
pub section: VarSection,
|
|
/// The declared type as written (e.g. `BOOL`, `INT`, `ARRAY[0..9] OF INT`).
|
|
pub type_name: String,
|
|
/// Whether the type is an ARRAY, and its declared bounds `(lo, hi)` when
|
|
/// they are literal integers — used by the array-bounds rule.
|
|
pub array_bounds: Option<(i64, i64)>,
|
|
/// The initializer expression, if any (`:= <expr>`).
|
|
pub init: Option<Expr>,
|
|
pub line: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum VarSection {
|
|
Var,
|
|
Input,
|
|
Output,
|
|
InOut,
|
|
Global,
|
|
Temp,
|
|
External,
|
|
}
|
|
|
|
/// A statement.
|
|
#[derive(Debug, Clone)]
|
|
pub enum Stmt {
|
|
Assign {
|
|
target: Expr,
|
|
value: Expr,
|
|
line: u32,
|
|
},
|
|
If {
|
|
/// (condition, body) for IF and each ELSIF, in order.
|
|
branches: Vec<(Expr, Vec<Stmt>)>,
|
|
else_body: Option<Vec<Stmt>>,
|
|
line: u32,
|
|
},
|
|
Case {
|
|
selector: Expr,
|
|
/// (label expressions, body) per CASE arm.
|
|
arms: Vec<(Vec<Expr>, Vec<Stmt>)>,
|
|
else_body: Option<Vec<Stmt>>,
|
|
line: u32,
|
|
},
|
|
For {
|
|
var: String,
|
|
from: Expr,
|
|
to: Expr,
|
|
by: Option<Expr>,
|
|
body: Vec<Stmt>,
|
|
line: u32,
|
|
},
|
|
While {
|
|
cond: Expr,
|
|
body: Vec<Stmt>,
|
|
line: u32,
|
|
},
|
|
Repeat {
|
|
body: Vec<Stmt>,
|
|
until: Expr,
|
|
line: u32,
|
|
},
|
|
/// A bare call statement, e.g. `TON1(IN := x, PT := T#5s);`.
|
|
Call {
|
|
callee: String,
|
|
args: Vec<CallArg>,
|
|
line: u32,
|
|
},
|
|
Return {
|
|
line: u32,
|
|
},
|
|
Exit {
|
|
line: u32,
|
|
},
|
|
/// `JMP label;` — an unstructured jump.
|
|
Jump {
|
|
label: String,
|
|
line: u32,
|
|
},
|
|
/// `label:` — a jump target.
|
|
Label {
|
|
name: String,
|
|
line: u32,
|
|
},
|
|
}
|
|
|
|
/// One argument in a call: positional (`name: None`) or named (`X := expr`).
|
|
#[derive(Debug, Clone)]
|
|
pub struct CallArg {
|
|
pub name: Option<String>,
|
|
pub value: Expr,
|
|
}
|
|
|
|
/// An expression.
|
|
#[derive(Debug, Clone)]
|
|
pub enum Expr {
|
|
Int(i64, u32),
|
|
Real(f64, u32),
|
|
Bool(bool, u32),
|
|
/// A string literal, with the unquoted contents.
|
|
Str(String, u32),
|
|
/// A duration / date / time literal, kept as raw text (`T#5s`, `DT#...`).
|
|
Time(String, u32),
|
|
Ident(String, u32),
|
|
/// `base[index]`.
|
|
Index {
|
|
base: Box<Expr>,
|
|
index: Box<Expr>,
|
|
line: u32,
|
|
},
|
|
/// `base.field`.
|
|
Member {
|
|
base: Box<Expr>,
|
|
field: String,
|
|
line: u32,
|
|
},
|
|
Unary {
|
|
op: UnOp,
|
|
expr: Box<Expr>,
|
|
line: u32,
|
|
},
|
|
Binary {
|
|
op: BinOp,
|
|
lhs: Box<Expr>,
|
|
rhs: Box<Expr>,
|
|
line: u32,
|
|
},
|
|
/// A function call used as an expression, e.g. `LIMIT(a, b, c)`.
|
|
Call {
|
|
callee: String,
|
|
args: Vec<CallArg>,
|
|
line: u32,
|
|
},
|
|
}
|
|
|
|
impl Expr {
|
|
/// The 1-based source line this expression starts on.
|
|
pub fn line(&self) -> u32 {
|
|
match self {
|
|
Expr::Int(_, l)
|
|
| Expr::Real(_, l)
|
|
| Expr::Bool(_, l)
|
|
| Expr::Str(_, l)
|
|
| Expr::Time(_, l)
|
|
| Expr::Ident(_, l)
|
|
| Expr::Index { line: l, .. }
|
|
| Expr::Member { line: l, .. }
|
|
| Expr::Unary { line: l, .. }
|
|
| Expr::Binary { line: l, .. }
|
|
| Expr::Call { line: l, .. } => *l,
|
|
}
|
|
}
|
|
|
|
/// If this expression is a plain identifier, its name.
|
|
pub fn as_ident(&self) -> Option<&str> {
|
|
match self {
|
|
Expr::Ident(name, _) => Some(name.as_str()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum UnOp {
|
|
Not,
|
|
Neg,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BinOp {
|
|
Add,
|
|
Sub,
|
|
Mul,
|
|
Div,
|
|
Mod,
|
|
Pow,
|
|
Eq,
|
|
Ne,
|
|
Lt,
|
|
Le,
|
|
Gt,
|
|
Ge,
|
|
And,
|
|
Or,
|
|
Xor,
|
|
}
|