use crate::context::entity::Entity;
use crate::context::{connective, function, quantifier, verifier};
use crate::error::{scope_error, NightjarLanguageError};
use crate::language::grammar::{
BoolExpr, Literal, Predicate, Program, SpannedBoolExpr, SpannedValueExpr, SymbolRoot,
UnaryCheckOp, ValueExpr,
};
use crate::language::parser::{parse_with_config, ParserConfig};
use crate::symbol_table::{resolve_in_entity, SymbolTable};
#[derive(Debug, Clone)]
pub struct ExecOptions {
pub float_epsilon: f64,
pub max_depth: usize,
}
impl Default for ExecOptions {
fn default() -> Self {
Self {
float_epsilon: 1e-10,
max_depth: 256,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExecResult {
True,
False,
Error(NightjarLanguageError),
}
impl ExecResult {
pub fn is_true(&self) -> bool {
matches!(self, ExecResult::True)
}
pub fn is_false(&self) -> bool {
matches!(self, ExecResult::False)
}
pub fn is_error(&self) -> bool {
matches!(self, ExecResult::Error(_))
}
}
impl From<Result<bool, NightjarLanguageError>> for ExecResult {
fn from(r: Result<bool, NightjarLanguageError>) -> Self {
match r {
Ok(true) => ExecResult::True,
Ok(false) => ExecResult::False,
Err(e) => ExecResult::Error(e),
}
}
}
pub fn exec_entity(expression: &str, data: Entity, options: ExecOptions) -> ExecResult {
let cfg = ParserConfig {
max_depth: options.max_depth,
};
let program = match parse_with_config(expression, &cfg) {
Ok(p) => p,
Err(e) => return ExecResult::Error(e),
};
let symbols = SymbolTable::from_entity(data);
eval_program(&program, &symbols, &options).into()
}
#[cfg(feature = "json")]
pub fn exec(expression: &str, data: serde_json::Value, options: ExecOptions) -> ExecResult {
exec_entity(expression, Entity::from(data), options)
}
fn eval_program(
p: &Program,
symbols: &SymbolTable,
opts: &ExecOptions,
) -> Result<bool, NightjarLanguageError> {
eval_bool(&p.expr, symbols, opts, None)
}
fn eval_bool(
expr: &SpannedBoolExpr,
symbols: &SymbolTable,
opts: &ExecOptions,
scope: Option<&Entity>,
) -> Result<bool, NightjarLanguageError> {
match &expr.node {
BoolExpr::Literal(b) => Ok(*b),
BoolExpr::Verifier { op, left, right } => {
let l = eval_value(left, symbols, opts, scope)?;
let r = eval_value(right, symbols, opts, scope)?;
verifier::apply_verifier(*op, &l, &r, opts.float_epsilon, expr.span)
}
BoolExpr::And(l, r) => {
let lv = eval_bool(l, symbols, opts, scope)?;
let rv = eval_bool(r, symbols, opts, scope)?;
Ok(connective::apply_and(lv, rv))
}
BoolExpr::Or(l, r) => {
let lv = eval_bool(l, symbols, opts, scope)?;
let rv = eval_bool(r, symbols, opts, scope)?;
Ok(connective::apply_or(lv, rv))
}
BoolExpr::Not(inner) => {
let v = eval_bool(inner, symbols, opts, scope)?;
Ok(connective::apply_not(v))
}
BoolExpr::UnaryCheck { op, operand } => {
let v = eval_value(operand, symbols, opts, scope)?;
match op {
UnaryCheckOp::NonEmpty => Ok(v.is_non_empty()),
}
}
BoolExpr::Quantifier {
op,
predicate,
operand,
} => {
let coll = eval_value(operand, symbols, opts, scope)?; match &predicate.node {
Predicate::Full(body) => {
quantifier::apply_quantifier_full(*op, &coll, expr.span, |element| {
eval_bool(body, symbols, opts, Some(element))
})
}
_ => {
let eval_pred = resolve_predicate(&predicate.node, symbols, opts, scope)?;
quantifier::apply_quantifier(
*op,
&eval_pred,
&coll,
opts.float_epsilon,
expr.span,
)
}
}
}
}
}
#[allow(clippy::only_used_in_recursion)]
fn eval_value(
expr: &SpannedValueExpr,
symbols: &SymbolTable,
opts: &ExecOptions,
scope: Option<&Entity>,
) -> Result<Entity, NightjarLanguageError> {
match &expr.node {
ValueExpr::Literal(lit) => Ok(literal_to_entity(lit)),
ValueExpr::Symbol { root, path } => match root {
SymbolRoot::Root => symbols.resolve_root_path(path, expr.span),
SymbolRoot::Element => match scope {
Some(elem) => resolve_in_entity(path, elem, expr.span),
None => Err(scope_error(
expr.span,
"`@` element-relative symbol evaluated without an enclosing quantifier",
)),
},
},
ValueExpr::FuncCall { op, args } => {
let mut evaluated = Vec::with_capacity(args.len());
for arg in args {
evaluated.push(eval_value(arg, symbols, opts, scope)?);
}
function::apply_function(*op, evaluated, expr.span)
}
}
}
fn resolve_predicate(
pred: &Predicate,
symbols: &SymbolTable,
opts: &ExecOptions,
scope: Option<&Entity>,
) -> Result<quantifier::EvalPredicate, NightjarLanguageError> {
match pred {
Predicate::PartialVerifier { op, bound } => {
let bound_val = eval_value(bound, symbols, opts, scope)?;
Ok(quantifier::EvalPredicate::PartialVerifier {
op: *op,
bound: bound_val,
})
}
Predicate::UnaryCheck(check_op) => Ok(quantifier::EvalPredicate::UnaryCheck(*check_op)),
Predicate::Full(_) => unreachable!("Predicate::Full handled in eval_bool"),
}
}
fn literal_to_entity(lit: &Literal) -> Entity {
match lit {
Literal::Int(i) => Entity::Int(*i),
Literal::Float(f) => Entity::Float(*f),
Literal::String(s) => Entity::String(s.clone()),
Literal::Bool(b) => Entity::Bool(*b),
Literal::Null => Entity::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::NightjarLanguageError;
use std::collections::HashMap;
fn run(expr: &str, data: Entity) -> ExecResult {
exec_entity(expr, data, ExecOptions::default())
}
fn empty() -> Entity {
Entity::Map(HashMap::new())
}
#[test]
fn top_level_true_and_false() {
assert_eq!(run("True", empty()), ExecResult::True);
assert_eq!(run("False", empty()), ExecResult::False);
}
#[test]
fn gt_simple() {
assert_eq!(run("(GT 1 2)", empty()), ExecResult::False);
assert_eq!(run("(GT 3 2)", empty()), ExecResult::True);
}
#[test]
fn eq_simple() {
assert_eq!(run("(EQ 1 1)", empty()), ExecResult::True);
assert_eq!(run("(EQ 1 2)", empty()), ExecResult::False);
}
#[test]
fn type_error_becomes_exec_error() {
let r = run("(GT GT 1)", empty());
assert!(matches!(r, ExecResult::Error(_)));
}
fn map_of(pairs: &[(&str, Entity)]) -> Entity {
let mut m = HashMap::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
Entity::Map(m)
}
#[test]
fn symbol_verifier() {
let data = map_of(&[("revenue", Entity::Int(100))]);
assert_eq!(run("(GE .revenue 100)", data), ExecResult::True);
}
#[test]
fn computed_verification_via_symbols() {
let data = map_of(&[
("dept1", Entity::Int(100)),
("dept2", Entity::Int(200)),
("total", Entity::Int(300)),
]);
assert_eq!(
run("(EQ (Add .dept1 .dept2) .total)", data),
ExecResult::True
);
}
#[test]
fn connective_and_nonempty() {
let data = map_of(&[
("revenue", Entity::Int(50)),
("name", Entity::String("Acme".into())),
]);
assert_eq!(
run("(AND (GE .revenue 0) (NonEmpty .name))", data),
ExecResult::True
);
}
#[test]
fn forall_list_positive() {
let data = map_of(&[(
"scores",
Entity::List(vec![Entity::Int(1), Entity::Int(2), Entity::Int(3)]),
)]);
assert_eq!(run("(ForAll (GT 0) .scores)", data), ExecResult::True);
}
#[test]
fn forall_list_zero_fails() {
let data = map_of(&[(
"scores",
Entity::List(vec![Entity::Int(0), Entity::Int(1), Entity::Int(2)]),
)]);
assert_eq!(run("(ForAll (GT 0) .scores)", data), ExecResult::False);
}
#[test]
fn exists_admin_role() {
let data = map_of(&[(
"roles",
Entity::List(vec![
Entity::String("user".into()),
Entity::String("admin".into()),
]),
)]);
assert_eq!(
run("(Exists (EQ \"admin\") .roles)", data),
ExecResult::True
);
}
#[test]
fn forall_scalar_fallback() {
let data = map_of(&[("count", Entity::Int(5))]);
assert_eq!(run("(ForAll (GT 0) .count)", data), ExecResult::True);
}
#[test]
fn forall_map_operand_is_type_error() {
let data = map_of(&[("data", map_of(&[("a", Entity::Int(1))]))]);
let r = run("(ForAll (GT 0) .data)", data);
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::TypeError { .. })
));
}
#[test]
fn forall_over_map_values_via_getvalues() {
let data = map_of(&[(
"revenue_by_dept",
map_of(&[("a", Entity::Int(10)), ("b", Entity::Int(20))]),
)]);
assert_eq!(
run("(ForAll (GE 0) (GetValues .revenue_by_dept))", data),
ExecResult::True
);
}
#[test]
fn missing_symbol_is_exec_error() {
let r = run("(GT .missing 0)", empty());
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::SymbolNotFound { .. })
));
}
#[test]
fn division_by_zero_is_exec_error() {
let r = run("(EQ (Div 1 0) 0)", empty());
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::DivisionByZero { .. })
));
}
#[test]
fn integer_overflow_is_exec_error() {
let r = run("(EQ (Add 9223372036854775807 1) 0)", empty());
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::IntegerOverflow { .. })
));
}
#[test]
fn nested_arithmetic_evaluates_inside_out() {
assert_eq!(
run("(EQ (Add (Mul 2 3) (Sub 10 4)) 12)", empty()),
ExecResult::True
);
}
#[test]
fn chained_quantifier_and_count() {
let data = map_of(&[(
"scores",
Entity::List(vec![
Entity::Int(1),
Entity::Int(2),
Entity::Int(3),
Entity::Int(4),
]),
)]);
assert_eq!(
run("(AND (ForAll (GT 0) .scores) (GT (Count .scores) 3))", data),
ExecResult::True
);
}
#[test]
fn epsilon_equality_via_default_options() {
assert_eq!(run("(EQ (Add 0.1 0.2) 0.3)", empty()), ExecResult::True);
}
fn obj(pairs: &[(&str, Entity)]) -> Entity {
map_of(pairs)
}
#[test]
fn forall_equal_fields_all_true() {
let data = map_of(&[(
"items",
Entity::List(vec![
obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
obj(&[("a", Entity::Int(2)), ("b", Entity::Int(2))]),
obj(&[("a", Entity::Int(3)), ("b", Entity::Int(3))]),
]),
)]);
assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::True);
}
#[test]
fn forall_equal_fields_one_mismatch_is_false() {
let data = map_of(&[(
"items",
Entity::List(vec![
obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
obj(&[("a", Entity::Int(2)), ("b", Entity::Int(9))]),
]),
)]);
assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::False);
}
#[test]
fn forall_sum_of_fields_equals_third_field() {
let data = map_of(&[(
"items",
Entity::List(vec![
obj(&[
("a", Entity::Int(1)),
("b", Entity::Int(1)),
("c", Entity::Int(2)),
]),
obj(&[
("a", Entity::Int(2)),
("b", Entity::Int(2)),
("c", Entity::Int(4)),
]),
obj(&[
("a", Entity::Int(3)),
("b", Entity::Int(3)),
("c", Entity::Int(6)),
]),
]),
)]);
assert_eq!(
run("(ForAll (EQ (Add @.a @.b) @.c) .items)", data),
ExecResult::True
);
}
#[test]
fn bare_at_refers_to_whole_element() {
let data = map_of(&[(
"scores",
Entity::List(vec![Entity::Int(1), Entity::Int(2), Entity::Int(3)]),
)]);
assert_eq!(run("(ForAll (GT @ 0) .scores)", data), ExecResult::True);
}
#[test]
fn at_field_missing_is_symbol_not_found() {
let data = map_of(&[(
"items",
Entity::List(vec![
obj(&[("a", Entity::Int(1)), ("b", Entity::Int(1))]),
obj(&[("a", Entity::Int(2))]), ]),
)]);
let r = run("(ForAll (EQ @.a @.b) .items)", data);
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::SymbolNotFound { .. })
));
}
#[test]
fn mixed_root_and_element_symbols_in_predicate() {
let data = map_of(&[
("threshold", Entity::Int(100)),
(
"employees",
Entity::List(vec![
obj(&[("salary", Entity::Int(150))]),
obj(&[("salary", Entity::Int(200))]),
]),
),
]);
assert_eq!(
run("(ForAll (GT @.salary .threshold) .employees)", data),
ExecResult::True
);
}
#[test]
fn nested_quantifier_inner_at_refers_to_inner_element() {
let data = map_of(&[(
"teams",
Entity::List(vec![
obj(&[("scores", Entity::List(vec![Entity::Int(1), Entity::Int(2)]))]),
obj(&[("scores", Entity::List(vec![Entity::Int(3), Entity::Int(4)]))]),
]),
)]);
assert_eq!(
run("(ForAll (ForAll (GT @ 0) @.scores) .teams)", data),
ExecResult::True
);
}
#[test]
fn exists_with_full_predicate_short_circuits() {
let data = map_of(&[(
"items",
Entity::List(vec![
obj(&[("a", Entity::Int(1)), ("b", Entity::Int(2))]),
obj(&[("a", Entity::Int(5)), ("b", Entity::Int(5))]),
obj(&[("a", Entity::Int(9)), ("b", Entity::Int(8))]),
]),
)]);
assert_eq!(run("(Exists (EQ @.a @.b) .items)", data), ExecResult::True);
}
#[test]
fn forall_full_predicate_on_empty_list_is_vacuously_true() {
let data = map_of(&[("items", Entity::List(vec![]))]);
assert_eq!(run("(ForAll (EQ @.a @.b) .items)", data), ExecResult::True);
}
#[test]
fn depth_limit_surfaces_as_exec_error() {
let mut s = String::new();
for _ in 0..20 {
s.push_str("(NOT ");
}
s.push_str("True");
for _ in 0..20 {
s.push(')');
}
let opts = ExecOptions {
max_depth: 10,
..ExecOptions::default()
};
let r = exec_entity(&s, empty(), opts);
assert!(matches!(
r,
ExecResult::Error(NightjarLanguageError::RecursionError { .. })
));
}
}