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