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