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, SemanticConversionTarget, SemanticDateTime,
8    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        use crate::computation::rational::checked_mul;
88        let lemma_type = std::sync::Arc::new(
89            lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::quantity())),
90        );
91        let unit_name = unit.into();
92        let rational = crate::literals::rational_from_parsed_decimal(value)
93            .expect("BUG: operation result quantity must lift at boundary");
94        let factor = if let TypeSpecification::Quantity { units, .. } = &lemma_type.specifications {
95            units.get(&unit_name).map(|u| u.factor).unwrap_or_else(|_| {
96                panic!(
97                    "BUG: OperationResult::quantity unit '{}' not declared on type",
98                    unit_name
99                )
100            })
101        } else {
102            crate::computation::rational::rational_one()
103        };
104        let canonical = checked_mul(&rational, &factor)
105            .expect("BUG: quantity canonicalization overflow in OperationResult::quantity");
106        Self::Value(Box::new(LiteralValue::quantity_with_type(
107            canonical, unit_name, lemma_type,
108        )))
109    }
110
111    pub fn text(text: impl Into<String>) -> Self {
112        Self::Value(Box::new(LiteralValue::text(text.into())))
113    }
114
115    pub fn date(date: impl Into<SemanticDateTime>) -> Self {
116        Self::Value(Box::new(LiteralValue::date(date.into())))
117    }
118
119    pub fn time(time: impl Into<SemanticTime>) -> Self {
120        Self::Value(Box::new(LiteralValue::time(time.into())))
121    }
122
123    pub fn boolean(boolean: bool) -> Self {
124        Self::Value(Box::new(LiteralValue::from_bool(boolean)))
125    }
126
127    pub fn ratio(rational: rust_decimal::Decimal) -> Self {
128        Self::Value(Box::new(LiteralValue::ratio_from_decimal(rational, None)))
129    }
130
131    pub fn veto(veto: impl Into<String>) -> Self {
132        Self::Veto(VetoType::UserDefined {
133            message: Some(veto.into()),
134        })
135    }
136}
137
138/// The kind of computation performed
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140#[serde(tag = "type", content = "computation", rename_all = "snake_case")]
141pub enum ComputationKind {
142    Arithmetic(ArithmeticComputation),
143    Comparison(ComparisonComputation),
144    Mathematical(MathematicalComputation),
145    Logical(LogicalComputation),
146    UnitConversion {
147        target: SemanticConversionTarget,
148    },
149    /// Operand result was tested for veto (`is veto` syntax).
150    ResultIsVeto,
151}
152
153#[cfg(test)]
154mod computation_kind_serde_tests {
155    use super::ComputationKind;
156    use crate::parsing::ast::{
157        ArithmeticComputation, ComparisonComputation, MathematicalComputation,
158    };
159
160    #[test]
161    fn computation_kind_arithmetic_round_trip() {
162        let k = ComputationKind::Arithmetic(ArithmeticComputation::Add);
163        let json = serde_json::to_string(&k).expect("serialize");
164        assert!(json.contains("\"type\"") && json.contains("\"computation\""));
165        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
166        assert_eq!(back, k);
167    }
168
169    #[test]
170    fn computation_kind_comparison_round_trip() {
171        let k = ComputationKind::Comparison(ComparisonComputation::GreaterThan);
172        let json = serde_json::to_string(&k).expect("serialize");
173        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
174        assert_eq!(back, k);
175    }
176
177    #[test]
178    fn computation_kind_mathematical_round_trip() {
179        let k = ComputationKind::Mathematical(MathematicalComputation::Sqrt);
180        let json = serde_json::to_string(&k).expect("serialize");
181        let back: ComputationKind = serde_json::from_str(&json).expect("deserialize");
182        assert_eq!(back, k);
183    }
184
185    #[test]
186    fn computation_kind_unit_conversion_round_trip() {
187        use crate::parsing::ast::PrimitiveKind;
188        use crate::planning::semantics::SemanticConversionTarget;
189        let k = ComputationKind::UnitConversion {
190            target: SemanticConversionTarget::Type(PrimitiveKind::Number),
191        };
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 veto_type_serializes_as_display_string() {
199        use super::VetoType;
200        use crate::planning::semantics::DataPath;
201        let v = VetoType::MissingData {
202            data: DataPath::new(vec![], "product".to_string()),
203        };
204        let json = serde_json::to_string(&v).expect("serialize");
205        assert_eq!(json, "\"Missing data: product\"");
206    }
207}