Skip to main content

lemma/evaluation/
operations.rs

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