Skip to main content

lemma/evaluation/
run_data.rs

1use crate::computation::{OperationResult, VetoType};
2use crate::planning::execution_plan::{validate_value_against_type, ExecutionPlan};
3use crate::planning::semantics::{
4    number_with_unit_to_value_kind, parse_value_from_string, parser_value_to_value_kind,
5    DataDefinition, DataPath, LemmaType, LiteralValue, Source, TypeSpecification, ValueKind,
6};
7use crate::Error;
8use crate::ResourceLimits;
9use rust_decimal::Decimal;
10use std::collections::{BTreeMap, HashMap, HashSet};
11use std::str::FromStr;
12use std::sync::Arc;
13
14/// Typed data value from a client (CLI/WASM). JSON parsing stays outside [`parse_data_value`].
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum RunDataValue {
17    /// Raw string, parsed against the target type's specification by [`parse_value_from_string`].
18    String(String),
19    Boolean(bool),
20    MeasureMap(BTreeMap<String, String>),
21    RatioMap(BTreeMap<String, String>),
22}
23
24impl RunDataValue {
25    pub fn string(value: impl Into<String>) -> Self {
26        Self::String(value.into())
27    }
28}
29
30pub fn parse_data_value(
31    input: &RunDataValue,
32    lemma_type: &Arc<LemmaType>,
33    source: &Source,
34) -> Result<LiteralValue, Error> {
35    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
36    let type_spec = &lemma_type.specifications;
37
38    let kind = match (input, type_spec) {
39        (RunDataValue::String(s), _) => {
40            let parsed = parse_value_from_string(s, type_spec, source)?;
41            parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
42        }
43        (RunDataValue::Boolean(b), TypeSpecification::Boolean { .. }) => ValueKind::Boolean(*b),
44        (RunDataValue::Boolean(_), _) => {
45            return Err(to_err(format!(
46                "boolean input is only valid for boolean data, not {}",
47                type_spec
48            )));
49        }
50        (RunDataValue::MeasureMap(map), TypeSpecification::Measure { .. }) => {
51            measure_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?
52        }
53        (
54            RunDataValue::MeasureMap(map) | RunDataValue::RatioMap(map),
55            TypeSpecification::Ratio { .. },
56        ) => ratio_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?,
57        (RunDataValue::MeasureMap(_), _) => {
58            return Err(to_err(format!(
59                "measure unit map is only valid for measure data, not {}",
60                type_spec
61            )));
62        }
63        (RunDataValue::RatioMap(_), _) => {
64            return Err(to_err(format!(
65                "ratio unit map is only valid for ratio data, not {}",
66                type_spec
67            )));
68        }
69    };
70
71    Ok(LiteralValue {
72        value: kind,
73        lemma_type: Arc::clone(lemma_type),
74    })
75}
76
77fn measure_from_unit_map(
78    map: &BTreeMap<String, String>,
79    lemma_type: &LemmaType,
80) -> Result<ValueKind, String> {
81    if map.is_empty() {
82        return Err("measure input map must contain at least one unit key".to_string());
83    }
84    if lemma_type
85        .measure_unit_names()
86        .is_none_or(|names| names.is_empty())
87    {
88        unreachable!("BUG: measure type has no units at data input");
89    }
90
91    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
92    for (unit_name, mag_str) in map {
93        let magnitude = Decimal::from_str(mag_str.trim())
94            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
95        kinds.push(number_with_unit_to_value_kind(
96            magnitude, unit_name, lemma_type,
97        )?);
98    }
99
100    let first = kinds.first().expect("BUG: map non-empty");
101    let ValueKind::Measure(first_magnitude, first_signature) = first else {
102        return Err("expected measure value".to_string());
103    };
104    if first_signature.len() != 1 || first_signature[0].1 != 1 {
105        return Err(
106            "measure map produced a compound signature; use a convenience string instead"
107                .to_string(),
108        );
109    }
110    for kind in kinds.iter().skip(1) {
111        let ValueKind::Measure(magnitude, signature) = kind else {
112            return Err("expected measure value".to_string());
113        };
114        if signature.len() != 1 || signature[0].1 != 1 {
115            return Err(
116                "measure map produced a compound signature; use a convenience string instead"
117                    .to_string(),
118            );
119        }
120        if magnitude != first_magnitude {
121            return Err(
122                "measure unit map values disagree when converted to a common basis".to_string(),
123            );
124        }
125    }
126    Ok(first.clone())
127}
128
129fn ratio_from_unit_map(
130    map: &BTreeMap<String, String>,
131    lemma_type: &LemmaType,
132) -> Result<ValueKind, String> {
133    if map.is_empty() {
134        return Err("ratio input map must contain at least one unit key".to_string());
135    }
136    match &lemma_type.specifications {
137        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
138        _ => unreachable!("BUG: ratio type has no units at data input"),
139    }
140
141    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
142    for (unit_name, mag_str) in map {
143        let magnitude = Decimal::from_str(mag_str.trim())
144            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
145        kinds.push(number_with_unit_to_value_kind(
146            magnitude, unit_name, lemma_type,
147        )?);
148    }
149
150    let first = kinds.first().expect("BUG: map non-empty");
151    let ValueKind::Ratio(first_canonical, first_unit) = first else {
152        return Err("expected ratio value".to_string());
153    };
154    for kind in kinds.iter().skip(1) {
155        let ValueKind::Ratio(canonical, _) = kind else {
156            return Err("expected ratio value".to_string());
157        };
158        if canonical != first_canonical {
159            return Err(
160                "ratio unit map values disagree when converted to a common basis".to_string(),
161            );
162        }
163    }
164    Ok(ValueKind::Ratio(
165        first_canonical.clone(),
166        first_unit.clone(),
167    ))
168}
169
170/// User-provided data values resolved against a plan's type declarations.
171///
172/// Lightweight and cheap to construct — no plan cloning required. The
173/// [`ExecutionPlan`] stays immutable; callers pass `(&ExecutionPlan, &RunData)`
174/// to evaluation and show paths.
175#[derive(Debug, Clone, Default)]
176pub struct RunData {
177    /// Caller bindings: successful literals or Veto (bad override) per Data.
178    pub bindings: HashMap<DataPath, OperationResult>,
179    /// Input keys that did not match any plan Data (including Import aliases).
180    pub ignored_unknown: Vec<String>,
181}
182
183impl RunData {
184    /// Parse and validate caller-supplied values against the plan's data declarations.
185    ///
186    /// Unknown keys and Import aliases are ignored (recorded in [`Self::ignored_unknown`]).
187    /// Parse, constraint, options, decimals, and input-oversize failures bind that Data
188    /// as [`OperationResult::Veto`]; evaluation still runs. Duplicate canonical keys Error.
189    pub fn resolve(
190        plan: &ExecutionPlan,
191        raw_values: HashMap<String, RunDataValue>,
192        limits: &ResourceLimits,
193    ) -> Result<Self, Error> {
194        let mut run_data = Self::default();
195        let mut seen_canonical = HashSet::with_capacity(raw_values.len());
196        let mut by_input_key: HashMap<String, &DataPath> = HashMap::with_capacity(plan.data.len());
197        for path in plan.data.keys() {
198            by_input_key.insert(path.input_key(), path);
199        }
200
201        for (name, raw_value) in raw_values {
202            let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
203            if !seen_canonical.insert(canonical.clone()) {
204                return Err(Error::request(
205                    format!("Duplicate data key '{canonical}'"),
206                    Some("Data keys are case-insensitive; remove the duplicate"),
207                ));
208            }
209
210            let Some(data_path) = by_input_key.get(canonical.as_str()) else {
211                run_data.ignored_unknown.push(name);
212                continue;
213            };
214            let data_path = (*data_path).clone();
215
216            let data_definition = plan
217                .data
218                .get(&data_path)
219                .expect("BUG: data_path was just resolved from plan.data, must exist");
220
221            let data_source = data_definition.source().clone();
222            let type_arc = match data_definition {
223                DataDefinition::TypeDeclaration { resolved_type, .. }
224                | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
225                DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
226                DataDefinition::Import { .. } => {
227                    run_data.ignored_unknown.push(name);
228                    continue;
229                }
230            };
231
232            let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
233                Ok(value) => value,
234                Err(error) => {
235                    run_data.bindings.insert(
236                        data_path,
237                        OperationResult::Veto(VetoType::computation(error.message().to_string())),
238                    );
239                    continue;
240                }
241            };
242
243            let size = literal_value.byte_size();
244            if size > limits.max_data_value_bytes {
245                run_data.bindings.insert(
246                    data_path,
247                    OperationResult::Veto(VetoType::computation(format!(
248                        "max_data_value_bytes (limit: {}, actual: {})",
249                        limits.max_data_value_bytes, size
250                    ))),
251                );
252                continue;
253            }
254
255            if let Err(message) = validate_value_against_type(
256                type_arc.as_ref(),
257                &literal_value,
258                plan.expression_unit_index(),
259            ) {
260                run_data.bindings.insert(
261                    data_path,
262                    OperationResult::Veto(VetoType::computation(message)),
263                );
264                continue;
265            }
266
267            run_data
268                .bindings
269                .insert(data_path, OperationResult::from_literal(literal_value));
270        }
271
272        Ok(run_data)
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
280    use crate::planning::semantics::{
281        primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
282    };
283
284    fn dummy_source() -> Source {
285        Source::new(
286            crate::parsing::source::SourceType::Volatile,
287            crate::parsing::ast::Span {
288                start: 0,
289                end: 0,
290                line: 1,
291                col: 1,
292            },
293        )
294    }
295
296    fn mass_measure_type() -> Arc<LemmaType> {
297        Arc::new(LemmaType::new(
298            "Mass".to_string(),
299            TypeSpecification::Measure {
300                minimum: None,
301                maximum: None,
302                decimals: None,
303                units: MeasureUnits::from(vec![
304                    MeasureUnit {
305                        name: "kilogram".to_string(),
306                        factor: rational_one(),
307                        derived_measure_factors: Vec::new(),
308                        decomposition: crate::literals::BaseMeasureVector::new(),
309                        minimum: None,
310                        maximum: None,
311                        suggestion_magnitude: None,
312                    },
313                    MeasureUnit {
314                        name: "gram".to_string(),
315                        factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
316                        derived_measure_factors: Vec::new(),
317                        decomposition: crate::literals::BaseMeasureVector::new(),
318                        minimum: None,
319                        maximum: None,
320                        suggestion_magnitude: None,
321                    },
322                ]),
323                traits: Vec::new(),
324                decomposition: None,
325                help: String::new(),
326            },
327            TypeExtends::Primitive,
328        ))
329    }
330
331    fn ratio_with_percent_type() -> Arc<LemmaType> {
332        Arc::new(LemmaType::new(
333            "Rate".to_string(),
334            TypeSpecification::Ratio {
335                minimum: None,
336                maximum: None,
337                decimals: None,
338                units: RatioUnits::from(vec![
339                    RatioUnit {
340                        name: "percent".to_string(),
341                        value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
342                        minimum: None,
343                        maximum: None,
344                        suggestion_magnitude: None,
345                    },
346                    RatioUnit {
347                        name: "fraction".to_string(),
348                        value: rational_one(),
349                        minimum: None,
350                        maximum: None,
351                        suggestion_magnitude: None,
352                    },
353                ]),
354                help: String::new(),
355            },
356            TypeExtends::Primitive,
357        ))
358    }
359
360    #[test]
361    fn string_input_parsed_against_type() {
362        let ty = primitive_number_arc();
363        let lit =
364            parse_data_value(&RunDataValue::String("42".to_string()), ty, &dummy_source()).unwrap();
365        assert!(matches!(lit.value, ValueKind::Number(_)));
366    }
367
368    #[test]
369    fn measure_map_agreeing_units_canonicalize() {
370        let ty = mass_measure_type();
371        let mut map = BTreeMap::new();
372        map.insert("kilogram".to_string(), "2".to_string());
373        map.insert("gram".to_string(), "2000".to_string());
374        let lit = parse_data_value(&RunDataValue::MeasureMap(map), &ty, &dummy_source()).unwrap();
375        let ValueKind::Measure(magnitude, signature) = &lit.value else {
376            panic!("expected measure");
377        };
378        assert_eq!(magnitude, &rational_new(2, 1));
379        assert_eq!(signature.len(), 1);
380        assert_eq!(signature[0].1, 1);
381    }
382
383    #[test]
384    fn measure_map_disagreeing_units_rejected() {
385        let ty = mass_measure_type();
386        let mut map = BTreeMap::new();
387        map.insert("kilogram".to_string(), "2".to_string());
388        map.insert("gram".to_string(), "3000".to_string());
389        let err =
390            parse_data_value(&RunDataValue::MeasureMap(map), &ty, &dummy_source()).unwrap_err();
391        assert!(err.message().contains("disagree"));
392    }
393
394    #[test]
395    fn ratio_map_percent_and_fraction_agree() {
396        let ty = ratio_with_percent_type();
397        let mut map = BTreeMap::new();
398        map.insert("percent".to_string(), "10".to_string());
399        map.insert("fraction".to_string(), "0.1".to_string());
400        let lit = parse_data_value(&RunDataValue::RatioMap(map), &ty, &dummy_source()).unwrap();
401        let ValueKind::Ratio(canonical, unit) = &lit.value else {
402            panic!("expected ratio");
403        };
404        assert_eq!(
405            *canonical,
406            decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
407        );
408        assert!(unit.is_some());
409    }
410}