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