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