Skip to main content

lemma/evaluation/
operations.rs

1//! Operation types and result handling for evaluation
2
3use std::fmt;
4
5use crate::planning::semantics::{
6    ArithmeticComputation, ComparisonComputation, DataPath, LemmaType, LiteralValue,
7    LogicalComputation, MathematicalComputation, RulePath, SemanticConversionTarget,
8    SemanticDateTime, SemanticTime, TypeSpecification,
9};
10use serde::{Deserialize, Serialize};
11
12/// Why an operation yielded no value (domain veto).
13///
14/// JSON serialization is a single string (see [`fmt::Display`]). There is intentionally no
15/// `Deserialize` implementation: veto payloads are engine output only.
16#[derive(Debug, Clone, PartialEq)]
17pub enum VetoType {
18    /// Evaluation needed a data that was not provided
19    MissingData { data: DataPath },
20    /// Explicit `veto "reason"` in Lemma source
21    UserDefined { message: Option<String> },
22    /// Runtime domain failure (division by zero, date overflow, etc.)
23    Computation { message: String },
24}
25
26impl fmt::Display for VetoType {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            VetoType::MissingData { data } => write!(f, "Missing data: {}", data),
30            VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
31            VetoType::UserDefined { message: None } => write!(f, "Vetoed"),
32            VetoType::Computation { message } => write!(f, "{message}"),
33        }
34    }
35}
36
37impl VetoType {
38    #[must_use]
39    pub fn computation(message: impl Into<String>) -> Self {
40        VetoType::Computation {
41            message: message.into(),
42        }
43    }
44}
45
46impl Serialize for VetoType {
47    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48    where
49        S: serde::Serializer,
50    {
51        serializer.serialize_str(&self.to_string())
52    }
53}
54
55/// Result of an operation (evaluating a rule or expression)
56#[derive(Debug, Clone, PartialEq, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum OperationResult {
59    /// Operation produced a value (boxed to keep enum small)
60    Value(Box<LiteralValue>),
61    /// Operation was vetoed (valid result, no value)
62    Veto(VetoType),
63}
64
65impl OperationResult {
66    pub fn vetoed(&self) -> bool {
67        matches!(self, OperationResult::Veto(_))
68    }
69
70    #[must_use]
71    pub fn value(&self) -> Option<&LiteralValue> {
72        match self {
73            OperationResult::Value(v) => Some(v.as_ref()),
74            OperationResult::Veto(_) => None,
75        }
76    }
77
78    pub fn number(number: rust_decimal::Decimal) -> Self {
79        Self::Value(Box::new(LiteralValue::number_from_decimal(number)))
80    }
81
82    pub fn quantity(
83        value: rust_decimal::Decimal,
84        unit: impl Into<String>,
85        lemma_type: Option<LemmaType>,
86    ) -> Self {
87        let lemma_type =
88            lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::quantity()));
89        Self::Value(Box::new(LiteralValue::quantity_with_type(
90            crate::literals::rational_from_parsed_decimal(value)
91                .expect("BUG: operation result quantity must lift at boundary"),
92            unit.into(),
93            lemma_type,
94        )))
95    }
96
97    pub fn text(text: impl Into<String>) -> Self {
98        Self::Value(Box::new(LiteralValue::text(text.into())))
99    }
100
101    pub fn date(date: impl Into<SemanticDateTime>) -> Self {
102        Self::Value(Box::new(LiteralValue::date(date.into())))
103    }
104
105    pub fn time(time: impl Into<SemanticTime>) -> Self {
106        Self::Value(Box::new(LiteralValue::time(time.into())))
107    }
108
109    pub fn boolean(boolean: bool) -> Self {
110        Self::Value(Box::new(LiteralValue::from_bool(boolean)))
111    }
112
113    pub fn ratio(rational: rust_decimal::Decimal) -> Self {
114        Self::Value(Box::new(LiteralValue::ratio_from_decimal(rational, None)))
115    }
116
117    pub fn veto(veto: impl Into<String>) -> Self {
118        Self::Veto(VetoType::UserDefined {
119            message: Some(veto.into()),
120        })
121    }
122}
123
124/// The kind of computation performed
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126#[serde(tag = "type", content = "computation", rename_all = "snake_case")]
127pub enum ComputationKind {
128    Arithmetic(ArithmeticComputation),
129    Comparison(ComparisonComputation),
130    Logical(LogicalComputation),
131    Mathematical(MathematicalComputation),
132    UnitConversion {
133        target: SemanticConversionTarget,
134    },
135    /// Operand result was tested for veto (`is veto` syntax).
136    ResultIsVeto,
137}
138
139/// A record of a single operation during evaluation
140#[derive(Debug, Clone, Serialize)]
141pub struct OperationRecord {
142    #[serde(flatten)]
143    pub kind: OperationKind,
144}
145
146/// The kind of operation performed
147#[derive(Debug, Clone, Serialize)]
148#[serde(tag = "type", rename_all = "snake_case")]
149pub enum OperationKind {
150    DataUsed {
151        data_ref: DataPath,
152        value: LiteralValue,
153    },
154    RuleUsed {
155        rule_path: RulePath,
156        result: OperationResult,
157    },
158    Computation {
159        kind: ComputationKind,
160        inputs: Vec<LiteralValue>,
161        result: LiteralValue,
162    },
163    RuleBranchEvaluated {
164        #[serde(skip_serializing_if = "Option::is_none")]
165        index: Option<usize>,
166        matched: bool,
167        #[serde(skip_serializing_if = "Option::is_none", default)]
168        result_value: Option<OperationResult>,
169    },
170}
171
172#[cfg(test)]
173mod computation_kind_serde_tests {
174    use super::ComputationKind;
175    use crate::parsing::ast::{
176        ArithmeticComputation, ComparisonComputation, MathematicalComputation,
177    };
178    use crate::planning::semantics::LogicalComputation;
179
180    #[test]
181    fn computation_kind_arithmetic_round_trip() {
182        let k = ComputationKind::Arithmetic(ArithmeticComputation::Add);
183        let json = serde_json::to_string(&k).expect("serialize");
184        assert!(json.contains("\"type\"") && json.contains("\"computation\""));
185        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
186        assert_eq!(back, k);
187    }
188
189    #[test]
190    fn computation_kind_comparison_round_trip() {
191        let k = ComputationKind::Comparison(ComparisonComputation::GreaterThan);
192        let json = serde_json::to_string(&k).expect("serialize");
193        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
194        assert_eq!(back, k);
195    }
196
197    #[test]
198    fn computation_kind_logical_round_trip() {
199        let k = ComputationKind::Logical(LogicalComputation::And);
200        let json = serde_json::to_string(&k).expect("serialize");
201        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
202        assert_eq!(back, k);
203    }
204
205    #[test]
206    fn computation_kind_mathematical_round_trip() {
207        let k = ComputationKind::Mathematical(MathematicalComputation::Sqrt);
208        let json = serde_json::to_string(&k).expect("serialize");
209        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
210        assert_eq!(back, k);
211    }
212
213    #[test]
214    fn computation_kind_unit_conversion_round_trip() {
215        use crate::planning::semantics::SemanticConversionTarget;
216        let k = ComputationKind::UnitConversion {
217            target: SemanticConversionTarget::QuantityUnit("days".to_string()),
218        };
219        let json = serde_json::to_string(&k).expect("serialize");
220        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
221        assert_eq!(back, k);
222    }
223
224    #[test]
225    fn veto_type_serializes_as_display_string() {
226        use super::VetoType;
227        use crate::planning::semantics::DataPath;
228        let v = VetoType::MissingData {
229            data: DataPath::new(vec![], "product".to_string()),
230        };
231        let json = serde_json::to_string(&v).expect("serialize");
232        assert_eq!(json, "\"Missing data: product\"");
233    }
234}