use crate::error::Span;
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
impl<T> Spanned<T> {
pub const fn new(node: T, span: Span) -> Self {
Self { node, span }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
LParen,
RParen,
Keyword(Keyword),
IntLiteral(i64),
FloatLiteral(f64),
StringLiteral(String),
BoolLiteral(bool),
NullLiteral,
Symbol {
root: SymbolRoot,
path: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolRoot {
Root,
Element,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Keyword {
EQ,
NE,
LT,
LE,
GT,
GE,
NonEmpty,
AND,
OR,
NOT,
ForAll,
Exists,
Add,
Sub,
Mul,
Div,
Mod,
Neg,
Abs,
Concat,
Length,
Substring,
Upper,
Lower,
Head,
Tail,
Get,
Count,
GetKeys,
GetValues,
}
impl Keyword {
pub fn from_ident(s: &str) -> Option<Keyword> {
Some(match s {
"EQ" => Keyword::EQ,
"NE" => Keyword::NE,
"LT" => Keyword::LT,
"LE" => Keyword::LE,
"GT" => Keyword::GT,
"GE" => Keyword::GE,
"NonEmpty" => Keyword::NonEmpty,
"AND" => Keyword::AND,
"OR" => Keyword::OR,
"NOT" => Keyword::NOT,
"ForAll" => Keyword::ForAll,
"Exists" => Keyword::Exists,
"Add" => Keyword::Add,
"Sub" => Keyword::Sub,
"Mul" => Keyword::Mul,
"Div" => Keyword::Div,
"Mod" => Keyword::Mod,
"Neg" => Keyword::Neg,
"Abs" => Keyword::Abs,
"Concat" => Keyword::Concat,
"Length" => Keyword::Length,
"Substring" => Keyword::Substring,
"Upper" => Keyword::Upper,
"Lower" => Keyword::Lower,
"Head" => Keyword::Head,
"Tail" => Keyword::Tail,
"Get" => Keyword::Get,
"Count" => Keyword::Count,
"GetKeys" => Keyword::GetKeys,
"GetValues" => Keyword::GetValues,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifierOp {
EQ,
NE,
LT,
LE,
GT,
GE,
}
impl VerifierOp {
pub fn from_keyword(kw: Keyword) -> Option<Self> {
Some(match kw {
Keyword::EQ => VerifierOp::EQ,
Keyword::NE => VerifierOp::NE,
Keyword::LT => VerifierOp::LT,
Keyword::LE => VerifierOp::LE,
Keyword::GT => VerifierOp::GT,
Keyword::GE => VerifierOp::GE,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryCheckOp {
NonEmpty,
}
impl UnaryCheckOp {
pub fn from_keyword(kw: Keyword) -> Option<Self> {
Some(match kw {
Keyword::NonEmpty => UnaryCheckOp::NonEmpty,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantifierOp {
ForAll,
Exists,
}
impl QuantifierOp {
pub fn from_keyword(kw: Keyword) -> Option<Self> {
Some(match kw {
Keyword::ForAll => QuantifierOp::ForAll,
Keyword::Exists => QuantifierOp::Exists,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuncOp {
Add,
Sub,
Mul,
Div,
Mod,
Neg,
Abs,
Concat,
Length,
Substring,
Upper,
Lower,
Head,
Tail,
Get,
Count,
GetKeys,
GetValues,
}
impl FuncOp {
pub fn from_keyword(kw: Keyword) -> Option<Self> {
Some(match kw {
Keyword::Add => FuncOp::Add,
Keyword::Sub => FuncOp::Sub,
Keyword::Mul => FuncOp::Mul,
Keyword::Div => FuncOp::Div,
Keyword::Mod => FuncOp::Mod,
Keyword::Neg => FuncOp::Neg,
Keyword::Abs => FuncOp::Abs,
Keyword::Concat => FuncOp::Concat,
Keyword::Length => FuncOp::Length,
Keyword::Substring => FuncOp::Substring,
Keyword::Upper => FuncOp::Upper,
Keyword::Lower => FuncOp::Lower,
Keyword::Head => FuncOp::Head,
Keyword::Tail => FuncOp::Tail,
Keyword::Get => FuncOp::Get,
Keyword::Count => FuncOp::Count,
Keyword::GetKeys => FuncOp::GetKeys,
Keyword::GetValues => FuncOp::GetValues,
_ => return None,
})
}
pub fn expected_arity(&self) -> usize {
match self {
FuncOp::Neg
| FuncOp::Abs
| FuncOp::Length
| FuncOp::Upper
| FuncOp::Lower
| FuncOp::Head
| FuncOp::Tail
| FuncOp::Count
| FuncOp::GetKeys
| FuncOp::GetValues => 1,
FuncOp::Add
| FuncOp::Sub
| FuncOp::Mul
| FuncOp::Div
| FuncOp::Mod
| FuncOp::Concat
| FuncOp::Get => 2,
FuncOp::Substring => 3,
}
}
pub fn name(&self) -> &'static str {
match self {
FuncOp::Add => "Add",
FuncOp::Sub => "Sub",
FuncOp::Mul => "Mul",
FuncOp::Div => "Div",
FuncOp::Mod => "Mod",
FuncOp::Neg => "Neg",
FuncOp::Abs => "Abs",
FuncOp::Concat => "Concat",
FuncOp::Length => "Length",
FuncOp::Substring => "Substring",
FuncOp::Upper => "Upper",
FuncOp::Lower => "Lower",
FuncOp::Head => "Head",
FuncOp::Tail => "Tail",
FuncOp::Get => "Get",
FuncOp::Count => "Count",
FuncOp::GetKeys => "GetKeys",
FuncOp::GetValues => "GetValues",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Int(i64),
Float(f64),
String(String),
Bool(bool),
Null,
}
pub type SpannedBoolExpr = Spanned<BoolExpr>;
pub type SpannedValueExpr = Spanned<ValueExpr>;
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
pub expr: SpannedBoolExpr,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BoolExpr {
Literal(bool),
Verifier {
op: VerifierOp,
left: Box<SpannedValueExpr>,
right: Box<SpannedValueExpr>,
},
And(Box<SpannedBoolExpr>, Box<SpannedBoolExpr>),
Or(Box<SpannedBoolExpr>, Box<SpannedBoolExpr>),
Not(Box<SpannedBoolExpr>),
UnaryCheck {
op: UnaryCheckOp,
operand: Box<SpannedValueExpr>,
},
Quantifier {
op: QuantifierOp,
predicate: Spanned<Predicate>,
operand: Box<SpannedValueExpr>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValueExpr {
Literal(Literal),
Symbol {
root: SymbolRoot,
path: String,
},
FuncCall {
op: FuncOp,
args: Vec<SpannedValueExpr>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Predicate {
PartialVerifier {
op: VerifierOp,
bound: Box<SpannedValueExpr>,
},
UnaryCheck(UnaryCheckOp),
Full(Box<SpannedBoolExpr>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyword_from_ident_matches_reserved_words() {
assert_eq!(Keyword::from_ident("EQ"), Some(Keyword::EQ));
assert_eq!(Keyword::from_ident("NonEmpty"), Some(Keyword::NonEmpty));
assert_eq!(Keyword::from_ident("ForAll"), Some(Keyword::ForAll));
assert_eq!(Keyword::from_ident("GetValues"), Some(Keyword::GetValues));
}
#[test]
fn keyword_from_ident_rejects_unknown_and_is_case_sensitive() {
assert_eq!(Keyword::from_ident("eq"), None);
assert_eq!(Keyword::from_ident("foobar"), None);
assert_eq!(Keyword::from_ident(""), None);
}
#[test]
fn verifier_op_conversion_covers_all_variants() {
let pairs = [
(Keyword::EQ, VerifierOp::EQ),
(Keyword::NE, VerifierOp::NE),
(Keyword::LT, VerifierOp::LT),
(Keyword::LE, VerifierOp::LE),
(Keyword::GT, VerifierOp::GT),
(Keyword::GE, VerifierOp::GE),
];
for (kw, op) in pairs {
assert_eq!(VerifierOp::from_keyword(kw), Some(op));
}
assert_eq!(VerifierOp::from_keyword(Keyword::AND), None);
}
#[test]
fn quantifier_op_conversion() {
assert_eq!(
QuantifierOp::from_keyword(Keyword::ForAll),
Some(QuantifierOp::ForAll)
);
assert_eq!(
QuantifierOp::from_keyword(Keyword::Exists),
Some(QuantifierOp::Exists)
);
assert_eq!(QuantifierOp::from_keyword(Keyword::NonEmpty), None);
}
#[test]
fn func_op_arity_table_matches_spec() {
let one = [
FuncOp::Neg,
FuncOp::Abs,
FuncOp::Length,
FuncOp::Upper,
FuncOp::Lower,
FuncOp::Head,
FuncOp::Tail,
FuncOp::Count,
FuncOp::GetKeys,
FuncOp::GetValues,
];
let two = [
FuncOp::Add,
FuncOp::Sub,
FuncOp::Mul,
FuncOp::Div,
FuncOp::Mod,
FuncOp::Concat,
FuncOp::Get,
];
let three = [FuncOp::Substring];
for op in one {
assert_eq!(op.expected_arity(), 1, "{:?} should have arity 1", op);
}
for op in two {
assert_eq!(op.expected_arity(), 2, "{:?} should have arity 2", op);
}
for op in three {
assert_eq!(op.expected_arity(), 3, "{:?} should have arity 3", op);
}
}
#[test]
fn func_op_from_keyword_rejects_non_func_keywords() {
assert_eq!(FuncOp::from_keyword(Keyword::AND), None);
assert_eq!(FuncOp::from_keyword(Keyword::EQ), None);
assert_eq!(FuncOp::from_keyword(Keyword::ForAll), None);
assert_eq!(FuncOp::from_keyword(Keyword::Add), Some(FuncOp::Add));
}
#[test]
fn spanned_wrapper_constructs_and_exposes_fields() {
let sp = Spanned::new(Literal::Int(42), Span::new(0, 2));
assert_eq!(sp.node, Literal::Int(42));
assert_eq!(sp.span, Span::new(0, 2));
}
#[test]
fn ast_structural_equality_sanity() {
let left = Spanned::new(
ValueExpr::FuncCall {
op: FuncOp::Add,
args: vec![
Spanned::new(ValueExpr::Literal(Literal::Int(1)), Span::new(5, 6)),
Spanned::new(ValueExpr::Literal(Literal::Int(2)), Span::new(7, 8)),
],
},
Span::new(1, 9),
);
let right = Spanned::new(ValueExpr::Literal(Literal::Int(0)), Span::new(10, 11));
let expr = Spanned::new(
BoolExpr::Verifier {
op: VerifierOp::GT,
left: Box::new(left.clone()),
right: Box::new(right.clone()),
},
Span::new(0, 12),
);
let program = Program { expr: expr.clone() };
assert_eq!(program.expr, expr);
}
#[test]
fn func_op_name_round_trips_through_keyword() {
for op in [
FuncOp::Add,
FuncOp::Sub,
FuncOp::Mul,
FuncOp::Div,
FuncOp::Mod,
FuncOp::Neg,
FuncOp::Abs,
FuncOp::Concat,
FuncOp::Length,
FuncOp::Substring,
FuncOp::Upper,
FuncOp::Lower,
FuncOp::Head,
FuncOp::Tail,
FuncOp::Get,
FuncOp::Count,
FuncOp::GetKeys,
FuncOp::GetValues,
] {
let kw = Keyword::from_ident(op.name()).expect("name should be a keyword");
assert_eq!(FuncOp::from_keyword(kw), Some(op));
}
}
}