use crate::computation::{
arithmetic_operation, comparison_operation, convert_unit, UnitResolutionContext,
};
use crate::evaluation::OperationResult;
use crate::planning::execution_plan::{DataOverlay, ExecutionPlan};
use crate::planning::semantics::{Expression, ExpressionKind, LiteralValue, ValueKind};
pub(crate) fn resolve_expression_value(
expression: &Expression,
plan: &ExecutionPlan,
overlay: &DataOverlay,
) -> Option<LiteralValue> {
let unit_index = plan.expression_unit_index();
match &expression.kind {
ExpressionKind::Literal(literal) => Some(*literal.clone()),
ExpressionKind::DataPath(data_path) => overlay.supplied_value(data_path).cloned(),
ExpressionKind::Comparison(left_expression, operator, right_expression) => {
let left_value = resolve_expression_value(left_expression, plan, overlay)?;
let right_value = resolve_expression_value(right_expression, plan, overlay)?;
match comparison_operation(
&left_value,
operator,
&right_value,
UnitResolutionContext::WithIndex(unit_index),
) {
OperationResult::Value(result) => Some(result.as_ref().clone()),
OperationResult::Veto(_) => None,
}
}
ExpressionKind::Arithmetic(left_expression, operator, right_expression) => {
let left_value = resolve_expression_value(left_expression, plan, overlay)?;
let right_value = resolve_expression_value(right_expression, plan, overlay)?;
match arithmetic_operation(
&left_value,
operator,
&right_value,
unit_index,
&plan.signature_index,
) {
OperationResult::Value(result) => Some(result.as_ref().clone()),
OperationResult::Veto(_) => None,
}
}
ExpressionKind::UnitConversion(inner_expression, target) => {
let inner_value = resolve_expression_value(inner_expression, plan, overlay)?;
match convert_unit(&inner_value, target) {
OperationResult::Value(result) => Some(result.as_ref().clone()),
OperationResult::Veto(_) => None,
}
}
ExpressionKind::RulePath(_)
| ExpressionKind::Veto(_)
| ExpressionKind::Now
| ExpressionKind::DateRelative(_, _)
| ExpressionKind::DateCalendar(_, _, _)
| ExpressionKind::PastFutureRange(_, _)
| ExpressionKind::RangeLiteral(_, _)
| ExpressionKind::RangeContainment(_, _)
| ExpressionKind::MathematicalComputation(_, _)
| ExpressionKind::ResultIsVeto(_)
| ExpressionKind::LogicalAnd(_, _)
| ExpressionKind::LogicalOr(_, _)
| ExpressionKind::LogicalNegation(_, _)
| ExpressionKind::Piecewise(_) => None,
}
}
pub(crate) fn unless_condition_truth(
expression: &Expression,
plan: &ExecutionPlan,
overlay: &DataOverlay,
) -> Option<bool> {
match &expression.kind {
ExpressionKind::LogicalAnd(left_expression, right_expression) => {
let left_result = unless_condition_truth(left_expression, plan, overlay);
let right_result = unless_condition_truth(right_expression, plan, overlay);
match (left_result, right_result) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), Some(true)) => Some(true),
_ => None,
}
}
ExpressionKind::LogicalOr(left_expression, right_expression) => {
let left_result = unless_condition_truth(left_expression, plan, overlay);
let right_result = unless_condition_truth(right_expression, plan, overlay);
match (left_result, right_result) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
}
}
ExpressionKind::LogicalNegation(inner_expression, _negation_type) => {
unless_condition_truth(inner_expression, plan, overlay).map(|boolean| !boolean)
}
_ => {
let value = resolve_expression_value(expression, plan, overlay)?;
match value.value {
ValueKind::Boolean(boolean) => Some(boolean),
_ => None,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::DateTimeValue;
use crate::planning::data_input::DataValueInput;
use crate::Engine;
use crate::SourceType;
fn unless_condition_from_plan(
plan: &ExecutionPlan,
rule_name: &str,
unless_index: usize,
) -> Expression {
let rule = plan.get_rule(rule_name).expect("rule must exist in plan");
let branch_index = unless_index + 1;
rule.branches[branch_index]
.condition
.clone()
.expect("unless branch must have condition")
}
fn load_spec(source: &str) -> (Engine, ExecutionPlan) {
let mut engine = Engine::new();
engine
.load(source, SourceType::Volatile)
.expect("spec must load");
let now = DateTimeValue::now();
let plan = engine
.get_plan(None, "t", Some(&now))
.expect("plan must build")
.clone();
(engine, plan)
}
#[test]
fn unless_truth_none_without_supplied_value() {
let source = r#"
spec t
data is_member: boolean
rule discount: 0%
unless is_member then 20%
"#;
let (_engine, plan) = load_spec(source);
let condition = unless_condition_from_plan(&plan, "discount", 0);
let overlay = DataOverlay::default();
assert_eq!(
unless_condition_truth(&condition, &plan, &overlay),
None,
"spec prefilled value must not make unless is_member definitely false"
);
}
#[test]
fn unless_truth_false_when_conjunct_constant_false() {
let source = r#"
spec t
data flag: boolean
rule discount: 0%
unless flag and (1 > 2) then 20%
"#;
let (_engine, plan) = load_spec(source);
let condition = unless_condition_from_plan(&plan, "discount", 0);
let overlay = DataOverlay::default();
assert_eq!(
unless_condition_truth(&condition, &plan, &overlay),
Some(false),
"unless arm cannot apply when (1 > 2) is false regardless of flag"
);
}
#[test]
fn unless_truth_true_when_supplied_text_matches() {
let source = r#"
spec t
data mode: text -> options "simple" "complex"
rule result: 0
unless mode is "simple" then 1
"#;
let (engine, plan) = load_spec(source);
let condition = unless_condition_from_plan(&plan, "result", 0);
let overlay = DataOverlay::resolve(
&plan,
[(
"mode".to_string(),
DataValueInput::convenience("simple".to_string()),
)]
.into(),
engine.limits(),
)
.expect("overlay must resolve");
assert_eq!(
unless_condition_truth(&condition, &plan, &overlay),
Some(true),
"unless condition truth must follow caller-supplied mode"
);
}
#[test]
fn unless_truth_true_when_supplied_overrides_prefilled_false() {
let source = r#"
spec t
data is_member: false
rule discount: 0%
unless is_member then 20%
"#;
let (engine, plan) = load_spec(source);
let condition = unless_condition_from_plan(&plan, "discount", 0);
let overlay = DataOverlay::resolve(
&plan,
[("is_member".to_string(), DataValueInput::Boolean(true))].into(),
engine.limits(),
)
.expect("overlay must resolve");
assert_eq!(
unless_condition_truth(&condition, &plan, &overlay),
Some(true),
"caller-supplied true must decide unless arm despite spec prefilled false"
);
}
#[test]
fn unless_truth_false_when_supplied_matches_prefilled_false() {
let source = r#"
spec t
data is_member: false
rule discount: 0%
unless is_member then 20%
"#;
let (engine, plan) = load_spec(source);
let condition = unless_condition_from_plan(&plan, "discount", 0);
let overlay = DataOverlay::resolve(
&plan,
[("is_member".to_string(), DataValueInput::Boolean(false))].into(),
engine.limits(),
)
.expect("overlay must resolve");
assert_eq!(
unless_condition_truth(&condition, &plan, &overlay),
Some(false),
"caller-supplied false must prune unless arm when overlay commits the value"
);
}
}