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#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum RunDataValue {
17 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#[derive(Debug, Clone, Default)]
176pub struct RunData {
177 pub bindings: HashMap<DataPath, OperationResult>,
179 pub ignored_unknown: Vec<String>,
181}
182
183impl RunData {
184 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}