1use crate::literals::Value;
13use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaSpec};
14use crate::parsing::source::{Source, SourceType};
15use crate::planning::graph::Graph;
16use crate::planning::graph::ResolvedSpecTypes;
17use crate::planning::normalize::{
18 data_path_result_type, NormalForm, NormalFormId, NormalFormInterner, NormalizeContext,
19 NormalizedRule,
20};
21use crate::planning::semantics::{
22 value_kind_matches_spec, ComparisonComputation, DataDefinition, DataPath, LemmaType,
23 LiteralValue, ReferenceEnd, ReferenceTarget, RulePath, TypeSpecification, ValueKind,
24};
25use crate::planning::show_expression::{project_depends_on_rules, show_branches_from};
26use crate::planning::spec_set::LemmaSpecSet;
27use crate::planning::unit_family::FamilyUnitCatalog;
28use crate::result_value::RuleResultValue;
29use crate::Error;
30use indexmap::{IndexMap, IndexSet};
31use serde::{Deserialize, Serialize};
32use std::collections::{BTreeMap, HashMap, HashSet};
33use std::sync::Arc;
34
35pub use crate::planning::show_expression::{
36 ShowBranch, ShowConversionTarget, ShowExpression, ShowRule,
37};
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ExecutionPlan {
45 pub spec_name: String,
47
48 pub commentary: Option<String>,
50
51 pub data: IndexMap<DataPath, DataDefinition>,
53
54 pub(crate) normal_forms: Vec<NormalForm>,
57
58 pub rules: IndexMap<RulePath, ExecutableRule>,
61
62 pub data_reference_order: Vec<DataPath>,
66
67 pub meta: IndexMap<String, Value>,
69
70 pub resolved_types: ResolvedSpecTypes,
74
75 pub family_units: Arc<FamilyUnitCatalog>,
77
78 pub signature_index: crate::computation::arithmetic::SignatureIndex,
84
85 pub effective: EffectiveDate,
86
87 pub effective_from: Option<DateTimeValue>,
90 pub effective_to: Option<DateTimeValue>,
91
92 pub versions: Arc<[ShowVersion]>,
96
97 pub start_line: usize,
99
100 pub source_type: Option<SourceType>,
102
103 pub(crate) needed_by_rules: Vec<Vec<u32>>,
109
110 pub(crate) data_display: IndexMap<DataPath, ShowDataCache>,
113
114 pub(crate) show_rules: IndexMap<RulePath, ShowRule>,
117
118 pub(crate) reference_ends: IndexMap<DataPath, ReferenceEnd>,
122
123 pub(crate) input_key_index: IndexMap<String, DataPath>,
126
127 pub(crate) show_rule_index: IndexMap<String, RulePath>,
130
131 pub(crate) data_leaf: IndexMap<DataPath, NormalFormId>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
142pub(crate) struct ShowDataCache {
143 pub fill: Option<crate::planning::semantics::BoundValueKind>,
144 pub suggestion: Option<crate::planning::semantics::BoundValueKind>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ExecutableRule {
150 pub path: RulePath,
152
153 pub normal_form: NormalFormId,
155
156 pub source: Source,
158
159 pub rule_type: Arc<LemmaType>,
164}
165
166impl ExecutableRule {
167 pub fn name(&self) -> &str {
168 &self.path.rule
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub(crate) struct RuleIndex(usize);
177
178impl RuleIndex {
179 pub(crate) fn index(self) -> usize {
180 self.0
181 }
182}
183
184pub(crate) fn plan_at<'a>(
187 plans: &'a BTreeMap<EffectiveDate, ExecutionPlan>,
188 instant: &EffectiveDate,
189) -> Option<&'a ExecutionPlan> {
190 plans
191 .range(..=instant.clone())
192 .next_back()
193 .map(|(_, plan)| plan)
194}
195
196pub(crate) fn build_execution_plan(
203 graph: &Graph<'_>,
204 resolved_types: ResolvedSpecTypes,
205 effective: &EffectiveDate,
206 limits: &crate::limits::ResourceLimits,
207 interner: &mut NormalFormInterner,
208) -> Result<ExecutionPlan, Vec<Error>> {
209 let rule_order = graph.rule_order();
210
211 let main_spec = graph.main_spec();
212 let data = graph.build_data(&resolved_types.resolved)?;
213
214 let undetermined_errors: Vec<Error> = data
221 .iter()
222 .filter_map(|(path, definition)| {
223 let (resolved_type, source) = match definition {
224 DataDefinition::TypeDeclaration {
225 resolved_type,
226 source,
227 ..
228 } => (resolved_type, source),
229 DataDefinition::Reference {
230 target: ReferenceTarget::Data(_),
231 resolved_type,
232 source,
233 ..
234 } => (resolved_type, source),
235 DataDefinition::Reference {
236 target: ReferenceTarget::Rule(_),
237 ..
238 }
239 | DataDefinition::Value { .. }
240 | DataDefinition::Import { .. } => return None,
241 };
242 if resolved_type.is_undetermined() {
243 Some(Error::validation(
244 format!("could not determine the type of '{path}'"),
245 Some(source.clone()),
246 None::<String>,
247 ))
248 } else {
249 None
250 }
251 })
252 .collect();
253 if !undetermined_errors.is_empty() {
254 return Err(undetermined_errors);
255 }
256
257 let signature_index = resolved_types.signature_index.clone();
258
259 let family_units = Arc::new(FamilyUnitCatalog::build(&resolved_types.unit_index));
260
261 let reference_ends = graph.reference_ends();
262 let rule_types: HashMap<RulePath, Arc<LemmaType>> = graph
263 .rules()
264 .iter()
265 .map(|(path, node)| (path.clone(), Arc::clone(&node.rule_type)))
266 .collect();
267 let mut rules: IndexMap<RulePath, ExecutableRule> = IndexMap::new();
268 let mut completed_rules: HashMap<RulePath, NormalFormId> = HashMap::new();
269 let mut show_rules: IndexMap<RulePath, ShowRule> = IndexMap::new();
270
271 let mut reachable: HashSet<RulePath> = HashSet::new();
273 let mut stack: Vec<RulePath> = graph
274 .rules()
275 .keys()
276 .filter(|path| path.segments.is_empty())
277 .cloned()
278 .collect();
279 while let Some(path) = stack.pop() {
280 if !reachable.insert(path.clone()) {
281 continue;
282 }
283 let node = graph
284 .rules()
285 .get(&path)
286 .expect("BUG: reachable rule path missing from graph.rules");
287 for dep in &node.depends_on_rules {
288 if !reachable.contains(dep) {
289 stack.push(dep.clone());
290 }
291 }
292 }
293 let show_keys: HashSet<String> = reachable.iter().map(|path| path.input_key()).collect();
294
295 for rule_path in rule_order {
296 let rule_node = graph.rules().get(rule_path).expect(
297 "bug: rule from topological sort not in graph - validation should have caught this",
298 );
299
300 let measure_scope = crate::planning::typing::MeasureScope {
301 unit_index: &resolved_types.unit_index,
302 signature_index: &resolved_types.signature_index,
303 };
304 let normalize_ctx = NormalizeContext {
305 data: &data,
306 measure_scope,
307 max_normalized_expression_nodes: limits.max_normalized_expression_nodes,
308 max_normal_form_depth: limits.max_normal_form_depth,
309 };
310 let normalized = crate::planning::normalize::build_normalized_rule(
311 &normalize_ctx,
312 &completed_rules,
313 &rule_types,
314 reference_ends,
315 &rule_node.branches,
316 Some(rule_node.source.clone()),
317 interner,
318 )
319 .map_err(|error| vec![error])?;
320 let NormalizedRule { body, source_type } = normalized;
321 assert_eq!(
322 &source_type, &rule_node.rule_type,
323 "BUG: rule '{}' typed differently by planning and validation",
324 rule_path.rule
325 );
326 assert!(
327 interner
328 .result_type(body)
329 .same_value_type(&rule_node.rule_type),
330 "BUG: rewrites changed the value type of rule '{}': {:?} from {:?}",
331 rule_path.rule,
332 interner.result_type(body),
333 rule_node.rule_type
334 );
335 completed_rules.insert(rule_path.clone(), body);
336
337 if reachable.contains(rule_path) {
338 show_rules.insert(
339 rule_path.clone(),
340 ShowRule {
341 lemma_type: family_units.rule_type_for_show(rule_node.rule_type.as_ref()),
342 path: rule_path.segments.clone(),
343 branches: show_branches_from(&rule_node.branches, &show_keys),
344 depends_on_rules: project_depends_on_rules(
345 &rule_node.depends_on_rules,
346 &show_keys,
347 ),
348 },
349 );
350 }
351
352 rules.insert(
353 rule_path.clone(),
354 ExecutableRule {
355 path: rule_path.clone(),
356 normal_form: body,
357 source: rule_node.source.clone(),
358 rule_type: source_type,
359 },
360 );
361 }
362
363 let root_ids: Vec<NormalFormId> = rules.values().map(|rule| rule.normal_form).collect();
364 let (normal_forms, remapped_roots) = interner.extract_reachable(&root_ids);
365 for (rule, remapped) in rules.values_mut().zip(remapped_roots) {
366 rule.normal_form = remapped;
367 }
368
369 let mut plan = ExecutionPlan {
370 spec_name: main_spec.name.clone(),
371 commentary: main_spec.commentary.clone(),
372 data,
373 normal_forms,
374 rules,
375 data_reference_order: graph.data_reference_order().to_vec(),
376 meta: main_spec
377 .meta_fields
378 .iter()
379 .map(|f| (f.key.clone(), f.value.clone()))
380 .collect(),
381 resolved_types,
382 family_units,
383 signature_index,
384 effective: effective.clone(),
385 effective_from: None,
387 effective_to: None,
388 versions: Arc::from(Vec::new().into_boxed_slice()),
389 start_line: 1,
390 source_type: None,
391 needed_by_rules: Vec::new(),
392 data_display: IndexMap::new(),
393 show_rules,
394 reference_ends: IndexMap::new(),
396 input_key_index: IndexMap::new(),
397 show_rule_index: IndexMap::new(),
398 data_leaf: IndexMap::new(),
399 };
400
401 let mut plan_errors = validate_literal_data_against_types(&plan);
402 if let Err(error) = validate_unit_conversion_targets(&plan) {
403 plan_errors.push(error);
404 }
405 if !plan_errors.is_empty() {
406 return Err(plan_errors);
407 }
408
409 plan.reference_ends = graph.reference_ends().clone();
410 plan.input_key_index = plan
411 .data
412 .keys()
413 .map(|path| (path.input_key(), path.clone()))
414 .collect();
415 plan.show_rule_index = plan
416 .show_rules
417 .keys()
418 .map(|path| (path.input_key(), path.clone()))
419 .collect();
420 plan.data_leaf = ensure_data_leaves(&mut plan.normal_forms, &plan.data);
421 for (path, &id) in &plan.data_leaf {
422 debug_assert_eq!(
423 plan.result_type(id).as_ref(),
424 data_path_result_type(&plan.data, path).as_ref(),
425 "BUG: DataPath leaf result_type drift for {path}"
426 );
427 }
428 plan.needed_by_rules = plan.build_needed_by_rules();
429 Ok(plan)
430}
431
432fn ensure_data_leaves(
441 normal_forms: &mut Vec<NormalForm>,
442 data: &IndexMap<DataPath, DataDefinition>,
443) -> IndexMap<DataPath, NormalFormId> {
444 use crate::planning::normalize::LeafKind;
445 use crate::planning::normalize::NormalFormKind;
446
447 let mut data_leaf: IndexMap<DataPath, NormalFormId> = IndexMap::new();
448 for (index, cell) in normal_forms.iter().enumerate() {
449 if cell.rule_ref.is_some() {
450 continue;
451 }
452 if let NormalFormKind::Leaf(LeafKind::DataPath(path)) = &cell.kind {
453 data_leaf.insert(path.clone(), NormalFormId::from_index(index));
454 }
455 }
456 for path in data.keys() {
457 if data_leaf.contains_key(path) {
458 continue;
459 }
460 let id = NormalFormId::from_index(normal_forms.len());
461 normal_forms.push(NormalForm {
462 kind: NormalFormKind::Leaf(LeafKind::DataPath(path.clone())),
463 result_type: data_path_result_type(data, path),
464 fold_types: Vec::new(),
465 source: None,
466 origin: None,
467 rule_ref: None,
468 });
469 data_leaf.insert(path.clone(), id);
470 }
471 data_leaf
472}
473
474pub(crate) fn attach_show_cache(
478 plan: &mut ExecutionPlan,
479 lemma_spec_set: &LemmaSpecSet,
480 spec: &LemmaSpec,
481 versions: &Arc<[ShowVersion]>,
482) {
483 let (effective_from, effective_to) = lemma_spec_set.effective_range(spec);
484 plan.effective_from = effective_from;
485 plan.effective_to = effective_to;
486 plan.versions = Arc::clone(versions);
487 plan.start_line = spec.start_line;
488 plan.source_type = spec.source_type.clone();
489 plan.data_display = build_data_display(plan);
490}
491
492fn build_data_display(plan: &ExecutionPlan) -> IndexMap<DataPath, ShowDataCache> {
493 let mut out = IndexMap::new();
494 for (path, data) in &plan.data {
495 if data.schema_type().is_none() || matches!(data, DataDefinition::Reference { .. }) {
496 continue;
497 }
498 let fill = match data {
499 DataDefinition::Value {
500 value,
501 resolved_type,
502 ..
503 } => Some(crate::planning::semantics::BoundValueKind {
504 value: value.value.clone(),
505 measure_binding_unit: resolved_type.measure_binding_unit.as_deref().map(Arc::from),
506 }),
507 _ => data.bound_fill().cloned(),
508 };
509 let suggestion = data.bound_suggestion().cloned();
510 if fill.is_some() || suggestion.is_some() {
511 out.insert(path.clone(), ShowDataCache { fill, suggestion });
512 }
513 }
514 out
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub(crate) enum ShowBoundRole {
520 Fill,
521 Suggestion,
522}
523
524pub(crate) fn show_bound_result_value(
525 bound: &crate::planning::semantics::BoundValueKind,
526 schema_type: &crate::planning::semantics::LemmaType,
527 input_key: &str,
528 role: ShowBoundRole,
529) -> crate::result_value::RuleResultValue {
530 let (fill, suggestion) = match role {
531 ShowBoundRole::Fill => (Some(bound), None),
532 ShowBoundRole::Suggestion => (None, Some(bound)),
533 };
534 let display_type = crate::planning::semantics::lemma_type_with_display_binding(
535 schema_type,
536 None,
537 fill,
538 suggestion,
539 );
540 crate::result_value::type_scoped_result_value_from_literal(&bound.to_literal(), &display_type)
541 .unwrap_or_else(|failure| {
542 let role_name = match role {
543 ShowBoundRole::Fill => "fill",
544 ShowBoundRole::Suggestion => "suggestion",
545 };
546 panic!(
547 "BUG: show {role_name} value for '{input_key}' failed type_scoped_result_value_from_literal: {}",
548 crate::result_value::rule_result_value_failure_message(failure)
549 )
550 })
551}
552
553#[derive(Debug, Clone, PartialEq)]
561pub struct ShowData {
562 pub lemma_type: LemmaType,
563 pub path: Vec<crate::planning::semantics::PathSegment>,
565 pub fill: Option<RuleResultValue>,
566 pub suggestion: Option<RuleResultValue>,
567 pub needed_by_rules: Vec<String>,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct ShowVersion {
575 pub effective_from: Option<crate::parsing::ast::DateTimeValue>,
576 pub effective_to: Option<crate::parsing::ast::DateTimeValue>,
577}
578
579impl std::fmt::Display for ShowVersion {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 match (&self.effective_from, &self.effective_to) {
582 (Some(from), Some(to)) => write!(f, "{from} → {to}"),
583 (Some(from), None) => write!(f, "{from} →"),
584 (None, Some(to)) => write!(f, "→ {to}"),
585 (None, None) => write!(f, "—"),
586 }
587 }
588}
589
590#[derive(Debug, Clone, PartialEq)]
595pub struct Show {
596 pub repository: Option<String>,
598 pub spec: String,
599 pub commentary: Option<String>,
600 pub effective_from: Option<crate::parsing::ast::DateTimeValue>,
601 pub effective_to: Option<crate::parsing::ast::DateTimeValue>,
602 pub versions: Vec<ShowVersion>,
603 pub start_line: usize,
604 pub source_type: Option<crate::parsing::source::SourceType>,
605 pub data: indexmap::IndexMap<String, ShowData>,
606 pub rules: indexmap::IndexMap<String, ShowRule>,
607 pub meta: IndexMap<String, Value>,
609}
610
611impl std::fmt::Display for Show {
612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 match &self.repository {
614 Some(repository) => write!(f, "Spec: {} {}", repository, self.spec)?,
615 None => write!(f, "Spec: {}", self.spec)?,
616 }
617
618 if let Some(commentary) = &self.commentary {
619 write!(f, "\n {}", commentary)?;
620 }
621
622 if let Some(from) = &self.effective_from {
623 write!(f, "\n effective_from: {}", from)?;
624 }
625 if let Some(to) = &self.effective_to {
626 write!(f, "\n effective_to: {}", to)?;
627 }
628
629 if self.versions.len() > 1 {
630 let version_strs: Vec<String> = self
631 .versions
632 .iter()
633 .map(|v| match (&v.effective_from, &v.effective_to) {
634 (Some(f), Some(t)) => format!("{f} → {t}"),
635 (Some(f), None) => format!("{f} →"),
636 (None, Some(t)) => format!("→ {t}"),
637 (None, None) => "—".to_string(),
638 })
639 .collect();
640 write!(f, "\n versions: {}", version_strs.join(", "))?;
641 }
642
643 if !self.meta.is_empty() {
644 write!(f, "\n\nMeta:")?;
645 let mut entries: Vec<(&String, &Value)> = self.meta.iter().collect();
646 entries.sort_by_key(|(k, _)| *k);
647 for (key, value) in entries {
648 write!(f, "\n {}: {}", key, value)?;
649 }
650 }
651
652 if !self.data.is_empty() {
653 write!(f, "\n\nData:")?;
654 for (name, entry) in &self.data {
655 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
656 for line in type_detail_lines(&entry.lemma_type.specifications) {
657 write!(f, "\n {}", line)?;
658 }
659 let help = entry.lemma_type.specifications.help();
660 if !help.is_empty() {
661 write!(f, "\n help: {}", help)?;
662 }
663 if let Some(val) = &entry.fill {
664 write!(f, "\n fill: {}", val)?;
665 }
666 if let Some(val) = &entry.suggestion {
667 write!(f, "\n suggestion: {}", val)?;
668 }
669 if !entry.needed_by_rules.is_empty() {
670 write!(
671 f,
672 "\n needed_by_rules: {}",
673 entry.needed_by_rules.join(", ")
674 )?;
675 }
676 }
677 }
678
679 if !self.rules.is_empty() {
680 write!(f, "\n\nRules:")?;
681 for (name, rule) in &self.rules {
682 write!(f, "\n {} ({})", name, rule.lemma_type.specifications)?;
683 if !rule.depends_on_rules.is_empty() {
684 write!(
685 f,
686 "\n depends_on_rules: {}",
687 rule.depends_on_rules.join(", ")
688 )?;
689 }
690 }
691 }
692
693 if self.data.is_empty() && self.rules.is_empty() {
694 write!(f, "\n (no data or rules)")?;
695 }
696
697 Ok(())
698 }
699}
700
701pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
707 let mut lines = Vec::new();
708 match spec {
709 TypeSpecification::Measure {
710 minimum,
711 maximum,
712 decimals,
713 units,
714 ..
715 } => {
716 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
717 if !unit_names.is_empty() {
718 lines.push(format!("units: {}", unit_names.join(", ")));
719 }
720 if let Some(d) = decimals {
721 lines.push(format!("decimals: {}", d));
722 }
723 if let Some((magnitude, unit_name)) = minimum {
724 lines.push(format!(
725 "minimum: {} {}",
726 magnitude.display_str(),
727 unit_name
728 ));
729 }
730 if let Some((magnitude, unit_name)) = maximum {
731 lines.push(format!(
732 "maximum: {} {}",
733 magnitude.display_str(),
734 unit_name
735 ));
736 }
737 }
738 TypeSpecification::Number {
739 minimum,
740 maximum,
741 decimals,
742 ..
743 } => {
744 if let Some(d) = decimals {
745 lines.push(format!("decimals: {}", d));
746 }
747 if let Some(v) = minimum {
748 lines.push(format!("minimum: {}", v.display_str()));
749 }
750 if let Some(v) = maximum {
751 lines.push(format!("maximum: {}", v.display_str()));
752 }
753 }
754 TypeSpecification::Ratio {
755 minimum,
756 maximum,
757 decimals,
758 units,
759 ..
760 } => {
761 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
762 if !unit_names.is_empty() {
763 lines.push(format!("units: {}", unit_names.join(", ")));
764 }
765 if let Some(d) = decimals {
766 lines.push(format!("decimals: {}", d));
767 }
768 if let Some(v) = minimum {
769 lines.push(format!("minimum: {}", v.display_str()));
770 }
771 if let Some(v) = maximum {
772 lines.push(format!("maximum: {}", v.display_str()));
773 }
774 }
775 TypeSpecification::Text {
776 options, length, ..
777 } => {
778 if let Some(l) = length {
779 lines.push(format!("length: {}", l));
780 }
781 if !options.is_empty() {
782 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
783 lines.push(format!("options: {}", quoted.join(", ")));
784 }
785 }
786 TypeSpecification::Date {
787 minimum, maximum, ..
788 } => {
789 if let Some(v) = minimum {
790 lines.push(format!("minimum: {}", v));
791 }
792 if let Some(v) = maximum {
793 lines.push(format!("maximum: {}", v));
794 }
795 }
796 TypeSpecification::Time {
797 minimum, maximum, ..
798 } => {
799 if let Some(v) = minimum {
800 lines.push(format!("minimum: {}", v));
801 }
802 if let Some(v) = maximum {
803 lines.push(format!("maximum: {}", v));
804 }
805 }
806 TypeSpecification::MeasureRange {
807 lower,
808 upper,
809 minimum,
810 maximum,
811 units,
812 ..
813 } => {
814 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
815 if !unit_names.is_empty() {
816 lines.push(format!("units: {}", unit_names.join(", ")));
817 }
818 if let Some((magnitude, unit_name)) = lower {
819 lines.push(format!("lower: {} {}", magnitude.display_str(), unit_name));
820 }
821 if let Some((magnitude, unit_name)) = upper {
822 lines.push(format!("upper: {} {}", magnitude.display_str(), unit_name));
823 }
824 if let Some((magnitude, unit_name)) = minimum {
825 lines.push(format!(
826 "minimum: {} {}",
827 magnitude.display_str(),
828 unit_name
829 ));
830 }
831 if let Some((magnitude, unit_name)) = maximum {
832 lines.push(format!(
833 "maximum: {} {}",
834 magnitude.display_str(),
835 unit_name
836 ));
837 }
838 }
839 TypeSpecification::RatioRange {
840 lower,
841 upper,
842 minimum,
843 maximum,
844 units,
845 ..
846 } => {
847 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
848 if !unit_names.is_empty() {
849 lines.push(format!("units: {}", unit_names.join(", ")));
850 }
851 if let Some(v) = lower {
852 lines.push(format!("lower: {}", v.display_str()));
853 }
854 if let Some(v) = upper {
855 lines.push(format!("upper: {}", v.display_str()));
856 }
857 if let Some(v) = minimum {
858 lines.push(format!("minimum: {}", v.display_str()));
859 }
860 if let Some(v) = maximum {
861 lines.push(format!("maximum: {}", v.display_str()));
862 }
863 }
864 TypeSpecification::NumberRange {
865 lower,
866 upper,
867 minimum,
868 maximum,
869 ..
870 } => {
871 if let Some(v) = lower {
872 lines.push(format!("lower: {}", v.display_str()));
873 }
874 if let Some(v) = upper {
875 lines.push(format!("upper: {}", v.display_str()));
876 }
877 if let Some(v) = minimum {
878 lines.push(format!("minimum: {}", v.display_str()));
879 }
880 if let Some(v) = maximum {
881 lines.push(format!("maximum: {}", v.display_str()));
882 }
883 }
884 TypeSpecification::DateRange {
885 lower,
886 upper,
887 minimum,
888 maximum,
889 ..
890 } => {
891 if let Some(v) = lower {
892 lines.push(format!("lower: {}", v));
893 }
894 if let Some(v) = upper {
895 lines.push(format!("upper: {}", v));
896 }
897 if let Some((magnitude, unit_name)) = minimum {
898 lines.push(format!(
899 "minimum: {} {}",
900 magnitude.display_str(),
901 unit_name
902 ));
903 }
904 if let Some((magnitude, unit_name)) = maximum {
905 lines.push(format!(
906 "maximum: {} {}",
907 magnitude.display_str(),
908 unit_name
909 ));
910 }
911 }
912 TypeSpecification::TimeRange {
913 lower,
914 upper,
915 minimum,
916 maximum,
917 ..
918 } => {
919 if let Some(v) = lower {
920 lines.push(format!("lower: {}", v));
921 }
922 if let Some(v) = upper {
923 lines.push(format!("upper: {}", v));
924 }
925 if let Some((magnitude, unit_name)) = minimum {
926 lines.push(format!(
927 "minimum: {} {}",
928 magnitude.display_str(),
929 unit_name
930 ));
931 }
932 if let Some((magnitude, unit_name)) = maximum {
933 lines.push(format!(
934 "maximum: {} {}",
935 magnitude.display_str(),
936 unit_name
937 ));
938 }
939 }
940 TypeSpecification::Boolean { .. }
941 | TypeSpecification::Veto { .. }
942 | TypeSpecification::Undetermined => {}
943 }
944 lines
945}
946
947impl ExecutionPlan {
948 pub(crate) fn expression_unit_index(&self) -> &crate::planning::unit_index::UnitIndex {
950 &self.resolved_types.unit_index
951 }
952
953 fn build_needed_by_rules(&self) -> Vec<Vec<u32>> {
961 use crate::planning::normalize::{LeafKind, NormalFormKind};
962
963 let data_len = self.data.len();
964 let words = data_len.div_ceil(64);
965 let mut bits_by_rule: Vec<Vec<u64>> = Vec::with_capacity(self.rules.len());
966
967 for (rule_pos, (_path, rule)) in self.rules.iter().enumerate() {
968 let mut bits = vec![0u64; words];
969 let mut visited = HashSet::new();
970 let mut stack = vec![rule.normal_form];
971 while let Some(id) = stack.pop() {
972 if !visited.insert(id) {
973 continue;
974 }
975 let nf = self.normal_form(id);
976 if let Some(ref_path) = &nf.rule_ref {
977 let ref_pos = self.rules.get_index_of(ref_path).unwrap_or_else(|| {
978 panic!(
979 "BUG: rule_ref target '{ref_path}' missing from plan.rules (rule '{}')",
980 rule.path
981 )
982 });
983 assert!(
984 ref_pos < rule_pos,
985 "BUG: rule_ref target '{ref_path}' must precede '{}' in topo order",
986 rule.path
987 );
988 let dep = &bits_by_rule[ref_pos];
989 for (word, dep_word) in bits.iter_mut().zip(dep.iter()) {
990 *word |= *dep_word;
991 }
992 continue;
993 }
994 match &nf.kind {
995 NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
996 if let Some(target) = self.promptable_data_path(path) {
997 let data_pos = self.data.get_index_of(target).unwrap_or_else(|| {
998 panic!(
999 "BUG: promptable target '{target}' absent from plan.data (rule '{}')",
1000 rule.path
1001 )
1002 });
1003 bits[data_pos / 64] |= 1u64 << (data_pos % 64);
1004 }
1005 }
1006 NormalFormKind::OrderedDispatch { .. } => {
1007 let origin = nf.origin.unwrap_or_else(|| {
1010 panic!(
1011 "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
1012 rule.path
1013 )
1014 });
1015 stack.push(origin);
1016 }
1017 _ => stack.extend(nf.kind.children()),
1018 }
1019 }
1020 bits_by_rule.push(bits);
1021 }
1022
1023 let mut consumers: Vec<usize> = self
1024 .rules
1025 .keys()
1026 .enumerate()
1027 .filter(|(_, path)| self.show_rules.contains_key(*path))
1028 .map(|(pos, _)| pos)
1029 .collect();
1030 consumers.sort_by(|&a, &b| {
1031 self.rules[a]
1032 .path
1033 .input_key()
1034 .cmp(&self.rules[b].path.input_key())
1035 });
1036
1037 let mut needed_by_rules = vec![Vec::new(); data_len];
1038 for &rule_pos in &consumers {
1039 let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
1040 let bits = &bits_by_rule[rule_pos];
1041 for (word_idx, &word) in bits.iter().enumerate() {
1042 let mut remaining = word;
1043 while remaining != 0 {
1044 let bit = remaining.trailing_zeros() as usize;
1045 let data_pos = word_idx * 64 + bit;
1046 needed_by_rules[data_pos].push(rule_id);
1047 remaining &= remaining - 1;
1048 }
1049 }
1050 }
1051 needed_by_rules
1052 }
1053
1054 pub(crate) fn promptable_data_path<'a>(&'a self, path: &'a DataPath) -> Option<&'a DataPath> {
1060 match self.data.get(path) {
1061 Some(DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }) => {
1062 Some(path)
1063 }
1064 Some(DataDefinition::Reference { .. }) => match self.reference_ends.get(path) {
1065 Some(ReferenceEnd::Promptable(target)) => Some(target),
1066 Some(ReferenceEnd::Rule(_) | ReferenceEnd::Import) => None,
1067 None => {
1068 panic!("BUG: reference '{path}' missing from reference_ends after planning")
1069 }
1070 },
1071 Some(DataDefinition::Import { .. }) => None,
1072 None => panic!("BUG: normal-form DataPath leaf absent from plan.data: {path}"),
1073 }
1074 }
1075
1076 pub fn validated_response_rule_paths(
1081 &self,
1082 rules: Option<&[String]>,
1083 ) -> Result<std::collections::HashSet<RulePath>, Error> {
1084 let Some(rules) = rules else {
1085 return Ok(self
1086 .rules
1087 .values()
1088 .filter(|r| r.path.segments.is_empty())
1089 .map(|r| r.path.clone())
1090 .collect());
1091 };
1092 if rules.is_empty() {
1093 return Err(Error::request(
1094 "at least one rule required".to_string(),
1095 None::<String>,
1096 ));
1097 }
1098 let mut paths = std::collections::HashSet::new();
1099 for rule_name in rules {
1100 let rule = self.get_rule(rule_name).ok_or_else(|| {
1101 Error::request(
1102 format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
1103 None::<String>,
1104 )
1105 })?;
1106 paths.insert(rule.path.clone());
1107 }
1108 Ok(paths)
1109 }
1110
1111 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
1113 let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1114 let path = self.show_rule_index.get(&canonical)?;
1115 self.rules.get(path)
1116 }
1117
1118 pub(crate) fn normal_form(&self, id: NormalFormId) -> &NormalForm {
1120 self.normal_forms.get(id.index()).unwrap_or_else(|| {
1121 panic!(
1122 "BUG: NormalFormId {} out of range (table len {})",
1123 id.index(),
1124 self.normal_forms.len()
1125 )
1126 })
1127 }
1128
1129 pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
1131 &self.normal_form(id).result_type
1132 }
1133
1134 pub(crate) fn rule_index(&self, path: &RulePath) -> RuleIndex {
1137 RuleIndex(
1138 self.rules
1139 .get_index_of(path)
1140 .unwrap_or_else(|| panic!("BUG: rule '{}' missing from execution plan", path.rule)),
1141 )
1142 }
1143
1144 pub(crate) fn rule_at(&self, index: RuleIndex) -> &ExecutableRule {
1145 self.rules
1146 .get_index(index.0)
1147 .map(|(_, rule)| rule)
1148 .unwrap_or_else(|| {
1149 panic!(
1150 "BUG: rule index {} out of plan.rules range ({} rules)",
1151 index.0,
1152 self.rules.len()
1153 )
1154 })
1155 }
1156
1157 pub(crate) fn promptable_data_paths(&self) -> impl Iterator<Item = &DataPath> {
1169 self.data
1170 .iter()
1171 .filter_map(|(path, definition)| match definition {
1172 DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. } => Some(path),
1173 DataDefinition::Reference { .. } | DataDefinition::Import { .. } => None,
1174 })
1175 }
1176}
1177
1178pub(crate) fn resolve_nested_dispatch_body(
1184 plan: &ExecutionPlan,
1185 values: &[Option<crate::computation::OperationResult>],
1186 mut body: NormalFormId,
1187) -> Option<NormalFormId> {
1188 use crate::computation::OperationResult;
1189 use crate::planning::normalize::NormalFormKind;
1190 use crate::planning::ordered_dispatch::region_of_scrutinee;
1191
1192 for _ in 0..=plan.normal_forms.len() {
1193 let cell = plan.normal_form(body);
1194 if cell.rule_ref.is_some() {
1195 return Some(body);
1196 }
1197 let NormalFormKind::OrderedDispatch {
1198 scrutinee,
1199 boundaries,
1200 regions,
1201 } = &cell.kind
1202 else {
1203 return Some(body);
1204 };
1205 let literal = match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
1206 Some(OperationResult::Value(literal)) => literal,
1207 Some(OperationResult::Veto(_)) | None => return None,
1208 };
1209 let region = region_of_scrutinee(boundaries, &literal.value).ok()?;
1210 body = regions[region];
1211 }
1212 panic!("BUG: nested OrderedDispatch chain longer than the normal-form table");
1213}
1214
1215pub(crate) fn reachable_data_paths(
1235 plan: &ExecutionPlan,
1236 root: NormalFormId,
1237 values: &[Option<crate::computation::OperationResult>],
1238) -> IndexSet<DataPath> {
1239 use crate::computation::OperationResult;
1240 use crate::evaluation::branch_semantics::{
1241 condition_outcome, piecewise_decision, BranchOutcome, PiecewiseDecision,
1242 };
1243 use crate::planning::normalize::LeafKind;
1244 use crate::planning::normalize::NormalFormKind;
1245 use crate::planning::ordered_dispatch::region_of_scrutinee;
1246
1247 fn slot_outcome(values: &[Option<OperationResult>], id: NormalFormId) -> Option<BranchOutcome> {
1248 values
1249 .get(id.index())
1250 .and_then(|slot| slot.as_ref())
1251 .map(condition_outcome)
1252 }
1253
1254 fn dispatch_origin(plan: &ExecutionPlan, cell: &NormalForm) -> NormalFormId {
1262 let mut current = cell;
1263 for _ in 0..=plan.rules.len() {
1264 if let Some(origin) = current.origin {
1265 return origin;
1266 }
1267 let ref_path = current.rule_ref.as_ref().unwrap_or_else(|| {
1268 panic!("BUG: OrderedDispatch without origin must carry rule_ref")
1269 });
1270 let body = plan
1271 .rules
1272 .get(ref_path)
1273 .unwrap_or_else(|| {
1274 panic!("BUG: rule reference '{ref_path}' missing from plan.rules")
1275 })
1276 .normal_form;
1277 current = plan.normal_form(body);
1278 }
1279 panic!("BUG: rule reference chain over a dispatch cell does not terminate");
1280 }
1281
1282 fn live_piecewise_children(
1285 arms: &[(NormalFormId, NormalFormId)],
1286 decision: PiecewiseDecision,
1287 ) -> Vec<NormalFormId> {
1288 let mut live = Vec::new();
1289 match decision {
1290 PiecewiseDecision::Taken { arm } => {
1291 for (condition, _) in arms.iter().skip(arm + 1).rev() {
1292 live.push(*condition);
1293 }
1294 live.push(arms[arm].0);
1295 live.push(arms[arm].1);
1296 }
1297 PiecewiseDecision::Default => {
1298 for (condition, _) in arms.iter().skip(1).rev() {
1299 live.push(*condition);
1300 }
1301 live.push(arms[0].1);
1302 }
1303 PiecewiseDecision::Undecided { arm } => {
1304 for (condition, _) in arms.iter().skip(arm + 1).rev() {
1305 live.push(*condition);
1306 }
1307 for (condition, body) in arms.iter().take(arm + 1).skip(1).rev() {
1308 live.push(*condition);
1309 live.push(*body);
1310 }
1311 live.push(arms[0].1);
1312 }
1313 }
1314 live
1315 }
1316
1317 let mut out = IndexSet::new();
1318 let mut visited = HashSet::new();
1319 let mut stack = vec![root];
1320
1321 while let Some(id) = stack.pop() {
1322 if !visited.insert(id) {
1323 continue;
1324 }
1325 let nf = plan.normal_form(id);
1326 let live: Vec<NormalFormId> = match &nf.kind {
1327 NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
1328 out.insert(path.clone());
1329 Vec::new()
1330 }
1331 NormalFormKind::And(left, right) => {
1332 match slot_outcome(values, *left) {
1334 Some(BranchOutcome::Taken) => vec![*left, *right],
1335 Some(BranchOutcome::NotTaken | BranchOutcome::Propagate(_)) | None => {
1336 vec![*left]
1337 }
1338 }
1339 }
1340 NormalFormKind::Piecewise(arms) => {
1341 let decision =
1342 piecewise_decision(arms.len(), |arm| slot_outcome(values, arms[arm].0));
1343 live_piecewise_children(arms, decision)
1344 }
1345 NormalFormKind::OrderedDispatch {
1346 scrutinee,
1347 boundaries,
1348 regions,
1349 } => {
1350 let origin_id = dispatch_origin(plan, nf);
1351 let NormalFormKind::Piecewise(arms) = &plan.normal_form(origin_id).kind else {
1352 panic!("BUG: OrderedDispatch origin must be Piecewise");
1353 };
1354 visited.insert(origin_id);
1355
1356 let selected = match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
1357 Some(OperationResult::Value(literal)) => {
1358 region_of_scrutinee(boundaries, &literal.value)
1359 .ok()
1360 .map(|region| regions[region])
1361 }
1362 Some(OperationResult::Veto(_)) | None => None,
1363 };
1364 let resolved =
1365 selected.and_then(|body| resolve_nested_dispatch_body(plan, values, body));
1366 let decision = match resolved {
1367 Some(resolved) => {
1368 let winner = arms.iter().rposition(|(_, body)| *body == resolved);
1369 match winner {
1370 Some(0) => PiecewiseDecision::Default,
1371 Some(arm) => PiecewiseDecision::Taken { arm },
1372 None => panic!(
1373 "BUG: OrderedDispatch selected region body is not an origin arm body"
1374 ),
1375 }
1376 }
1377 None => PiecewiseDecision::Undecided {
1380 arm: arms.len() - 1,
1381 },
1382 };
1383 let mut live = vec![*scrutinee];
1384 if let (Some(selected_body), None) = (selected, resolved) {
1389 live.push(selected_body);
1390 }
1391 live.extend(live_piecewise_children(arms, decision));
1392 live
1393 }
1394 NormalFormKind::Leaf(LeafKind::Literal(_))
1395 | NormalFormKind::Now
1396 | NormalFormKind::Veto(_)
1397 | NormalFormKind::Sum(_)
1398 | NormalFormKind::Product(_)
1399 | NormalFormKind::Subtract(_, _)
1400 | NormalFormKind::Divide(_, _)
1401 | NormalFormKind::Power(_, _)
1402 | NormalFormKind::Modulo(_, _)
1403 | NormalFormKind::Comparison(_, _, _)
1404 | NormalFormKind::RangeLiteral(_, _)
1405 | NormalFormKind::RangeContainment(_, _)
1406 | NormalFormKind::Negate(_)
1407 | NormalFormKind::Reciprocal(_)
1408 | NormalFormKind::Not(_)
1409 | NormalFormKind::MathOp(_, _)
1410 | NormalFormKind::UnitConversion(_, _)
1411 | NormalFormKind::DateRelative(_, _)
1412 | NormalFormKind::DateCalendar(_, _, _)
1413 | NormalFormKind::PastFutureRange(_, _)
1414 | NormalFormKind::ResultIsVeto(_) => nf.kind.children(),
1415 };
1416 stack.extend(live.into_iter().rev());
1417 }
1418 out
1419}
1420
1421pub(crate) fn validate_value_against_type(
1422 expected_type: &LemmaType,
1423 value: &LiteralValue,
1424 unit_index: &crate::planning::unit_index::UnitIndex,
1425) -> Result<(), String> {
1426 use crate::computation::rational::{checked_mul, rational_new, try_pow_i32, RationalInteger};
1427 use crate::planning::semantics::TypeSpecification;
1428
1429 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
1430 let scale = match try_pow_i32(&rational_new(10, 1), i32::from(max_decimals)) {
1431 Ok(value) => value,
1432 Err(_) => return true,
1433 };
1434 let scaled = match checked_mul(magnitude, &scale) {
1435 Ok(value) => value,
1436 Err(_) => return true,
1437 };
1438 match RationalInteger::try_reduce_ref(&scaled) {
1439 Ok(reduced) => !reduced.is_integer(),
1440 Err(_) => true,
1441 }
1442 }
1443
1444 fn format_rational_for_validation_message(
1445 expected_type: &crate::planning::semantics::LemmaType,
1446 magnitude: &RationalInteger,
1447 ) -> String {
1448 expected_type
1449 .try_rational_as_decimal(magnitude)
1450 .map(|decimal| decimal.to_string())
1451 .unwrap_or_else(|_| magnitude.display_str())
1452 }
1453
1454 match (&expected_type.specifications, &value.value) {
1455 (
1456 TypeSpecification::Number {
1457 minimum,
1458 maximum,
1459 decimals,
1460 ..
1461 },
1462 ValueKind::Number(n),
1463 ) => {
1464 use std::cmp::Ordering;
1465 if let Some(d) = decimals {
1466 if exceeds_decimal_places(n, *d) {
1467 return Err(format!(
1468 "{} exceeds decimals constraint {d}",
1469 n.display_str()
1470 ));
1471 }
1472 }
1473 if let Some(min) = minimum {
1474 if n.try_cmp(min)
1475 .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
1476 == Ordering::Less
1477 {
1478 return Err(format!(
1479 "{} is below minimum {}",
1480 format_rational_for_validation_message(expected_type, n),
1481 format_rational_for_validation_message(expected_type, min)
1482 ));
1483 }
1484 }
1485 if let Some(max) = maximum {
1486 if n.try_cmp(max)
1487 .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
1488 == Ordering::Greater
1489 {
1490 return Err(format!(
1491 "{} is above maximum {}",
1492 format_rational_for_validation_message(expected_type, n),
1493 format_rational_for_validation_message(expected_type, max)
1494 ));
1495 }
1496 }
1497 Ok(())
1498 }
1499 (
1500 TypeSpecification::Measure {
1501 minimum,
1502 maximum,
1503 decimals,
1504 units,
1505 ..
1506 },
1507 ValueKind::Measure(magnitude),
1508 ) => {
1509 use crate::computation::rational::checked_div;
1510 use crate::planning::semantics::measure_declared_bound_to_canonical;
1511 use std::cmp::Ordering;
1512 let unit = expected_type
1513 .measure_binding_unit
1514 .as_deref()
1515 .or_else(|| {
1516 units
1517 .iter()
1518 .find(|u| u.is_canonical_factor())
1519 .or_else(|| units.iter().next())
1520 .map(|u| u.name.as_str())
1521 })
1522 .ok_or_else(|| {
1523 format!(
1524 "measure type '{}' has no declared units for validation",
1525 expected_type.name()
1526 )
1527 })?;
1528 let measure_unit = units.get(unit)?;
1529 let factor = &measure_unit.factor;
1530 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1531 format!("cannot de-canonicalize measure for validation: {failure}")
1532 })?;
1533 if let Some(d) = decimals {
1534 if exceeds_decimal_places(&in_unit, *d) {
1535 return Err(format!(
1536 "{} {unit} exceeds decimals constraint {d}",
1537 in_unit.display_str()
1538 ));
1539 }
1540 }
1541 if let Some(bound) = minimum {
1542 let canonical_min = measure_declared_bound_to_canonical(
1543 &bound.0,
1544 &bound.1,
1545 units,
1546 expected_type.name().as_str(),
1547 "minimum",
1548 )?;
1549 if magnitude
1550 .try_cmp(&canonical_min)
1551 .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
1552 == Ordering::Less
1553 {
1554 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1555 format!("cannot de-canonicalize minimum for validation: {failure}")
1556 })?;
1557 let value_display = format!(
1558 "{} {}",
1559 format_rational_for_validation_message(expected_type, &in_unit),
1560 unit
1561 );
1562 let bound_display = format!(
1563 "{} {}",
1564 format_rational_for_validation_message(expected_type, &min_in_unit),
1565 measure_unit.name
1566 );
1567 return Err(format!("{value_display} is below minimum {bound_display}"));
1568 }
1569 }
1570 if let Some(bound) = maximum {
1571 let canonical_max = measure_declared_bound_to_canonical(
1572 &bound.0,
1573 &bound.1,
1574 units,
1575 expected_type.name().as_str(),
1576 "maximum",
1577 )?;
1578 if magnitude
1579 .try_cmp(&canonical_max)
1580 .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
1581 == Ordering::Greater
1582 {
1583 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1584 format!("cannot de-canonicalize maximum for validation: {failure}")
1585 })?;
1586 let value_display = format!(
1587 "{} {}",
1588 format_rational_for_validation_message(expected_type, &in_unit),
1589 unit
1590 );
1591 let bound_display = format!(
1592 "{} {}",
1593 format_rational_for_validation_message(expected_type, &max_in_unit),
1594 measure_unit.name
1595 );
1596 return Err(format!("{value_display} is above maximum {bound_display}"));
1597 }
1598 }
1599 Ok(())
1600 }
1601 (
1602 TypeSpecification::Text {
1603 length, options, ..
1604 },
1605 ValueKind::Text(s),
1606 ) => {
1607 let len = s.chars().count();
1608 if let Some(exact) = length {
1609 if len != *exact {
1610 return Err(format!(
1611 "'{}' has length {} but required length is {}",
1612 s, len, exact
1613 ));
1614 }
1615 }
1616 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1617 return Err(format!(
1618 "'{}' is not in allowed options: {}",
1619 s,
1620 options.join(", ")
1621 ));
1622 }
1623 Ok(())
1624 }
1625 (
1626 TypeSpecification::Ratio {
1627 minimum,
1628 maximum,
1629 decimals,
1630 units,
1631 ..
1632 },
1633 ValueKind::Ratio(r),
1634 ) => {
1635 use crate::computation::rational::checked_mul;
1636 use std::cmp::Ordering;
1637
1638 let primary_unit = expected_type
1639 .measure_binding_unit
1640 .as_deref()
1641 .or_else(|| expected_type.ratio_primary_unit());
1642
1643 if let Some(d) = decimals {
1644 let magnitude_for_decimals = match primary_unit {
1645 Some(unit) => {
1646 let ratio_unit = units.get(unit)?;
1647 checked_mul(r, &ratio_unit.value).map_err(|failure| failure.to_string())?
1648 }
1649 None => r.clone(),
1650 };
1651 if exceeds_decimal_places(&magnitude_for_decimals, *d) {
1652 return Err(format!(
1653 "{} exceeds decimals constraint {d}",
1654 magnitude_for_decimals.display_str()
1655 ));
1656 }
1657 }
1658 if let Some(type_minimum) = minimum {
1659 if r.try_cmp(type_minimum)
1660 .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
1661 == Ordering::Less
1662 {
1663 let message = match primary_unit {
1664 Some(unit) => {
1665 let ratio_unit = units.get(unit)?;
1666 let value_per_unit = checked_mul(r, &ratio_unit.value)
1667 .map_err(|failure| failure.to_string())?;
1668 let bound_per_unit = ratio_unit.minimum.clone().expect(
1669 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1670 );
1671 format!(
1672 "{} {unit} is below minimum {} {unit}",
1673 format_rational_for_validation_message(
1674 expected_type,
1675 &value_per_unit
1676 ),
1677 format_rational_for_validation_message(
1678 expected_type,
1679 &bound_per_unit.clone()
1680 ),
1681 )
1682 }
1683 None => format!(
1684 "{} is below minimum {}",
1685 format_rational_for_validation_message(expected_type, r),
1686 format_rational_for_validation_message(expected_type, type_minimum),
1687 ),
1688 };
1689 return Err(message);
1690 }
1691 }
1692 if let Some(type_maximum) = maximum {
1693 if r.try_cmp(type_maximum)
1694 .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
1695 == Ordering::Greater
1696 {
1697 let message = match primary_unit {
1698 Some(unit) => {
1699 let ratio_unit = units.get(unit)?;
1700 let value_per_unit = checked_mul(r, &ratio_unit.value)
1701 .map_err(|failure| failure.to_string())?;
1702 let bound_per_unit = ratio_unit.maximum.clone().expect(
1703 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1704 );
1705 format!(
1706 "{} {unit} is above maximum {} {unit}",
1707 format_rational_for_validation_message(
1708 expected_type,
1709 &value_per_unit
1710 ),
1711 format_rational_for_validation_message(
1712 expected_type,
1713 &bound_per_unit.clone()
1714 ),
1715 )
1716 }
1717 None => format!(
1718 "{} is above maximum {}",
1719 format_rational_for_validation_message(expected_type, r),
1720 format_rational_for_validation_message(expected_type, type_maximum),
1721 ),
1722 };
1723 return Err(message);
1724 }
1725 }
1726 Ok(())
1727 }
1728 (
1729 TypeSpecification::Ratio {
1730 minimum,
1731 maximum,
1732 decimals,
1733 units: _,
1734 ..
1735 },
1736 ValueKind::Number(n),
1737 ) => {
1738 use std::cmp::Ordering;
1739 if let Some(d) = decimals {
1740 if exceeds_decimal_places(n, *d) {
1741 return Err(format!(
1742 "{} exceeds decimals constraint {d}",
1743 n.display_str()
1744 ));
1745 }
1746 }
1747 if let Some(type_minimum) = minimum {
1748 if n.try_cmp(type_minimum)
1749 .map_err(|failure| format!("minimum bound compare failed: {failure}"))?
1750 == Ordering::Less
1751 {
1752 return Err(format!(
1753 "{} is below minimum {}",
1754 format_rational_for_validation_message(expected_type, n),
1755 format_rational_for_validation_message(expected_type, type_minimum)
1756 ));
1757 }
1758 }
1759 if let Some(type_maximum) = maximum {
1760 if n.try_cmp(type_maximum)
1761 .map_err(|failure| format!("maximum bound compare failed: {failure}"))?
1762 == Ordering::Greater
1763 {
1764 return Err(format!(
1765 "{} is above maximum {}",
1766 format_rational_for_validation_message(expected_type, n),
1767 format_rational_for_validation_message(expected_type, type_maximum)
1768 ));
1769 }
1770 }
1771 Ok(())
1772 }
1773 (
1774 TypeSpecification::Date {
1775 minimum, maximum, ..
1776 },
1777 ValueKind::Date(dt),
1778 ) => {
1779 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1780 use std::cmp::Ordering;
1781 if let Some(min) = minimum {
1782 let min_sem = date_time_to_semantic(min);
1783 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1784 return Err(format!("{} is below minimum {}", dt, min));
1785 }
1786 }
1787 if let Some(max) = maximum {
1788 let max_sem = date_time_to_semantic(max);
1789 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1790 return Err(format!("{} is above maximum {}", dt, max));
1791 }
1792 }
1793 Ok(())
1794 }
1795 (
1796 TypeSpecification::Time {
1797 minimum, maximum, ..
1798 },
1799 ValueKind::Time(t),
1800 ) => {
1801 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1802 use std::cmp::Ordering;
1803 if let Some(min) = minimum {
1804 let min_sem = time_to_semantic(min);
1805 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1806 return Err(format!("{} is below minimum {}", t, min));
1807 }
1808 }
1809 if let Some(max) = maximum {
1810 let max_sem = time_to_semantic(max);
1811 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1812 return Err(format!("{} is above maximum {}", t, max));
1813 }
1814 }
1815 Ok(())
1816 }
1817 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_)) => Ok(()),
1818 (
1819 range_spec @ (TypeSpecification::NumberRange { .. }
1820 | TypeSpecification::DateRange { .. }
1821 | TypeSpecification::TimeRange { .. }
1822 | TypeSpecification::MeasureRange { .. }
1823 | TypeSpecification::RatioRange { .. }),
1824 ValueKind::Range(left, right),
1825 ) => validate_range_literal(
1826 expected_type,
1827 range_spec,
1828 left.as_ref(),
1829 right.as_ref(),
1830 unit_index,
1831 ),
1832 (TypeSpecification::Veto { .. }, _) | (TypeSpecification::Undetermined, _) => Ok(()),
1833 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1834 "BUG: validate_value_against_type called with mismatched type/value: \
1835 spec={:?}, value={:?} — typing must be enforced before validation",
1836 spec, value_kind
1837 ),
1838 (spec, value_kind) => unreachable!(
1839 "BUG: validate_value_against_type missed a value_kind_matches_spec pair: \
1840 spec={:?}, value={:?}",
1841 spec, value_kind
1842 ),
1843 }
1844}
1845
1846fn validate_range_literal(
1847 expected_type: &LemmaType,
1848 range_spec: &TypeSpecification,
1849 left: &LiteralValue,
1850 right: &LiteralValue,
1851 unit_index: &crate::planning::unit_index::UnitIndex,
1852) -> Result<(), String> {
1853 use crate::computation::{comparison_operation, OperationResult};
1854 use crate::planning::semantics::{
1855 compare_semantic_dates, compare_semantic_times, measure_declared_bound_to_canonical,
1856 ValueKind,
1857 };
1858 use std::cmp::Ordering;
1859 use std::sync::Arc;
1860
1861 let mut element_spec = range_spec
1862 .element_from_range()
1863 .expect("BUG: element_from_range missing arm for validated range");
1864 if let TypeSpecification::Measure {
1865 units,
1866 decomposition,
1867 ..
1868 } = &mut element_spec
1869 {
1870 if decomposition.is_none() && !units.0.is_empty() {
1871 *decomposition = Some([(expected_type.name(), 1i32)].into_iter().collect());
1872 }
1873 }
1874 let element_type = Arc::new(LemmaType::primitive(element_spec));
1875 let left = LiteralValue {
1876 value: left.value.clone(),
1877 };
1878 let right = LiteralValue {
1879 value: right.value.clone(),
1880 };
1881 validate_value_against_type(element_type.as_ref(), &left, unit_index)?;
1882 validate_value_against_type(element_type.as_ref(), &right, unit_index)?;
1883
1884 let ordering = match (&left.value, &right.value) {
1885 (ValueKind::Number(l), ValueKind::Number(r)) => l
1886 .try_cmp(r)
1887 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
1888 (ValueKind::Date(l), ValueKind::Date(r)) => compare_semantic_dates(l, r),
1889 (ValueKind::Time(l), ValueKind::Time(r)) => compare_semantic_times(l, r),
1890 (ValueKind::Ratio(l), ValueKind::Ratio(r)) => l
1891 .try_cmp(r)
1892 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
1893 (ValueKind::Measure(l), ValueKind::Measure(r)) => l
1894 .try_cmp(r)
1895 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?,
1896 (left_kind, right_kind) => unreachable!(
1897 "BUG: range endpoints have mismatched value kinds after typing: {left_kind:?} vs {right_kind:?}"
1898 ),
1899 };
1900 if ordering == Ordering::Greater {
1901 return Err(format!(
1902 "range left endpoint {left} is above right endpoint {right}"
1903 ));
1904 }
1905
1906 let range_lit = LiteralValue {
1907 value: ValueKind::Range(Box::new(left.clone()), Box::new(right.clone())),
1908 };
1909 let range_type = Arc::new(expected_type.clone());
1910
1911 let compare_width = |bound: &LiteralValue,
1912 bound_type: &Arc<LemmaType>,
1913 op: ComparisonComputation,
1914 fail_msg: String|
1915 -> Result<(), String> {
1916 match comparison_operation(&range_lit.value, &range_type, &op, &bound.value, bound_type) {
1917 OperationResult::Value(result) => match &result.value {
1918 ValueKind::Boolean(true) => Ok(()),
1919 ValueKind::Boolean(false) => Err(fail_msg),
1920 other => unreachable!("BUG: width comparison must return boolean, got {other:?}"),
1921 },
1922 OperationResult::Veto(veto) => Err(veto.to_string()),
1923 }
1924 };
1925
1926 match range_spec {
1927 TypeSpecification::NumberRange {
1928 minimum, maximum, ..
1929 } => {
1930 if let Some(min_w) = minimum {
1931 compare_width(
1932 &LiteralValue::number(min_w.clone()),
1933 crate::planning::semantics::primitive_number_arc(),
1934 ComparisonComputation::GreaterThanOrEqual,
1935 format!("span is below minimum width {}", min_w.display_str()),
1936 )?;
1937 }
1938 if let Some(max_w) = maximum {
1939 compare_width(
1940 &LiteralValue::number(max_w.clone()),
1941 crate::planning::semantics::primitive_number_arc(),
1942 ComparisonComputation::LessThanOrEqual,
1943 format!("span is above maximum width {}", max_w.display_str()),
1944 )?;
1945 }
1946 }
1947 TypeSpecification::RatioRange {
1948 minimum, maximum, ..
1949 } => {
1950 if let Some(min_w) = minimum {
1951 compare_width(
1952 &LiteralValue::ratio(min_w.clone()),
1953 crate::planning::semantics::primitive_ratio_arc(),
1954 ComparisonComputation::GreaterThanOrEqual,
1955 format!("span is below minimum width {}", min_w.display_str()),
1956 )?;
1957 }
1958 if let Some(max_w) = maximum {
1959 compare_width(
1960 &LiteralValue::ratio(max_w.clone()),
1961 crate::planning::semantics::primitive_ratio_arc(),
1962 ComparisonComputation::LessThanOrEqual,
1963 format!("span is above maximum width {}", max_w.display_str()),
1964 )?;
1965 }
1966 }
1967 TypeSpecification::MeasureRange {
1968 minimum,
1969 maximum,
1970 units,
1971 ..
1972 } => {
1973 if let Some(min_w) = minimum {
1974 let canonical = measure_declared_bound_to_canonical(
1975 &min_w.0, &min_w.1, units, "range", "minimum",
1976 )?;
1977 let bound = LiteralValue::measure_with_type(canonical, Arc::clone(&element_type));
1978 compare_width(
1979 &bound,
1980 &element_type,
1981 ComparisonComputation::GreaterThanOrEqual,
1982 format!(
1983 "span is below minimum width {} {}",
1984 min_w.0.display_str(),
1985 min_w.1
1986 ),
1987 )?;
1988 }
1989 if let Some(max_w) = maximum {
1990 let canonical = measure_declared_bound_to_canonical(
1991 &max_w.0, &max_w.1, units, "range", "maximum",
1992 )?;
1993 let bound = LiteralValue::measure_with_type(canonical, Arc::clone(&element_type));
1994 compare_width(
1995 &bound,
1996 &element_type,
1997 ComparisonComputation::LessThanOrEqual,
1998 format!(
1999 "span is above maximum width {} {}",
2000 max_w.0.display_str(),
2001 max_w.1
2002 ),
2003 )?;
2004 }
2005 }
2006 TypeSpecification::DateRange {
2007 minimum, maximum, ..
2008 }
2009 | TypeSpecification::TimeRange {
2010 minimum, maximum, ..
2011 } => {
2012 let allow_calendar = matches!(range_spec, TypeSpecification::DateRange { .. });
2013 let resolve_bound =
2014 |bound: &(crate::computation::rational::RationalInteger, String),
2015 command: &str|
2016 -> Result<(LiteralValue, Arc<LemmaType>), String> {
2017 let (bare, owner) = unit_index
2018 .resolve(bound.1.as_str())
2019 .map_err(|err| format!("{command} width unit '{}': {err}", bound.1))?;
2020 if allow_calendar {
2021 if !owner.is_duration_like() && !owner.is_calendar_like() {
2022 return Err(format!(
2023 "{command} width unit '{bare}' must be a duration or calendar unit",
2024 ));
2025 }
2026 } else if !owner.is_duration_like() {
2027 return Err(format!(
2028 "{command} width unit '{bare}' must be a duration unit",
2029 ));
2030 }
2031 let TypeSpecification::Measure { units, .. } = &owner.specifications else {
2032 return Err(format!(
2033 "{command} width unit '{bare}' must resolve to a measure type",
2034 ));
2035 };
2036 let type_name = owner.name();
2037 let canonical = measure_declared_bound_to_canonical(
2038 &bound.0,
2039 &bare,
2040 units,
2041 type_name.as_str(),
2042 command,
2043 )?;
2044 Ok((
2045 LiteralValue::measure_with_type(canonical, Arc::clone(&owner)),
2046 Arc::clone(&owner),
2047 ))
2048 };
2049 if let Some(min_w) = minimum {
2050 let (bound, bound_type) = resolve_bound(min_w, "minimum")?;
2051 compare_width(
2052 &bound,
2053 &bound_type,
2054 ComparisonComputation::GreaterThanOrEqual,
2055 format!(
2056 "span is below minimum width {} {}",
2057 min_w.0.display_str(),
2058 min_w.1
2059 ),
2060 )?;
2061 }
2062 if let Some(max_w) = maximum {
2063 let (bound, bound_type) = resolve_bound(max_w, "maximum")?;
2064 compare_width(
2065 &bound,
2066 &bound_type,
2067 ComparisonComputation::LessThanOrEqual,
2068 format!(
2069 "span is above maximum width {} {}",
2070 max_w.0.display_str(),
2071 max_w.1
2072 ),
2073 )?;
2074 }
2075 }
2076 _ => {}
2077 }
2078
2079 Ok(())
2080}
2081
2082fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
2083 let mut errors = Vec::new();
2084
2085 for (data_path, data_definition) in &plan.data {
2086 let (expected_type, lit) = match data_definition {
2087 DataDefinition::Value {
2088 value,
2089 resolved_type,
2090 ..
2091 } => (resolved_type, value),
2092 DataDefinition::TypeDeclaration { .. }
2093 | DataDefinition::Import { .. }
2094 | DataDefinition::Reference { .. } => continue,
2095 };
2096
2097 if let Err(msg) =
2098 validate_value_against_type(expected_type, lit, plan.expression_unit_index())
2099 {
2100 let source = data_definition.source().clone();
2101 errors.push(Error::validation(
2102 format!(
2103 "Invalid value for data {} (expected {}): {}",
2104 data_path,
2105 expected_type.name().as_str(),
2106 msg
2107 ),
2108 Some(source),
2109 None::<String>,
2110 ));
2111 }
2112 }
2113
2114 errors
2115}
2116
2117fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
2118 use crate::planning::normalize::NormalFormKind;
2119
2120 let mut errors: Vec<Error> = Vec::new();
2121 let mut visited = HashSet::new();
2122 let mut worklist: Vec<NormalFormId> =
2123 plan.rules.values().map(|rule| rule.normal_form).collect();
2124 while let Some(id) = worklist.pop() {
2125 if !visited.insert(id) {
2126 continue;
2127 }
2128 let nf = plan.normal_form(id);
2129 worklist.extend(nf.kind.children());
2130 if let NormalFormKind::UnitConversion(_, target) = &nf.kind {
2131 if let Some((unit_name, owning_type)) =
2132 crate::computation::units::conversion_target_declares_unit(target)
2133 {
2134 if !crate::computation::units::owning_type_declares_unit_name(
2135 owning_type.as_ref(),
2136 unit_name,
2137 ) {
2138 errors.push(Error::validation(
2139 format!(
2140 "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
2141 owning_type.name()
2142 ),
2143 None::<Source>,
2144 Some(plan.spec_name.clone()),
2145 ));
2146 }
2147 }
2148 }
2149 }
2150 if let Some(error) = errors.into_iter().next() {
2151 return Err(error);
2152 }
2153 Ok(())
2154}
2155
2156#[cfg(test)]
2157mod tests {
2158 use super::*;
2159 use crate::computation::rational::{rational_new, rational_zero};
2160 use crate::computation::{OperationResult, VetoType};
2161 use crate::evaluation::run_data::RunData;
2162 use crate::literals::DateGranularity;
2163 use crate::literals::TimezoneValue;
2164 use crate::parsing::ast::DateTimeValue;
2165 use crate::planning::semantics::{DataDefinition, DataPath, PathSegment, TypeSpecification};
2166 use crate::Engine;
2167 use crate::{ResourceLimits, RunDataValue};
2168 use serde_json;
2169 use std::collections::HashMap;
2170 use std::str::FromStr;
2171 use std::sync::Arc;
2172
2173 fn default_limits() -> ResourceLimits {
2174 ResourceLimits::default()
2175 }
2176
2177 fn resolve_run_data(plan: &ExecutionPlan, values: HashMap<String, RunDataValue>) -> RunData {
2178 RunData::resolve(plan, values, &default_limits()).expect("resolve")
2179 }
2180
2181 fn veto_reason<'a>(run_data: &'a RunData, path: &DataPath) -> Option<&'a str> {
2182 match run_data.bindings.get(path) {
2183 Some(OperationResult::Veto(veto)) => Some(match veto {
2184 VetoType::Computation { message } => message.as_str(),
2185 other => panic!("expected Computation veto, got {other:?}"),
2186 }),
2187 _ => None,
2188 }
2189 }
2190
2191 fn bound_value<'a>(
2192 run_data: &'a RunData,
2193 path: &DataPath,
2194 ) -> Option<&'a crate::planning::semantics::BoundValueKind> {
2195 run_data.bindings.get(path).and_then(OperationResult::value)
2196 }
2197
2198 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, RunDataValue> {
2199 pairs
2200 .iter()
2201 .map(|(k, v)| (k.to_string(), RunDataValue::string(*v)))
2202 .collect()
2203 }
2204
2205 #[test]
2206 fn test_with_raw_values() {
2207 let mut engine = Engine::new();
2208 engine
2209 .load([(
2210 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2211 "test.lemma",
2212 ))),
2213 r#"
2214 spec test
2215 data age: number -> suggest 25
2216 "#
2217 .to_string(),
2218 )])
2219 .unwrap();
2220
2221 let plans = engine
2222 .plans
2223 .get_plans(None, "test")
2224 .expect("plans for test");
2225 let plan = plans.values().next().expect("plan");
2226 let data_path = DataPath::new(vec![], "age".to_string());
2227
2228 let values = input_data(&[("age", "30")]);
2229
2230 let run_data = resolve_run_data(plan, values);
2231 let updated_value = bound_value(&run_data, &data_path).expect("bound value");
2232 match &updated_value.value {
2233 crate::planning::semantics::ValueKind::Number(n) => {
2234 assert_eq!(n, &rational_new(30, 1));
2235 }
2236 other => panic!("Expected number literal, got {:?}", other),
2237 }
2238 }
2239
2240 #[test]
2241 fn test_with_raw_values_type_mismatch() {
2242 let mut engine = Engine::new();
2243 engine
2244 .load([(
2245 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2246 "test.lemma",
2247 ))),
2248 r#"
2249 spec test
2250 data age: number
2251 "#
2252 .to_string(),
2253 )])
2254 .unwrap();
2255
2256 let plans = engine
2257 .plans
2258 .get_plans(None, "test")
2259 .expect("plans for test");
2260 let plan = plans.values().next().expect("plan");
2261
2262 let values = input_data(&[("age", "thirty")]);
2263
2264 let run_data = resolve_run_data(plan, values);
2265 let data_path = DataPath::new(vec![], "age".to_string());
2266 match veto_reason(&run_data, &data_path) {
2267 Some(reason) => {
2268 assert!(
2269 reason.contains("number"),
2270 "type mismatch must record violation reason, got: {reason}"
2271 );
2272 }
2273 None => panic!("expected veto-bound data for age=thirty"),
2274 }
2275 }
2276
2277 #[test]
2278 fn test_with_raw_values_unknown_data_ignored() {
2279 let mut engine = Engine::new();
2280 engine
2281 .load([(
2282 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2283 "test.lemma",
2284 ))),
2285 r#"
2286 spec test
2287 data known: number
2288 "#
2289 .to_string(),
2290 )])
2291 .unwrap();
2292
2293 let plans = engine
2294 .plans
2295 .get_plans(None, "test")
2296 .expect("plans for test");
2297 let plan = plans.values().next().expect("plan");
2298
2299 let values = input_data(&[("unknown", "30")]);
2300
2301 let run_data = resolve_run_data(plan, values);
2302 assert!(run_data.bindings.is_empty());
2303 assert!(run_data.ignored_unknown.iter().any(|k| k == "unknown"));
2304 }
2305
2306 #[test]
2307 fn test_with_raw_values_nested() {
2308 let mut engine = Engine::new();
2309 engine
2310 .load([(
2311 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2312 "test.lemma",
2313 ))),
2314 r#"
2315 spec private
2316 data base_price: number
2317
2318 spec test
2319 uses rules: private
2320 "#
2321 .to_string(),
2322 )])
2323 .unwrap();
2324
2325 let plans = engine
2326 .plans
2327 .get_plans(None, "test")
2328 .expect("plans for test");
2329 let plan = plans.values().next().expect("plan");
2330
2331 let values = input_data(&[("rules.base_price", "100")]);
2332
2333 let run_data = resolve_run_data(plan, values);
2334 let data_path = DataPath {
2335 segments: vec![PathSegment {
2336 uses: "rules".to_string(),
2337 repository: None,
2338 spec: "private".to_string(),
2339 }],
2340 data: "base_price".to_string(),
2341 };
2342 let updated_value = bound_value(&run_data, &data_path).expect("bound value");
2343 match &updated_value.value {
2344 crate::planning::semantics::ValueKind::Number(n) => {
2345 assert_eq!(n, &rational_new(100, 1));
2346 }
2347 other => panic!("Expected number literal, got {:?}", other),
2348 }
2349 }
2350
2351 #[test]
2352 fn run_data_should_enforce_number_maximum_constraint() {
2353 let data_path = DataPath::new(vec![], "x".to_string());
2356
2357 let max10 = crate::planning::semantics::LemmaType::primitive(
2358 crate::planning::semantics::TypeSpecification::Number {
2359 minimum: None,
2360 maximum: Some(rational_new(10, 1)),
2361 decimals: None,
2362 help: String::new(),
2363 },
2364 );
2365 let source = Source::new(
2366 crate::parsing::source::SourceType::Volatile,
2367 crate::parsing::ast::Span {
2368 start: 0,
2369 end: 0,
2370 line: 1,
2371 col: 0,
2372 },
2373 );
2374 let mut data = IndexMap::new();
2375 data.insert(
2376 data_path.clone(),
2377 crate::planning::semantics::DataDefinition::Value {
2378 value: crate::planning::semantics::LiteralValue::number_with_type(
2379 rational_new(0, 1),
2380 Arc::new(max10.clone()),
2381 ),
2382 resolved_type: Arc::new(max10.clone()),
2383 source: source.clone(),
2384 },
2385 );
2386
2387 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
2388 let data_len = data.len();
2389 let plan = ExecutionPlan {
2390 spec_name: "test".to_string(),
2391 commentary: None,
2392 data,
2393 normal_forms: Vec::new(),
2394 rules: IndexMap::new(),
2395 data_reference_order: Vec::new(),
2396 meta: IndexMap::new(),
2397 resolved_types: ResolvedSpecTypes::default(),
2398 family_units: Arc::new(FamilyUnitCatalog::default()),
2399 signature_index: IndexMap::new(),
2400 effective: EffectiveDate::Origin,
2401 effective_from: None,
2402 effective_to: None,
2403 versions: std::sync::Arc::from([]),
2404 start_line: 1,
2405 source_type: None,
2406 needed_by_rules: vec![Vec::new(); data_len],
2407 data_display: IndexMap::new(),
2408 show_rules: IndexMap::new(),
2409 reference_ends: IndexMap::new(),
2410 input_key_index,
2411 show_rule_index: IndexMap::new(),
2412 data_leaf: IndexMap::new(),
2413 };
2414
2415 let values = input_data(&[("x", "11")]);
2416
2417 let run_data = resolve_run_data(&plan, values);
2418 match veto_reason(&run_data, &data_path) {
2419 Some(reason) => {
2420 assert!(
2421 reason.contains("maximum") || reason.contains("10"),
2422 "x=11 must violate maximum 10, got: {reason}"
2423 );
2424 }
2425 None => panic!("expected veto-bound data for x=11"),
2426 }
2427 }
2428
2429 #[test]
2430 fn run_data_should_enforce_text_enum_options() {
2431 let data_path = DataPath::new(vec![], "tier".to_string());
2433
2434 let tier = crate::planning::semantics::LemmaType::primitive(
2435 crate::planning::semantics::TypeSpecification::Text {
2436 length: None,
2437 options: vec!["silver".to_string(), "gold".to_string()],
2438 help: String::new(),
2439 },
2440 );
2441 let source = Source::new(
2442 crate::parsing::source::SourceType::Volatile,
2443 crate::parsing::ast::Span {
2444 start: 0,
2445 end: 0,
2446 line: 1,
2447 col: 0,
2448 },
2449 );
2450 let mut data = IndexMap::new();
2451 data.insert(
2452 data_path.clone(),
2453 crate::planning::semantics::DataDefinition::Value {
2454 value: crate::planning::semantics::LiteralValue::text_with_type(
2455 "silver".to_string(),
2456 Arc::new(tier.clone()),
2457 ),
2458 resolved_type: Arc::new(tier.clone()),
2459 source,
2460 },
2461 );
2462
2463 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
2464 let data_len = data.len();
2465 let plan = ExecutionPlan {
2466 spec_name: "test".to_string(),
2467 commentary: None,
2468 data,
2469 normal_forms: Vec::new(),
2470 rules: IndexMap::new(),
2471 data_reference_order: Vec::new(),
2472 meta: IndexMap::new(),
2473 resolved_types: ResolvedSpecTypes::default(),
2474 family_units: Arc::new(FamilyUnitCatalog::default()),
2475 signature_index: IndexMap::new(),
2476 effective: EffectiveDate::Origin,
2477 effective_from: None,
2478 effective_to: None,
2479 versions: std::sync::Arc::from([]),
2480 start_line: 1,
2481 source_type: None,
2482 needed_by_rules: vec![Vec::new(); data_len],
2483 data_display: IndexMap::new(),
2484 show_rules: IndexMap::new(),
2485 reference_ends: IndexMap::new(),
2486 input_key_index,
2487 show_rule_index: IndexMap::new(),
2488 data_leaf: IndexMap::new(),
2489 };
2490
2491 let values = input_data(&[("tier", "platinum")]);
2492
2493 let run_data = resolve_run_data(&plan, values);
2494 match veto_reason(&run_data, &data_path) {
2495 Some(reason) => {
2496 assert!(
2497 reason.contains("allowed options") || reason.contains("platinum"),
2498 "invalid enum must record violation, got: {reason}"
2499 );
2500 }
2501 None => panic!("expected veto-bound data for tier=platinum"),
2502 }
2503 }
2504
2505 #[test]
2506 fn run_data_should_enforce_measure_decimals() {
2507 let data_path = DataPath::new(vec![], "price".to_string());
2510
2511 let money = crate::planning::semantics::LemmaType::primitive(
2512 crate::planning::semantics::TypeSpecification::Measure {
2513 minimum: None,
2514 maximum: None,
2515 decimals: Some(2),
2516 units: crate::planning::semantics::MeasureUnits::from(vec![
2517 crate::planning::semantics::MeasureUnit::from_decimal_factor(
2518 "eur".to_string(),
2519 rust_decimal::Decimal::from_str("1.0").unwrap(),
2520 Vec::new(),
2521 )
2522 .expect("eur unit factor must be exact decimal"),
2523 ]),
2524 traits: Vec::new(),
2525 decomposition: None,
2526 help: String::new(),
2527 },
2528 );
2529 let source = Source::new(
2530 crate::parsing::source::SourceType::Volatile,
2531 crate::parsing::ast::Span {
2532 start: 0,
2533 end: 0,
2534 line: 1,
2535 col: 0,
2536 },
2537 );
2538 let mut data = IndexMap::new();
2539 data.insert(
2540 data_path.clone(),
2541 crate::planning::semantics::DataDefinition::Value {
2542 value: crate::planning::semantics::LiteralValue::measure_with_type(
2543 rational_zero(),
2544 Arc::new(money.clone()),
2545 ),
2546 resolved_type: Arc::new(money.clone()),
2547 source,
2548 },
2549 );
2550
2551 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
2552 let data_len = data.len();
2553 let plan = ExecutionPlan {
2554 spec_name: "test".to_string(),
2555 commentary: None,
2556 data,
2557 normal_forms: Vec::new(),
2558 rules: IndexMap::new(),
2559 data_reference_order: Vec::new(),
2560 meta: IndexMap::new(),
2561 resolved_types: ResolvedSpecTypes::default(),
2562 family_units: Arc::new(FamilyUnitCatalog::default()),
2563 signature_index: IndexMap::new(),
2564 effective: EffectiveDate::Origin,
2565 effective_from: None,
2566 effective_to: None,
2567 versions: std::sync::Arc::from([]),
2568 start_line: 1,
2569 source_type: None,
2570 needed_by_rules: vec![Vec::new(); data_len],
2571 data_display: IndexMap::new(),
2572 show_rules: IndexMap::new(),
2573 reference_ends: IndexMap::new(),
2574 input_key_index,
2575 show_rule_index: IndexMap::new(),
2576 data_leaf: IndexMap::new(),
2577 };
2578
2579 let values = input_data(&[("price", "1.234 eur")]);
2580
2581 let run_data = resolve_run_data(&plan, values);
2582 match veto_reason(&run_data, &data_path) {
2583 Some(reason) => {
2584 assert!(
2585 reason.contains("decimals") || reason.contains("decimal"),
2586 "1.234 eur must violate decimals=2, got: {reason}"
2587 );
2588 }
2589 None => panic!("expected veto-bound data for price=1.234 eur"),
2590 }
2591 }
2592
2593 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2594 ExecutionPlan {
2595 spec_name: "s".into(),
2596 commentary: None,
2597 data: IndexMap::new(),
2598 normal_forms: Vec::new(),
2599 rules: IndexMap::new(),
2600 data_reference_order: Vec::new(),
2601 meta: IndexMap::new(),
2602 resolved_types: ResolvedSpecTypes::default(),
2603 family_units: Arc::new(FamilyUnitCatalog::default()),
2604 signature_index: IndexMap::new(),
2605 effective,
2606 effective_from: None,
2607 effective_to: None,
2608 versions: std::sync::Arc::from([]),
2609 start_line: 1,
2610 source_type: None,
2611 needed_by_rules: Vec::new(),
2612 data_display: IndexMap::new(),
2613 show_rules: IndexMap::new(),
2614 reference_ends: IndexMap::new(),
2615 input_key_index: IndexMap::new(),
2616 show_rule_index: IndexMap::new(),
2617 data_leaf: IndexMap::new(),
2618 }
2619 }
2620
2621 fn plans_by_effective(
2622 plans: impl IntoIterator<Item = ExecutionPlan>,
2623 ) -> BTreeMap<EffectiveDate, ExecutionPlan> {
2624 plans
2625 .into_iter()
2626 .map(|plan| (plan.effective.clone(), plan))
2627 .collect()
2628 }
2629
2630 #[test]
2632 fn plan_set_plans_are_in_ascending_effective_order() {
2633 let june = DateTimeValue {
2634 year: 2025,
2635 month: 6,
2636 day: 1,
2637 hour: 0,
2638 minute: 0,
2639 second: 0,
2640 microsecond: 0,
2641 timezone: None,
2642 granularity: DateGranularity::Full,
2643 };
2644 let dec = DateTimeValue {
2645 year: 2025,
2646 month: 12,
2647 day: 1,
2648 hour: 0,
2649 minute: 0,
2650 second: 0,
2651 microsecond: 0,
2652 timezone: None,
2653 granularity: DateGranularity::Full,
2654 };
2655
2656 let plans = plans_by_effective([
2657 empty_plan(EffectiveDate::Origin),
2658 empty_plan(EffectiveDate::DateTimeValue(june)),
2659 empty_plan(EffectiveDate::DateTimeValue(dec)),
2660 ]);
2661
2662 let effectives: Vec<_> = plans.keys().cloned().collect();
2663 for window in effectives.windows(2) {
2664 assert!(
2665 window[0] < window[1],
2666 "plans must be strictly ascending: {:?} >= {:?}",
2667 window[0],
2668 window[1]
2669 );
2670 }
2671 }
2672
2673 #[test]
2674 fn plan_at_exact_boundary_selects_later_slice() {
2675 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2676
2677 let june = DateTimeValue {
2678 year: 2025,
2679 month: 6,
2680 day: 1,
2681 hour: 0,
2682 minute: 0,
2683 second: 0,
2684 microsecond: 0,
2685 timezone: None,
2686
2687 granularity: DateGranularity::Full,
2688 };
2689 let dec = DateTimeValue {
2690 year: 2025,
2691 month: 12,
2692 day: 1,
2693 hour: 0,
2694 minute: 0,
2695 second: 0,
2696 microsecond: 0,
2697 timezone: None,
2698
2699 granularity: DateGranularity::Full,
2700 };
2701
2702 let june_key = EffectiveDate::DateTimeValue(june.clone());
2703 let dec_key = EffectiveDate::DateTimeValue(dec.clone());
2704 let plans = plans_by_effective([
2705 empty_plan(EffectiveDate::Origin),
2706 empty_plan(june_key.clone()),
2707 empty_plan(dec_key.clone()),
2708 ]);
2709
2710 let june_plan = plan_at(&plans, &june_key).expect("boundary instant");
2711 assert!(std::ptr::eq(
2712 june_plan,
2713 plans.get(&june_key).expect("june slice")
2714 ));
2715
2716 let dec_plan = plan_at(&plans, &dec_key).expect("dec boundary");
2717 assert!(std::ptr::eq(
2718 dec_plan,
2719 plans.get(&dec_key).expect("dec slice")
2720 ));
2721 }
2722
2723 #[test]
2724 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2725 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2726
2727 let june = DateTimeValue {
2728 year: 2025,
2729 month: 6,
2730 day: 1,
2731 hour: 0,
2732 minute: 0,
2733 second: 0,
2734 microsecond: 0,
2735 timezone: None,
2736
2737 granularity: DateGranularity::Full,
2738 };
2739 let may_end = DateTimeValue {
2740 year: 2025,
2741 month: 5,
2742 day: 31,
2743 hour: 23,
2744 minute: 59,
2745 second: 59,
2746 microsecond: 0,
2747 timezone: None,
2748
2749 granularity: DateGranularity::DateTime,
2750 };
2751
2752 let origin = EffectiveDate::Origin;
2753 let plans = plans_by_effective([
2754 empty_plan(origin.clone()),
2755 empty_plan(EffectiveDate::DateTimeValue(june)),
2756 ]);
2757
2758 let may_instant = EffectiveDate::DateTimeValue(may_end);
2759 let may_plan = plan_at(&plans, &may_instant).expect("may 31");
2760 assert!(std::ptr::eq(
2761 may_plan,
2762 plans.get(&origin).expect("origin slice")
2763 ));
2764 }
2765
2766 #[test]
2767 fn plan_at_single_plan_matches_any_instant_after_start() {
2768 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2769
2770 let t = DateTimeValue {
2771 year: 2025,
2772 month: 3,
2773 day: 1,
2774 hour: 0,
2775 minute: 0,
2776 second: 0,
2777 microsecond: 0,
2778 timezone: None,
2779
2780 granularity: DateGranularity::Full,
2781 };
2782 let start = EffectiveDate::DateTimeValue(DateTimeValue {
2783 year: 2025,
2784 month: 1,
2785 day: 1,
2786 hour: 0,
2787 minute: 0,
2788 second: 0,
2789 microsecond: 0,
2790 timezone: None,
2791
2792 granularity: DateGranularity::Full,
2793 });
2794 let plans = plans_by_effective([empty_plan(start.clone())]);
2795 let instant = EffectiveDate::DateTimeValue(t);
2796 let selected = plan_at(&plans, &instant).expect("inside single slice");
2797 assert!(std::ptr::eq(
2798 selected,
2799 plans.get(&start).expect("single slice")
2800 ));
2801 }
2802
2803 #[test]
2806 fn show_json_shape_contract() {
2807 let mut engine = Engine::new();
2808 engine
2809 .load([(
2810 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2811 "test.lemma",
2812 ))),
2813 r#"
2814 spec pricing
2815 data bridge_height: measure
2816 -> unit meter: 1
2817 -> suggest 100 meter
2818 data quantity: number -> minimum 0
2819 rule cost: bridge_height * quantity
2820 "#
2821 .to_string(),
2822 )])
2823 .unwrap();
2824 let now = DateTimeValue::now();
2825 let schema = engine.show(None, "pricing", Some(&now)).unwrap();
2826
2827 let value: serde_json::Value =
2828 serde_json::to_value(crate::api::Show::from(&schema)).unwrap();
2829
2830 let bh = &value["data"]["bridge_height"];
2831 assert!(
2832 bh.is_object(),
2833 "data entry must be a named object, not tuple"
2834 );
2835 assert!(
2836 bh.get("type").is_some(),
2837 "data entry must expose `type` field"
2838 );
2839 assert!(
2840 bh.get("suggestion").is_some(),
2841 "bridge_height exposes `-> suggest` as schema suggestion"
2842 );
2843 assert!(
2844 bh.get("fill").is_none(),
2845 "bridge_height is not filled from spec"
2846 );
2847
2848 let ty = &bh["type"];
2849 assert_eq!(
2850 ty["kind"], "measure",
2851 "kind tag sits on the type object itself"
2852 );
2853 assert!(
2854 ty["units"].is_array(),
2855 "measure-only fields flatten up to top level"
2856 );
2857 assert!(
2858 ty.get("options").is_none(),
2859 "text-only fields must not leak"
2860 );
2861
2862 let quantity = &value["data"]["quantity"];
2863 assert_eq!(quantity["type"]["kind"], "number");
2864 assert!(
2865 quantity.get("suggestion").is_none(),
2866 "quantity has no suggestion"
2867 );
2868 assert!(
2869 quantity.get("fill").is_none(),
2870 "quantity has no fill literal"
2871 );
2872
2873 let cost = &value["rules"]["cost"];
2874 assert_eq!(
2875 cost["type"]["kind"], "measure",
2876 "rule types nest under type"
2877 );
2878 assert!(
2879 cost["type"]["units"].is_array()
2880 && !cost["type"]["units"].as_array().unwrap().is_empty(),
2881 "measure rule result types expose declared units"
2882 );
2883 assert!(
2884 cost["type"]["units"][0].get("factor").is_some(),
2885 "measure rule units use factor field"
2886 );
2887 assert!(cost.get("branches").is_some(), "ShowRule exposes branches");
2888 assert!(
2889 cost.get("depends_on_rules").is_some(),
2890 "ShowRule exposes depends_on_rules"
2891 );
2892 }
2893
2894 #[test]
2895 fn show_rule_result_units_contract() {
2896 let mut engine = Engine::new();
2897 engine
2898 .load([(
2899 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2900 "units_contract.lemma",
2901 ))),
2902 r#"
2903 spec units_contract
2904 data money: measure
2905 -> unit eur: 1
2906 -> unit usd: 0.91
2907 data rate: ratio
2908 -> unit basis_points: 10000
2909 -> unit percent: 100
2910 -> suggest 500 basis_points
2911 rule total: money
2912 rule rate_out: rate
2913 "#
2914 .to_string(),
2915 )])
2916 .unwrap();
2917 let now = DateTimeValue::now();
2918 let schema = engine.show(None, "units_contract", Some(&now)).unwrap();
2919 let value: serde_json::Value =
2920 serde_json::to_value(crate::api::Show::from(&schema)).unwrap();
2921
2922 let money_units = &value["data"]["money"]["type"]["units"];
2923 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2924 assert!(money_units[0].get("name").is_some());
2925 assert!(money_units[0].get("factor").is_some());
2926 assert!(money_units[0]["factor"].get("numer").is_some());
2927 assert!(money_units[0]["factor"].get("denom").is_some());
2928
2929 let rate_units = &value["data"]["rate"]["type"]["units"];
2930 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2931 assert!(rate_units[0].get("name").is_some());
2932 assert!(rate_units[0].get("value").is_some());
2933 assert!(rate_units[0]["value"].get("numer").is_some());
2934 assert!(rate_units[0]["value"].get("denom").is_some());
2935
2936 let total_rule_units = &value["rules"]["total"]["type"]["units"];
2937 let money_unit_names: Vec<_> = money_units
2938 .as_array()
2939 .unwrap()
2940 .iter()
2941 .map(|u| u["name"].as_str().unwrap())
2942 .collect();
2943 let total_rule_unit_names: Vec<_> = total_rule_units
2944 .as_array()
2945 .unwrap()
2946 .iter()
2947 .map(|u| u["name"].as_str().unwrap())
2948 .collect();
2949 assert_eq!(total_rule_unit_names, money_unit_names);
2950
2951 let rate_out_rule_units = &value["rules"]["rate_out"]["type"]["units"];
2952 let rate_unit_names: Vec<_> = rate_units
2953 .as_array()
2954 .unwrap()
2955 .iter()
2956 .map(|u| u["name"].as_str().unwrap())
2957 .collect();
2958 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2959 .as_array()
2960 .unwrap()
2961 .iter()
2962 .map(|u| u["name"].as_str().unwrap())
2963 .collect();
2964 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2965 }
2966
2967 #[test]
2968 fn show_json_round_trip_preserves_shape() {
2969 let mut engine = Engine::new();
2970 engine
2971 .load([(
2972 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2973 r#"
2974 spec s
2975 data age: number -> minimum 0 -> suggest 18
2976 data grade: text -> options "A" "B" "C"
2977 rule adult: age >= 18
2978 "#
2979 .to_string(),
2980 )])
2981 .unwrap();
2982 let now = DateTimeValue::now();
2983 let schema = engine.show(None, "s", Some(&now)).unwrap();
2984
2985 let api_show = crate::api::Show::from(&schema);
2986 let json = serde_json::to_string(&api_show).unwrap();
2987 let round_tripped: crate::api::Show = serde_json::from_str(&json).unwrap();
2988 assert_eq!(api_show, round_tripped);
2989 }
2990
2991 const COST_PRICE_SPEC: &str = r#"
2992spec cost_price
2993uses lemma units
2994
2995data money: measure
2996 -> unit eur: 1.00
2997 -> unit inr: 0.0092
2998 -> decimals 2
2999
3000data labor_cost: measure
3001 -> unit eur_per_hour: eur/hour
3002 -> unit inr_per_hour: inr/hour
3003 -> suggest 25 eur_per_hour
3004
3005data product_cost: measure
3006 -> unit eur_per_kg: eur/kilogram
3007 -> unit inr_per_kg: inr/kilogram
3008 -> suggest 4 eur_per_kg
3009
3010data throughput: measure
3011 -> unit kg_per_hour: kilogram/hour
3012 -> suggest 12 kg_per_hour
3013
3014rule cost_price: product_cost + labor_cost / throughput
3015"#;
3016
3017 fn cost_price_inputs() -> HashMap<String, RunDataValue> {
3018 let mut data = HashMap::new();
3019 data.insert("product_cost".into(), RunDataValue::string("4 eur_per_kg"));
3020 data.insert("labor_cost".into(), RunDataValue::string("25 eur_per_hour"));
3021 data.insert("throughput".into(), RunDataValue::string("12 kg_per_hour"));
3022 data
3023 }
3024
3025 const FILM_ACCESS: &str = r#"
3026spec premium_membership
3027uses lemma units
3028data start: date
3029data length: units.calendar
3030rule valid: now in start...start + length
3031
3032spec film_access
3033uses premium_membership
3034data type: text
3035 -> option "rental"
3036 -> option "purchase"
3037data views_consumed: number
3038data premium_member: boolean
3039rule max_views: 3
3040 unless premium_membership.valid then 10
3041 unless premium_member then 5
3042rule can_view: no
3043 unless type is "rental" and views_consumed < max_views then yes
3044 unless type is "purchase" then yes
3045"#;
3046
3047 fn film_access_effective() -> DateTimeValue {
3048 DateTimeValue {
3049 year: 2027,
3050 month: 2,
3051 day: 14,
3052 hour: 12,
3053 minute: 0,
3054 second: 0,
3055 microsecond: 0,
3056 timezone: Some(TimezoneValue {
3057 offset_hours: 0,
3058 offset_minutes: 0,
3059 }),
3060 granularity: DateGranularity::DateTime,
3061 }
3062 }
3063
3064 #[test]
3065 fn run_data_accepts_per_unit_measure_equivalent_to_canonical_magnitude() {
3066 let mut engine = Engine::new();
3067 engine
3068 .load([(
3069 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3070 "cost_price.lemma",
3071 ))),
3072 COST_PRICE_SPEC.to_string(),
3073 )])
3074 .expect("load");
3075 let plans = engine
3076 .plans
3077 .get_plans(None, "cost_price")
3078 .expect("plans for cost_price");
3079 let plan = plans.values().next().expect("plan");
3080 let mut data = HashMap::new();
3081 data.insert("product_cost".into(), RunDataValue::string("4 eur_per_kg"));
3082 data.insert(
3083 "labor_cost".into(),
3084 RunDataValue::string("0.0069444444444444444444444444 eur_per_hour"),
3085 );
3086 data.insert(
3087 "throughput".into(),
3088 RunDataValue::string("0.0033333333333333333333333333 kg_per_hour"),
3089 );
3090 let run_data = resolve_run_data(plan, data);
3091 assert!(
3092 !run_data
3093 .bindings
3094 .values()
3095 .any(|b| matches!(b, OperationResult::Veto(_))),
3096 "parsed decimal run data values must not be veto-bound after input boundary: {:?}",
3097 run_data.bindings
3098 );
3099 }
3100
3101 #[test]
3102 fn run_data_accepts_per_unit_measure() {
3103 let mut engine = Engine::new();
3104 engine
3105 .load([(
3106 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3107 "cost_price.lemma",
3108 ))),
3109 COST_PRICE_SPEC.to_string(),
3110 )])
3111 .expect("load");
3112 let plans = engine
3113 .plans
3114 .get_plans(None, "cost_price")
3115 .expect("plans for cost_price");
3116 let plan = plans.values().next().expect("plan");
3117 let run_data = resolve_run_data(plan, cost_price_inputs());
3118 assert!(!run_data
3119 .bindings
3120 .values()
3121 .any(|b| matches!(b, OperationResult::Veto(_))));
3122 }
3123
3124 #[test]
3125 fn run_data_rejects_oversize_input() {
3126 let mut engine = Engine::new();
3127 engine
3128 .load([(
3129 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3130 "cost_price.lemma",
3131 ))),
3132 COST_PRICE_SPEC.to_string(),
3133 )])
3134 .expect("load");
3135 let plans = engine
3136 .plans
3137 .get_plans(None, "cost_price")
3138 .expect("plans for cost_price");
3139 let plan = plans.values().next().expect("plan");
3140 let mut data = cost_price_inputs();
3141 data.insert(
3142 "labor_cost".into(),
3143 RunDataValue::string(
3144 "1000000000000000000000000000000000000000000000000000000000000 eur_per_hour",
3145 ),
3146 );
3147 let run_data = resolve_run_data(plan, data);
3148 assert!(matches!(
3149 run_data.bindings.get(&DataPath::local("labor_cost".into())),
3150 Some(OperationResult::Veto(_))
3151 ));
3152 }
3153 #[test]
3154 fn typedecl_default_stays_typedecl_on_immutable_plan() {
3155 let mut engine = Engine::new();
3156 engine
3157 .load([(
3158 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3159 r#"
3160 spec s
3161 data n: number -> suggest 42
3162 rule r: n
3163 "#
3164 .to_string(),
3165 )])
3166 .expect("load");
3167
3168 let plans = engine.plans.get_plans(None, "s").expect("plans for s");
3169 let plan = plans.values().next().expect("plan");
3170 let path = DataPath::local("n".into());
3171 match plan.data.get(&path).expect("n") {
3172 DataDefinition::TypeDeclaration {
3173 declared_suggestion: Some(_),
3174 ..
3175 } => {}
3176 other => panic!("expected TypeDeclaration with default, got {other:?}"),
3177 }
3178 }
3179
3180 fn response_missing_data_union(response: &crate::Response) -> Vec<String> {
3181 let mut seen = std::collections::HashSet::new();
3182 let mut names = Vec::new();
3183 for result in response.results.values() {
3184 for key in result.missing_data() {
3185 if seen.insert(key.clone()) {
3186 names.push(key.clone());
3187 }
3188 }
3189 }
3190 names
3191 }
3192
3193 #[test]
3194 fn run_prunes_inactive_nut_branches_for_total_price() {
3195 let code = r#"
3196spec bag
3197uses lemma units
3198
3199data weight: measure
3200 -> unit kg: 1
3201
3202data money: measure
3203 -> unit eur: 1
3204
3205data price_per_weight: measure
3206 -> unit eur_per_kg: eur/kg
3207
3208data item_cost: price_per_weight
3209data roasting: price_per_weight
3210data chocolatizing: price_per_weight
3211
3212rule total_price: weight * (item_cost + roasting + chocolatizing)
3213
3214spec calc
3215uses bag
3216 -> with item_cost: item_cost
3217 -> with roasting: roasting
3218
3219data type_of_nut: text -> options "peanut" "cashew"
3220
3221rule price_peanut: 1.5 eur_per_kg
3222rule price_peanut_roasting: 0.45 eur_per_kg
3223
3224rule price_cashew: 2.0 eur_per_kg
3225rule price_cashew_roasting: 0.55 eur_per_kg
3226
3227rule item_cost: veto "No item cost"
3228 unless type_of_nut is "peanut" then price_peanut
3229 unless type_of_nut is "cashew" then price_cashew
3230
3231rule roasting: veto "No roasting"
3232 unless type_of_nut is "peanut" then price_peanut_roasting
3233 unless type_of_nut is "cashew" then price_cashew_roasting
3234
3235rule total_price: bag.total_price
3236"#;
3237
3238 let mut engine = Engine::new();
3239 engine
3240 .load([(
3241 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3242 "calc.lemma",
3243 ))),
3244 code.to_string(),
3245 )])
3246 .unwrap();
3247
3248 let now = DateTimeValue::now();
3249 let mut inputs = HashMap::new();
3250 inputs.insert("type_of_nut".to_string(), "peanut".to_string());
3251 let response = engine
3252 .run(
3253 None,
3254 "calc",
3255 Some(&now),
3256 inputs,
3257 Some(&["total_price".to_string()]),
3258 false,
3259 )
3260 .expect("run must succeed");
3261
3262 let names = response_missing_data_union(&response);
3263 assert!(
3264 !names.contains(&"type_of_nut".to_string()),
3265 "supplied type_of_nut is bound and must not appear in missing_data: {names:?}"
3266 );
3267 assert!(names.contains(&"bag.weight".to_string()));
3268 assert!(names.contains(&"bag.chocolatizing".to_string()));
3269 assert!(!names.contains(&"bag.item_cost".to_string()));
3270 assert!(!names.contains(&"bag.roasting".to_string()));
3271 }
3272
3273 #[test]
3274 fn run_includes_membership_dates_when_premium_member_false() {
3275 let mut engine = Engine::new();
3276 engine
3277 .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
3278 .expect("film_access spec must load");
3279 let now = film_access_effective();
3280 let mut inputs = HashMap::new();
3281 inputs.insert("type".to_string(), "rental".to_string());
3282 inputs.insert("views_consumed".to_string(), "6".to_string());
3283 inputs.insert("premium_member".to_string(), "false".to_string());
3284 let response = engine
3285 .run(
3286 None,
3287 "film_access",
3288 Some(&now),
3289 inputs,
3290 Some(&["can_view".to_string()]),
3291 false,
3292 )
3293 .expect("run must succeed");
3294
3295 let names = response_missing_data_union(&response);
3296 assert!(names.contains(&"premium_membership.start".to_string()));
3297 assert!(names.contains(&"premium_membership.length".to_string()));
3298 }
3299
3300 #[test]
3301 fn run_includes_membership_dates_when_premium_member_unknown() {
3302 let mut engine = Engine::new();
3303 engine
3304 .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
3305 .expect("film_access spec must load");
3306 let now = film_access_effective();
3307 let mut inputs = HashMap::new();
3308 inputs.insert("type".to_string(), "rental".to_string());
3309 inputs.insert("views_consumed".to_string(), "6".to_string());
3310 let response = engine
3311 .run(
3312 None,
3313 "film_access",
3314 Some(&now),
3315 inputs,
3316 Some(&["can_view".to_string()]),
3317 false,
3318 )
3319 .expect("run must succeed");
3320
3321 let names = response_missing_data_union(&response);
3322 assert!(names.contains(&"premium_member".to_string()));
3323 assert!(names.contains(&"premium_membership.start".to_string()));
3324 assert!(names.contains(&"premium_membership.length".to_string()));
3325 }
3326
3327 const UNITS_SPEC: &str = r#"
3328spec units
3329uses lemma units
3330data money: measure
3331 -> unit eur: 1
3332 -> decimals 2
3333"#;
3334
3335 const WAREHOUSING_SPEC: &str = r#"
3336spec warehousing
3337uses units
3338uses si: lemma units
3339
3340data units_per_pallet: number
3341 -> minimum 1
3342 -> suggest 1
3343
3344data storage_duration: si.duration
3345 -> minimum 0 week
3346 -> suggest 10 day
3347
3348data interbranch_transport_per_pallet: units.money
3349 -> minimum 0 eur
3350 -> suggest 0 eur
3351
3352data inbound_handling_per_pallet: units.money
3353 -> minimum 0 eur
3354 -> suggest 0 eur
3355
3356data storage_per_pallet_per_week: units.money
3357 -> minimum 0 eur
3358 -> suggest 10 eur
3359
3360data labeling_per_pallet: units.money
3361 -> minimum 0 eur
3362 -> suggest 0 eur
3363
3364data outbound_handling_per_pallet: units.money
3365 -> minimum 0 eur
3366 -> suggest 0 eur
3367
3368rule storage_cost_per_pallet:
3369 storage_per_pallet_per_week
3370 * ceil storage_duration as week as Number
3371
3372rule total_logistics_per_pallet:
3373 interbranch_transport_per_pallet
3374 + inbound_handling_per_pallet
3375 + storage_cost_per_pallet
3376 + labeling_per_pallet
3377 + outbound_handling_per_pallet
3378
3379rule total_logistics_per_ce:
3380 total_logistics_per_pallet / units_per_pallet
3381"#;
3382
3383 const QUOTATION_SPEC: &str = r#"
3384spec quotation
3385uses wh: warehousing
3386rule total: wh.total_logistics_per_ce
3387"#;
3388
3389 fn load_cross_spec_fixtures(engine: &mut Engine) {
3390 engine
3391 .load([(crate::SourceType::Volatile, UNITS_SPEC.to_string())])
3392 .expect("units spec must load");
3393 engine
3394 .load([(crate::SourceType::Volatile, WAREHOUSING_SPEC.to_string())])
3395 .expect("warehousing spec must load");
3396 }
3397
3398 #[test]
3399 fn quotation_plans_without_consumer_stdlib_units() {
3400 let mut engine = Engine::new();
3401 load_cross_spec_fixtures(&mut engine);
3402 engine
3403 .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
3404 .expect("quotation must plan without uses lemma units");
3405 let plans = engine
3406 .plans
3407 .get_plans(None, "quotation")
3408 .expect("plans for quotation");
3409 let plan = plans.values().next().expect("plan");
3410
3411 let expression_units = &plan.resolved_types.unit_index;
3412 assert!(
3413 expression_units.unique_owner("week").is_none(),
3414 "consumer expression scope must not contain week: {:?}",
3415 expression_units.keys().collect::<Vec<_>>()
3416 );
3417 assert!(
3418 expression_units.unique_owner("minute").is_none(),
3419 "consumer expression scope must not contain minute: {:?}",
3420 expression_units.keys().collect::<Vec<_>>()
3421 );
3422 let mut keys: Vec<_> = expression_units.keys().cloned().collect();
3423 keys.sort();
3424 assert_eq!(
3425 keys,
3426 ["percent", "permille"],
3427 "consumer expression scope must only have builtin ratio units, not dependency units"
3428 );
3429 }
3430
3431 fn warehousing_default_inputs(prefix: &str) -> HashMap<String, String> {
3432 let key = |name: &str| {
3433 if prefix.is_empty() {
3434 name.to_string()
3435 } else {
3436 format!("{prefix}.{name}")
3437 }
3438 };
3439 HashMap::from([
3440 (key("units_per_pallet"), "1".to_string()),
3441 (key("storage_duration"), "10 day".to_string()),
3442 (key("interbranch_transport_per_pallet"), "0 eur".to_string()),
3443 (key("inbound_handling_per_pallet"), "0 eur".to_string()),
3444 (key("storage_per_pallet_per_week"), "10 eur".to_string()),
3445 (key("labeling_per_pallet"), "0 eur".to_string()),
3446 (key("outbound_handling_per_pallet"), "0 eur".to_string()),
3447 ])
3448 }
3449
3450 #[test]
3451 fn quotation_evaluates_cross_spec_duration_conversion() {
3452 let mut engine = Engine::new();
3453 load_cross_spec_fixtures(&mut engine);
3454 engine
3455 .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
3456 .expect("quotation must load");
3457 let plans = engine
3458 .plans
3459 .get_plans(None, "quotation")
3460 .expect("plans for quotation");
3461 let plan = plans.values().next().expect("plan");
3462 assert!(
3463 plan.resolved_types
3464 .unit_index
3465 .unique_owner("week")
3466 .is_none(),
3467 "consumer unit_index must not contain week"
3468 );
3469 let now = DateTimeValue::now();
3470 let response = engine
3471 .run(
3472 None,
3473 "quotation",
3474 Some(&now),
3475 warehousing_default_inputs("wh"),
3476 None,
3477 false,
3478 )
3479 .expect("quotation must evaluate");
3480 let display = response
3481 .results
3482 .get("total")
3483 .expect("rule total must be present")
3484 .result()
3485 .expect("total must have display")
3486 .to_string();
3487 assert_eq!(
3488 display, "20.00 eur",
3489 "10 eur/week * ceil(10 day as week) / 1 CE must be 20.00 eur, got: {display}"
3490 );
3491 }
3492
3493 #[test]
3494 fn ratio_range_default_endpoints_must_be_ratio_not_measure() {
3495 let code = r#"
3496spec policy
3497data allowed_band: ratio range -> suggest 10%...50%
3498rule band: allowed_band
3499"#;
3500 let mut engine = Engine::new();
3501 engine
3502 .load([(
3503 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3504 "ratio_range_endpoint_typing.lemma",
3505 ))),
3506 code.to_string(),
3507 )])
3508 .unwrap();
3509
3510 let plans = engine
3511 .plans
3512 .get_plans(None, "policy")
3513 .expect("plans for policy");
3514 let plan = plans.values().next().expect("plan");
3515 let path = DataPath::local("allowed_band".into());
3516 let def = plan.data.get(&path).expect("allowed_band in plan.data");
3517 let suggestion = def
3518 .bound_suggestion()
3519 .expect("declared default must exist")
3520 .to_literal();
3521
3522 let (left, right) = match &suggestion.value {
3523 crate::planning::semantics::ValueKind::Range(l, r) => (l.as_ref(), r.as_ref()),
3524 other => panic!("expected Range, got {other:?}"),
3525 };
3526 for (label, endpoint) in [("left", left), ("right", right)] {
3527 assert!(
3528 matches!(
3529 &endpoint.value,
3530 crate::planning::semantics::ValueKind::Ratio(_)
3531 ),
3532 "{label} endpoint must be Ratio for a percent literal in a ratio range default, got {:?}",
3533 endpoint.value
3534 );
3535 assert!(
3536 matches!(
3537 &endpoint.value,
3538 crate::planning::semantics::ValueKind::Ratio(_)
3539 ),
3540 "{label} endpoint ValueKind must be Ratio (got {:?})",
3541 endpoint.value
3542 );
3543 }
3544 }
3545
3546 #[test]
3547 fn ratio_range_typedef_with_second_ratio_field_loads() {
3548 let code = r#"
3549spec policy
3550data margin_pct: ratio -> suggest 15%
3551data allowed_band: ratio range
3552rule margin: margin_pct
3553rule band_slot: allowed_band
3554"#;
3555 let mut engine = Engine::new();
3556 engine
3557 .load([(
3558 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3559 "ratio_range_load.lemma",
3560 ))),
3561 code.to_string(),
3562 )])
3563 .unwrap();
3564
3565 let plans = engine
3566 .plans
3567 .get_plans(None, "policy")
3568 .expect("plans for policy");
3569 let plan = plans.values().next().expect("plan");
3570 let path = DataPath::local("allowed_band".into());
3571 let def = plan.data.get(&path).expect("allowed_band in plan.data");
3572 let lemma_type = def
3573 .schema_type()
3574 .expect("allowed_band must be a typed data slot");
3575 match &lemma_type.specifications {
3576 TypeSpecification::RatioRange { units, .. } => {
3577 let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3578 assert!(
3579 names.contains(&"percent"),
3580 "ratio range must inherit builtin percent, got {names:?}"
3581 );
3582 }
3583 other => panic!("allowed_band must be RatioRange, got {other:?}"),
3584 }
3585 }
3586}