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