Skip to main content

lemma/computation/
operation_result.rs

1//! Result envelope for computation operations: a produced value or a domain veto.
2
3use std::fmt;
4
5use crate::planning::semantics::{
6    BoundValueKind, DataPath, LemmaType, LiteralValue, SemanticDateTime, SemanticTime,
7    TypeSpecification,
8};
9
10/// Why an operation yielded no value (domain veto).
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum VetoType {
13    /// Evaluation needed a data that was not provided
14    MissingData {
15        data: DataPath,
16        suggestion: Option<String>,
17    },
18    /// Evaluation needed a field of a list row that was not provided
19    /// (`key` is the row path, e.g. `menu.2.price`).
20    MissingRowData { key: String },
21    /// Explicit `veto "reason"` in Lemma source
22    UserDefined { message: Option<String> },
23    /// Runtime domain failure (division by zero, date overflow, bad Data override, 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 {
31                data,
32                suggestion: Some(suggestion),
33            } => write!(
34                f,
35                "Required data '{data}' was not provided. Did you mean '{suggestion}'?"
36            ),
37            VetoType::MissingData {
38                data,
39                suggestion: None,
40            } => write!(f, "Required data '{data}' was not provided."),
41            VetoType::MissingRowData { key } => match split_numbered_row_field_key(key) {
42                Some((list, number, field)) => write!(
43                    f,
44                    "Required field '{field}' was not provided on row {number} of list '{list}'."
45                ),
46                None => write!(f, "Required data '{key}' was not provided."),
47            },
48            VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
49            VetoType::UserDefined { message: None } => write!(f, "This rule was vetoed."),
50            VetoType::Computation { message } => write!(f, "{message}"),
51        }
52    }
53}
54
55/// `list.number.field` from [`crate::planning::semantics::row_field_input_key`].
56fn split_numbered_row_field_key(key: &str) -> Option<(&str, &str, &str)> {
57    let (list_and_number, field) = key.rsplit_once('.')?;
58    let (list, number) = list_and_number.rsplit_once('.')?;
59    if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
60        return None;
61    }
62    if list.is_empty() || field.is_empty() {
63        return None;
64    }
65    Some((list, number, field))
66}
67
68impl VetoType {
69    #[must_use]
70    pub fn computation(message: impl Into<String>) -> Self {
71        VetoType::Computation {
72            message: message.into(),
73        }
74    }
75
76    #[must_use]
77    pub fn missing_data(data: DataPath, suggestion: Option<String>) -> Self {
78        VetoType::MissingData { data, suggestion }
79    }
80}
81
82/// Result of evaluating a rule, expression, or data leaf.
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum OperationResult {
85    /// Operation produced a value (canonical magnitude + optional written unit)
86    Value(BoundValueKind),
87    /// Operation was vetoed (valid result, no value)
88    Veto(VetoType),
89}
90
91impl OperationResult {
92    /// Wrap a typeless literal with no written-unit binding.
93    pub fn from_literal(value: LiteralValue) -> Self {
94        Self::Value(BoundValueKind::unbound(value.value))
95    }
96
97    pub fn from_bound(bound: BoundValueKind) -> Self {
98        Self::Value(bound)
99    }
100
101    pub fn vetoed(&self) -> bool {
102        matches!(self, OperationResult::Veto(_))
103    }
104
105    /// True when this result is a [`VetoType::MissingData`] or [`VetoType::MissingRowData`] veto.
106    #[must_use]
107    pub fn is_missing_data(&self) -> bool {
108        matches!(
109            self,
110            OperationResult::Veto(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
111        )
112    }
113
114    #[must_use]
115    pub fn value(&self) -> Option<&BoundValueKind> {
116        match self {
117            OperationResult::Value(value) => Some(value),
118            OperationResult::Veto(_) => None,
119        }
120    }
121
122    /// Typeless payload (written unit dropped). Prefer [`Self::value`] when the binding matters.
123    #[must_use]
124    pub fn literal_value(&self) -> Option<LiteralValue> {
125        self.value().map(BoundValueKind::to_literal)
126    }
127
128    pub fn number(number: rust_decimal::Decimal) -> Self {
129        Self::from_literal(LiteralValue::number_from_decimal(number))
130    }
131
132    pub fn measure(
133        value: rust_decimal::Decimal,
134        unit: impl Into<String>,
135        lemma_type: Option<LemmaType>,
136    ) -> Self {
137        use crate::computation::rational::checked_mul;
138        let lemma_type = std::sync::Arc::new(
139            lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::measure())),
140        );
141        let unit_name = unit.into();
142        let rational = crate::literals::rational_from_parsed_decimal(value)
143            .expect("BUG: operation result measure must lift at boundary");
144        let factor = if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
145            units
146                .get(&unit_name)
147                .map(|u| u.require_literal_factor().clone())
148                .unwrap_or_else(|_| {
149                    panic!(
150                        "BUG: OperationResult::measure unit '{}' not declared on type",
151                        unit_name
152                    )
153                })
154        } else {
155            crate::computation::rational::rational_one()
156        };
157        let canonical = checked_mul(&rational, &factor)
158            .expect("BUG: measure canonicalization overflow in OperationResult::measure");
159        Self::from_bound(BoundValueKind {
160            value: crate::planning::semantics::ValueKind::Measure(canonical),
161            unit: Some(std::sync::Arc::from(unit_name)),
162            instance_factors: None,
163        })
164    }
165
166    pub fn text(text: impl Into<String>) -> Self {
167        Self::from_literal(LiteralValue::text(text.into()))
168    }
169
170    pub fn date(date: impl Into<SemanticDateTime>) -> Self {
171        Self::from_literal(LiteralValue::date(date.into()))
172    }
173
174    pub fn time(time: impl Into<SemanticTime>) -> Self {
175        Self::from_literal(LiteralValue::time(time.into()))
176    }
177
178    pub fn boolean(boolean: bool) -> Self {
179        Self::from_literal(LiteralValue::from_bool(boolean))
180    }
181
182    pub fn ratio(rational: rust_decimal::Decimal) -> Self {
183        Self::from_literal(LiteralValue::ratio_from_decimal(rational))
184    }
185
186    pub fn veto(veto: impl Into<String>) -> Self {
187        Self::Veto(VetoType::UserDefined {
188            message: Some(veto.into()),
189        })
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::VetoType;
196
197    #[test]
198    fn missing_row_data_display_names_list_row_and_field() {
199        let v = VetoType::MissingRowData {
200            key: "menu.2.price".to_string(),
201        };
202        assert_eq!(
203            v.to_string(),
204            "Required field 'price' was not provided on row 2 of list 'menu'."
205        );
206    }
207}