1use crate::literals::Value;
13use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaSpec};
14use crate::parsing::source::{Source, SourceType};
15use crate::planning::graph::absolute_import_member_path;
16use crate::planning::graph::peel_rule_from;
17use crate::planning::graph::Graph;
18use crate::planning::graph::ResolvedSpecTypes;
19use crate::planning::instance_list::{row_end_type, DataSource, InstanceListSource};
20use crate::planning::normalize::{
21 data_path_result_type, lookup_data_definition, DispatchRegion, NormalForm, NormalFormId,
22 NormalFormInterner, NormalizeContext, NormalizedRule,
23};
24use crate::planning::semantics::{
25 value_kind_matches_spec, ComparisonComputation, DataDefinition, DataPath, ImportOverlay,
26 LemmaType, LiteralValue, PathSegment, ReferenceEnd, ReferenceTarget, RowEnd, RulePath,
27 SpecIdentity, SpecInstance, TypeSpecification, ValueKind,
28};
29use crate::planning::show_expression::{
30 prefix_show_expression, project_depends_on_rules, show_branches_from,
31};
32use crate::planning::spec_set::LemmaSpecSet;
33use crate::planning::unit_family::FamilyUnitCatalog;
34use crate::result_value::RuleResultValue;
35use crate::Error;
36use indexmap::IndexMap;
37use serde::{Deserialize, Serialize};
38use std::collections::{BTreeMap, HashMap, HashSet};
39use std::sync::Arc;
40
41pub use crate::planning::show_expression::{
42 ShowBranch, ShowConversionTarget, ShowExpression, ShowListCommand, ShowRule, SortDirection,
43};
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub(crate) struct NeededByRules {
52 pub rules: Vec<u32>,
53 pub fields: Vec<Vec<u32>>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ExecutionPlan {
66 pub spec_name: String,
68
69 pub commentary: Option<String>,
71
72 pub data: IndexMap<DataPath, DataDefinition>,
74
75 pub(crate) normal_forms: Vec<NormalForm>,
78
79 pub rules: IndexMap<RulePath, ExecutableRule>,
82
83 pub data_reference_order: Vec<DataPath>,
87
88 pub meta: IndexMap<String, Value>,
90
91 pub resolved_types: ResolvedSpecTypes,
95
96 #[serde(with = "crate::snapshot::shared::arc")]
98 pub family_units: Arc<FamilyUnitCatalog>,
99
100 pub signature_index: crate::computation::arithmetic::SignatureIndex,
106
107 pub effective: EffectiveDate,
108
109 pub effective_from: Option<DateTimeValue>,
112 pub effective_to: Option<DateTimeValue>,
113
114 pub start_line: usize,
116
117 pub source_type: Option<SourceType>,
119
120 pub(crate) needed_by_rules: Vec<NeededByRules>,
125
126 pub(crate) nested_needed_by_rules: IndexMap<DataPath, Vec<u32>>,
129
130 pub(crate) data_display: IndexMap<DataPath, ShowDataCache>,
133
134 pub(crate) show_rules: IndexMap<RulePath, ShowRule>,
137
138 pub(crate) reference_ends: IndexMap<DataPath, ReferenceEnd>,
142
143 pub(crate) input_key_index: IndexMap<String, DataPath>,
147
148 pub(crate) show_rule_index: IndexMap<String, RulePath>,
151
152 pub(crate) data_leaf: IndexMap<DataPath, NormalFormId>,
156
157 #[serde(with = "crate::snapshot::shared::arc_map")]
160 pub(crate) row_templates: BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
161
162 pub(crate) identity: SpecIdentity,
164
165 #[serde(with = "crate::snapshot::shared::arc")]
168 pub(crate) planning_input: Arc<Vec<u8>>,
169
170 pub(crate) rule_candidates: BTreeMap<RulePath, Vec<SpecInstance>>,
174
175 pub(crate) self_row_rules: Vec<RulePath>,
179
180 pub(crate) instance_lists: IndexMap<DataPath, InstanceListSource>,
182
183 pub(crate) row_mask: Vec<u32>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
194pub(crate) struct ShowDataCache {
195 pub value: Option<crate::planning::semantics::BoundValueKind>,
196 pub suggestion: Option<crate::planning::semantics::BoundValueKind>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ExecutableRule {
202 pub path: RulePath,
204
205 pub normal_form: NormalFormId,
207
208 pub source: Source,
210
211 #[serde(with = "crate::snapshot::shared::arc")]
216 pub rule_type: Arc<LemmaType>,
217}
218
219impl ExecutableRule {
220 pub fn name(&self) -> &str {
221 &self.path.rule
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub(crate) struct RuleIndex(usize);
230
231impl RuleIndex {
232 pub(crate) fn index(self) -> usize {
233 self.0
234 }
235}
236
237pub(crate) fn plan_at<'a>(
240 plans: &'a BTreeMap<EffectiveDate, std::sync::Arc<ExecutionPlan>>,
241 instant: &EffectiveDate,
242) -> Option<&'a ExecutionPlan> {
243 plans
244 .range(..=instant.clone())
245 .next_back()
246 .map(|(_, plan)| plan.as_ref())
247}
248
249pub(crate) fn build_execution_plan(
255 graph: &Graph<'_>,
256 resolved_types: ResolvedSpecTypes,
257 effective: &EffectiveDate,
258 limits: &crate::limits::ResourceLimits,
259 interner: &mut NormalFormInterner,
260 planning_input: Arc<Vec<u8>>,
261) -> Result<ExecutionPlan, Vec<Error>> {
262 let rule_order = graph.rule_order();
263
264 let main_spec = graph.main_spec();
265 let data = graph.build_data(&resolved_types.resolved)?;
266
267 let undetermined_errors: Vec<Error> = data
274 .iter()
275 .filter_map(|(path, definition)| {
276 let (resolved_type, source) = match definition {
277 DataDefinition::TypeDeclaration {
278 resolved_type,
279 source,
280 ..
281 } => (resolved_type, source),
282 DataDefinition::Reference {
283 target: ReferenceTarget::Data(_),
284 resolved_type,
285 source,
286 ..
287 } => (resolved_type, source),
288 DataDefinition::Reference {
289 target: ReferenceTarget::Rule(_),
290 ..
291 }
292 | DataDefinition::Value { .. }
293 | DataDefinition::InstanceList { .. }
294 | DataDefinition::Import { .. } => return None,
295 };
296 if resolved_type.is_undetermined() {
297 Some(Error::validation(
298 format!("could not determine the type of '{path}'"),
299 Some(source.clone()),
300 None::<String>,
301 ))
302 } else {
303 None
304 }
305 })
306 .collect();
307 if !undetermined_errors.is_empty() {
308 return Err(undetermined_errors);
309 }
310
311 let signature_index = resolved_types.signature_index.clone();
312
313 let family_units = Arc::new(FamilyUnitCatalog::build(&resolved_types.unit_index));
314
315 let reference_ends = graph.reference_ends();
316 let rule_types: HashMap<RulePath, Arc<LemmaType>> = graph
317 .rules()
318 .iter()
319 .map(|(path, node)| (path.clone(), Arc::clone(&node.rule_type)))
320 .collect();
321 let mut rules: IndexMap<RulePath, ExecutableRule> = IndexMap::new();
322 let mut completed_rules: HashMap<RulePath, NormalFormId> = HashMap::new();
323 let mut show_rules: IndexMap<RulePath, ShowRule> = IndexMap::new();
324
325 let mut reachable: HashSet<RulePath> = HashSet::new();
327 let mut stack: Vec<RulePath> = graph
328 .rules()
329 .keys()
330 .filter(|path| path.segments.is_empty())
331 .cloned()
332 .collect();
333 while let Some(path) = stack.pop() {
334 if !reachable.insert(path.clone()) {
335 continue;
336 }
337 let node = graph
338 .rules()
339 .get(&path)
340 .expect("BUG: reachable rule path missing from graph.rules");
341 for dep in &node.depends_on_rules {
342 if graph.has_local_rule(dep) && !reachable.contains(dep) {
344 stack.push(dep.clone());
345 }
346 }
347 }
348 let (show_keys, import_show_rules) = import_show_universe(graph, &reachable);
349
350 let list_element_identities: HashSet<SpecIdentity> = graph
351 .instance_lists()
352 .sources
353 .values()
354 .map(|source| source.identity.clone())
355 .collect();
356
357 for rule_path in rule_order {
358 if !reachable.contains(rule_path) {
359 continue;
360 }
361 let rule_node = graph.rules().get(rule_path).expect(
362 "bug: rule from topological sort not in graph - validation should have caught this",
363 );
364
365 let measure_scope = crate::planning::typing::MeasureScope {
366 unit_index: &resolved_types.unit_index,
367 signature_index: &resolved_types.signature_index,
368 };
369 let normalize_ctx = NormalizeContext {
370 data: &data,
371 row_templates: &graph.instance_lists().templates,
372 self_identity: &graph.instance_lists().self_identity,
373 list_element_identities: &list_element_identities,
374 rule_order,
375 measure_scope,
376 max_normalized_expression_nodes: limits.max_normalized_expression_nodes,
377 };
378 let normalized = crate::planning::normalize::build_normalized_rule(
379 &normalize_ctx,
380 &completed_rules,
381 &rule_types,
382 reference_ends,
383 &rule_node.expression,
384 Some(rule_node.source.clone()),
385 interner,
386 )
387 .map_err(|error| vec![error])?;
388 let NormalizedRule { body, source_type } = normalized;
389 assert_eq!(
390 &source_type, &rule_node.rule_type,
391 "BUG: rule '{}' typed differently by planning and validation",
392 rule_path.rule
393 );
394 assert!(
395 interner
396 .result_type(body)
397 .same_value_type(&rule_node.rule_type),
398 "BUG: rewrites changed the value type of rule '{}': {:?} from {:?}",
399 rule_path.rule,
400 interner.result_type(body),
401 rule_node.rule_type
402 );
403 completed_rules.insert(rule_path.clone(), body);
404
405 show_rules.insert(
406 rule_path.clone(),
407 ShowRule {
408 lemma_type: family_units.rule_type_for_show(rule_node.rule_type.as_ref()),
409 path: rule_path.segments.clone(),
410 branches: show_branches_from(
411 &rule_node.expression,
412 &rule_node.branches,
413 &show_keys,
414 ),
415 depends_on_rules: project_depends_on_rules(&rule_node.depends_on_rules, &show_keys),
416 },
417 );
418
419 rules.insert(
420 rule_path.clone(),
421 ExecutableRule {
422 path: rule_path.clone(),
423 normal_form: body,
424 source: rule_node.source.clone(),
425 rule_type: source_type,
426 },
427 );
428 }
429
430 for (path, show_rule) in import_show_rules {
431 show_rules.insert(path, show_rule);
432 }
433
434 let root_ids: Vec<NormalFormId> = rules.values().map(|rule| rule.normal_form).collect();
435 let (mut normal_forms, remapped_roots) = interner.extract_reachable(&root_ids);
436 for (rule, remapped) in rules.values_mut().zip(remapped_roots) {
437 rule.normal_form = remapped;
438 }
439
440 let import_rule_paths: Vec<RulePath> = show_rules
443 .keys()
444 .filter(|path| !path.segments.is_empty())
445 .cloned()
446 .collect();
447 for path in import_rule_paths {
448 if rules.contains_key(&path) {
449 continue;
450 }
451 let (template, local) = peel_rule_from(&data, &graph.instance_lists().templates, &path)
452 .unwrap_or_else(|| {
453 panic!(
454 "BUG: import show rule '{}' does not peel while building ExecutableRule",
455 path.input_key()
456 )
457 });
458 let template_rule = template.rules.get(local).unwrap_or_else(|| {
459 panic!(
460 "BUG: peeled import rule '{}' missing from template.rules",
461 path.input_key()
462 )
463 });
464 let id = NormalFormId::from_index(normal_forms.len());
465 normal_forms.push(NormalForm {
466 kind: crate::planning::normalize::NormalFormKind::Leaf(
467 crate::planning::normalize::LeafKind::ImportRule(path.clone()),
468 ),
469 result_type: Arc::clone(&template_rule.rule_type),
470 fold_types: Vec::new(),
471 source: Some(template_rule.source.clone()),
472 origin: None,
473 rule_ref: None,
474 });
475 rules.insert(
476 path.clone(),
477 ExecutableRule {
478 path: path.clone(),
479 normal_form: id,
480 source: template_rule.source.clone(),
481 rule_type: Arc::clone(&template_rule.rule_type),
482 },
483 );
484 }
485
486 let mut ordered: IndexMap<RulePath, ExecutableRule> = IndexMap::with_capacity(rules.len());
487 for path in graph.execution_order() {
488 match rules.shift_remove(path) {
489 Some(rule) => {
490 ordered.insert(path.clone(), rule);
491 }
492 None => assert!(
493 graph.has_local_rule(path) && !reachable.contains(path),
494 "BUG: execution-order rule '{}' has no ExecutableRule",
495 path.input_key()
496 ),
497 }
498 }
499 ordered.extend(rules);
502 let rules = ordered;
503
504 let mut plan = ExecutionPlan {
505 spec_name: main_spec.name.clone(),
506 commentary: main_spec.commentary.clone(),
507 data,
508 normal_forms,
509 rules,
510 data_reference_order: graph.data_reference_order().to_vec(),
511 meta: main_spec
512 .meta_fields
513 .iter()
514 .map(|f| (f.key.clone(), f.value.clone()))
515 .collect(),
516 resolved_types,
517 family_units,
518 signature_index,
519 effective: effective.clone(),
520 effective_from: None,
522 effective_to: None,
523 start_line: 1,
524 source_type: None,
525 needed_by_rules: Vec::new(),
526 nested_needed_by_rules: IndexMap::new(),
527 data_display: IndexMap::new(),
528 show_rules,
529 reference_ends: IndexMap::new(),
531 input_key_index: IndexMap::new(),
532 show_rule_index: IndexMap::new(),
533 data_leaf: IndexMap::new(),
534 row_templates: graph.instance_lists().templates.clone(),
535 identity: graph.instance_lists().self_identity.clone(),
536 planning_input,
537 rule_candidates: graph
538 .rule_candidates()
539 .iter()
540 .map(|(rule, candidates)| (rule.clone(), candidates.clone()))
541 .collect(),
542 self_row_rules: Vec::new(),
543 instance_lists: graph.instance_lists().sources.clone(),
544 row_mask: Vec::new(),
545 };
546
547 let mut plan_errors = validate_literal_data_against_types(&plan);
548 if let Err(error) = validate_unit_conversion_targets(&plan) {
549 plan_errors.push(error);
550 }
551 if !plan_errors.is_empty() {
552 return Err(plan_errors);
553 }
554
555 plan.reference_ends = graph.reference_ends().clone();
556 let input_keys: Vec<DataPath> = plan
557 .input_key_entries()
558 .into_iter()
559 .map(|(path, _)| path.clone())
560 .collect();
561 plan.data_leaf = ensure_data_leaves(
562 &mut plan.normal_forms,
563 &input_keys,
564 &plan.data,
565 &plan.row_templates,
566 );
567 plan.input_key_index = input_keys
568 .iter()
569 .map(|path| (path.input_key(), path.clone()))
570 .collect();
571 plan.show_rule_index = plan
572 .show_rules
573 .keys()
574 .map(|path| (path.input_key(), path.clone()))
575 .collect();
576 for (path, &id) in &plan.data_leaf {
577 debug_assert_eq!(
578 plan.result_type(id).as_ref(),
579 data_path_result_type(&plan.data, path, &plan.row_templates).as_ref(),
580 "BUG: DataPath leaf result_type drift for {path}"
581 );
582 }
583 let (needed, nested_needed) = plan.build_needed_by_rules();
584 plan.needed_by_rules = needed;
585 plan.nested_needed_by_rules = nested_needed;
586 plan.row_mask = build_row_mask(&plan.normal_forms).map_err(|error| vec![error])?;
587 plan.self_row_rules = self_row_rules(&plan);
588 Ok(plan)
589}
590
591fn row_bits_entry<'a>(
593 plan: &ExecutionPlan,
594 row_bits: &'a mut BTreeMap<SpecIdentity, Vec<u64>>,
595 identity: &SpecIdentity,
596) -> &'a mut Vec<u64> {
597 let words = plan
598 .row_template(identity)
599 .nested_input_keys()
600 .len()
601 .div_ceil(64);
602 row_bits
603 .entry(identity.clone())
604 .or_insert_with(|| vec![0u64; words])
605}
606
607fn template_bindable_field_index(template: &ExecutionPlan, path: &DataPath) -> Option<usize> {
608 template
609 .nested_input_keys()
610 .iter()
611 .position(|(candidate, _)| candidate == path)
612}
613
614fn mark_template_data_path(template: &ExecutionPlan, path: &DataPath, bits: &mut [u64]) {
615 let target = match template.peel_data(path) {
616 Some((_, _, DataDefinition::InstanceList { .. })) => Some(path),
617 Some((_, _, DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. })) => {
618 Some(path)
619 }
620 Some((_, _, DataDefinition::Reference { .. })) => template.promptable_data_path(path),
621 Some((_, _, DataDefinition::Import { .. })) | None => {
622 if path.segments.is_empty() {
623 template.promptable_data_path(path)
624 } else {
625 None
626 }
627 }
628 };
629 if let Some(target) = target {
630 let data_pos = template_bindable_field_index(template, target).unwrap_or_else(|| {
631 panic!(
632 "BUG: promptable row field '{}' absent from template bindable fields",
633 target.input_key()
634 )
635 });
636 bits[data_pos / 64] |= 1u64 << (data_pos % 64);
637 }
638}
639
640fn mark_template_rule_data(template: &ExecutionPlan, rule: &RulePath, bits: &mut [u64]) {
645 let rule_pos = u32::try_from(template.rules.get_index_of(rule).unwrap_or_else(|| {
646 panic!(
647 "BUG: row rule '{rule}' missing from template plan '{}'",
648 template.spec_name
649 )
650 }))
651 .expect("BUG: rule count exceeds u32");
652 for (data_pos, consumers) in template.needed_by_rules.iter().enumerate() {
653 if !consumers.rules.contains(&rule_pos) {
654 continue;
655 }
656 let (path, definition) = template
657 .data
658 .get_index(data_pos)
659 .expect("BUG: needed_by_rules index past template.data");
660 if matches!(definition, DataDefinition::Import { .. }) {
661 panic!(
662 "BUG: Import marker '{}' carries needed_by bits in template '{}'",
663 path.input_key(),
664 template.spec_name
665 );
666 }
667 let field_pos = template_bindable_field_index(template, path).unwrap_or_else(|| {
668 panic!(
669 "BUG: needed template data '{}' absent from template bindable fields",
670 path.input_key()
671 )
672 });
673 bits[field_pos / 64] |= 1u64 << (field_pos % 64);
674 }
675 for (nested_path, rule_positions) in &template.nested_needed_by_rules {
676 if !rule_positions.contains(&rule_pos) {
677 continue;
678 }
679 let field_pos = template_bindable_field_index(template, nested_path).unwrap_or_else(|| {
680 panic!(
681 "BUG: nested needed_by path '{}' absent from template bindable fields",
682 nested_path.input_key()
683 )
684 });
685 bits[field_pos / 64] |= 1u64 << (field_pos % 64);
686 }
687}
688
689fn mark_row_end_needs(
690 plan: &ExecutionPlan,
691 identity: &SpecIdentity,
692 end: &RowEnd,
693 bits_by_rule: &[Vec<u64>],
694 row_bits: &mut BTreeMap<SpecIdentity, Vec<u64>>,
695 consumer_rule: &RulePath,
696) {
697 let bits = row_bits_entry(plan, row_bits, identity);
698 match end {
699 RowEnd::Data(path) => {
700 if *identity == plan.identity {
701 if let Some(ReferenceEnd::Rule(rule)) = plan.reference_ends.get(path) {
702 let rule_pos = plan.rules.get_index_of(rule).unwrap_or_else(|| {
703 panic!(
704 "BUG: reference rule '{rule}' missing from plan.rules (rule '{consumer_rule}')"
705 )
706 });
707 for (word, dep) in bits.iter_mut().zip(bits_by_rule[rule_pos].iter()) {
708 *word |= *dep;
709 }
710 } else {
711 mark_template_data_path(plan, path, bits);
712 }
713 } else {
714 let template = plan.row_template(identity);
715 if let Some(ReferenceEnd::Rule(rule)) = template.reference_ends.get(path) {
716 mark_template_rule_data(template, rule, bits);
717 } else {
718 mark_template_data_path(template, path, bits);
719 }
720 }
721 }
722 RowEnd::Rule(rule) => {
723 if *identity == plan.identity {
724 let rule_pos = plan.rules.get_index_of(rule).unwrap_or_else(|| {
725 panic!(
726 "BUG: row rule '{rule}' missing from plan.rules (rule '{consumer_rule}')"
727 )
728 });
729 for (word, dep) in bits.iter_mut().zip(bits_by_rule[rule_pos].iter()) {
730 *word |= *dep;
731 }
732 } else {
733 mark_template_rule_data(plan.row_template(identity), rule, bits);
734 }
735 }
736 }
737}
738
739fn mark_list_key_fields(
740 plan: &ExecutionPlan,
741 list: NormalFormId,
742 row_bits: &mut BTreeMap<SpecIdentity, Vec<u64>>,
743 consumer_rule: &RulePath,
744) {
745 use crate::planning::semantics::ListKey;
746
747 let list_type = plan.result_type(list);
748 let TypeSpecification::List { element, key } = &list_type.specifications else {
749 panic!(
750 "BUG: Find/WithRow list cell is not a list type (rule '{consumer_rule}'): {list_type:?}"
751 );
752 };
753 let TypeSpecification::Spec { spec: identity } = &element.specifications else {
754 return;
755 };
756 let ListKey::Fields(fields) = key else {
757 return;
758 };
759 let template = plan.row_template(identity);
760 let bits = row_bits_entry(plan, row_bits, identity);
761 for field in fields {
762 let path = template.input_key_index.get(field).unwrap_or_else(|| {
763 panic!(
764 "BUG: list key field '{field}' missing from template '{}' (rule '{consumer_rule}')",
765 identity
766 )
767 });
768 mark_template_data_path(template, path, bits);
769 }
770}
771
772fn self_row_rules(plan: &ExecutionPlan) -> Vec<RulePath> {
775 use crate::planning::normalize::{LeafKind, NormalFormKind};
776 use crate::planning::semantics::RowEnd;
777
778 let mut rules_read: HashSet<&RulePath> = HashSet::new();
779 for (index, cell) in plan.normal_forms.iter().enumerate() {
780 let end = match &cell.kind {
781 NormalFormKind::Leaf(LeafKind::Row { identity, end, .. })
782 if *identity == plan.identity =>
783 {
784 end
785 }
786 NormalFormKind::SpecMember {
787 base,
788 row: Some(row_read),
789 ..
790 } => match &plan.result_type(*base).specifications {
791 TypeSpecification::Spec { spec } if *spec == plan.identity => &row_read.end,
792 TypeSpecification::Spec { .. } => continue,
793 other => panic!("BUG: row member read on cell {index} of non-spec type {other:?}"),
794 },
795 _ => continue,
796 };
797 match end {
798 RowEnd::Rule(rule) => {
799 rules_read.insert(rule);
800 }
801 RowEnd::Data(path) => {
802 if let Some(ReferenceEnd::Rule(rule)) = plan.reference_ends.get(path) {
803 rules_read.insert(rule);
804 }
805 }
806 }
807 }
808 plan.rules
809 .keys()
810 .filter(|rule| rules_read.contains(rule))
811 .cloned()
812 .collect()
813}
814
815impl ExecutionPlan {
816 pub(crate) fn row_template(&self, identity: &SpecIdentity) -> &ExecutionPlan {
818 if *identity == self.identity {
819 return self;
820 }
821 self.row_templates
822 .get(identity)
823 .unwrap_or_else(|| panic!("BUG: rows of '{identity}' without a template plan"))
824 }
825
826 fn import_identity(&self, name: &str) -> Option<&SpecIdentity> {
828 let import_path = self.input_key_index.get(name)?;
829 let DataDefinition::Import { identity, .. } = self.data.get(import_path)? else {
830 return None;
831 };
832 Some(identity)
833 }
834
835 pub(crate) fn peel_hops(&self, hops: &[PathSegment]) -> Option<&ExecutionPlan> {
837 let mut plan = self;
838 for hop in hops {
839 plan = plan.row_template(plan.import_identity(&hop.uses)?);
840 }
841 Some(plan)
842 }
843
844 fn qualify_hops<'a>(
847 &'a self,
848 hop_names: &[&str],
849 ) -> Option<(&'a ExecutionPlan, Vec<PathSegment>)> {
850 let mut plan = self;
851 let mut segments = Vec::with_capacity(hop_names.len());
852 for hop_name in hop_names {
853 let identity = plan.import_identity(hop_name)?;
854 segments.push(PathSegment {
855 uses: (*hop_name).to_string(),
856 repository: identity.repository.clone(),
857 spec: identity.spec.clone(),
858 });
859 plan = plan.row_template(identity);
860 }
861 Some((plan, segments))
862 }
863
864 pub(crate) fn qualify_data_names(&self, names: &[&str]) -> Option<DataPath> {
867 let (data_name, hop_names) = names.split_last()?;
868 let (plan, segments) = self.qualify_hops(hop_names)?;
869 plan.data.get(plan.input_key_index.get(*data_name)?)?;
870 Some(DataPath {
871 segments,
872 data: (*data_name).to_string(),
873 })
874 }
875
876 pub(crate) fn qualify_rule_names(&self, names: &[&str]) -> Option<RulePath> {
879 let (rule_name, hop_names) = names.split_last()?;
880 let (plan, segments) = self.qualify_hops(hop_names)?;
881 let rule = RulePath {
882 segments,
883 rule: (*rule_name).to_string(),
884 };
885 plan.peel_rule(&RulePath {
886 segments: Vec::new(),
887 rule: (*rule_name).to_string(),
888 })?;
889 Some(rule)
890 }
891
892 pub(crate) fn peel_data<'a>(
895 &'a self,
896 path: &DataPath,
897 ) -> Option<(&'a ExecutionPlan, &'a DataPath, &'a DataDefinition)> {
898 let plan = self.peel_hops(&path.segments)?;
899 let local = plan.input_key_index.get(path.data.as_str())?;
900 let definition = plan.data.get(local)?;
901 Some((plan, local, definition))
902 }
903
904 pub(crate) fn peel_rule<'a>(
906 &'a self,
907 path: &RulePath,
908 ) -> Option<(&'a ExecutionPlan, &'a RulePath)> {
909 let plan = self.peel_hops(&path.segments)?;
910 let local = plan
911 .rules
912 .keys()
913 .find(|rule| rule.segments.is_empty() && rule.rule == path.rule)?;
914 Some((plan, local))
915 }
916
917 pub(crate) fn input_key_entries(&self) -> Vec<(DataPath, &DataDefinition)> {
922 let mut out = Vec::new();
923 Self::push_input_key_entries(self, &mut out);
924 out
925 }
926
927 pub(crate) fn nested_input_keys(&self) -> Vec<(DataPath, &DataDefinition)> {
930 self.input_key_entries()
931 .into_iter()
932 .filter(|(_, definition)| !matches!(definition, DataDefinition::Import { .. }))
933 .collect()
934 }
935
936 fn push_input_key_entries<'a>(
937 root: &'a ExecutionPlan,
938 out: &mut Vec<(DataPath, &'a DataDefinition)>,
939 ) {
940 struct PlanLevel<'a> {
941 plan: &'a ExecutionPlan,
942 next: usize,
943 prefix: Vec<PathSegment>,
944 }
945
946 let mut levels = vec![PlanLevel {
947 plan: root,
948 next: 0,
949 prefix: Vec::new(),
950 }];
951 while let Some(top) = levels.last_mut() {
952 let Some((path, definition)) = top.plan.data.get_index(top.next) else {
953 levels.pop();
954 continue;
955 };
956 top.next += 1;
957 if !path.segments.is_empty() {
958 continue;
959 }
960 let child = match definition {
961 DataDefinition::Import { identity, .. } => {
962 if top.prefix.is_empty() {
963 out.push((path.clone(), definition));
964 }
965 let mut nested_prefix = top.prefix.clone();
966 nested_prefix.push(PathSegment {
967 uses: path.data.clone(),
968 repository: identity.repository.clone(),
969 spec: identity.spec.clone(),
970 });
971 PlanLevel {
972 plan: top.plan.row_template(identity),
973 next: 0,
974 prefix: nested_prefix,
975 }
976 }
977 _ => {
978 out.push((
979 DataPath {
980 segments: top.prefix.clone(),
981 data: path.data.clone(),
982 },
983 definition,
984 ));
985 continue;
986 }
987 };
988 levels.push(child);
989 }
990 }
991}
992
993const MAX_ROW_LEVELS: u32 = u32::BITS;
995
996fn build_row_mask(normal_forms: &[NormalForm]) -> Result<Vec<u32>, Error> {
999 use crate::planning::normalize::{LeafKind, NormalFormKind};
1000
1001 let mut mask: Vec<Option<u32>> = vec![None; normal_forms.len()];
1002 let mut stack: Vec<(usize, bool)> = Vec::new();
1003 for root in 0..normal_forms.len() {
1004 if mask[root].is_some() {
1005 continue;
1006 }
1007 stack.push((root, false));
1008 while let Some((index, children_done)) = stack.pop() {
1009 if mask[index].is_some() {
1010 continue;
1011 }
1012 let cell = &normal_forms[index];
1013 if !children_done {
1014 stack.push((index, true));
1015 cell.kind.for_each_child(|child| {
1016 if mask[child.index()].is_none() {
1017 stack.push((child.index(), false));
1018 }
1019 });
1020 continue;
1021 }
1022 let mut union = 0u32;
1023 cell.kind.for_each_child(|child| {
1024 union |= mask[child.index()]
1025 .expect("BUG: child row mask missing after post-order visit");
1026 });
1027 let bits = match &cell.kind {
1028 NormalFormKind::Leaf(LeafKind::Row { level, .. }) => {
1029 if *level >= MAX_ROW_LEVELS {
1030 return Err(Error::validation(
1031 format!(
1032 "list commands nest {} levels deep; at most {MAX_ROW_LEVELS} are supported",
1033 level + 1
1034 ),
1035 cell.source.clone(),
1036 None::<String>,
1037 ));
1038 }
1039 1u32 << level
1040 }
1041 NormalFormKind::ListCommand {
1042 row_level: Some(level),
1043 ..
1044 } if *level < MAX_ROW_LEVELS => union & !(1u32 << level),
1045 _ => union,
1046 };
1047 mask[index] = Some(bits);
1048 }
1049 }
1050 Ok(mask
1051 .into_iter()
1052 .map(|bits| bits.expect("BUG: row mask missing for a cell"))
1053 .collect())
1054}
1055
1056fn import_show_universe(
1059 graph: &Graph<'_>,
1060 local_reachable: &HashSet<RulePath>,
1061) -> (HashSet<String>, IndexMap<RulePath, ShowRule>) {
1062 let mut show_keys: HashSet<String> = local_reachable
1063 .iter()
1064 .map(|path| path.input_key())
1065 .collect();
1066 let mut import_shows: IndexMap<RulePath, ShowRule> = IndexMap::new();
1067 let mut stack: Vec<RulePath> = local_reachable
1068 .iter()
1069 .flat_map(|path| {
1070 graph
1071 .rules()
1072 .get(path)
1073 .expect("BUG: reachable local rule missing from graph.rules")
1074 .depends_on_rules
1075 .iter()
1076 .cloned()
1077 })
1078 .filter(|dep| !graph.has_local_rule(dep))
1079 .collect();
1080
1081 while let Some(path) = stack.pop() {
1082 let key = path.input_key();
1083 if !show_keys.insert(key.clone()) {
1084 continue;
1085 }
1086 let (template, local) =
1087 peel_rule_from(graph.data(), &graph.instance_lists().templates, &path).unwrap_or_else(
1088 || {
1089 panic!(
1090 "BUG: import Show rule '{}' does not peel after planning accepted it",
1091 path.input_key()
1092 )
1093 },
1094 );
1095 let template_show = template.show_rules.get(local).unwrap_or_else(|| {
1096 panic!(
1097 "BUG: peeled import rule '{}' missing from template show_rules",
1098 path.input_key()
1099 )
1100 });
1101 let prefix = path
1102 .segments
1103 .iter()
1104 .map(|segment| segment.uses.as_str())
1105 .collect::<Vec<_>>()
1106 .join(".");
1107 let mut abs_deps = Vec::with_capacity(template_show.depends_on_rules.len());
1108 for dep_key in &template_show.depends_on_rules {
1109 let abs_key = format!("{prefix}.{dep_key}");
1110 abs_deps.push(abs_key.clone());
1111 let abs_path = absolute_import_rule_path(&path.segments, dep_key, template);
1112 if !show_keys.contains(&abs_key) {
1113 stack.push(abs_path);
1114 }
1115 }
1116 let branches = template_show
1117 .branches
1118 .iter()
1119 .map(|branch| ShowBranch {
1120 condition: branch
1121 .condition
1122 .as_ref()
1123 .map(|expression| prefix_show_expression(&prefix, expression)),
1124 result: prefix_show_expression(&prefix, &branch.result),
1125 })
1126 .collect();
1127 import_shows.insert(
1128 path.clone(),
1129 ShowRule {
1130 lemma_type: template_show.lemma_type.clone(),
1131 path: path.segments.clone(),
1132 branches,
1133 depends_on_rules: abs_deps,
1134 },
1135 );
1136 }
1137
1138 (show_keys, import_shows)
1139}
1140
1141fn absolute_import_rule_path(
1142 prefix: &[crate::planning::semantics::PathSegment],
1143 local_key: &str,
1144 template: &ExecutionPlan,
1145) -> RulePath {
1146 let local = template
1147 .show_rules
1148 .keys()
1149 .find(|path| path.input_key() == local_key)
1150 .unwrap_or_else(|| {
1151 panic!(
1152 "BUG: template depends_on_rules entry '{local_key}' missing from template show_rules"
1153 )
1154 });
1155 RulePath {
1156 segments: prefix
1157 .iter()
1158 .cloned()
1159 .chain(local.segments.iter().cloned())
1160 .collect(),
1161 rule: local.rule.clone(),
1162 }
1163}
1164
1165#[derive(Debug, Clone, PartialEq, Eq)]
1170pub(crate) enum ImportNeed {
1171 Data(DataPath),
1174 Rule(RulePath),
1176}
1177
1178pub(crate) trait ReferenceOwner {
1181 fn owner_data(&self) -> &IndexMap<DataPath, DataDefinition>;
1182 fn owner_templates(&self) -> &BTreeMap<SpecIdentity, Arc<ExecutionPlan>>;
1183}
1184
1185impl ReferenceOwner for ExecutionPlan {
1186 fn owner_data(&self) -> &IndexMap<DataPath, DataDefinition> {
1187 &self.data
1188 }
1189
1190 fn owner_templates(&self) -> &BTreeMap<SpecIdentity, Arc<ExecutionPlan>> {
1191 &self.row_templates
1192 }
1193}
1194
1195struct ImportHop<'p> {
1196 marker: DataPath,
1197 identity: &'p SpecIdentity,
1198 overlay: &'p IndexMap<DataPath, ImportOverlay>,
1199 template: &'p ExecutionPlan,
1200}
1201
1202impl ImportHop<'_> {
1203 fn absolute(&self, relative: &DataPath) -> DataPath {
1204 absolute_import_member_path(&self.marker, self.identity, relative)
1205 }
1206
1207 fn absolute_rule(&self, relative: &RulePath) -> RulePath {
1208 let absolute = self.absolute(&DataPath {
1209 segments: relative.segments.clone(),
1210 data: relative.rule.clone(),
1211 });
1212 RulePath {
1213 segments: absolute.segments,
1214 rule: absolute.data,
1215 }
1216 }
1217}
1218
1219fn import_hop<'p>(
1222 data: &'p IndexMap<DataPath, DataDefinition>,
1223 templates: &'p BTreeMap<SpecIdentity, Arc<ExecutionPlan>>,
1224 segment: &PathSegment,
1225) -> Option<ImportHop<'p>> {
1226 let marker = DataPath::local(segment.uses.clone());
1227 let DataDefinition::Import {
1228 identity, overlay, ..
1229 } = data.get(&marker)?
1230 else {
1231 return None;
1232 };
1233 let template = templates.get(identity)?;
1234 Some(ImportHop {
1235 marker,
1236 identity,
1237 overlay,
1238 template,
1239 })
1240}
1241
1242enum ReferenceFrameOwner<'a, O: ReferenceOwner> {
1248 Root(&'a O),
1249 Template(&'a ExecutionPlan),
1250}
1251
1252impl<O: ReferenceOwner> Clone for ReferenceFrameOwner<'_, O> {
1253 fn clone(&self) -> Self {
1254 *self
1255 }
1256}
1257
1258impl<O: ReferenceOwner> Copy for ReferenceFrameOwner<'_, O> {}
1259
1260impl<'a, O: ReferenceOwner> ReferenceFrameOwner<'a, O> {
1261 fn data(self) -> &'a IndexMap<DataPath, DataDefinition> {
1262 match self {
1263 Self::Root(owner) => owner.owner_data(),
1264 Self::Template(plan) => plan.owner_data(),
1265 }
1266 }
1267
1268 fn templates(self) -> &'a BTreeMap<SpecIdentity, Arc<ExecutionPlan>> {
1269 match self {
1270 Self::Root(owner) => owner.owner_templates(),
1271 Self::Template(plan) => plan.owner_templates(),
1272 }
1273 }
1274
1275 fn reference_end_on(self, path: &DataPath) -> Option<ReferenceEnd> {
1276 match self {
1277 Self::Root(owner) => reference_end_on(owner, path),
1278 Self::Template(plan) => reference_end_on(plan, path),
1279 }
1280 }
1281}
1282
1283struct ReferenceFrame<'a, O: ReferenceOwner> {
1284 owner: ReferenceFrameOwner<'a, O>,
1285 path: DataPath,
1286 hop: ImportHop<'a>,
1287 relative: DataPath,
1288}
1289
1290enum ReferenceStep<'a, O: ReferenceOwner> {
1291 Value(Option<ReferenceEnd>),
1293 Next(ReferenceFrameOwner<'a, O>, DataPath),
1295}
1296
1297pub(crate) fn reference_end_on<O: ReferenceOwner>(
1302 owner: &O,
1303 path: &DataPath,
1304) -> Option<ReferenceEnd> {
1305 let mut frames: Vec<ReferenceFrame<'_, O>> = Vec::new();
1306 let mut owner = ReferenceFrameOwner::Root(owner);
1307 let mut path = path.clone();
1308 let mut chain = 0usize;
1309 loop {
1310 let depth = frames.len();
1311 let data_len = owner.data().len();
1312 let mut end = match resolve_reference_step(owner, &path, &mut frames) {
1313 ReferenceStep::Value(end) => end,
1314 ReferenceStep::Next(next_owner, next_path) => {
1315 chain = if frames.len() == depth { chain + 1 } else { 0 };
1316 assert!(
1317 chain <= data_len,
1318 "BUG: data reference chain from '{path}' outlasts plan data (cycles are rejected)"
1319 );
1320 owner = next_owner;
1321 path = next_path;
1322 continue;
1323 }
1324 };
1325 loop {
1326 let Some(frame) = frames.pop() else {
1327 return end;
1328 };
1329 let template_end = end?;
1330 match finish_reference_frame(frame, template_end) {
1331 ReferenceStep::Value(value) => end = value,
1332 ReferenceStep::Next(next_owner, next_path) => {
1333 chain = 0;
1334 owner = next_owner;
1335 path = next_path;
1336 break;
1337 }
1338 }
1339 }
1340 }
1341}
1342
1343fn resolve_reference_step<'a, O: ReferenceOwner>(
1344 owner: ReferenceFrameOwner<'a, O>,
1345 path: &DataPath,
1346 frames: &mut Vec<ReferenceFrame<'a, O>>,
1347) -> ReferenceStep<'a, O> {
1348 match owner.data().get(path) {
1349 Some(
1350 DataDefinition::Value { .. }
1351 | DataDefinition::TypeDeclaration { .. }
1352 | DataDefinition::InstanceList { .. },
1353 ) => return ReferenceStep::Value(Some(ReferenceEnd::Promptable(path.clone()))),
1354 Some(DataDefinition::Import { .. }) => {
1355 return ReferenceStep::Value(Some(ReferenceEnd::Import));
1356 }
1357 Some(DataDefinition::Reference {
1358 target: ReferenceTarget::Rule(rule),
1359 ..
1360 }) => return ReferenceStep::Value(Some(ReferenceEnd::Rule(rule.clone()))),
1361 Some(DataDefinition::Reference {
1362 target: ReferenceTarget::Data(next),
1363 ..
1364 }) => return ReferenceStep::Next(owner, next.clone()),
1365 None => {}
1366 }
1367 if path.segments.is_empty() {
1368 return ReferenceStep::Value(None);
1369 }
1370 let (first, rest) = path
1371 .segments
1372 .split_first()
1373 .unwrap_or_else(|| panic!("BUG: reference_end_on empty member path '{path}'"));
1374 let Some(hop) = import_hop(owner.data(), owner.templates(), first) else {
1375 return ReferenceStep::Value(None);
1376 };
1377 let relative = DataPath {
1378 segments: rest.to_vec(),
1379 data: path.data.clone(),
1380 };
1381 if let Some(entry) = hop.overlay.get(&relative) {
1382 return match entry {
1383 ImportOverlay::Literal(_) => {
1384 ReferenceStep::Value(Some(ReferenceEnd::Promptable(path.clone())))
1385 }
1386 ImportOverlay::Reference {
1387 target: ReferenceTarget::Data(target),
1388 ..
1389 } => ReferenceStep::Next(owner, target.clone()),
1390 ImportOverlay::Reference {
1391 target: ReferenceTarget::Rule(rule),
1392 ..
1393 } => ReferenceStep::Value(Some(ReferenceEnd::Rule(rule.clone()))),
1394 };
1395 }
1396 let template = hop.template;
1397 if relative.segments.is_empty() && !template.data.contains_key(&relative) {
1398 return ReferenceStep::Value(None);
1399 }
1400 frames.push(ReferenceFrame {
1401 owner,
1402 path: path.clone(),
1403 hop,
1404 relative: relative.clone(),
1405 });
1406 ReferenceStep::Next(ReferenceFrameOwner::Template(template), relative)
1407}
1408
1409fn finish_reference_frame<'a, O: ReferenceOwner>(
1410 frame: ReferenceFrame<'a, O>,
1411 template_end: ReferenceEnd,
1412) -> ReferenceStep<'a, O> {
1413 match template_end {
1414 ReferenceEnd::Promptable(target) if target == frame.relative => {
1415 ReferenceStep::Value(Some(ReferenceEnd::Promptable(frame.path)))
1416 }
1417 ReferenceEnd::Promptable(target) => {
1418 ReferenceStep::Next(frame.owner, frame.hop.absolute(&target))
1419 }
1420 ReferenceEnd::Rule(rule) => {
1421 ReferenceStep::Value(Some(ReferenceEnd::Rule(frame.hop.absolute_rule(&rule))))
1422 }
1423 ReferenceEnd::Import => ReferenceStep::Value(Some(ReferenceEnd::Import)),
1424 }
1425}
1426
1427fn need_of_end(end: ReferenceEnd) -> Result<Option<ImportNeed>, RulePath> {
1430 match end {
1431 ReferenceEnd::Promptable(path) => Ok(Some(ImportNeed::Data(path))),
1432 ReferenceEnd::Rule(rule) if rule.segments.is_empty() => Ok(Some(ImportNeed::Rule(rule))),
1433 ReferenceEnd::Rule(rule) => Err(rule),
1434 ReferenceEnd::Import => Ok(None),
1435 }
1436}
1437
1438fn needs_of_end<O: ReferenceOwner>(owner: &O, end: ReferenceEnd) -> Vec<ImportNeed> {
1439 match need_of_end(end) {
1440 Ok(need) => need.into_iter().collect(),
1441 Err(rule) => import_rule_needs(owner, &rule),
1442 }
1443}
1444
1445pub(crate) fn import_rule_needs<O: ReferenceOwner>(owner: &O, path: &RulePath) -> Vec<ImportNeed> {
1449 let mut frames = vec![NeedsFrame::new(
1450 ReferenceFrameOwner::Root(owner),
1451 path.clone(),
1452 false,
1453 )];
1454 loop {
1455 let top = frames
1456 .last_mut()
1457 .expect("BUG: import rule frames empty before the root finished");
1458 if top.hop.is_none() {
1459 let (first, rest) = top.rule.segments.split_first().unwrap_or_else(|| {
1460 panic!("BUG: import_rule_needs on local rule '{}'", top.rule.rule)
1461 });
1462 let hop =
1463 import_hop(top.owner.data(), top.owner.templates(), first).unwrap_or_else(|| {
1464 panic!(
1465 "BUG: import rule '{}' first hop does not peel",
1466 top.rule.input_key()
1467 )
1468 });
1469 let relative = RulePath {
1470 segments: rest.to_vec(),
1471 rule: top.rule.rule.clone(),
1472 };
1473 let template = hop.template;
1474 top.hop = Some(hop);
1475 if relative.segments.is_empty() {
1476 top.needs = Some(template.rule_needs(&relative));
1477 } else {
1478 frames.push(NeedsFrame::new(
1479 ReferenceFrameOwner::Template(template),
1480 relative,
1481 true,
1482 ));
1483 continue;
1484 }
1485 }
1486
1487 let needs = top
1488 .needs
1489 .as_ref()
1490 .expect("BUG: entered import rule frame has no template needs");
1491 if top.next == needs.len() {
1492 let finished = frames
1493 .pop()
1494 .expect("BUG: finished import rule frame missing from stack");
1495 match frames.last_mut() {
1496 None => return finished.out,
1497 Some(parent) if finished.feeds_parent_needs => {
1498 parent.needs = Some(finished.out);
1499 parent.next = 0;
1500 }
1501 Some(parent) => parent.out.extend(finished.out),
1502 }
1503 continue;
1504 }
1505
1506 let item = needs[top.next].clone();
1507 top.next += 1;
1508 let owner = top.owner;
1509 let hop = top
1510 .hop
1511 .as_ref()
1512 .expect("BUG: entered import rule frame has no hop");
1513 let child = match item {
1514 ImportNeed::Data(field) => {
1515 let absolute = hop.absolute(&field);
1516 let end = owner.reference_end_on(&absolute).unwrap_or_else(|| {
1517 panic!("BUG: template need '{absolute}' does not peel on its owner")
1518 });
1519 match need_of_end(end) {
1520 Ok(Some(need)) => {
1521 top.out.push(need);
1522 None
1523 }
1524 Ok(None) => None,
1525 Err(rule) => Some(NeedsFrame::new(owner, rule, false)),
1526 }
1527 }
1528 ImportNeed::Rule(rule) => Some(NeedsFrame::new(owner, hop.absolute_rule(&rule), false)),
1529 };
1530 if let Some(child) = child {
1531 frames.push(child);
1532 }
1533 }
1534}
1535
1536struct NeedsFrame<'a, O: ReferenceOwner> {
1541 owner: ReferenceFrameOwner<'a, O>,
1542 rule: RulePath,
1543 hop: Option<ImportHop<'a>>,
1544 needs: Option<Vec<ImportNeed>>,
1545 next: usize,
1546 out: Vec<ImportNeed>,
1547 feeds_parent_needs: bool,
1548}
1549
1550impl<'a, O: ReferenceOwner> NeedsFrame<'a, O> {
1551 fn new(owner: ReferenceFrameOwner<'a, O>, rule: RulePath, feeds_parent_needs: bool) -> Self {
1552 Self {
1553 owner,
1554 rule,
1555 hop: None,
1556 needs: None,
1557 next: 0,
1558 out: Vec::new(),
1559 feeds_parent_needs,
1560 }
1561 }
1562}
1563
1564struct NeedsMarker<'a> {
1566 plan: &'a ExecutionPlan,
1567 rule_pos: usize,
1568 bits: &'a mut [u64],
1569 nested_hits: &'a mut HashSet<DataPath>,
1570 bits_by_rule: &'a [Vec<u64>],
1571 nested_hits_by_rule: &'a [HashSet<DataPath>],
1572}
1573
1574impl NeedsMarker<'_> {
1575 fn mark_slot(&mut self, data_pos: usize) {
1576 self.bits[data_pos / 64] |= 1u64 << (data_pos % 64);
1577 }
1578
1579 fn mark(&mut self, need: ImportNeed) {
1580 match need {
1581 ImportNeed::Rule(rule) => {
1582 let ref_pos =
1583 self.plan.rules.get_index_of(&rule).unwrap_or_else(|| {
1584 panic!("BUG: needed rule '{rule}' missing from plan.rules")
1585 });
1586 if ref_pos >= self.rule_pos {
1587 panic!(
1588 "BUG: needed rule '{rule}' must precede '{}' in topo order",
1589 self.plan.rules[self.rule_pos].path
1590 );
1591 }
1592 for (word, dep_word) in self.bits.iter_mut().zip(&self.bits_by_rule[ref_pos]) {
1593 *word |= *dep_word;
1594 }
1595 self.nested_hits
1596 .extend(self.nested_hits_by_rule[ref_pos].iter().cloned());
1597 }
1598 ImportNeed::Data(path) if path.segments.is_empty() => {
1599 let data_pos =
1600 self.plan.data.get_index_of(&path).unwrap_or_else(|| {
1601 panic!("BUG: needed data '{path}' missing from plan.data")
1602 });
1603 self.mark_slot(data_pos);
1604 }
1605 ImportNeed::Data(path) => match self.plan.data.get_index_of(&path) {
1606 Some(data_pos) => self.mark_slot(data_pos),
1607 None => {
1608 self.nested_hits.insert(path);
1609 }
1610 },
1611 }
1612 }
1613}
1614
1615fn ensure_data_leaves(
1624 normal_forms: &mut Vec<NormalForm>,
1625 input_keys: &[DataPath],
1626 data: &IndexMap<DataPath, DataDefinition>,
1627 row_templates: &std::collections::BTreeMap<
1628 crate::planning::semantics::SpecIdentity,
1629 std::sync::Arc<ExecutionPlan>,
1630 >,
1631) -> IndexMap<DataPath, NormalFormId> {
1632 use crate::planning::normalize::LeafKind;
1633 use crate::planning::normalize::NormalFormKind;
1634
1635 let mut data_leaf: IndexMap<DataPath, NormalFormId> = IndexMap::new();
1636 for (index, cell) in normal_forms.iter().enumerate() {
1637 if cell.rule_ref.is_some() {
1638 continue;
1639 }
1640 if let NormalFormKind::Leaf(LeafKind::DataPath(path)) = &cell.kind {
1641 data_leaf.insert(path.clone(), NormalFormId::from_index(index));
1642 }
1643 }
1644 for path in input_keys {
1645 if data_leaf.contains_key(path) {
1646 continue;
1647 }
1648 let id = NormalFormId::from_index(normal_forms.len());
1649 normal_forms.push(NormalForm {
1650 kind: NormalFormKind::Leaf(LeafKind::DataPath(path.clone())),
1651 result_type: data_path_result_type(data, path, row_templates),
1652 fold_types: Vec::new(),
1653 source: None,
1654 origin: None,
1655 rule_ref: None,
1656 });
1657 data_leaf.insert(path.clone(), id);
1658 }
1659 data_leaf
1660}
1661
1662pub(crate) fn attach_show_cache(
1666 plan: &mut ExecutionPlan,
1667 lemma_spec_set: &LemmaSpecSet,
1668 spec: &LemmaSpec,
1669) {
1670 let (effective_from, effective_to) = lemma_spec_set.effective_range(spec);
1671 plan.effective_from = effective_from;
1672 plan.effective_to = effective_to;
1673 plan.start_line = spec.start_line;
1674 plan.source_type = spec.source_type.clone();
1675 plan.data_display = build_data_display(plan);
1676}
1677
1678fn build_data_display(plan: &ExecutionPlan) -> IndexMap<DataPath, ShowDataCache> {
1679 let mut out = IndexMap::new();
1680 for (path, data) in &plan.data {
1681 insert_data_display_entry(&mut out, path, data);
1682 }
1683 for (path, data) in plan.nested_input_keys() {
1684 if out.contains_key(&path) {
1685 continue;
1686 }
1687 if let Some(bound) = overlay_literal_for(plan, &path) {
1689 out.insert(
1690 path,
1691 ShowDataCache {
1692 value: Some(bound),
1693 suggestion: None,
1694 },
1695 );
1696 continue;
1697 }
1698 insert_data_display_entry(&mut out, &path, data);
1699 }
1700 out
1701}
1702
1703fn overlay_literal_for(
1704 plan: &ExecutionPlan,
1705 path: &DataPath,
1706) -> Option<crate::planning::semantics::BoundValueKind> {
1707 match plan.import_overlay_entry(path)? {
1708 ImportOverlay::Literal(bound) => Some(bound.clone()),
1709 ImportOverlay::Reference { .. } => None,
1710 }
1711}
1712
1713fn insert_data_display_entry(
1714 out: &mut IndexMap<DataPath, ShowDataCache>,
1715 path: &DataPath,
1716 data: &DataDefinition,
1717) {
1718 if data.schema_type().is_none() || matches!(data, DataDefinition::Reference { .. }) {
1719 return;
1720 }
1721 let value = match data {
1722 DataDefinition::Value {
1723 value,
1724 resolved_type,
1725 ..
1726 } => Some(crate::planning::semantics::BoundValueKind {
1727 value: value.value.clone(),
1728 unit: resolved_type.unit().map(Arc::from),
1729 }),
1730 _ => data.bound_value().cloned(),
1731 };
1732 let suggestion = data.bound_suggestion().cloned();
1733 if value.is_some() || suggestion.is_some() {
1734 out.insert(path.clone(), ShowDataCache { value, suggestion });
1735 }
1736}
1737
1738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1740pub(crate) enum ShowBoundRole {
1741 Value,
1742 Suggestion,
1743}
1744
1745pub(crate) fn show_bound_result_value(
1746 bound: &crate::planning::semantics::BoundValueKind,
1747 schema_type: &crate::planning::semantics::LemmaType,
1748 input_key: &str,
1749 role: ShowBoundRole,
1750) -> crate::result_value::RuleResultValue {
1751 let (fill, suggestion) = match role {
1752 ShowBoundRole::Value => (Some(bound), None),
1753 ShowBoundRole::Suggestion => (None, Some(bound)),
1754 };
1755 let display_type = crate::planning::semantics::lemma_type_with_display_binding(
1756 schema_type,
1757 None,
1758 fill,
1759 suggestion,
1760 );
1761 crate::result_value::type_scoped_result_value_from_literal(&bound.to_literal(), &display_type)
1762 .unwrap_or_else(|failure| {
1763 let role_name = match role {
1764 ShowBoundRole::Value => "value",
1765 ShowBoundRole::Suggestion => "suggestion",
1766 };
1767 panic!(
1768 "BUG: show {role_name} value for '{input_key}' failed type_scoped_result_value_from_literal: {}",
1769 crate::result_value::rule_result_value_failure_message(failure)
1770 )
1771 })
1772}
1773
1774pub(crate) fn show_instance_list_value(
1779 plan: &ExecutionPlan,
1780 path: &DataPath,
1781) -> Option<RuleResultValue> {
1782 let source = plan
1783 .instance_lists
1784 .get(path)
1785 .expect("BUG: instance list data without a source");
1786 if source.rows.is_empty() {
1787 return None;
1788 }
1789 let template = plan.row_template(&source.identity);
1790 let mut items = Vec::with_capacity(source.rows.len());
1791 for row in source.rows.iter() {
1792 let mut fields = IndexMap::with_capacity(row.fields.len());
1793 for (field, row_source) in &row.fields {
1794 let key = field.input_key();
1795 let value = match row_source {
1796 DataSource::Literal(bound) => {
1797 let field_type = row_end_type(template, &RowEnd::Data(field.clone()));
1798 show_bound_result_value(bound, &field_type, &key, ShowBoundRole::Value)
1799 }
1800 DataSource::Reference(relative) => {
1801 let consumer =
1802 crate::planning::instance_list::absolute_reference(path, relative);
1803 RuleResultValue {
1804 reference: Some(consumer.input_key()),
1805 ..Default::default()
1806 }
1807 }
1808 };
1809 fields.insert(key, value);
1810 }
1811 items.push(RuleResultValue {
1812 fields: Some(fields),
1813 ..Default::default()
1814 });
1815 }
1816 Some(RuleResultValue {
1817 list: Some(items),
1818 ..Default::default()
1819 })
1820}
1821
1822#[derive(Debug, Clone, PartialEq)]
1830pub struct ShowData {
1831 pub lemma_type: LemmaType,
1832 pub path: Vec<crate::planning::semantics::PathSegment>,
1834 pub value: Option<RuleResultValue>,
1835 pub suggestion: Option<RuleResultValue>,
1836 pub needed_by_rules: Vec<String>,
1839}
1840
1841#[derive(Debug, Clone, PartialEq)]
1845pub struct Show {
1846 pub repository: Option<String>,
1848 pub spec: String,
1849 pub commentary: Option<String>,
1850 pub effective_from: Option<crate::parsing::ast::DateTimeValue>,
1851 pub effective_to: Option<crate::parsing::ast::DateTimeValue>,
1852 pub plan_effective: EffectiveDate,
1855 pub start_line: usize,
1856 pub source_type: Option<crate::parsing::source::SourceType>,
1857 pub types: indexmap::IndexMap<String, LemmaType>,
1859 pub data: indexmap::IndexMap<String, ShowData>,
1861 pub rules: indexmap::IndexMap<String, ShowRule>,
1862 pub meta: IndexMap<String, Value>,
1864}
1865
1866impl std::fmt::Display for Show {
1867 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1868 match &self.repository {
1869 Some(repository) => write!(f, "Spec: {} {}", repository, self.spec)?,
1870 None => write!(f, "Spec: {}", self.spec)?,
1871 }
1872 crate::engine::write_effective_window(
1873 f,
1874 self.effective_from.as_ref(),
1875 self.effective_to.as_ref(),
1876 )?;
1877
1878 if let Some(commentary) = &self.commentary {
1879 for line in commentary.lines() {
1880 if line.is_empty() {
1881 writeln!(f)?;
1882 } else {
1883 write!(f, "\n {line}")?;
1884 }
1885 }
1886 }
1887
1888 if !self.meta.is_empty() {
1889 write!(f, "\n\nMeta:")?;
1890 let mut entries: Vec<(&String, &Value)> = self.meta.iter().collect();
1891 entries.sort_by_key(|(k, _)| *k);
1892 for (key, value) in entries {
1893 write!(f, "\n {}: {}", key, value)?;
1894 }
1895 }
1896
1897 if !self.types.is_empty() {
1898 write!(f, "\n\nTypes:")?;
1899 for (name, lemma_type) in &self.types {
1900 write!(f, "\n {} ({})", name, lemma_type.specifications)?;
1901 for line in type_detail_lines(&lemma_type.specifications) {
1902 write!(f, "\n {}", line)?;
1903 }
1904 let help = lemma_type.specifications.help();
1905 if !help.is_empty() {
1906 write!(f, "\n help: {}", help)?;
1907 }
1908 }
1909 }
1910
1911 if !self.data.is_empty() {
1912 write!(f, "\n\nData:")?;
1913 for (name, entry) in &self.data {
1914 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
1915 for line in type_detail_lines(&entry.lemma_type.specifications) {
1916 write!(f, "\n {}", line)?;
1917 }
1918 let help = entry.lemma_type.specifications.help();
1919 if !help.is_empty() {
1920 write!(f, "\n help: {}", help)?;
1921 }
1922 match &entry.value {
1923 Some(RuleResultValue {
1924 list: Some(rows), ..
1925 }) if rows.iter().all(|row| row.fields.is_some()) => {
1926 write!(f, "\n value:")?;
1927 for (index, row) in rows.iter().enumerate() {
1928 let fields = row
1929 .fields
1930 .as_ref()
1931 .expect("BUG: guard checked every row has fields")
1932 .iter()
1933 .map(|(name, field)| format!("{name} {field}"))
1934 .collect::<Vec<_>>()
1935 .join(", ");
1936 write!(f, "\n {}: {fields}", index + 1)?;
1937 }
1938 }
1939 Some(val) => write!(f, "\n value: {}", val)?,
1940 None => {}
1941 }
1942 if let Some(val) = &entry.suggestion {
1943 write!(f, "\n suggestion: {}", val)?;
1944 }
1945 if !entry.needed_by_rules.is_empty() {
1946 write!(
1947 f,
1948 "\n needed_by_rules: {}",
1949 entry.needed_by_rules.join(", ")
1950 )?;
1951 }
1952 }
1953 }
1954
1955 if !self.rules.is_empty() {
1956 write!(f, "\n\nRules:")?;
1957 for (name, rule) in &self.rules {
1958 write!(f, "\n {} ({})", name, rule.lemma_type.specifications)?;
1959 if !rule.depends_on_rules.is_empty() {
1960 write!(
1961 f,
1962 "\n depends_on_rules: {}",
1963 rule.depends_on_rules.join(", ")
1964 )?;
1965 }
1966 }
1967 }
1968
1969 if self.types.is_empty() && self.data.is_empty() && self.rules.is_empty() {
1970 write!(f, "\n (no types, data, or rules)")?;
1971 }
1972
1973 Ok(())
1974 }
1975}
1976
1977pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1983 let mut lines = Vec::new();
1984 match spec {
1985 TypeSpecification::Measure {
1986 minimum,
1987 maximum,
1988 decimals,
1989 units,
1990 ..
1991 } => {
1992 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1993 if !unit_names.is_empty() {
1994 lines.push(format!("units: {}", unit_names.join(", ")));
1995 }
1996 if let Some(d) = decimals {
1997 lines.push(format!("decimals: {}", d));
1998 }
1999 if let Some((magnitude, unit_name)) = minimum {
2000 lines.push(format!(
2001 "minimum: {} {}",
2002 magnitude.display_str(),
2003 unit_name
2004 ));
2005 }
2006 if let Some((magnitude, unit_name)) = maximum {
2007 lines.push(format!(
2008 "maximum: {} {}",
2009 magnitude.display_str(),
2010 unit_name
2011 ));
2012 }
2013 }
2014 TypeSpecification::Number {
2015 minimum,
2016 maximum,
2017 decimals,
2018 ..
2019 } => {
2020 if let Some(d) = decimals {
2021 lines.push(format!("decimals: {}", d));
2022 }
2023 if let Some(v) = minimum {
2024 lines.push(format!("minimum: {}", v.display_str()));
2025 }
2026 if let Some(v) = maximum {
2027 lines.push(format!("maximum: {}", v.display_str()));
2028 }
2029 }
2030 TypeSpecification::Ratio {
2031 minimum,
2032 maximum,
2033 decimals,
2034 units,
2035 ..
2036 } => {
2037 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2038 if !unit_names.is_empty() {
2039 lines.push(format!("units: {}", unit_names.join(", ")));
2040 }
2041 if let Some(d) = decimals {
2042 lines.push(format!("decimals: {}", d));
2043 }
2044 if let Some(v) = minimum {
2045 lines.push(format!("minimum: {}", v.display_str()));
2046 }
2047 if let Some(v) = maximum {
2048 lines.push(format!("maximum: {}", v.display_str()));
2049 }
2050 }
2051 TypeSpecification::Text {
2052 options, length, ..
2053 } => {
2054 if let Some(l) = length {
2055 lines.push(format!("length: {}", l));
2056 }
2057 if !options.is_empty() {
2058 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
2059 lines.push(format!("options: {}", quoted.join(", ")));
2060 }
2061 }
2062 TypeSpecification::Date {
2063 minimum, maximum, ..
2064 } => {
2065 if let Some(v) = minimum {
2066 lines.push(format!("minimum: {}", v));
2067 }
2068 if let Some(v) = maximum {
2069 lines.push(format!("maximum: {}", v));
2070 }
2071 }
2072 TypeSpecification::Time {
2073 minimum, maximum, ..
2074 } => {
2075 if let Some(v) = minimum {
2076 lines.push(format!("minimum: {}", v));
2077 }
2078 if let Some(v) = maximum {
2079 lines.push(format!("maximum: {}", v));
2080 }
2081 }
2082 TypeSpecification::MeasureRange {
2083 lower,
2084 upper,
2085 minimum,
2086 maximum,
2087 units,
2088 ..
2089 } => {
2090 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2091 if !unit_names.is_empty() {
2092 lines.push(format!("units: {}", unit_names.join(", ")));
2093 }
2094 if let Some((magnitude, unit_name)) = lower {
2095 lines.push(format!("lower: {} {}", magnitude.display_str(), unit_name));
2096 }
2097 if let Some((magnitude, unit_name)) = upper {
2098 lines.push(format!("upper: {} {}", magnitude.display_str(), unit_name));
2099 }
2100 if let Some((magnitude, unit_name)) = minimum {
2101 lines.push(format!(
2102 "minimum: {} {}",
2103 magnitude.display_str(),
2104 unit_name
2105 ));
2106 }
2107 if let Some((magnitude, unit_name)) = maximum {
2108 lines.push(format!(
2109 "maximum: {} {}",
2110 magnitude.display_str(),
2111 unit_name
2112 ));
2113 }
2114 }
2115 TypeSpecification::RatioRange {
2116 lower,
2117 upper,
2118 minimum,
2119 maximum,
2120 units,
2121 ..
2122 } => {
2123 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
2124 if !unit_names.is_empty() {
2125 lines.push(format!("units: {}", unit_names.join(", ")));
2126 }
2127 if let Some(v) = lower {
2128 lines.push(format!("lower: {}", v.display_str()));
2129 }
2130 if let Some(v) = upper {
2131 lines.push(format!("upper: {}", v.display_str()));
2132 }
2133 if let Some(v) = minimum {
2134 lines.push(format!("minimum: {}", v.display_str()));
2135 }
2136 if let Some(v) = maximum {
2137 lines.push(format!("maximum: {}", v.display_str()));
2138 }
2139 }
2140 TypeSpecification::NumberRange {
2141 lower,
2142 upper,
2143 minimum,
2144 maximum,
2145 ..
2146 } => {
2147 if let Some(v) = lower {
2148 lines.push(format!("lower: {}", v.display_str()));
2149 }
2150 if let Some(v) = upper {
2151 lines.push(format!("upper: {}", v.display_str()));
2152 }
2153 if let Some(v) = minimum {
2154 lines.push(format!("minimum: {}", v.display_str()));
2155 }
2156 if let Some(v) = maximum {
2157 lines.push(format!("maximum: {}", v.display_str()));
2158 }
2159 }
2160 TypeSpecification::DateRange {
2161 lower,
2162 upper,
2163 minimum,
2164 maximum,
2165 ..
2166 } => {
2167 if let Some(v) = lower {
2168 lines.push(format!("lower: {}", v));
2169 }
2170 if let Some(v) = upper {
2171 lines.push(format!("upper: {}", v));
2172 }
2173 if let Some((magnitude, unit_name)) = minimum {
2174 lines.push(format!(
2175 "minimum: {} {}",
2176 magnitude.display_str(),
2177 unit_name
2178 ));
2179 }
2180 if let Some((magnitude, unit_name)) = maximum {
2181 lines.push(format!(
2182 "maximum: {} {}",
2183 magnitude.display_str(),
2184 unit_name
2185 ));
2186 }
2187 }
2188 TypeSpecification::TimeRange {
2189 lower,
2190 upper,
2191 minimum,
2192 maximum,
2193 ..
2194 } => {
2195 if let Some(v) = lower {
2196 lines.push(format!("lower: {}", v));
2197 }
2198 if let Some(v) = upper {
2199 lines.push(format!("upper: {}", v));
2200 }
2201 if let Some((magnitude, unit_name)) = minimum {
2202 lines.push(format!(
2203 "minimum: {} {}",
2204 magnitude.display_str(),
2205 unit_name
2206 ));
2207 }
2208 if let Some((magnitude, unit_name)) = maximum {
2209 lines.push(format!(
2210 "maximum: {} {}",
2211 magnitude.display_str(),
2212 unit_name
2213 ));
2214 }
2215 }
2216 TypeSpecification::Boolean { .. }
2217 | TypeSpecification::Veto { .. }
2218 | TypeSpecification::Undetermined
2219 | TypeSpecification::Spec { .. } => {}
2220 TypeSpecification::List { element, key } => {
2221 lines.push(format!("element: {element}"));
2222 match key {
2223 crate::planning::semantics::ListKey::Numbered => {
2224 lines.push("key: numbered".to_string());
2225 }
2226 crate::planning::semantics::ListKey::Fields(fields) => {
2227 lines.push(format!("key: {}", fields.join(", ")));
2228 }
2229 }
2230 }
2231 }
2232 lines
2233}
2234
2235impl ExecutionPlan {
2236 pub(crate) fn expression_unit_index(&self) -> &crate::planning::unit_index::UnitIndex {
2238 &self.resolved_types.unit_index
2239 }
2240
2241 fn build_needed_by_rules(&self) -> (Vec<NeededByRules>, IndexMap<DataPath, Vec<u32>>) {
2250 use crate::planning::normalize::{LeafKind, NormalFormKind, NormalFormListOp};
2251
2252 let data_len = self.data.len();
2253 let words = data_len.div_ceil(64);
2254 let mut bits_by_rule: Vec<Vec<u64>> = Vec::with_capacity(self.rules.len());
2255 let mut nested_hits_by_rule: Vec<HashSet<DataPath>> = Vec::with_capacity(self.rules.len());
2256
2257 let mut visited = HashSet::new();
2258 let mut stack = Vec::new();
2259
2260 for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2262 let mut bits = vec![0u64; words];
2263 let mut nested_hits: HashSet<DataPath> = HashSet::new();
2264 visited.clear();
2265 stack.push(rule.normal_form);
2266 while let Some(id) = stack.pop() {
2267 if !visited.insert(id) {
2268 continue;
2269 }
2270 let nf = self.normal_form(id);
2271 let mut needs = NeedsMarker {
2272 plan: self,
2273 rule_pos,
2274 bits: &mut bits,
2275 nested_hits: &mut nested_hits,
2276 bits_by_rule: &bits_by_rule,
2277 nested_hits_by_rule: &nested_hits_by_rule,
2278 };
2279 if let Some(ref_path) = &nf.rule_ref {
2280 needs.mark(ImportNeed::Rule(ref_path.clone()));
2281 continue;
2282 }
2283 match &nf.kind {
2284 NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
2285 for need in self.data_read_needs(path) {
2286 needs.mark(need);
2287 }
2288 }
2289 NormalFormKind::Leaf(LeafKind::ImportRule(path)) if *path == rule.path => {
2290 for need in self.rule_needs(path) {
2291 needs.mark(need);
2292 }
2293 }
2294 NormalFormKind::Leaf(LeafKind::ImportRule(path)) => {
2295 needs.mark(ImportNeed::Rule(path.clone()));
2296 }
2297 NormalFormKind::OrderedDispatch { .. } => {
2298 let origin = nf.origin.unwrap_or_else(|| {
2299 panic!(
2300 "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2301 rule.path
2302 )
2303 });
2304 stack.push(origin);
2305 }
2306 _ => nf.kind.for_each_child(|child| stack.push(child)),
2307 }
2308 }
2309 bits_by_rule.push(bits);
2310 nested_hits_by_rule.push(nested_hits);
2311 }
2312
2313 let mut row_bits_by_rule: Vec<BTreeMap<SpecIdentity, Vec<u64>>> =
2316 Vec::with_capacity(self.rules.len());
2317 for (rule_pos, (_, rule)) in self.rules.iter().enumerate() {
2318 let mut row_bits: BTreeMap<SpecIdentity, Vec<u64>> = BTreeMap::new();
2319 visited.clear();
2320 stack.push(rule.normal_form);
2321 while let Some(id) = stack.pop() {
2322 if !visited.insert(id) {
2323 continue;
2324 }
2325 let nf = self.normal_form(id);
2326 if let Some(ref_path) = &nf.rule_ref {
2327 let ref_pos = self.rules.get_index_of(ref_path).unwrap_or_else(|| {
2328 panic!(
2329 "BUG: rule_ref target '{ref_path}' missing from plan.rules (rule '{}')",
2330 rule.path
2331 )
2332 });
2333 if ref_pos >= rule_pos {
2334 panic!(
2335 "BUG: rule_ref target '{ref_path}' must precede '{}' in topo order",
2336 rule.path
2337 );
2338 }
2339 for (identity, dep_row) in &row_bits_by_rule[ref_pos] {
2340 let entry = row_bits_entry(self, &mut row_bits, identity);
2341 for (word, dep_word) in entry.iter_mut().zip(dep_row.iter()) {
2342 *word |= *dep_word;
2343 }
2344 }
2345 continue;
2346 }
2347 match &nf.kind {
2348 NormalFormKind::Leaf(LeafKind::Row { identity, end, .. }) => {
2349 mark_row_end_needs(
2350 self,
2351 identity,
2352 end,
2353 &bits_by_rule,
2354 &mut row_bits,
2355 &rule.path,
2356 );
2357 }
2358 NormalFormKind::SpecMember {
2359 base,
2360 row: Some(row_read),
2361 ..
2362 } => {
2363 let identity = match &self.result_type(*base).specifications {
2364 TypeSpecification::Spec { spec } => spec,
2365 other => panic!(
2366 "BUG: SpecMember row read on non-spec type {other:?} (rule '{}')",
2367 rule.path
2368 ),
2369 };
2370 mark_row_end_needs(
2371 self,
2372 identity,
2373 &row_read.end,
2374 &bits_by_rule,
2375 &mut row_bits,
2376 &rule.path,
2377 );
2378 stack.push(*base);
2379 }
2380 NormalFormKind::ListCommand {
2381 list,
2382 op: NormalFormListOp::Find | NormalFormListOp::WithRow { .. },
2383 row_level: Some(_),
2384 ..
2385 } => {
2386 mark_list_key_fields(self, *list, &mut row_bits, &rule.path);
2387 nf.kind.for_each_child(|child| stack.push(child));
2388 }
2389 NormalFormKind::OrderedDispatch { .. } => {
2390 let origin = nf.origin.unwrap_or_else(|| {
2391 panic!(
2392 "BUG: non-rule_ref OrderedDispatch must carry origin (rule '{}')",
2393 rule.path
2394 )
2395 });
2396 stack.push(origin);
2397 }
2398 _ => nf.kind.for_each_child(|child| stack.push(child)),
2399 }
2400 }
2401 row_bits_by_rule.push(row_bits);
2402 }
2403
2404 let mut consumers: Vec<usize> = self
2405 .rules
2406 .keys()
2407 .enumerate()
2408 .filter(|(_, path)| self.show_rules.contains_key(*path))
2409 .map(|(pos, _)| pos)
2410 .collect();
2411 consumers.sort_by(|&a, &b| {
2412 self.rules[a]
2413 .path
2414 .input_key()
2415 .cmp(&self.rules[b].path.input_key())
2416 });
2417
2418 let mut needed_by_rules: Vec<NeededByRules> = self
2419 .data
2420 .iter()
2421 .map(|(path, definition)| {
2422 let fields = match definition {
2423 DataDefinition::InstanceList { .. } => {
2424 let identity = &self
2425 .instance_lists
2426 .get(path)
2427 .unwrap_or_else(|| {
2428 panic!("BUG: InstanceList '{path}' missing from instance_lists")
2429 })
2430 .identity;
2431 vec![Vec::new(); self.row_template(identity).nested_input_keys().len()]
2432 }
2433 _ => Vec::new(),
2434 };
2435 NeededByRules {
2436 rules: Vec::new(),
2437 fields,
2438 }
2439 })
2440 .collect();
2441
2442 for &rule_pos in &consumers {
2443 let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2444 let bits = &bits_by_rule[rule_pos];
2445 for (word_idx, &word) in bits.iter().enumerate() {
2446 let mut remaining = word;
2447 while remaining != 0 {
2448 let bit = remaining.trailing_zeros() as usize;
2449 let data_pos = word_idx * 64 + bit;
2450 needed_by_rules[data_pos].rules.push(rule_id);
2451 remaining &= remaining - 1;
2452 }
2453 }
2454 for (identity, row_bits) in &row_bits_by_rule[rule_pos] {
2455 let list_positions = self.instance_list_data_positions(identity);
2456 if list_positions.is_empty() {
2457 panic!(
2458 "BUG: row field needs for '{identity}' but no instance list of that type"
2459 );
2460 }
2461 let template_len = self.row_template(identity).nested_input_keys().len();
2462 for (word_idx, &word) in row_bits.iter().enumerate() {
2463 let mut remaining = word;
2464 while remaining != 0 {
2465 let bit = remaining.trailing_zeros() as usize;
2466 let field_pos = word_idx * 64 + bit;
2467 if field_pos < template_len {
2468 for &list_pos in &list_positions {
2469 needed_by_rules[list_pos].fields[field_pos].push(rule_id);
2470 }
2471 }
2472 remaining &= remaining - 1;
2473 }
2474 }
2475 }
2476 }
2477 let mut nested_needed: IndexMap<DataPath, Vec<u32>> = IndexMap::new();
2478 for &rule_pos in &consumers {
2479 let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2480 let mut paths: Vec<DataPath> = nested_hits_by_rule[rule_pos].iter().cloned().collect();
2481 paths.sort();
2482 for path in paths {
2483 nested_needed.entry(path).or_default().push(rule_id);
2484 }
2485 }
2486 (needed_by_rules, nested_needed)
2487 }
2488
2489 fn instance_list_data_positions(&self, identity: &SpecIdentity) -> Vec<usize> {
2491 self.instance_lists
2492 .iter()
2493 .filter(|(_, source)| source.identity == *identity)
2494 .map(|(path, _)| {
2495 self.data
2496 .get_index_of(path)
2497 .unwrap_or_else(|| panic!("BUG: instance list '{path}' absent from plan.data"))
2498 })
2499 .collect()
2500 }
2501
2502 pub(crate) fn row_field_needed_by_rules(
2505 &self,
2506 identity: &SpecIdentity,
2507 field_input_key: &str,
2508 ) -> Vec<u32> {
2509 let template = self.row_template(identity);
2510 let path = template
2511 .input_key_index
2512 .get(field_input_key)
2513 .unwrap_or_else(|| {
2514 panic!(
2515 "BUG: row field '{field_input_key}' absent from template data of '{identity}'"
2516 )
2517 });
2518 let field_pos = template_bindable_field_index(template, path).unwrap_or_else(|| {
2519 panic!(
2520 "BUG: row field '{field_input_key}' path missing from template bindable fields of '{identity}'"
2521 )
2522 });
2523 let mut seen = HashSet::new();
2524 let mut union = Vec::new();
2525 for list_pos in self.instance_list_data_positions(identity) {
2526 let fields = &self.needed_by_rules[list_pos].fields;
2527 let consumers = fields.get(field_pos).unwrap_or_else(|| {
2528 panic!(
2529 "BUG: needed_by_rules.fields len {} < field {field_pos} for list data pos {list_pos}",
2530 fields.len()
2531 )
2532 });
2533 for &rule_id in consumers {
2534 if seen.insert(rule_id) {
2535 union.push(rule_id);
2536 }
2537 }
2538 }
2539 union.sort_by(|&a, &b| {
2540 self.rules
2541 .get_index(a as usize)
2542 .expect("BUG: needed_by_rules rule id out of range")
2543 .1
2544 .path
2545 .input_key()
2546 .cmp(
2547 &self
2548 .rules
2549 .get_index(b as usize)
2550 .expect("BUG: needed_by_rules rule id out of range")
2551 .1
2552 .path
2553 .input_key(),
2554 )
2555 });
2556 union
2557 }
2558
2559 pub(crate) fn promptable_data_path<'a>(&'a self, path: &'a DataPath) -> Option<&'a DataPath> {
2566 match self.data.get(path) {
2567 Some(DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }) => {
2568 Some(path)
2569 }
2570 Some(DataDefinition::Reference { .. }) => match self.reference_ends.get(path) {
2571 Some(ReferenceEnd::Promptable(target)) => Some(target),
2572 Some(ReferenceEnd::Rule(_) | ReferenceEnd::Import) => None,
2573 None => {
2574 panic!("BUG: reference '{path}' missing from reference_ends after planning")
2575 }
2576 },
2577 Some(DataDefinition::Import { .. } | DataDefinition::InstanceList { .. }) => None,
2578 None => {
2579 if let Some(end) = self.reference_ends.get(path) {
2580 return match end {
2581 ReferenceEnd::Promptable(target) => Some(target),
2582 ReferenceEnd::Rule(_) | ReferenceEnd::Import => None,
2583 };
2584 }
2585 match lookup_data_definition(&self.data, &self.row_templates, path) {
2586 Some(DataDefinition::TypeDeclaration { .. } | DataDefinition::Value { .. })
2587 if !self.import_overlay_binds(path) =>
2588 {
2589 Some(path)
2590 }
2591 Some(
2592 DataDefinition::Value { .. }
2593 | DataDefinition::TypeDeclaration { .. }
2594 | DataDefinition::Reference { .. }
2595 | DataDefinition::Import { .. }
2596 | DataDefinition::InstanceList { .. },
2597 ) => None,
2598 None => panic!("BUG: normal-form DataPath leaf absent from plan.data: {path}"),
2599 }
2600 }
2601 }
2602 }
2603
2604 pub(crate) fn rule_needs(&self, path: &RulePath) -> Vec<ImportNeed> {
2608 if !path.segments.is_empty() {
2609 return import_rule_needs(self, path);
2610 }
2611 let rule_pos = self.rules.get_index_of(path).unwrap_or_else(|| {
2612 panic!(
2613 "BUG: rule '{}' missing from plan '{}' while reading its needs",
2614 path.rule, self.spec_name
2615 )
2616 });
2617 let rule_id = u32::try_from(rule_pos).expect("BUG: rule count exceeds u32");
2618 let mut out: Vec<ImportNeed> = self
2619 .needed_by_rules
2620 .iter()
2621 .zip(self.data.keys())
2622 .filter(|(needed, _)| needed.rules.contains(&rule_id))
2623 .map(|(_, data_path)| ImportNeed::Data(data_path.clone()))
2624 .collect();
2625 out.extend(
2626 self.nested_needed_by_rules
2627 .iter()
2628 .filter(|(_, rules)| rules.contains(&rule_id))
2629 .map(|(nested, _)| ImportNeed::Data(nested.clone())),
2630 );
2631 out
2632 }
2633
2634 pub(crate) fn data_needed_by(&self, path: &DataPath) -> &[u32] {
2638 match self.data.get_index_of(path) {
2639 Some(position) => {
2640 &self
2641 .needed_by_rules
2642 .get(position)
2643 .unwrap_or_else(|| {
2644 panic!(
2645 "BUG: needed_by_rules len {} < data position {position}",
2646 self.needed_by_rules.len()
2647 )
2648 })
2649 .rules
2650 }
2651 None => self
2652 .nested_needed_by_rules
2653 .get(path)
2654 .map_or(&[], Vec::as_slice),
2655 }
2656 }
2657
2658 pub(crate) fn data_read_needs(&self, path: &DataPath) -> Vec<ImportNeed> {
2664 let mut needs = needs_of_end(self, self.reference_end_of(path));
2665 if self.import_overlay_reference_target(path).is_some() {
2666 needs.push(ImportNeed::Data(path.clone()));
2667 }
2668 needs
2669 }
2670
2671 pub(crate) fn reference_end_of(&self, path: &DataPath) -> ReferenceEnd {
2674 if !path.segments.is_empty() {
2675 return reference_end_on(self, path).unwrap_or_else(|| {
2676 panic!(
2677 "BUG: member '{path}' does not peel on plan '{}'",
2678 self.spec_name
2679 )
2680 });
2681 }
2682 match self
2683 .data
2684 .get(path)
2685 .unwrap_or_else(|| panic!("BUG: data '{path}' missing from plan '{}'", self.spec_name))
2686 {
2687 DataDefinition::Value { .. }
2688 | DataDefinition::TypeDeclaration { .. }
2689 | DataDefinition::InstanceList { .. } => ReferenceEnd::Promptable(path.clone()),
2690 DataDefinition::Import { .. } => ReferenceEnd::Import,
2691 DataDefinition::Reference { .. } => {
2692 self.reference_ends.get(path).cloned().unwrap_or_else(|| {
2693 panic!("BUG: reference '{path}' missing from reference_ends")
2694 })
2695 }
2696 }
2697 }
2698
2699 pub(crate) fn import_overlay_binds(&self, path: &DataPath) -> bool {
2701 self.import_overlay_entry(path).is_some()
2702 }
2703
2704 pub(crate) fn import_overlay_entry(&self, path: &DataPath) -> Option<&ImportOverlay> {
2706 let mut plan: &ExecutionPlan = self;
2707 let mut path = path.clone();
2708 loop {
2709 let mut hops = path.segments.iter();
2710 let first = hops.next()?;
2711 let import_marker = DataPath::local(first.uses.clone());
2712 let Some(DataDefinition::Import {
2713 identity, overlay, ..
2714 }) = plan.data.get(&import_marker)
2715 else {
2716 return None;
2717 };
2718 let relative = DataPath {
2719 segments: hops.cloned().collect(),
2720 data: path.data.clone(),
2721 };
2722 if let Some(entry) = overlay.get(&relative) {
2723 return Some(entry);
2724 }
2725 if relative.segments.is_empty() {
2726 return None;
2727 }
2728 plan = plan.row_template(identity);
2729 path = relative;
2730 }
2731 }
2732
2733 pub(crate) fn import_overlay_reference_target(&self, path: &DataPath) -> Option<String> {
2735 match self.import_overlay_entry(path)? {
2736 ImportOverlay::Reference {
2737 target: ReferenceTarget::Data(target),
2738 ..
2739 } => Some(target.input_key()),
2740 ImportOverlay::Reference {
2741 target: ReferenceTarget::Rule(target),
2742 ..
2743 } => Some(target.input_key()),
2744 ImportOverlay::Literal(_) => None,
2745 }
2746 }
2747
2748 pub fn validated_response_rule_paths(
2753 &self,
2754 rules: Option<&[String]>,
2755 ) -> Result<std::collections::HashSet<&RulePath>, Error> {
2756 let Some(rules) = rules else {
2757 return Ok(self
2758 .rules
2759 .values()
2760 .filter(|r| r.path.segments.is_empty())
2761 .map(|r| &r.path)
2762 .collect());
2763 };
2764 if rules.is_empty() {
2765 return Err(Error::request(
2766 "at least one rule required".to_string(),
2767 None::<String>,
2768 ));
2769 }
2770 let mut paths = std::collections::HashSet::new();
2771 for rule_name in rules {
2772 let rule = self.get_rule(rule_name).ok_or_else(|| {
2773 Error::request(
2774 format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
2775 None::<String>,
2776 )
2777 })?;
2778 paths.insert(&rule.path);
2779 }
2780 Ok(paths)
2781 }
2782
2783 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
2785 let canonical = crate::parsing::ast::canonical_logical_name(name);
2786 let path = self.show_rule_index.get(canonical.as_ref())?;
2787 self.rules.get(path)
2788 }
2789
2790 pub(crate) fn normal_form(&self, id: NormalFormId) -> &NormalForm {
2792 self.normal_forms.get(id.index()).unwrap_or_else(|| {
2793 panic!(
2794 "BUG: NormalFormId {} out of range (table len {})",
2795 id.index(),
2796 self.normal_forms.len()
2797 )
2798 })
2799 }
2800
2801 pub(crate) fn result_type(&self, id: NormalFormId) -> &Arc<LemmaType> {
2803 &self.normal_form(id).result_type
2804 }
2805
2806 pub(crate) fn rule_index(&self, path: &RulePath) -> RuleIndex {
2809 RuleIndex(
2810 self.rules
2811 .get_index_of(path)
2812 .unwrap_or_else(|| panic!("BUG: rule '{}' missing from execution plan", path.rule)),
2813 )
2814 }
2815
2816 pub(crate) fn rule_at(&self, index: RuleIndex) -> &ExecutableRule {
2817 self.rules
2818 .get_index(index.0)
2819 .map(|(_, rule)| rule)
2820 .unwrap_or_else(|| {
2821 panic!(
2822 "BUG: rule index {} out of plan.rules range ({} rules)",
2823 index.0,
2824 self.rules.len()
2825 )
2826 })
2827 }
2828
2829 pub(crate) fn promptable_data_paths(&self) -> impl Iterator<Item = &DataPath> {
2841 self.data
2842 .iter()
2843 .filter_map(|(path, definition)| match definition {
2844 DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. } => Some(path),
2845 DataDefinition::Reference { .. }
2846 | DataDefinition::Import { .. }
2847 | DataDefinition::InstanceList { .. } => None,
2848 })
2849 }
2850}
2851
2852pub(crate) fn resolve_nested_dispatch_body(
2858 plan: &ExecutionPlan,
2859 values: &[Option<crate::computation::OperationResult>],
2860 mut region: DispatchRegion,
2861) -> Option<NormalFormId> {
2862 use crate::computation::OperationResult;
2863 use crate::planning::normalize::NormalFormKind;
2864 use crate::planning::ordered_dispatch::region_of_scrutinee;
2865
2866 for _ in 0..=plan.normal_forms.len() {
2867 let body = match region {
2868 DispatchRegion::Body(body) => body,
2869 DispatchRegion::Conjunctive(group) => {
2870 region = DispatchRegion::Body(conjunctive_winner_body(plan, values, group)?);
2871 continue;
2872 }
2873 };
2874 let cell = plan.normal_form(body);
2875 if cell.rule_ref.is_some() {
2876 return Some(body);
2877 }
2878 let (scrutinee, boundaries, regions) = match &cell.kind {
2879 NormalFormKind::OrderedDispatch {
2880 scrutinee,
2881 boundaries,
2882 regions,
2883 } => (scrutinee, boundaries, regions),
2884 NormalFormKind::Piecewise(_) => {
2885 panic!("BUG: single-scrutinee region body is a Piecewise, not a conjunctive group")
2886 }
2887 NormalFormKind::Leaf(_)
2888 | NormalFormKind::Sum(_)
2889 | NormalFormKind::Product(_)
2890 | NormalFormKind::Subtract(_, _)
2891 | NormalFormKind::Divide(_, _)
2892 | NormalFormKind::Power(_, _)
2893 | NormalFormKind::Modulo(_, _)
2894 | NormalFormKind::Negate(_)
2895 | NormalFormKind::Reciprocal(_)
2896 | NormalFormKind::Comparison(_, _, _)
2897 | NormalFormKind::And(_)
2898 | NormalFormKind::Not(_)
2899 | NormalFormKind::MathOp(_, _)
2900 | NormalFormKind::UnitConversion(_, _)
2901 | NormalFormKind::Veto(_)
2902 | NormalFormKind::DateRelative(_, _)
2903 | NormalFormKind::DateCalendar(_, _, _)
2904 | NormalFormKind::RangeLiteral(_, _)
2905 | NormalFormKind::PastFutureRange(_, _)
2906 | NormalFormKind::RangeContainment(_, _)
2907 | NormalFormKind::RangeBound(_, _)
2908 | NormalFormKind::ResultIsVeto(_)
2909 | NormalFormKind::Now
2910 | NormalFormKind::SpecMember { .. }
2911 | NormalFormKind::ListCommand { .. } => return Some(body),
2912 };
2913 let literal = match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
2914 Some(OperationResult::Value(literal)) => literal,
2915 Some(OperationResult::Veto(_)) | None => return None,
2916 };
2917 let index = region_of_scrutinee(boundaries, &literal.value).ok()?;
2918 region = regions[index];
2919 }
2920 panic!("BUG: nested OrderedDispatch chain longer than the normal-form table");
2921}
2922
2923fn conjunctive_winner_body(
2927 plan: &ExecutionPlan,
2928 values: &[Option<crate::computation::OperationResult>],
2929 group: NormalFormId,
2930) -> Option<NormalFormId> {
2931 use crate::evaluation::branch_semantics::{
2932 condition_outcome, piecewise_decision, PiecewiseDecision,
2933 };
2934 use crate::planning::normalize::NormalFormKind;
2935
2936 let NormalFormKind::Piecewise(arms) = &plan.normal_form(group).kind else {
2937 return Some(group);
2938 };
2939 let outcome = |arm: usize| {
2940 values
2941 .get(arms[arm].0.index())
2942 .and_then(|slot| slot.as_ref())
2943 .map(condition_outcome)
2944 };
2945 match piecewise_decision(arms.len(), outcome) {
2946 PiecewiseDecision::Taken { arm } => Some(arms[arm].1),
2947 PiecewiseDecision::Default => Some(arms[0].1),
2948 PiecewiseDecision::Undecided { .. } => None,
2949 }
2950}
2951
2952pub(crate) fn reachable_data_paths(
2974 plan: &ExecutionPlan,
2975 root: NormalFormId,
2976 values: &[Option<crate::computation::OperationResult>],
2977) -> Vec<ReachableInput> {
2978 use crate::computation::OperationResult;
2979 use crate::evaluation::branch_semantics::{
2980 condition_outcome, piecewise_decision, BranchOutcome, PiecewiseDecision,
2981 };
2982 use crate::planning::normalize::LeafKind;
2983 use crate::planning::normalize::NormalFormKind;
2984 use crate::planning::ordered_dispatch::region_of_scrutinee;
2985
2986 fn slot_outcome(values: &[Option<OperationResult>], id: NormalFormId) -> Option<BranchOutcome> {
2987 values
2988 .get(id.index())
2989 .and_then(|slot| slot.as_ref())
2990 .map(condition_outcome)
2991 }
2992
2993 fn dispatch_origin(plan: &ExecutionPlan, cell: &NormalForm) -> NormalFormId {
3001 let mut current = cell;
3002 for _ in 0..=plan.rules.len() {
3003 if let Some(origin) = current.origin {
3004 return origin;
3005 }
3006 let ref_path = current.rule_ref.as_ref().unwrap_or_else(|| {
3007 panic!("BUG: OrderedDispatch without origin must carry rule_ref")
3008 });
3009 let body = plan
3010 .rules
3011 .get(ref_path)
3012 .unwrap_or_else(|| {
3013 panic!("BUG: rule reference '{ref_path}' missing from plan.rules")
3014 })
3015 .normal_form;
3016 current = plan.normal_form(body);
3017 }
3018 panic!("BUG: rule reference chain over a dispatch cell does not terminate");
3019 }
3020
3021 fn live_piecewise_children(
3024 arms: &[(NormalFormId, NormalFormId)],
3025 decision: PiecewiseDecision,
3026 ) -> Vec<NormalFormId> {
3027 let mut live = Vec::new();
3028 match decision {
3029 PiecewiseDecision::Taken { arm } => {
3030 for (condition, _) in arms.iter().skip(arm + 1).rev() {
3031 live.push(*condition);
3032 }
3033 live.push(arms[arm].0);
3034 live.push(arms[arm].1);
3035 }
3036 PiecewiseDecision::Default => {
3037 for (condition, _) in arms.iter().skip(1).rev() {
3038 live.push(*condition);
3039 }
3040 live.push(arms[0].1);
3041 }
3042 PiecewiseDecision::Undecided { arm } => {
3043 for (condition, _) in arms.iter().skip(arm + 1).rev() {
3044 live.push(*condition);
3045 }
3046 for (condition, body) in arms.iter().take(arm + 1).skip(1).rev() {
3047 live.push(*condition);
3048 live.push(*body);
3049 }
3050 live.push(arms[0].1);
3051 }
3052 }
3053 live
3054 }
3055
3056 let mut out = Vec::new();
3057 let mut seen_paths = HashSet::new();
3058 let mut visited = HashSet::new();
3059 let mut stack = vec![root];
3060
3061 while let Some(id) = stack.pop() {
3062 if !visited.insert(id) {
3063 continue;
3064 }
3065 let nf = plan.normal_form(id);
3066 let live: Vec<NormalFormId> = match &nf.kind {
3067 NormalFormKind::Leaf(LeafKind::DataPath(path)) => {
3068 if seen_paths.insert(path.clone()) {
3069 out.push(ReachableInput::Data(path.clone()));
3070 }
3071 Vec::new()
3072 }
3073 NormalFormKind::Leaf(LeafKind::ImportRule(path)) => {
3074 out.push(ReachableInput::ImportRule(path.clone()));
3075 Vec::new()
3076 }
3077 NormalFormKind::Leaf(LeafKind::Row { .. }) => Vec::new(),
3078 NormalFormKind::ListCommand { row_level, .. } => {
3079 if row_level.is_some() {
3080 out.push(ReachableInput::RowReads(id));
3081 }
3082 nf.kind.children()
3083 }
3084 NormalFormKind::And(children) => {
3085 match children.iter().copied().find(|&child| {
3086 matches!(slot_outcome(values, child), Some(BranchOutcome::NotTaken))
3087 }) {
3088 Some(decider) => vec![decider],
3089 None => children
3090 .iter()
3091 .copied()
3092 .filter(|&child| {
3093 !matches!(slot_outcome(values, child), Some(BranchOutcome::Taken))
3094 })
3095 .collect(),
3096 }
3097 }
3098 NormalFormKind::Piecewise(arms) => {
3099 let decision =
3100 piecewise_decision(arms.len(), |arm| slot_outcome(values, arms[arm].0));
3101 live_piecewise_children(arms, decision)
3102 }
3103 NormalFormKind::OrderedDispatch {
3104 scrutinee,
3105 boundaries,
3106 regions,
3107 } => {
3108 let origin_id = dispatch_origin(plan, nf);
3109 let NormalFormKind::Piecewise(arms) = &plan.normal_form(origin_id).kind else {
3110 panic!("BUG: OrderedDispatch origin must be Piecewise");
3111 };
3112 visited.insert(origin_id);
3113
3114 match values.get(scrutinee.index()).and_then(|slot| slot.as_ref()) {
3115 Some(OperationResult::Value(literal)) => {
3116 match region_of_scrutinee(boundaries, &literal.value) {
3117 Ok(region) => vec![*scrutinee, regions[region].cell()],
3118 Err(_) => vec![*scrutinee],
3119 }
3120 }
3121 Some(OperationResult::Veto(_)) => vec![*scrutinee],
3122 None => {
3125 let mut live = vec![*scrutinee];
3126 live.extend(live_piecewise_children(
3127 arms,
3128 PiecewiseDecision::Undecided {
3129 arm: arms.len() - 1,
3130 },
3131 ));
3132 live
3133 }
3134 }
3135 }
3136 NormalFormKind::SpecMember {
3137 base, targets, row, ..
3138 } => {
3139 if row.is_some() {
3140 out.push(ReachableInput::RowReads(id));
3141 }
3142 let chosen = match values.get(base.index()).and_then(|slot| slot.as_ref()) {
3143 Some(OperationResult::Value(bound)) => match &bound.value {
3144 crate::planning::semantics::ValueKind::Spec(
3145 crate::planning::semantics::SpecValue::Fixed(instance),
3146 ) => targets
3147 .iter()
3148 .find(|(candidate, _)| candidate == instance)
3149 .map(|(_, target)| *target),
3150 _ => None,
3151 },
3152 Some(OperationResult::Veto(_)) | None => None,
3153 };
3154 match chosen {
3155 Some(target) => vec![*base, target],
3156 None => vec![*base],
3157 }
3158 }
3159 NormalFormKind::Leaf(LeafKind::Literal(_))
3160 | NormalFormKind::Now
3161 | NormalFormKind::Veto(_)
3162 | NormalFormKind::Sum(_)
3163 | NormalFormKind::Product(_)
3164 | NormalFormKind::Subtract(_, _)
3165 | NormalFormKind::Divide(_, _)
3166 | NormalFormKind::Power(_, _)
3167 | NormalFormKind::Modulo(_, _)
3168 | NormalFormKind::Comparison(_, _, _)
3169 | NormalFormKind::RangeLiteral(_, _)
3170 | NormalFormKind::RangeContainment(_, _)
3171 | NormalFormKind::Negate(_)
3172 | NormalFormKind::Reciprocal(_)
3173 | NormalFormKind::Not(_)
3174 | NormalFormKind::MathOp(_, _)
3175 | NormalFormKind::UnitConversion(_, _)
3176 | NormalFormKind::DateRelative(_, _)
3177 | NormalFormKind::DateCalendar(_, _, _)
3178 | NormalFormKind::PastFutureRange(_, _)
3179 | NormalFormKind::RangeBound(_, _)
3180 | NormalFormKind::ResultIsVeto(_) => nf.kind.children(),
3181 };
3182 stack.extend(live.into_iter().rev());
3183 }
3184 out
3185}
3186
3187pub(crate) enum ReachableInput {
3189 Data(DataPath),
3190 RowReads(NormalFormId),
3192 ImportRule(RulePath),
3195}
3196
3197pub(crate) fn validate_value_against_type(
3198 expected_type: &LemmaType,
3199 value: &LiteralValue,
3200 unit_index: &crate::planning::unit_index::UnitIndex,
3201) -> Result<(), String> {
3202 use std::borrow::Cow;
3203
3204 let mut pending = vec![(expected_type, Cow::Borrowed(value))];
3205 while let Some((expected, value)) = pending.pop() {
3206 match (&expected.specifications, &value.value) {
3207 (TypeSpecification::List { element, .. }, ValueKind::List(rows)) => {
3208 for row in rows.iter().rev() {
3209 pending.push((element.as_ref(), Cow::Owned(row.to_literal())));
3210 }
3211 }
3212 (
3213 range_spec @ (TypeSpecification::NumberRange { .. }
3214 | TypeSpecification::DateRange { .. }
3215 | TypeSpecification::TimeRange { .. }
3216 | TypeSpecification::MeasureRange { .. }
3217 | TypeSpecification::RatioRange { .. }),
3218 ValueKind::Range(left, right),
3219 ) => validate_range_literal(
3220 expected,
3221 range_spec,
3222 left.as_ref(),
3223 right.as_ref(),
3224 unit_index,
3225 )?,
3226 _ => validate_scalar_against_type(expected, &value)?,
3227 }
3228 }
3229 Ok(())
3230}
3231
3232fn validate_scalar_against_type(
3233 expected_type: &LemmaType,
3234 value: &LiteralValue,
3235) -> Result<(), String> {
3236 use crate::computation::bigint::BigInt;
3237 use crate::computation::rational::{
3238 checked_mul, rational_new, try_pow_integer, RationalInteger,
3239 };
3240
3241 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
3242 let scale = match try_pow_integer(
3243 &rational_new(10, 1),
3244 &BigInt::from_i64(i64::from(max_decimals)),
3245 ) {
3246 Ok(value) => value,
3247 Err(_) => return true,
3248 };
3249 let scaled = match checked_mul(magnitude, &scale) {
3250 Ok(value) => value,
3251 Err(_) => return true,
3252 };
3253 match RationalInteger::try_reduce_ref(&scaled) {
3254 Ok(reduced) => !reduced.is_integer(),
3255 Err(_) => true,
3256 }
3257 }
3258
3259 fn format_rational_for_validation_message(
3260 expected_type: &crate::planning::semantics::LemmaType,
3261 magnitude: &RationalInteger,
3262 ) -> String {
3263 expected_type
3264 .try_rational_as_decimal(magnitude)
3265 .map(|decimal| decimal.to_string())
3266 .unwrap_or_else(|_| magnitude.display_str())
3267 }
3268
3269 match (&expected_type.specifications, &value.value) {
3270 (
3271 TypeSpecification::Number {
3272 minimum,
3273 maximum,
3274 decimals,
3275 ..
3276 },
3277 ValueKind::Number(n),
3278 ) => {
3279 use std::cmp::Ordering;
3280 if let Some(d) = decimals {
3281 if exceeds_decimal_places(n, *d) {
3282 return Err(format!(
3283 "{} exceeds the decimals constraint of {d}.",
3284 n.display_str()
3285 ));
3286 }
3287 }
3288 if let Some(min) = minimum {
3289 if n.try_cmp(min).map_err(|failure| {
3290 format!("Comparing against the minimum bound failed: {failure}.")
3291 })? == Ordering::Less
3292 {
3293 return Err(format!(
3294 "{} is below the minimum of {}.",
3295 format_rational_for_validation_message(expected_type, n),
3296 format_rational_for_validation_message(expected_type, min)
3297 ));
3298 }
3299 }
3300 if let Some(max) = maximum {
3301 if n.try_cmp(max).map_err(|failure| {
3302 format!("Comparing against the maximum bound failed: {failure}.")
3303 })? == Ordering::Greater
3304 {
3305 return Err(format!(
3306 "{} is above the maximum of {}.",
3307 format_rational_for_validation_message(expected_type, n),
3308 format_rational_for_validation_message(expected_type, max)
3309 ));
3310 }
3311 }
3312 Ok(())
3313 }
3314 (
3315 TypeSpecification::Measure {
3316 minimum,
3317 maximum,
3318 decimals,
3319 units,
3320 ..
3321 },
3322 ValueKind::Measure(magnitude),
3323 ) => {
3324 use crate::computation::rational::checked_div;
3325 use crate::planning::semantics::measure_declared_bound_to_canonical;
3326 use std::cmp::Ordering;
3327 let unit = expected_type
3328 .unit()
3329 .or_else(|| {
3330 units
3331 .iter()
3332 .find(|u| u.is_canonical_factor())
3333 .or_else(|| units.iter().next())
3334 .map(|u| u.name.as_str())
3335 })
3336 .ok_or_else(|| {
3337 format!(
3338 "Measure type '{}' has no declared units for validation.",
3339 expected_type.name()
3340 )
3341 })?;
3342 let measure_unit = units.get(unit)?;
3343 let factor = &measure_unit.factor;
3344 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
3345 format!("Cannot de-canonicalize the measure for validation: {failure}.")
3346 })?;
3347 if let Some(d) = decimals {
3348 if exceeds_decimal_places(&in_unit, *d) {
3349 return Err(format!(
3350 "{} {unit} exceeds the decimals constraint of {d}.",
3351 in_unit.display_str()
3352 ));
3353 }
3354 }
3355 if let Some(bound) = minimum {
3356 let canonical_min = measure_declared_bound_to_canonical(
3357 &bound.0,
3358 &bound.1,
3359 units,
3360 expected_type.name().as_str(),
3361 "minimum",
3362 )?;
3363 if magnitude.try_cmp(&canonical_min).map_err(|failure| {
3364 format!("Comparing against the minimum bound failed: {failure}.")
3365 })? == Ordering::Less
3366 {
3367 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
3368 format!("Cannot de-canonicalize the minimum for validation: {failure}.")
3369 })?;
3370 let value_display = format!(
3371 "{} {}",
3372 format_rational_for_validation_message(expected_type, &in_unit),
3373 unit
3374 );
3375 let bound_display = format!(
3376 "{} {}",
3377 format_rational_for_validation_message(expected_type, &min_in_unit),
3378 measure_unit.name
3379 );
3380 return Err(format!(
3381 "{value_display} is below the minimum of {bound_display}."
3382 ));
3383 }
3384 }
3385 if let Some(bound) = maximum {
3386 let canonical_max = measure_declared_bound_to_canonical(
3387 &bound.0,
3388 &bound.1,
3389 units,
3390 expected_type.name().as_str(),
3391 "maximum",
3392 )?;
3393 if magnitude.try_cmp(&canonical_max).map_err(|failure| {
3394 format!("Comparing against the maximum bound failed: {failure}.")
3395 })? == Ordering::Greater
3396 {
3397 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
3398 format!("Cannot de-canonicalize the maximum for validation: {failure}.")
3399 })?;
3400 let value_display = format!(
3401 "{} {}",
3402 format_rational_for_validation_message(expected_type, &in_unit),
3403 unit
3404 );
3405 let bound_display = format!(
3406 "{} {}",
3407 format_rational_for_validation_message(expected_type, &max_in_unit),
3408 measure_unit.name
3409 );
3410 return Err(format!(
3411 "{value_display} is above the maximum of {bound_display}."
3412 ));
3413 }
3414 }
3415 Ok(())
3416 }
3417 (
3418 TypeSpecification::Text {
3419 length, options, ..
3420 },
3421 ValueKind::Text(s),
3422 ) => {
3423 let len = s.chars().count();
3424 if let Some(exact) = length {
3425 if len != *exact {
3426 return Err(format!(
3427 "'{}' has length {}, but the required length is {}.",
3428 s, len, exact
3429 ));
3430 }
3431 }
3432 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
3433 return Err(format!(
3434 "'{}' is not one of the allowed options: {}.",
3435 s,
3436 options.join(", ")
3437 ));
3438 }
3439 Ok(())
3440 }
3441 (
3442 TypeSpecification::Ratio {
3443 minimum,
3444 maximum,
3445 decimals,
3446 units,
3447 ..
3448 },
3449 ValueKind::Ratio(r),
3450 ) => {
3451 use crate::computation::rational::checked_mul;
3452 use std::cmp::Ordering;
3453
3454 let primary_unit = expected_type
3455 .unit()
3456 .or_else(|| expected_type.ratio_primary_unit());
3457
3458 if let Some(d) = decimals {
3459 let magnitude_for_decimals = match primary_unit {
3460 Some(unit) => {
3461 let ratio_unit = units.get(unit)?;
3462 checked_mul(r, &ratio_unit.value).map_err(|failure| failure.to_string())?
3463 }
3464 None => r.clone(),
3465 };
3466 if exceeds_decimal_places(&magnitude_for_decimals, *d) {
3467 return Err(format!(
3468 "{} exceeds the decimals constraint of {d}.",
3469 magnitude_for_decimals.display_str()
3470 ));
3471 }
3472 }
3473 if let Some(type_minimum) = minimum {
3474 if r.try_cmp(type_minimum).map_err(|failure| {
3475 format!("Comparing against the minimum bound failed: {failure}.")
3476 })? == Ordering::Less
3477 {
3478 let message = match primary_unit {
3479 Some(unit) => {
3480 let ratio_unit = units.get(unit)?;
3481 let value_per_unit = checked_mul(r, &ratio_unit.value)
3482 .map_err(|failure| failure.to_string())?;
3483 let bound_per_unit = ratio_unit.minimum.clone().expect(
3484 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
3485 );
3486 format!(
3487 "{} {unit} is below the minimum of {} {unit}.",
3488 format_rational_for_validation_message(
3489 expected_type,
3490 &value_per_unit
3491 ),
3492 format_rational_for_validation_message(
3493 expected_type,
3494 &bound_per_unit.clone()
3495 ),
3496 )
3497 }
3498 None => format!(
3499 "{} is below the minimum of {}.",
3500 format_rational_for_validation_message(expected_type, r),
3501 format_rational_for_validation_message(expected_type, type_minimum),
3502 ),
3503 };
3504 return Err(message);
3505 }
3506 }
3507 if let Some(type_maximum) = maximum {
3508 if r.try_cmp(type_maximum).map_err(|failure| {
3509 format!("Comparing against the maximum bound failed: {failure}.")
3510 })? == Ordering::Greater
3511 {
3512 let message = match primary_unit {
3513 Some(unit) => {
3514 let ratio_unit = units.get(unit)?;
3515 let value_per_unit = checked_mul(r, &ratio_unit.value)
3516 .map_err(|failure| failure.to_string())?;
3517 let bound_per_unit = ratio_unit.maximum.clone().expect(
3518 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
3519 );
3520 format!(
3521 "{} {unit} is above the maximum of {} {unit}.",
3522 format_rational_for_validation_message(
3523 expected_type,
3524 &value_per_unit
3525 ),
3526 format_rational_for_validation_message(
3527 expected_type,
3528 &bound_per_unit.clone()
3529 ),
3530 )
3531 }
3532 None => format!(
3533 "{} is above the maximum of {}.",
3534 format_rational_for_validation_message(expected_type, r),
3535 format_rational_for_validation_message(expected_type, type_maximum),
3536 ),
3537 };
3538 return Err(message);
3539 }
3540 }
3541 Ok(())
3542 }
3543 (
3544 TypeSpecification::Date {
3545 minimum, maximum, ..
3546 },
3547 ValueKind::Date(dt),
3548 ) => {
3549 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
3550 use std::cmp::Ordering;
3551 if let Some(min) = minimum {
3552 let min_sem = date_time_to_semantic(min);
3553 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
3554 return Err(format!("{} is below the minimum of {}.", dt, min));
3555 }
3556 }
3557 if let Some(max) = maximum {
3558 let max_sem = date_time_to_semantic(max);
3559 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
3560 return Err(format!("{} is above the maximum of {}.", dt, max));
3561 }
3562 }
3563 Ok(())
3564 }
3565 (
3566 TypeSpecification::Time {
3567 minimum, maximum, ..
3568 },
3569 ValueKind::Time(t),
3570 ) => {
3571 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
3572 use std::cmp::Ordering;
3573 if let Some(min) = minimum {
3574 let min_sem = time_to_semantic(min);
3575 if compare_semantic_times(t, &min_sem) == Ordering::Less {
3576 return Err(format!("{} is below the minimum of {}.", t, min));
3577 }
3578 }
3579 if let Some(max) = maximum {
3580 let max_sem = time_to_semantic(max);
3581 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
3582 return Err(format!("{} is above the maximum of {}.", t, max));
3583 }
3584 }
3585 Ok(())
3586 }
3587 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_)) => Ok(()),
3588 (TypeSpecification::List { .. }, ValueKind::List(_))
3589 | (
3590 TypeSpecification::NumberRange { .. }
3591 | TypeSpecification::DateRange { .. }
3592 | TypeSpecification::TimeRange { .. }
3593 | TypeSpecification::MeasureRange { .. }
3594 | TypeSpecification::RatioRange { .. },
3595 ValueKind::Range(_, _),
3596 ) => unreachable!("BUG: list and range values are walked by validate_value_against_type"),
3597 (TypeSpecification::Veto { .. }, _) | (TypeSpecification::Undetermined, _) => Ok(()),
3598 (TypeSpecification::Spec { .. }, ValueKind::Spec(_)) => Ok(()),
3599 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
3600 "BUG: validate_value_against_type called with mismatched type/value: \
3601 spec={:?}, value={:?} — typing must be enforced before validation",
3602 spec, value_kind
3603 ),
3604 (spec, value_kind) => unreachable!(
3605 "BUG: validate_value_against_type missed a value_kind_matches_spec pair: \
3606 spec={:?}, value={:?}",
3607 spec, value_kind
3608 ),
3609 }
3610}
3611
3612fn validate_range_literal(
3613 expected_type: &LemmaType,
3614 range_spec: &TypeSpecification,
3615 left: &crate::planning::semantics::TypedLiteral,
3616 right: &crate::planning::semantics::TypedLiteral,
3617 unit_index: &crate::planning::unit_index::UnitIndex,
3618) -> Result<(), String> {
3619 use crate::computation::{comparison_operation, OperationResult};
3620 use crate::planning::semantics::{
3621 compare_semantic_dates, compare_semantic_times, measure_declared_bound_to_canonical,
3622 ValueKind,
3623 };
3624 use std::cmp::Ordering;
3625 use std::sync::Arc;
3626
3627 let element_spec = range_spec
3628 .element_from_range()
3629 .expect("BUG: element_from_range missing arm for validated range");
3630 if let TypeSpecification::Measure {
3631 units,
3632 decomposition,
3633 ..
3634 } = &element_spec
3635 {
3636 assert!(
3637 decomposition.is_some() || units.is_empty(),
3638 "BUG: measure range '{}' with units must carry a decomposition after planning",
3639 expected_type.name()
3640 );
3641 }
3642 let element_type = Arc::new(LemmaType::primitive(element_spec));
3643 validate_scalar_against_type(element_type.as_ref(), &left.to_literal())?;
3644 validate_scalar_against_type(element_type.as_ref(), &right.to_literal())?;
3645
3646 let ordering = match (&left.value, &right.value) {
3647 (ValueKind::Number(l), ValueKind::Number(r)) => l
3648 .try_cmp(r)
3649 .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3650 (ValueKind::Date(l), ValueKind::Date(r)) => compare_semantic_dates(l, r),
3651 (ValueKind::Time(l), ValueKind::Time(r)) => compare_semantic_times(l, r),
3652 (ValueKind::Ratio(l), ValueKind::Ratio(r)) => l
3653 .try_cmp(r)
3654 .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3655 (ValueKind::Measure(l), ValueKind::Measure(r)) => l
3656 .try_cmp(r)
3657 .map_err(|failure| format!("Comparing range endpoints failed: {failure}."))?,
3658 (left_kind, right_kind) => unreachable!(
3659 "BUG: range endpoints have mismatched value kinds after typing: {left_kind:?} vs {right_kind:?}"
3660 ),
3661 };
3662 if ordering == Ordering::Greater {
3663 return Err(format!(
3664 "The range left endpoint {} is above the right endpoint {}.",
3665 left.display_value(),
3666 right.display_value()
3667 ));
3668 }
3669
3670 let range_lit = LiteralValue {
3671 value: ValueKind::Range(Box::new(left.clone()), Box::new(right.clone())),
3672 };
3673 let range_type = Arc::new(expected_type.clone());
3674
3675 let compare_width = |bound: &LiteralValue,
3676 bound_type: &Arc<LemmaType>,
3677 op: ComparisonComputation,
3678 fail_msg: String|
3679 -> Result<(), String> {
3680 match comparison_operation(&range_lit.value, &range_type, &op, &bound.value, bound_type) {
3681 OperationResult::Value(result) => match &result.value {
3682 ValueKind::Boolean(true) => Ok(()),
3683 ValueKind::Boolean(false) => Err(fail_msg),
3684 other => unreachable!("BUG: width comparison must return boolean, got {other:?}"),
3685 },
3686 OperationResult::Veto(veto) => Err(veto.to_string()),
3687 }
3688 };
3689
3690 match range_spec {
3691 TypeSpecification::NumberRange {
3692 minimum, maximum, ..
3693 } => {
3694 if let Some(min_w) = minimum {
3695 compare_width(
3696 &LiteralValue::number(min_w.clone()),
3697 crate::planning::semantics::primitive_number_arc(),
3698 ComparisonComputation::GreaterThanOrEqual,
3699 format!(
3700 "The span is below the minimum width of {}.",
3701 min_w.display_str()
3702 ),
3703 )?;
3704 }
3705 if let Some(max_w) = maximum {
3706 compare_width(
3707 &LiteralValue::number(max_w.clone()),
3708 crate::planning::semantics::primitive_number_arc(),
3709 ComparisonComputation::LessThanOrEqual,
3710 format!(
3711 "The span is above the maximum width of {}.",
3712 max_w.display_str()
3713 ),
3714 )?;
3715 }
3716 }
3717 TypeSpecification::RatioRange {
3718 minimum, maximum, ..
3719 } => {
3720 if let Some(min_w) = minimum {
3721 compare_width(
3722 &LiteralValue::ratio(min_w.clone()),
3723 crate::planning::semantics::primitive_ratio_arc(),
3724 ComparisonComputation::GreaterThanOrEqual,
3725 format!(
3726 "The span is below the minimum width of {}.",
3727 min_w.display_str()
3728 ),
3729 )?;
3730 }
3731 if let Some(max_w) = maximum {
3732 compare_width(
3733 &LiteralValue::ratio(max_w.clone()),
3734 crate::planning::semantics::primitive_ratio_arc(),
3735 ComparisonComputation::LessThanOrEqual,
3736 format!(
3737 "The span is above the maximum width of {}.",
3738 max_w.display_str()
3739 ),
3740 )?;
3741 }
3742 }
3743 TypeSpecification::MeasureRange {
3744 minimum,
3745 maximum,
3746 units,
3747 ..
3748 } => {
3749 if let Some(min_w) = minimum {
3750 let canonical = measure_declared_bound_to_canonical(
3751 &min_w.0, &min_w.1, units, "range", "minimum",
3752 )?;
3753 let bound = LiteralValue::measure(canonical);
3754 compare_width(
3755 &bound,
3756 &element_type,
3757 ComparisonComputation::GreaterThanOrEqual,
3758 format!(
3759 "The span is below the minimum width of {} {}.",
3760 min_w.0.display_str(),
3761 min_w.1
3762 ),
3763 )?;
3764 }
3765 if let Some(max_w) = maximum {
3766 let canonical = measure_declared_bound_to_canonical(
3767 &max_w.0, &max_w.1, units, "range", "maximum",
3768 )?;
3769 let bound = LiteralValue::measure(canonical);
3770 compare_width(
3771 &bound,
3772 &element_type,
3773 ComparisonComputation::LessThanOrEqual,
3774 format!(
3775 "The span is above the maximum width of {} {}.",
3776 max_w.0.display_str(),
3777 max_w.1
3778 ),
3779 )?;
3780 }
3781 }
3782 TypeSpecification::DateRange {
3783 minimum, maximum, ..
3784 }
3785 | TypeSpecification::TimeRange {
3786 minimum, maximum, ..
3787 } => {
3788 let allow_calendar = matches!(range_spec, TypeSpecification::DateRange { .. });
3789 let resolve_bound = |bound: &(
3790 crate::computation::rational::RationalInteger,
3791 String,
3792 ),
3793 command: &str|
3794 -> Result<(LiteralValue, Arc<LemmaType>), String> {
3795 let (bare, owner) = unit_index
3796 .resolve(bound.1.as_str())
3797 .map_err(|err| format!("{command} width unit '{}': {err}", bound.1))?;
3798 if allow_calendar {
3799 if !owner.is_duration_like() && !owner.is_calendar_like() {
3800 return Err(format!(
3801 "The {command} width unit '{bare}' must be a duration or calendar unit.",
3802 ));
3803 }
3804 } else if !owner.is_duration_like() {
3805 return Err(format!(
3806 "The {command} width unit '{bare}' must be a duration unit.",
3807 ));
3808 }
3809 let TypeSpecification::Measure { units, .. } = &owner.specifications else {
3810 return Err(format!(
3811 "The {command} width unit '{bare}' must resolve to a measure type.",
3812 ));
3813 };
3814 let type_name = owner.name();
3815 let canonical = measure_declared_bound_to_canonical(
3816 &bound.0,
3817 &bare,
3818 units,
3819 type_name.as_str(),
3820 command,
3821 )?;
3822 Ok((LiteralValue::measure(canonical), Arc::clone(&owner)))
3823 };
3824 if let Some(min_w) = minimum {
3825 let (bound, bound_type) = resolve_bound(min_w, "minimum")?;
3826 compare_width(
3827 &bound,
3828 &bound_type,
3829 ComparisonComputation::GreaterThanOrEqual,
3830 format!(
3831 "The span is below the minimum width of {} {}.",
3832 min_w.0.display_str(),
3833 min_w.1
3834 ),
3835 )?;
3836 }
3837 if let Some(max_w) = maximum {
3838 let (bound, bound_type) = resolve_bound(max_w, "maximum")?;
3839 compare_width(
3840 &bound,
3841 &bound_type,
3842 ComparisonComputation::LessThanOrEqual,
3843 format!(
3844 "The span is above the maximum width of {} {}.",
3845 max_w.0.display_str(),
3846 max_w.1
3847 ),
3848 )?;
3849 }
3850 }
3851 _ => {}
3852 }
3853
3854 Ok(())
3855}
3856
3857fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
3858 let mut errors = Vec::new();
3859
3860 for (data_path, data_definition) in &plan.data {
3861 let (expected_type, lit) = match data_definition {
3862 DataDefinition::Value {
3863 value,
3864 resolved_type,
3865 ..
3866 } => (resolved_type, value),
3867 DataDefinition::TypeDeclaration { .. }
3868 | DataDefinition::Import { .. }
3869 | DataDefinition::InstanceList { .. }
3870 | DataDefinition::Reference { .. } => continue,
3871 };
3872
3873 if let Err(msg) =
3874 validate_value_against_type(expected_type, lit, plan.expression_unit_index())
3875 {
3876 let source = data_definition.source().clone();
3877 errors.push(Error::validation(
3878 format!(
3879 "Invalid value for data {} (expected {}): {}",
3880 data_path,
3881 expected_type.name().as_str(),
3882 msg
3883 ),
3884 Some(source),
3885 None::<String>,
3886 ));
3887 }
3888 }
3889
3890 errors
3891}
3892
3893fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
3894 use crate::planning::normalize::NormalFormKind;
3895
3896 let mut errors: Vec<Error> = Vec::new();
3897 let mut visited = HashSet::new();
3898 let mut worklist: Vec<NormalFormId> =
3899 plan.rules.values().map(|rule| rule.normal_form).collect();
3900 while let Some(id) = worklist.pop() {
3901 if !visited.insert(id) {
3902 continue;
3903 }
3904 let nf = plan.normal_form(id);
3905 nf.kind.for_each_child(|child| worklist.push(child));
3906 if let NormalFormKind::UnitConversion(_, target) = &nf.kind {
3907 if let Some((unit_name, owning_type)) =
3908 crate::computation::units::conversion_target_declares_unit(target)
3909 {
3910 if !crate::computation::units::owning_type_declares_unit_name(
3911 owning_type.as_ref(),
3912 unit_name,
3913 ) {
3914 errors.push(Error::validation(
3915 format!(
3916 "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
3917 owning_type.name()
3918 ),
3919 None::<Source>,
3920 Some(plan.spec_name.clone()),
3921 ));
3922 }
3923 }
3924 }
3925 }
3926 if let Some(error) = errors.into_iter().next() {
3927 return Err(error);
3928 }
3929 Ok(())
3930}
3931
3932#[cfg(test)]
3933mod tests {
3934 use super::*;
3935 use crate::computation::rational::{rational_new, rational_zero};
3936 use crate::computation::{OperationResult, VetoType};
3937 use crate::evaluation::evaluate_data::EvaluateData;
3938 use crate::literals::DateGranularity;
3939 use crate::literals::TimezoneValue;
3940 use crate::parsing::ast::DateTimeValue;
3941 use crate::planning::semantics::{DataDefinition, DataPath, PathSegment, TypeSpecification};
3942 use crate::Engine;
3943 use crate::{EvaluateDataValue, ResourceLimits};
3944 use serde_json;
3945 use std::str::FromStr;
3946 use std::sync::Arc;
3947
3948 fn default_limits() -> ResourceLimits {
3949 ResourceLimits::default()
3950 }
3951
3952 fn resolve_evaluate_data(
3953 plan: &ExecutionPlan,
3954 values: IndexMap<String, EvaluateDataValue>,
3955 ) -> EvaluateData {
3956 EvaluateData::resolve(plan, values, &default_limits()).expect("resolve")
3957 }
3958
3959 fn veto_reason<'a>(evaluate_data: &'a EvaluateData, path: &DataPath) -> Option<&'a str> {
3960 match evaluate_data.bindings.get(path) {
3961 Some(OperationResult::Veto(veto)) => Some(match veto {
3962 VetoType::Computation { message } => message.as_str(),
3963 other => panic!("expected Computation veto, got {other:?}"),
3964 }),
3965 _ => None,
3966 }
3967 }
3968
3969 fn bound_value<'a>(
3970 evaluate_data: &'a EvaluateData,
3971 path: &DataPath,
3972 ) -> Option<&'a crate::planning::semantics::BoundValueKind> {
3973 evaluate_data
3974 .bindings
3975 .get(path)
3976 .and_then(OperationResult::value)
3977 }
3978
3979 fn input_data(pairs: &[(&str, &str)]) -> IndexMap<String, EvaluateDataValue> {
3980 pairs
3981 .iter()
3982 .map(|(k, v)| (k.to_string(), EvaluateDataValue::string(*v)))
3983 .collect()
3984 }
3985
3986 #[test]
3987 fn test_with_raw_values() {
3988 let mut engine = Engine::new();
3989 engine
3990 .load([(
3991 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3992 "test.lemma",
3993 ))),
3994 r#"
3995 spec test
3996 data age: number -> suggest 25
3997 "#
3998 .to_string(),
3999 )])
4000 .unwrap();
4001
4002 let plans = engine
4003 .plans
4004 .get_plans(None, "test")
4005 .expect("plans for test");
4006 let plan = plans.values().next().expect("plan");
4007 let data_path = DataPath::new(vec![], "age".to_string());
4008
4009 let values = input_data(&[("age", "30")]);
4010
4011 let evaluate_data = resolve_evaluate_data(plan, values);
4012 let updated_value = bound_value(&evaluate_data, &data_path).expect("bound value");
4013 match &updated_value.value {
4014 crate::planning::semantics::ValueKind::Number(n) => {
4015 assert_eq!(n, &rational_new(30, 1));
4016 }
4017 other => panic!("Expected number literal, got {:?}", other),
4018 }
4019 }
4020
4021 #[test]
4022 fn test_with_raw_values_type_mismatch() {
4023 let mut engine = Engine::new();
4024 engine
4025 .load([(
4026 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4027 "test.lemma",
4028 ))),
4029 r#"
4030 spec test
4031 data age: number
4032 "#
4033 .to_string(),
4034 )])
4035 .unwrap();
4036
4037 let plans = engine
4038 .plans
4039 .get_plans(None, "test")
4040 .expect("plans for test");
4041 let plan = plans.values().next().expect("plan");
4042
4043 let values = input_data(&[("age", "thirty")]);
4044
4045 let evaluate_data = resolve_evaluate_data(plan, values);
4046 let data_path = DataPath::new(vec![], "age".to_string());
4047 match veto_reason(&evaluate_data, &data_path) {
4048 Some(reason) => {
4049 assert!(
4050 reason.contains("number"),
4051 "type mismatch must record violation reason, got: {reason}"
4052 );
4053 }
4054 None => panic!("expected veto-bound data for age=thirty"),
4055 }
4056 }
4057
4058 #[test]
4059 fn test_with_raw_values_unknown_data_ignored() {
4060 let mut engine = Engine::new();
4061 engine
4062 .load([(
4063 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4064 "test.lemma",
4065 ))),
4066 r#"
4067 spec test
4068 data known: number
4069 "#
4070 .to_string(),
4071 )])
4072 .unwrap();
4073
4074 let plans = engine
4075 .plans
4076 .get_plans(None, "test")
4077 .expect("plans for test");
4078 let plan = plans.values().next().expect("plan");
4079
4080 let values = input_data(&[("unknown", "30")]);
4081
4082 let evaluate_data = resolve_evaluate_data(plan, values);
4083 assert!(evaluate_data.bindings.is_empty());
4084 assert!(evaluate_data.ignored_unknown.iter().any(|k| k == "unknown"));
4085 }
4086
4087 #[test]
4088 fn test_with_raw_values_nested() {
4089 let mut engine = Engine::new();
4090 engine
4091 .load([(
4092 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4093 "test.lemma",
4094 ))),
4095 r#"
4096 spec private
4097 data base_price: number
4098
4099 spec test
4100 uses rules: private
4101 "#
4102 .to_string(),
4103 )])
4104 .unwrap();
4105
4106 let plans = engine
4107 .plans
4108 .get_plans(None, "test")
4109 .expect("plans for test");
4110 let plan = plans.values().next().expect("plan");
4111
4112 let values = input_data(&[("rules.base_price", "100")]);
4113
4114 let evaluate_data = resolve_evaluate_data(plan, values);
4115 let data_path = DataPath {
4116 segments: vec![PathSegment {
4117 uses: "rules".to_string(),
4118 repository: None,
4119 spec: "private".to_string(),
4120 }],
4121 data: "base_price".to_string(),
4122 };
4123 let updated_value = bound_value(&evaluate_data, &data_path).expect("bound value");
4124 match &updated_value.value {
4125 crate::planning::semantics::ValueKind::Number(n) => {
4126 assert_eq!(n, &rational_new(100, 1));
4127 }
4128 other => panic!("Expected number literal, got {:?}", other),
4129 }
4130 }
4131
4132 #[test]
4133 fn evaluate_data_should_enforce_number_maximum_constraint() {
4134 let data_path = DataPath::new(vec![], "x".to_string());
4137
4138 let max10 = crate::planning::semantics::LemmaType::primitive(
4139 crate::planning::semantics::TypeSpecification::Number {
4140 minimum: None,
4141 maximum: Some(rational_new(10, 1)),
4142 decimals: None,
4143 help: String::new(),
4144 },
4145 );
4146 let source = Source::new(
4147 crate::parsing::source::SourceType::Volatile,
4148 crate::parsing::ast::Span {
4149 start: 0,
4150 end: 0,
4151 line: 1,
4152 col: 0,
4153 },
4154 );
4155 let mut data = IndexMap::new();
4156 data.insert(
4157 data_path.clone(),
4158 crate::planning::semantics::DataDefinition::Value {
4159 value: crate::planning::semantics::LiteralValue::number(rational_new(0, 1)),
4160 resolved_type: Arc::new(max10.clone()),
4161 source: source.clone(),
4162 },
4163 );
4164
4165 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4166 let data_len = data.len();
4167 let plan = ExecutionPlan {
4168 spec_name: "test".to_string(),
4169 commentary: None,
4170 data,
4171 normal_forms: Vec::new(),
4172 rules: IndexMap::new(),
4173 data_reference_order: Vec::new(),
4174 meta: IndexMap::new(),
4175 resolved_types: ResolvedSpecTypes::default(),
4176 family_units: Arc::new(FamilyUnitCatalog::default()),
4177 signature_index: IndexMap::new(),
4178 effective: EffectiveDate::Origin,
4179 effective_from: None,
4180 effective_to: None,
4181 start_line: 1,
4182 source_type: None,
4183 needed_by_rules: vec![NeededByRules::default(); data_len],
4184 nested_needed_by_rules: IndexMap::new(),
4185 data_display: IndexMap::new(),
4186 show_rules: IndexMap::new(),
4187 reference_ends: IndexMap::new(),
4188 input_key_index,
4189 show_rule_index: IndexMap::new(),
4190 data_leaf: IndexMap::new(),
4191 row_templates: std::collections::BTreeMap::new(),
4192 planning_input: Arc::new(Vec::new()),
4193 identity: SpecIdentity {
4194 repository: None,
4195 spec: "test".to_string(),
4196 effective: EffectiveDate::Origin,
4197 },
4198 rule_candidates: std::collections::BTreeMap::new(),
4199 self_row_rules: Vec::new(),
4200 instance_lists: IndexMap::new(),
4201 row_mask: Vec::new(),
4202 };
4203
4204 let values = input_data(&[("x", "11")]);
4205
4206 let evaluate_data = resolve_evaluate_data(&plan, values);
4207 match veto_reason(&evaluate_data, &data_path) {
4208 Some(reason) => {
4209 assert!(
4210 reason.contains("maximum") || reason.contains("10"),
4211 "x=11 must violate maximum 10, got: {reason}"
4212 );
4213 }
4214 None => panic!("expected veto-bound data for x=11"),
4215 }
4216 }
4217
4218 #[test]
4219 fn evaluate_data_should_enforce_text_enum_options() {
4220 let data_path = DataPath::new(vec![], "tier".to_string());
4222
4223 let tier = crate::planning::semantics::LemmaType::primitive(
4224 crate::planning::semantics::TypeSpecification::Text {
4225 length: None,
4226 options: vec!["silver".to_string(), "gold".to_string()],
4227 help: String::new(),
4228 },
4229 );
4230 let source = Source::new(
4231 crate::parsing::source::SourceType::Volatile,
4232 crate::parsing::ast::Span {
4233 start: 0,
4234 end: 0,
4235 line: 1,
4236 col: 0,
4237 },
4238 );
4239 let mut data = IndexMap::new();
4240 data.insert(
4241 data_path.clone(),
4242 crate::planning::semantics::DataDefinition::Value {
4243 value: crate::planning::semantics::LiteralValue::text("silver".to_string()),
4244 resolved_type: Arc::new(tier.clone()),
4245 source,
4246 },
4247 );
4248
4249 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4250 let data_len = data.len();
4251 let plan = ExecutionPlan {
4252 spec_name: "test".to_string(),
4253 commentary: None,
4254 data,
4255 normal_forms: Vec::new(),
4256 rules: IndexMap::new(),
4257 data_reference_order: Vec::new(),
4258 meta: IndexMap::new(),
4259 resolved_types: ResolvedSpecTypes::default(),
4260 family_units: Arc::new(FamilyUnitCatalog::default()),
4261 signature_index: IndexMap::new(),
4262 effective: EffectiveDate::Origin,
4263 effective_from: None,
4264 effective_to: None,
4265 start_line: 1,
4266 source_type: None,
4267 needed_by_rules: vec![NeededByRules::default(); data_len],
4268 nested_needed_by_rules: IndexMap::new(),
4269 data_display: IndexMap::new(),
4270 show_rules: IndexMap::new(),
4271 reference_ends: IndexMap::new(),
4272 input_key_index,
4273 show_rule_index: IndexMap::new(),
4274 data_leaf: IndexMap::new(),
4275 row_templates: std::collections::BTreeMap::new(),
4276 planning_input: Arc::new(Vec::new()),
4277 identity: SpecIdentity {
4278 repository: None,
4279 spec: "test".to_string(),
4280 effective: EffectiveDate::Origin,
4281 },
4282 rule_candidates: std::collections::BTreeMap::new(),
4283 self_row_rules: Vec::new(),
4284 instance_lists: IndexMap::new(),
4285 row_mask: Vec::new(),
4286 };
4287
4288 let values = input_data(&[("tier", "platinum")]);
4289
4290 let evaluate_data = resolve_evaluate_data(&plan, values);
4291 match veto_reason(&evaluate_data, &data_path) {
4292 Some(reason) => {
4293 assert!(
4294 reason.contains("allowed options") || reason.contains("platinum"),
4295 "invalid enum must record violation, got: {reason}"
4296 );
4297 }
4298 None => panic!("expected veto-bound data for tier=platinum"),
4299 }
4300 }
4301
4302 #[test]
4303 fn evaluate_data_should_enforce_measure_decimals() {
4304 let data_path = DataPath::new(vec![], "price".to_string());
4307
4308 let money = crate::planning::semantics::LemmaType::primitive(
4309 crate::planning::semantics::TypeSpecification::Measure {
4310 minimum: None,
4311 maximum: None,
4312 decimals: Some(2),
4313 units: crate::planning::semantics::MeasureUnits::from(vec![
4314 crate::planning::semantics::MeasureUnit::from_decimal_factor(
4315 "eur".to_string(),
4316 rust_decimal::Decimal::from_str("1.0").unwrap(),
4317 Vec::new(),
4318 )
4319 .expect("eur unit factor must be exact decimal"),
4320 ]),
4321 traits: Vec::new(),
4322 decomposition: None,
4323 unit: None,
4324 help: String::new(),
4325 },
4326 );
4327 let source = Source::new(
4328 crate::parsing::source::SourceType::Volatile,
4329 crate::parsing::ast::Span {
4330 start: 0,
4331 end: 0,
4332 line: 1,
4333 col: 0,
4334 },
4335 );
4336 let mut data = IndexMap::new();
4337 data.insert(
4338 data_path.clone(),
4339 crate::planning::semantics::DataDefinition::Value {
4340 value: crate::planning::semantics::LiteralValue::measure(rational_zero()),
4341 resolved_type: Arc::new(money.clone()),
4342 source,
4343 },
4344 );
4345
4346 let input_key_index = data.keys().map(|p| (p.input_key(), p.clone())).collect();
4347 let data_len = data.len();
4348 let plan = ExecutionPlan {
4349 spec_name: "test".to_string(),
4350 commentary: None,
4351 data,
4352 normal_forms: Vec::new(),
4353 rules: IndexMap::new(),
4354 data_reference_order: Vec::new(),
4355 meta: IndexMap::new(),
4356 resolved_types: ResolvedSpecTypes::default(),
4357 family_units: Arc::new(FamilyUnitCatalog::default()),
4358 signature_index: IndexMap::new(),
4359 effective: EffectiveDate::Origin,
4360 effective_from: None,
4361 effective_to: None,
4362 start_line: 1,
4363 source_type: None,
4364 needed_by_rules: vec![NeededByRules::default(); data_len],
4365 nested_needed_by_rules: IndexMap::new(),
4366 data_display: IndexMap::new(),
4367 show_rules: IndexMap::new(),
4368 reference_ends: IndexMap::new(),
4369 input_key_index,
4370 show_rule_index: IndexMap::new(),
4371 data_leaf: IndexMap::new(),
4372 row_templates: std::collections::BTreeMap::new(),
4373 planning_input: Arc::new(Vec::new()),
4374 identity: SpecIdentity {
4375 repository: None,
4376 spec: "test".to_string(),
4377 effective: EffectiveDate::Origin,
4378 },
4379 rule_candidates: std::collections::BTreeMap::new(),
4380 self_row_rules: Vec::new(),
4381 instance_lists: IndexMap::new(),
4382 row_mask: Vec::new(),
4383 };
4384
4385 let values = input_data(&[("price", "1.234 eur")]);
4386
4387 let evaluate_data = resolve_evaluate_data(&plan, values);
4388 match veto_reason(&evaluate_data, &data_path) {
4389 Some(reason) => {
4390 assert!(
4391 reason.contains("decimals") || reason.contains("decimal"),
4392 "1.234 eur must violate decimals=2, got: {reason}"
4393 );
4394 }
4395 None => panic!("expected veto-bound data for price=1.234 eur"),
4396 }
4397 }
4398
4399 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
4400 ExecutionPlan {
4401 spec_name: "s".into(),
4402 commentary: None,
4403 data: IndexMap::new(),
4404 normal_forms: Vec::new(),
4405 rules: IndexMap::new(),
4406 data_reference_order: Vec::new(),
4407 meta: IndexMap::new(),
4408 resolved_types: ResolvedSpecTypes::default(),
4409 family_units: Arc::new(FamilyUnitCatalog::default()),
4410 signature_index: IndexMap::new(),
4411 effective,
4412 effective_from: None,
4413 effective_to: None,
4414 start_line: 1,
4415 source_type: None,
4416 needed_by_rules: Vec::new(),
4417 nested_needed_by_rules: IndexMap::new(),
4418 data_display: IndexMap::new(),
4419 show_rules: IndexMap::new(),
4420 reference_ends: IndexMap::new(),
4421 input_key_index: IndexMap::new(),
4422 show_rule_index: IndexMap::new(),
4423 data_leaf: IndexMap::new(),
4424 row_templates: std::collections::BTreeMap::new(),
4425 planning_input: Arc::new(Vec::new()),
4426 identity: SpecIdentity {
4427 repository: None,
4428 spec: "s".to_string(),
4429 effective: EffectiveDate::Origin,
4430 },
4431 rule_candidates: std::collections::BTreeMap::new(),
4432 self_row_rules: Vec::new(),
4433 instance_lists: IndexMap::new(),
4434 row_mask: Vec::new(),
4435 }
4436 }
4437
4438 fn plans_by_effective(
4439 plans: impl IntoIterator<Item = ExecutionPlan>,
4440 ) -> BTreeMap<EffectiveDate, std::sync::Arc<ExecutionPlan>> {
4441 plans
4442 .into_iter()
4443 .map(|plan| (plan.effective.clone(), std::sync::Arc::new(plan)))
4444 .collect()
4445 }
4446
4447 #[test]
4449 fn plan_set_plans_are_in_ascending_effective_order() {
4450 let june = DateTimeValue {
4451 year: 2025,
4452 month: 6,
4453 day: 1,
4454 hour: 0,
4455 minute: 0,
4456 second: 0,
4457 microsecond: 0,
4458 timezone: None,
4459 granularity: DateGranularity::Full,
4460 };
4461 let dec = DateTimeValue {
4462 year: 2025,
4463 month: 12,
4464 day: 1,
4465 hour: 0,
4466 minute: 0,
4467 second: 0,
4468 microsecond: 0,
4469 timezone: None,
4470 granularity: DateGranularity::Full,
4471 };
4472
4473 let plans = plans_by_effective([
4474 empty_plan(EffectiveDate::Origin),
4475 empty_plan(EffectiveDate::DateTimeValue(june)),
4476 empty_plan(EffectiveDate::DateTimeValue(dec)),
4477 ]);
4478
4479 let effectives: Vec<_> = plans.keys().cloned().collect();
4480 for window in effectives.windows(2) {
4481 assert!(
4482 window[0] < window[1],
4483 "plans must be strictly ascending: {:?} >= {:?}",
4484 window[0],
4485 window[1]
4486 );
4487 }
4488 }
4489
4490 #[test]
4491 fn plan_at_exact_boundary_selects_later_slice() {
4492 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4493
4494 let june = DateTimeValue {
4495 year: 2025,
4496 month: 6,
4497 day: 1,
4498 hour: 0,
4499 minute: 0,
4500 second: 0,
4501 microsecond: 0,
4502 timezone: None,
4503
4504 granularity: DateGranularity::Full,
4505 };
4506 let dec = DateTimeValue {
4507 year: 2025,
4508 month: 12,
4509 day: 1,
4510 hour: 0,
4511 minute: 0,
4512 second: 0,
4513 microsecond: 0,
4514 timezone: None,
4515
4516 granularity: DateGranularity::Full,
4517 };
4518
4519 let june_key = EffectiveDate::DateTimeValue(june.clone());
4520 let dec_key = EffectiveDate::DateTimeValue(dec.clone());
4521 let plans = plans_by_effective([
4522 empty_plan(EffectiveDate::Origin),
4523 empty_plan(june_key.clone()),
4524 empty_plan(dec_key.clone()),
4525 ]);
4526
4527 let june_plan = plan_at(&plans, &june_key).expect("boundary instant");
4528 assert!(std::ptr::eq(
4529 june_plan,
4530 plans.get(&june_key).expect("june slice").as_ref()
4531 ));
4532
4533 let dec_plan = plan_at(&plans, &dec_key).expect("dec boundary");
4534 assert!(std::ptr::eq(
4535 dec_plan,
4536 plans.get(&dec_key).expect("dec slice").as_ref()
4537 ));
4538 }
4539
4540 #[test]
4541 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
4542 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4543
4544 let june = DateTimeValue {
4545 year: 2025,
4546 month: 6,
4547 day: 1,
4548 hour: 0,
4549 minute: 0,
4550 second: 0,
4551 microsecond: 0,
4552 timezone: None,
4553
4554 granularity: DateGranularity::Full,
4555 };
4556 let may_end = DateTimeValue {
4557 year: 2025,
4558 month: 5,
4559 day: 31,
4560 hour: 23,
4561 minute: 59,
4562 second: 59,
4563 microsecond: 0,
4564 timezone: None,
4565
4566 granularity: DateGranularity::DateTime,
4567 };
4568
4569 let origin = EffectiveDate::Origin;
4570 let plans = plans_by_effective([
4571 empty_plan(origin.clone()),
4572 empty_plan(EffectiveDate::DateTimeValue(june)),
4573 ]);
4574
4575 let may_instant = EffectiveDate::DateTimeValue(may_end);
4576 let may_plan = plan_at(&plans, &may_instant).expect("may 31");
4577 assert!(std::ptr::eq(
4578 may_plan,
4579 plans.get(&origin).expect("origin slice").as_ref()
4580 ));
4581 }
4582
4583 #[test]
4584 fn plan_at_single_plan_matches_any_instant_after_start() {
4585 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
4586
4587 let t = DateTimeValue {
4588 year: 2025,
4589 month: 3,
4590 day: 1,
4591 hour: 0,
4592 minute: 0,
4593 second: 0,
4594 microsecond: 0,
4595 timezone: None,
4596
4597 granularity: DateGranularity::Full,
4598 };
4599 let start = EffectiveDate::DateTimeValue(DateTimeValue {
4600 year: 2025,
4601 month: 1,
4602 day: 1,
4603 hour: 0,
4604 minute: 0,
4605 second: 0,
4606 microsecond: 0,
4607 timezone: None,
4608
4609 granularity: DateGranularity::Full,
4610 });
4611 let plans = plans_by_effective([empty_plan(start.clone())]);
4612 let instant = EffectiveDate::DateTimeValue(t);
4613 let selected = plan_at(&plans, &instant).expect("inside single slice");
4614 assert!(std::ptr::eq(
4615 selected,
4616 plans.get(&start).expect("single slice").as_ref()
4617 ));
4618 }
4619
4620 #[test]
4623 fn show_json_shape_contract() {
4624 let mut engine = Engine::new();
4625 engine
4626 .load([(
4627 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4628 "test.lemma",
4629 ))),
4630 r#"
4631 spec pricing
4632 data bridge_height: measure
4633 -> unit meter: 1
4634 -> suggest 100 meter
4635 data quantity: number -> minimum 0
4636 rule cost: bridge_height * quantity
4637 "#
4638 .to_string(),
4639 )])
4640 .unwrap();
4641 let now = DateTimeValue::now();
4642 let schema = engine.show(None, "pricing", Some(&now)).unwrap();
4643
4644 let value = crate::api::show_document(&engine, &schema);
4645
4646 let bh = &value["properties"]["bridge_height"];
4647 assert!(
4648 bh.is_object(),
4649 "data entry must be a named object, not tuple"
4650 );
4651 assert!(
4652 bh.get("x-lemma-type").is_some(),
4653 "data entry must expose `x-lemma-type` field"
4654 );
4655 assert!(
4656 bh.get("x-lemma-suggestion").is_some(),
4657 "bridge_height exposes `-> suggest` as schema suggestion"
4658 );
4659 assert!(
4660 bh.get("x-lemma-value").is_none(),
4661 "bridge_height is not filled from spec"
4662 );
4663
4664 let ty = &bh["x-lemma-type"];
4665 assert_eq!(
4666 ty["kind"], "measure",
4667 "kind tag sits on the type object itself"
4668 );
4669 assert!(
4670 ty["units"].is_array(),
4671 "measure-only fields flatten up to top level"
4672 );
4673 assert!(
4674 ty.get("options").is_none(),
4675 "text-only fields must not leak"
4676 );
4677
4678 let quantity = &value["properties"]["quantity"];
4679 assert_eq!(quantity["x-lemma-type"]["kind"], "number");
4680 assert!(
4681 quantity.get("x-lemma-suggestion").is_none(),
4682 "quantity has no suggestion"
4683 );
4684 assert!(
4685 quantity.get("x-lemma-value").is_none(),
4686 "quantity has no fill literal"
4687 );
4688
4689 let cost = &value["x-lemma-rules"]["cost"];
4690 assert_eq!(
4691 cost["type"]["kind"], "measure",
4692 "rule types nest under type"
4693 );
4694 assert!(
4695 cost["type"]["units"].is_array()
4696 && !cost["type"]["units"].as_array().unwrap().is_empty(),
4697 "measure rule result types expose declared units"
4698 );
4699 assert!(
4700 cost["type"]["units"][0].get("factor").is_some(),
4701 "measure rule units use factor field"
4702 );
4703 assert!(cost.get("branches").is_some(), "ShowRule exposes branches");
4704 assert!(
4705 cost.get("depends_on_rules").is_some(),
4706 "ShowRule exposes depends_on_rules"
4707 );
4708 }
4709
4710 #[test]
4711 fn show_rule_result_units_contract() {
4712 let mut engine = Engine::new();
4713 engine
4714 .load([(
4715 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4716 "units_contract.lemma",
4717 ))),
4718 r#"
4719 spec units_contract
4720 data money: measure
4721 -> unit eur: 1
4722 -> unit usd: 0.91
4723 data rate: ratio
4724 -> unit basis_points: 10000
4725 -> unit percent: 100
4726 -> suggest 500 basis_points
4727 rule total: money
4728 rule rate_out: rate
4729 "#
4730 .to_string(),
4731 )])
4732 .unwrap();
4733 let now = DateTimeValue::now();
4734 let schema = engine.show(None, "units_contract", Some(&now)).unwrap();
4735 let value = crate::api::show_document(&engine, &schema);
4736
4737 let money_units = &value["properties"]["money"]["x-lemma-type"]["units"];
4738 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
4739 assert!(money_units[0].get("name").is_some());
4740 assert!(money_units[0].get("factor").is_some());
4741 assert!(money_units[0]["factor"].get("numer").is_some());
4742 assert!(money_units[0]["factor"].get("denom").is_some());
4743
4744 let rate_units = &value["properties"]["rate"]["x-lemma-type"]["units"];
4745 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
4746 assert!(rate_units[0].get("name").is_some());
4747 assert!(rate_units[0].get("value").is_some());
4748 assert!(rate_units[0]["value"].get("numer").is_some());
4749 assert!(rate_units[0]["value"].get("denom").is_some());
4750
4751 let total_rule_units = &value["x-lemma-rules"]["total"]["type"]["units"];
4752 let money_unit_names: Vec<_> = money_units
4753 .as_array()
4754 .unwrap()
4755 .iter()
4756 .map(|u| u["name"].as_str().unwrap())
4757 .collect();
4758 let total_rule_unit_names: Vec<_> = total_rule_units
4759 .as_array()
4760 .unwrap()
4761 .iter()
4762 .map(|u| u["name"].as_str().unwrap())
4763 .collect();
4764 assert_eq!(total_rule_unit_names, money_unit_names);
4765
4766 let rate_out_rule_units = &value["x-lemma-rules"]["rate_out"]["type"]["units"];
4767 let rate_unit_names: Vec<_> = rate_units
4768 .as_array()
4769 .unwrap()
4770 .iter()
4771 .map(|u| u["name"].as_str().unwrap())
4772 .collect();
4773 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
4774 .as_array()
4775 .unwrap()
4776 .iter()
4777 .map(|u| u["name"].as_str().unwrap())
4778 .collect();
4779 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
4780 }
4781
4782 #[test]
4783 fn show_document_rules_round_trip_as_show_rule() {
4784 let mut engine = Engine::new();
4785 engine
4786 .load([(
4787 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4788 r#"
4789 spec s
4790 data age: number -> minimum 0 -> suggest 18
4791 data grade: text -> options "A", "B", "C"
4792 rule adult: age >= 18
4793 "#
4794 .to_string(),
4795 )])
4796 .unwrap();
4797 let now = DateTimeValue::now();
4798 let schema = engine.show(None, "s", Some(&now)).unwrap();
4799
4800 let document = crate::api::show_document(&engine, &schema);
4801 let rule_json = document["x-lemma-rules"]["adult"].clone();
4802 let rule: crate::api::ShowRule = serde_json::from_value(rule_json.clone()).unwrap();
4803 assert_eq!(serde_json::to_value(&rule).unwrap(), rule_json);
4804 assert!(
4805 document["properties"].get("grade").is_none(),
4806 "unused grade is Types-only, not Data"
4807 );
4808 let grade = &document["x-lemma-types"]["grade"];
4809 assert_eq!(grade["options"], serde_json::json!(["A", "B", "C"]));
4810 assert_eq!(grade["kind"], "text");
4811 }
4812
4813 const COST_PRICE_SPEC: &str = r#"
4814spec cost_price
4815uses lemma units
4816
4817data money: measure
4818 -> unit eur: 1.00
4819 -> unit inr: 0.0092
4820 -> decimals 2
4821
4822data labor_cost: measure
4823 -> unit eur_per_hour: eur/hour
4824 -> unit inr_per_hour: inr/hour
4825 -> suggest 25 eur_per_hour
4826
4827data product_cost: measure
4828 -> unit eur_per_kg: eur/kilogram
4829 -> unit inr_per_kg: inr/kilogram
4830 -> suggest 4 eur_per_kg
4831
4832data throughput: measure
4833 -> unit kg_per_hour: kilogram/hour
4834 -> suggest 12 kg_per_hour
4835
4836rule cost_price: product_cost + labor_cost / throughput
4837"#;
4838
4839 fn cost_price_inputs() -> IndexMap<String, EvaluateDataValue> {
4840 let mut data = IndexMap::new();
4841 data.insert(
4842 "product_cost".into(),
4843 EvaluateDataValue::string("4 eur_per_kg"),
4844 );
4845 data.insert(
4846 "labor_cost".into(),
4847 EvaluateDataValue::string("25 eur_per_hour"),
4848 );
4849 data.insert(
4850 "throughput".into(),
4851 EvaluateDataValue::string("12 kg_per_hour"),
4852 );
4853 data
4854 }
4855
4856 const FILM_ACCESS: &str = r#"
4857spec premium_membership
4858uses lemma units
4859data start: date
4860data length: units.calendar
4861rule valid: now in start...start + length
4862
4863spec film_access
4864uses premium_membership
4865data type: text
4866 -> option "rental"
4867 -> option "purchase"
4868data views_consumed: number
4869data premium_member: boolean
4870rule max_views: 3
4871 unless premium_membership.valid then 10
4872 unless premium_member then 5
4873rule can_view: no
4874 unless type is "rental" and views_consumed < max_views then yes
4875 unless type is "purchase" then yes
4876"#;
4877
4878 fn film_access_effective() -> DateTimeValue {
4879 DateTimeValue {
4880 year: 2027,
4881 month: 2,
4882 day: 14,
4883 hour: 12,
4884 minute: 0,
4885 second: 0,
4886 microsecond: 0,
4887 timezone: Some(TimezoneValue { offset_minutes: 0 }),
4888 granularity: DateGranularity::DateTime,
4889 }
4890 }
4891
4892 #[test]
4893 fn evaluate_data_accepts_per_unit_measure_equivalent_to_canonical_magnitude() {
4894 let mut engine = Engine::new();
4895 engine
4896 .load([(
4897 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4898 "cost_price.lemma",
4899 ))),
4900 COST_PRICE_SPEC.to_string(),
4901 )])
4902 .expect("load");
4903 let plans = engine
4904 .plans
4905 .get_plans(None, "cost_price")
4906 .expect("plans for cost_price");
4907 let plan = plans.values().next().expect("plan");
4908 let mut data = IndexMap::new();
4909 data.insert(
4910 "product_cost".into(),
4911 EvaluateDataValue::string("4 eur_per_kg"),
4912 );
4913 data.insert(
4914 "labor_cost".into(),
4915 EvaluateDataValue::string("0.0069444444444444444444444444 eur_per_hour"),
4916 );
4917 data.insert(
4918 "throughput".into(),
4919 EvaluateDataValue::string("0.0033333333333333333333333333 kg_per_hour"),
4920 );
4921 let evaluate_data = resolve_evaluate_data(plan, data);
4922 assert!(
4923 !evaluate_data
4924 .bindings
4925 .values()
4926 .any(|b| matches!(b, OperationResult::Veto(_))),
4927 "parsed decimal run data values must not be veto-bound after input boundary: {:?}",
4928 evaluate_data.bindings
4929 );
4930 }
4931
4932 #[test]
4933 fn evaluate_data_accepts_per_unit_measure() {
4934 let mut engine = Engine::new();
4935 engine
4936 .load([(
4937 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4938 "cost_price.lemma",
4939 ))),
4940 COST_PRICE_SPEC.to_string(),
4941 )])
4942 .expect("load");
4943 let plans = engine
4944 .plans
4945 .get_plans(None, "cost_price")
4946 .expect("plans for cost_price");
4947 let plan = plans.values().next().expect("plan");
4948 let evaluate_data = resolve_evaluate_data(plan, cost_price_inputs());
4949 assert!(!evaluate_data
4950 .bindings
4951 .values()
4952 .any(|b| matches!(b, OperationResult::Veto(_))));
4953 }
4954
4955 #[test]
4956 fn evaluate_data_rejects_oversize_input() {
4957 let mut engine = Engine::new();
4958 engine
4959 .load([(
4960 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
4961 "cost_price.lemma",
4962 ))),
4963 COST_PRICE_SPEC.to_string(),
4964 )])
4965 .expect("load");
4966 let plans = engine
4967 .plans
4968 .get_plans(None, "cost_price")
4969 .expect("plans for cost_price");
4970 let plan = plans.values().next().expect("plan");
4971 let mut data = cost_price_inputs();
4972 data.insert(
4973 "labor_cost".into(),
4974 EvaluateDataValue::string(
4975 "1000000000000000000000000000000000000000000000000000000000000 eur_per_hour",
4976 ),
4977 );
4978 let evaluate_data = resolve_evaluate_data(plan, data);
4979 assert!(matches!(
4980 evaluate_data
4981 .bindings
4982 .get(&DataPath::local("labor_cost".into())),
4983 Some(OperationResult::Veto(_))
4984 ));
4985 }
4986 #[test]
4987 fn typedecl_default_stays_typedecl_on_immutable_plan() {
4988 let mut engine = Engine::new();
4989 engine
4990 .load([(
4991 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
4992 r#"
4993 spec s
4994 data n: number -> suggest 42
4995 rule r: n
4996 "#
4997 .to_string(),
4998 )])
4999 .expect("load");
5000
5001 let plans = engine.plans.get_plans(None, "s").expect("plans for s");
5002 let plan = plans.values().next().expect("plan");
5003 let path = DataPath::local("n".into());
5004 match plan.data.get(&path).expect("n") {
5005 DataDefinition::TypeDeclaration {
5006 declared_suggestion: Some(_),
5007 ..
5008 } => {}
5009 other => panic!("expected TypeDeclaration with default, got {other:?}"),
5010 }
5011 }
5012
5013 fn response_missing_data_union(response: &crate::Response) -> Vec<String> {
5014 let mut seen = std::collections::HashSet::new();
5015 let mut names = Vec::new();
5016 for result in response.results.values() {
5017 for key in result.missing_data() {
5018 if seen.insert(key.clone()) {
5019 names.push(key.clone());
5020 }
5021 }
5022 }
5023 names
5024 }
5025
5026 #[test]
5027 fn evaluate_prunes_inactive_nut_branches_for_total_price() {
5028 let code = r#"
5029spec bag
5030uses lemma units
5031
5032data weight: measure
5033 -> unit kg: 1
5034
5035data money: measure
5036 -> unit eur: 1
5037
5038data price_per_weight: measure
5039 -> unit eur_per_kg: eur/kg
5040
5041data item_cost: price_per_weight
5042data roasting: price_per_weight
5043data chocolatizing: price_per_weight
5044
5045rule total_price: weight * (item_cost + roasting + chocolatizing)
5046
5047spec calc
5048uses bag
5049 -> with item_cost: item_cost
5050 -> with roasting: roasting
5051
5052data type_of_nut: text -> options "peanut", "cashew"
5053
5054rule price_peanut: 1.5 eur_per_kg
5055rule price_peanut_roasting: 0.45 eur_per_kg
5056
5057rule price_cashew: 2.0 eur_per_kg
5058rule price_cashew_roasting: 0.55 eur_per_kg
5059
5060rule item_cost: veto "No item cost"
5061 unless type_of_nut is "peanut" then price_peanut
5062 unless type_of_nut is "cashew" then price_cashew
5063
5064rule roasting: veto "No roasting"
5065 unless type_of_nut is "peanut" then price_peanut_roasting
5066 unless type_of_nut is "cashew" then price_cashew_roasting
5067
5068rule total_price: bag.total_price
5069"#;
5070
5071 let mut engine = Engine::new();
5072 engine
5073 .load([(
5074 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5075 "calc.lemma",
5076 ))),
5077 code.to_string(),
5078 )])
5079 .unwrap();
5080
5081 let now = DateTimeValue::now();
5082 let mut inputs = IndexMap::new();
5083 inputs.insert("type_of_nut".to_string(), "peanut".into());
5084 let response = engine
5085 .evaluate(
5086 None,
5087 "calc",
5088 Some(&now),
5089 inputs,
5090 Some(&["total_price".to_string()]),
5091 false,
5092 )
5093 .expect("evaluate must succeed");
5094
5095 assert_eq!(
5096 response_missing_data_union(&response),
5097 vec!["bag.weight".to_string(), "bag.chocolatizing".to_string(),],
5098 "supplied type_of_nut and with-bound roasting/item_cost omitted"
5099 );
5100 }
5101
5102 #[test]
5103 fn evaluate_includes_membership_dates_when_premium_member_false() {
5104 let mut engine = Engine::new();
5105 engine
5106 .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5107 .expect("film_access spec must load");
5108 let now = film_access_effective();
5109 let mut inputs = IndexMap::new();
5110 inputs.insert("type".to_string(), "rental".into());
5111 inputs.insert("views_consumed".to_string(), "6".into());
5112 inputs.insert("premium_member".to_string(), "false".into());
5113 let response = engine
5114 .evaluate(
5115 None,
5116 "film_access",
5117 Some(&now),
5118 inputs,
5119 Some(&["can_view".to_string()]),
5120 false,
5121 )
5122 .expect("evaluate must succeed");
5123
5124 assert_eq!(
5125 response_missing_data_union(&response),
5126 vec![
5127 "premium_membership.start".to_string(),
5128 "premium_membership.length".to_string(),
5129 ],
5130 "membership dates when premium_member is false"
5131 );
5132 }
5133
5134 #[test]
5135 fn evaluate_includes_membership_dates_when_premium_member_unknown() {
5136 let mut engine = Engine::new();
5137 engine
5138 .load([(crate::SourceType::Volatile, FILM_ACCESS.to_string())])
5139 .expect("film_access spec must load");
5140 let now = film_access_effective();
5141 let mut inputs = IndexMap::new();
5142 inputs.insert("type".to_string(), "rental".into());
5143 inputs.insert("views_consumed".to_string(), "6".into());
5144 let response = engine
5145 .evaluate(
5146 None,
5147 "film_access",
5148 Some(&now),
5149 inputs,
5150 Some(&["can_view".to_string()]),
5151 false,
5152 )
5153 .expect("evaluate must succeed");
5154
5155 assert_eq!(
5156 response_missing_data_union(&response),
5157 vec![
5158 "premium_member".to_string(),
5159 "premium_membership.start".to_string(),
5160 "premium_membership.length".to_string(),
5161 ],
5162 "premium_member plus membership dates when unknown"
5163 );
5164 }
5165
5166 const UNITS_SPEC: &str = r#"
5167spec units
5168uses lemma units
5169data money: measure
5170 -> unit eur: 1
5171 -> decimals 2
5172"#;
5173
5174 const WAREHOUSING_SPEC: &str = r#"
5175spec warehousing
5176uses units
5177uses si: lemma units
5178
5179data units_per_pallet: number
5180 -> minimum 1
5181 -> suggest 1
5182
5183data storage_duration: si.duration
5184 -> minimum 0 week
5185 -> suggest 10 day
5186
5187data interbranch_transport_per_pallet: units.money
5188 -> minimum 0 eur
5189 -> suggest 0 eur
5190
5191data inbound_handling_per_pallet: units.money
5192 -> minimum 0 eur
5193 -> suggest 0 eur
5194
5195data storage_per_pallet_per_week: units.money
5196 -> minimum 0 eur
5197 -> suggest 10 eur
5198
5199data labeling_per_pallet: units.money
5200 -> minimum 0 eur
5201 -> suggest 0 eur
5202
5203data outbound_handling_per_pallet: units.money
5204 -> minimum 0 eur
5205 -> suggest 0 eur
5206
5207rule storage_cost_per_pallet:
5208 storage_per_pallet_per_week
5209 * ceil (storage_duration as week as number)
5210
5211rule total_logistics_per_pallet:
5212 interbranch_transport_per_pallet
5213 + inbound_handling_per_pallet
5214 + storage_cost_per_pallet
5215 + labeling_per_pallet
5216 + outbound_handling_per_pallet
5217
5218rule total_logistics_per_ce:
5219 total_logistics_per_pallet / units_per_pallet
5220"#;
5221
5222 const QUOTATION_SPEC: &str = r#"
5223spec quotation
5224uses wh: warehousing
5225rule total: wh.total_logistics_per_ce
5226"#;
5227
5228 fn load_cross_spec_fixtures(engine: &mut Engine) {
5229 engine
5230 .load([(crate::SourceType::Volatile, UNITS_SPEC.to_string())])
5231 .expect("units spec must load");
5232 engine
5233 .load([(crate::SourceType::Volatile, WAREHOUSING_SPEC.to_string())])
5234 .expect("warehousing spec must load");
5235 }
5236
5237 #[test]
5238 fn quotation_inherits_unit_scope_from_warehousing_import() {
5239 let mut engine = Engine::new();
5240 load_cross_spec_fixtures(&mut engine);
5241 engine
5242 .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5243 .expect("quotation must plan without uses lemma units");
5244 let plans = engine
5245 .plans
5246 .get_plans(None, "quotation")
5247 .expect("plans for quotation");
5248 let plan = plans.values().next().expect("plan");
5249
5250 let expression_units = &plan.resolved_types.unit_index;
5251 expression_units
5252 .resolve("week")
5253 .expect("week reaches quotation via wh → si");
5254 expression_units
5255 .resolve("wh.week")
5256 .expect("alias.unit resolves week in warehousing scope");
5257 expression_units
5258 .resolve("minute")
5259 .expect("minute reaches quotation via transitive unit scope");
5260 }
5261
5262 fn warehousing_default_inputs(prefix: &str) -> IndexMap<String, EvaluateDataValue> {
5263 let key = |name: &str| {
5264 if prefix.is_empty() {
5265 name.to_string()
5266 } else {
5267 format!("{prefix}.{name}")
5268 }
5269 };
5270 IndexMap::from([
5271 (key("units_per_pallet"), EvaluateDataValue::string("1")),
5272 (key("storage_duration"), EvaluateDataValue::string("10 day")),
5273 (
5274 key("interbranch_transport_per_pallet"),
5275 EvaluateDataValue::string("0 eur"),
5276 ),
5277 (
5278 key("inbound_handling_per_pallet"),
5279 EvaluateDataValue::string("0 eur"),
5280 ),
5281 (
5282 key("storage_per_pallet_per_week"),
5283 EvaluateDataValue::string("10 eur"),
5284 ),
5285 (
5286 key("labeling_per_pallet"),
5287 EvaluateDataValue::string("0 eur"),
5288 ),
5289 (
5290 key("outbound_handling_per_pallet"),
5291 EvaluateDataValue::string("0 eur"),
5292 ),
5293 ])
5294 }
5295
5296 #[test]
5297 fn quotation_evaluates_cross_spec_duration_conversion() {
5298 let mut engine = Engine::new();
5299 load_cross_spec_fixtures(&mut engine);
5300 engine
5301 .load([(crate::SourceType::Volatile, QUOTATION_SPEC.to_string())])
5302 .expect("quotation must load");
5303 let plans = engine
5304 .plans
5305 .get_plans(None, "quotation")
5306 .expect("plans for quotation");
5307 let plan = plans.values().next().expect("plan");
5308 assert!(
5309 plan.resolved_types
5310 .unit_index
5311 .unique_owner("week")
5312 .is_none(),
5313 "consumer unit_index must not contain week"
5314 );
5315 let now = DateTimeValue::now();
5316 let response = engine
5317 .evaluate(
5318 None,
5319 "quotation",
5320 Some(&now),
5321 warehousing_default_inputs("wh"),
5322 None,
5323 false,
5324 )
5325 .expect("quotation must evaluate");
5326 let display = response
5327 .results
5328 .get("total")
5329 .expect("rule total must be present")
5330 .result()
5331 .expect("total must have display")
5332 .to_string();
5333 assert_eq!(
5334 display, "20.00 eur",
5335 "10 eur/week * ceil(10 day as week) / 1 CE must be 20.00 eur, got: {display}"
5336 );
5337 }
5338
5339 #[test]
5340 fn ratio_range_default_endpoints_must_be_ratio_not_measure() {
5341 let code = r#"
5342spec policy
5343data allowed_band: ratio range -> suggest 10%...50%
5344rule band: allowed_band
5345"#;
5346 let mut engine = Engine::new();
5347 engine
5348 .load([(
5349 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5350 "ratio_range_endpoint_typing.lemma",
5351 ))),
5352 code.to_string(),
5353 )])
5354 .unwrap();
5355
5356 let plans = engine
5357 .plans
5358 .get_plans(None, "policy")
5359 .expect("plans for policy");
5360 let plan = plans.values().next().expect("plan");
5361 let path = DataPath::local("allowed_band".into());
5362 let def = plan.data.get(&path).expect("allowed_band in plan.data");
5363 let suggestion = def
5364 .bound_suggestion()
5365 .expect("declared default must exist")
5366 .to_literal();
5367
5368 let (left, right) = match &suggestion.value {
5369 crate::planning::semantics::ValueKind::Range(l, r) => (l.as_ref(), r.as_ref()),
5370 other => panic!("expected Range, got {other:?}"),
5371 };
5372 for (label, endpoint) in [("left", left), ("right", right)] {
5373 assert!(
5374 matches!(
5375 &endpoint.value,
5376 crate::planning::semantics::ValueKind::Ratio(_)
5377 ),
5378 "{label} endpoint must be Ratio for a percent literal in a ratio range default, got {:?}",
5379 endpoint.value
5380 );
5381 assert!(
5382 matches!(
5383 &endpoint.value,
5384 crate::planning::semantics::ValueKind::Ratio(_)
5385 ),
5386 "{label} endpoint ValueKind must be Ratio (got {:?})",
5387 endpoint.value
5388 );
5389 }
5390 }
5391
5392 #[test]
5393 fn ratio_range_typedef_with_second_ratio_field_loads() {
5394 let code = r#"
5395spec policy
5396data margin_pct: ratio -> suggest 15%
5397data allowed_band: ratio range
5398rule margin: margin_pct
5399rule band_slot: allowed_band
5400"#;
5401 let mut engine = Engine::new();
5402 engine
5403 .load([(
5404 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5405 "ratio_range_load.lemma",
5406 ))),
5407 code.to_string(),
5408 )])
5409 .unwrap();
5410
5411 let plans = engine
5412 .plans
5413 .get_plans(None, "policy")
5414 .expect("plans for policy");
5415 let plan = plans.values().next().expect("plan");
5416 let path = DataPath::local("allowed_band".into());
5417 let def = plan.data.get(&path).expect("allowed_band in plan.data");
5418 let lemma_type = def
5419 .schema_type()
5420 .expect("allowed_band must be a typed data slot");
5421 match &lemma_type.specifications {
5422 TypeSpecification::RatioRange { units, .. } => {
5423 let names: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5424 assert!(
5425 names.contains(&"percent"),
5426 "ratio range must inherit builtin percent, got {names:?}"
5427 );
5428 }
5429 other => panic!("allowed_band must be RatioRange, got {other:?}"),
5430 }
5431 }
5432
5433 #[test]
5434 fn show_display_indents_every_commentary_line() {
5435 let mut engine = Engine::new();
5436 engine
5437 .load([(
5438 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5439 "alpha2.lemma",
5440 ))),
5441 "spec alpha2\n\"\"\"\nAdded:\nSS: South Sudan\n\nRemoved:\nYU: Yugoslavia\n\"\"\"\ndata code: text\nrule name: code\n"
5442 .to_string(),
5443 )])
5444 .unwrap();
5445 let now = DateTimeValue::now();
5446 let text = engine.show(None, "alpha2", Some(&now)).unwrap().to_string();
5447 assert!(
5448 text.starts_with(
5449 "Spec: alpha2\n Added:\n SS: South Sudan\n\n Removed:\n YU: Yugoslavia\n"
5450 ),
5451 "{text}"
5452 );
5453 }
5454
5455 #[test]
5456 fn show_display_puts_effective_window_on_header_without_other_versions() {
5457 let mut engine = Engine::new();
5458 engine
5459 .load([(
5460 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5461 "rates.lemma",
5462 ))),
5463 "spec rates 2024-01-01\n\"\"\"\nStandard rate\n\"\"\"\ndata x: 1\nrule r: x\n\nspec rates 2025-01-01\ndata x: 2\nrule r: x\n"
5464 .to_string(),
5465 )])
5466 .unwrap();
5467 let effective: DateTimeValue = "2024-06-01".parse().expect("date must parse");
5468 let text = engine
5469 .show(None, "rates", Some(&effective))
5470 .unwrap()
5471 .to_string();
5472 assert!(
5473 text.starts_with("Spec: rates 2024-01-01...2025-01-01\n Standard rate\n"),
5474 "{text}"
5475 );
5476 assert!(!text.contains("versions"), "{text}");
5477 assert!(!text.contains("effective_"), "{text}");
5478 }
5479
5480 #[test]
5481 fn show_display_undated_predecessor_uses_until() {
5482 let mut engine = Engine::new();
5483 engine
5484 .load([(
5485 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5486 "calc.lemma",
5487 ))),
5488 "spec calc\ndata x: 1\nrule r: x\n\nspec calc 2027\ndata x: 2\nrule r: x\n"
5489 .to_string(),
5490 )])
5491 .unwrap();
5492 let effective: DateTimeValue = "2026-01-01".parse().expect("date must parse");
5493 let text = engine
5494 .show(None, "calc", Some(&effective))
5495 .unwrap()
5496 .to_string();
5497 assert!(text.starts_with("Spec: calc ...2027\n"), "{text}");
5498 assert!(!text.contains('→'), "{text}");
5499 }
5500
5501 fn nested_import_plan() -> (Engine, Arc<ExecutionPlan>) {
5502 let mut engine = Engine::new();
5503 engine
5504 .load([(
5505 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
5506 "peel.lemma",
5507 ))),
5508 r#"
5509spec leaf
5510data x: number
5511rule rx: x
5512
5513spec mid
5514uses nested: leaf
5515data y: number
5516rule ry: y
5517
5518spec outer
5519uses m: mid
5520"#
5521 .to_string(),
5522 )])
5523 .expect("load nested import fixture");
5524 let plans = engine
5525 .plans
5526 .get_plans(None, "outer")
5527 .expect("plans for outer");
5528 let plan = Arc::clone(plans.values().next().expect("outer plan"));
5529 (engine, plan)
5530 }
5531
5532 #[test]
5533 fn input_key_index_matches_input_key_entries_for_nested_plan() {
5534 let (_, plan) = nested_import_plan();
5535
5536 let mut entry_keys: Vec<String> = plan
5537 .input_key_entries()
5538 .into_iter()
5539 .map(|(path, _)| path.input_key())
5540 .collect();
5541 let mut index_keys: Vec<String> = plan.input_key_index.keys().cloned().collect();
5542 entry_keys.sort();
5543 index_keys.sort();
5544 assert_eq!(entry_keys, index_keys);
5545
5546 let show_keys: Vec<String> = plan
5547 .nested_input_keys()
5548 .into_iter()
5549 .map(|(path, _)| path.input_key())
5550 .collect();
5551 assert!(!show_keys.iter().any(|key| key == "m"), "{show_keys:?}");
5552 assert!(entry_keys.iter().any(|key| key == "m"), "{entry_keys:?}");
5553 assert!(
5554 show_keys.len() < entry_keys.len(),
5555 "{show_keys:?} vs {entry_keys:?}"
5556 );
5557 }
5558
5559 #[test]
5560 fn peel_data_local_one_hop_two_hops_and_non_import() {
5561 let (_, plan) = nested_import_plan();
5562
5563 let local = DataPath::local("m".into());
5564 let (_, path, def) = plan
5565 .peel_data(&local)
5566 .expect("local import marker peels on outer");
5567 assert_eq!(path.data, "m");
5568 assert!(matches!(def, DataDefinition::Import { .. }));
5569
5570 let one = DataPath {
5571 segments: vec![PathSegment {
5572 uses: "m".into(),
5573 repository: None,
5574 spec: "mid".into(),
5575 }],
5576 data: "y".into(),
5577 };
5578 let (body, path, def) = plan.peel_data(&one).expect("one hop");
5579 assert_eq!(body.identity.spec, "mid");
5580 assert_eq!(path.data, "y");
5581 assert!(matches!(
5582 def,
5583 DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
5584 ));
5585
5586 let two = DataPath {
5587 segments: vec![
5588 PathSegment {
5589 uses: "m".into(),
5590 repository: None,
5591 spec: "mid".into(),
5592 },
5593 PathSegment {
5594 uses: "nested".into(),
5595 repository: None,
5596 spec: "leaf".into(),
5597 },
5598 ],
5599 data: "x".into(),
5600 };
5601 let (body, path, def) = plan.peel_data(&two).expect("two hops");
5602 assert_eq!(body.identity.spec, "leaf");
5603 assert_eq!(path.data, "x");
5604 assert!(matches!(
5605 def,
5606 DataDefinition::Value { .. } | DataDefinition::TypeDeclaration { .. }
5607 ));
5608
5609 let bad = DataPath {
5610 segments: vec![PathSegment {
5611 uses: "y".into(),
5612 repository: None,
5613 spec: "mid".into(),
5614 }],
5615 data: "x".into(),
5616 };
5617 assert!(plan.peel_data(&bad).is_none());
5618
5619 let (mid, _, _) = plan.peel_data(&one).expect("mid for non-import hop");
5620 let non_import_hop = DataPath {
5621 segments: vec![PathSegment {
5622 uses: "y".into(),
5623 repository: None,
5624 spec: "mid".into(),
5625 }],
5626 data: "x".into(),
5627 };
5628 assert!(mid.peel_data(&non_import_hop).is_none());
5629 }
5630
5631 #[test]
5632 fn peel_rule_local_one_hop_two_hops() {
5633 let (_, plan) = nested_import_plan();
5634
5635 let one = RulePath {
5636 segments: vec![PathSegment {
5637 uses: "m".into(),
5638 repository: None,
5639 spec: "mid".into(),
5640 }],
5641 rule: "ry".into(),
5642 };
5643 let (body, local) = plan.peel_rule(&one).expect("one hop rule");
5644 assert_eq!(body.identity.spec, "mid");
5645 assert_eq!(local.rule, "ry");
5646 assert!(local.segments.is_empty());
5647
5648 let two = RulePath {
5649 segments: vec![
5650 PathSegment {
5651 uses: "m".into(),
5652 repository: None,
5653 spec: "mid".into(),
5654 },
5655 PathSegment {
5656 uses: "nested".into(),
5657 repository: None,
5658 spec: "leaf".into(),
5659 },
5660 ],
5661 rule: "rx".into(),
5662 };
5663 let (body, local) = plan.peel_rule(&two).expect("two hop rule");
5664 assert_eq!(body.identity.spec, "leaf");
5665 assert_eq!(local.rule, "rx");
5666 }
5667
5668 #[test]
5669 fn qualify_data_names_returns_qualified_path_that_peels() {
5670 let (_, plan) = nested_import_plan();
5671 let qualified = plan
5672 .qualify_data_names(&["m", "nested", "x"])
5673 .expect("qualified path");
5674 assert_eq!(qualified.input_key(), "m.nested.x");
5675 assert_eq!(qualified.segments.len(), 2);
5676 assert_eq!(qualified.data, "x");
5677 let (body, _, _) = plan.peel_data(&qualified).expect("qualified path peels");
5678 assert_eq!(body.identity.spec, "leaf");
5679 }
5680}