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