1use crate::computation::UnitResolutionContext;
12use crate::parsing::ast::{CalendarPeriodUnit, DateCalendarKind, DateRelativeKind};
13use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
14use crate::parsing::source::Source;
15use crate::planning::data_input::{parse_data_value, DataValueInput};
16use crate::planning::graph::Graph;
17use crate::planning::graph::ResolvedSpecTypes;
18use crate::planning::normalize::{build_normalized_rule_instructions, CompiledRule};
19use crate::planning::semantics::{
20 value_kind_matches_spec, ArithmeticComputation, ComparisonComputation, DataDefinition,
21 DataPath, Expression, LemmaType, LiteralValue, MathematicalComputation, ReferenceTarget,
22 RulePath, SemanticConversionTarget, TypeSpecification, ValueKind,
23};
24use crate::Error;
25use crate::ResourceLimits;
26use indexmap::IndexMap;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::collections::{HashMap, HashSet};
29use std::sync::Arc;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SpecSource {
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub repository: Option<String>,
41 pub name: String,
42 pub effective_from: EffectiveDate,
43 pub source: String,
44}
45
46pub type SpecSources = Vec<SpecSource>;
47
48#[derive(Debug, Clone)]
53pub struct ExecutionPlan {
54 pub spec_name: String,
56
57 pub commentary: Option<String>,
59
60 pub data: IndexMap<DataPath, DataDefinition>,
62
63 pub rules: Vec<ExecutableRule>,
65
66 pub max_register_count: u16,
68
69 pub reference_evaluation_order: Vec<DataPath>,
74
75 pub meta: HashMap<String, MetaValue>,
77
78 pub resolved_types: ResolvedSpecTypes,
82
83 pub signature_index: crate::computation::arithmetic::SignatureIndex,
89
90 pub effective: EffectiveDate,
91
92 pub sources: SpecSources,
95}
96
97#[derive(Debug, Clone)]
100pub struct ExecutionPlanSet {
101 pub spec_name: String,
102 pub plans: Vec<ExecutionPlan>,
103}
104
105impl ExecutionPlanSet {
106 #[must_use]
108 pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
109 for (i, plan) in self.plans.iter().enumerate() {
110 let from_ok = *effective >= plan.effective;
111 let to_ok = self
112 .plans
113 .get(i + 1)
114 .map(|next| *effective < next.effective)
115 .unwrap_or(true);
116 if from_ok && to_ok {
117 return Some(plan);
118 }
119 }
120 None
121 }
122}
123
124pub const INSTRUCTIONS_VERSION: u32 = 2;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
128#[serde(rename_all = "snake_case")]
129pub enum JumpVetoSemantics {
130 #[default]
132 UnlessExpression,
133 UnlessRuleReference,
135}
136
137pub fn validate_instruction_jumps(code: &[Instruction]) {
141 if let Err(message) = check_instruction_jumps(code) {
142 panic!("BUG: {message}");
143 }
144}
145
146fn check_instruction_jumps(code: &[Instruction]) -> Result<(), String> {
149 let code_len = code.len();
150 for (index, instruction) in code.iter().enumerate() {
151 match instruction {
152 Instruction::JumpIfFalse {
153 target_instruction, ..
154 } => {
155 if *target_instruction == 0 {
156 return Err(format!("unpatched JumpIfFalse at instruction {index}"));
157 }
158 if (*target_instruction as usize) >= code_len {
159 return Err(format!(
160 "JumpIfFalse at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
161 ));
162 }
163 }
164 Instruction::Jump { target_instruction } => {
165 if *target_instruction == 0 {
166 return Err(format!("unpatched Jump at instruction {index}"));
167 }
168 if (*target_instruction as usize) >= code_len {
169 return Err(format!(
170 "Jump at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
171 ));
172 }
173 }
174 _ => {}
175 }
176 }
177 Ok(())
178}
179
180pub fn validate_instructions(instructions: &Instructions) -> Result<(), String> {
190 if instructions.version != INSTRUCTIONS_VERSION {
191 return Err(format!(
192 "instructions version {} does not match supported version {}",
193 instructions.version, INSTRUCTIONS_VERSION
194 ));
195 }
196
197 check_instruction_jumps(&instructions.code)?;
198
199 let register_count = instructions.register_count;
200 let constant_count = instructions.constants.len();
201 let data_count = instructions.data_manifest.len();
202 let veto_message_count = instructions.veto_messages.len();
203
204 let check_register = |index: usize, name: &str, register: u16| -> Result<(), String> {
205 if register >= register_count {
206 return Err(format!(
207 "instruction {index} {name} register r{register} is out of bounds (register count {register_count})"
208 ));
209 }
210 Ok(())
211 };
212
213 for (index, instruction) in instructions.code.iter().enumerate() {
214 match instruction {
215 Instruction::LoadConstant {
216 destination_register,
217 constant_index,
218 } => {
219 check_register(index, "destination", *destination_register)?;
220 if (*constant_index as usize) >= constant_count {
221 return Err(format!(
222 "instruction {index} constant index {constant_index} is out of bounds (constant count {constant_count})"
223 ));
224 }
225 }
226 Instruction::LoadData {
227 destination_register,
228 data_index,
229 } => {
230 check_register(index, "destination", *destination_register)?;
231 if (*data_index as usize) >= data_count {
232 return Err(format!(
233 "instruction {index} data index {data_index} is out of bounds (data manifest size {data_count})"
234 ));
235 }
236 }
237 Instruction::LoadNow {
238 destination_register,
239 } => {
240 check_register(index, "destination", *destination_register)?;
241 }
242 Instruction::Arithmetic {
243 destination_register,
244 operation: _,
245 left_register,
246 right_register,
247 }
248 | Instruction::Comparison {
249 destination_register,
250 operation: _,
251 left_register,
252 right_register,
253 }
254 | Instruction::RangeLiteral {
255 destination_register,
256 left_register,
257 right_register,
258 } => {
259 check_register(index, "destination", *destination_register)?;
260 check_register(index, "left", *left_register)?;
261 check_register(index, "right", *right_register)?;
262 }
263 Instruction::UnitConversion {
264 destination_register,
265 source_register,
266 target: _,
267 }
268 | Instruction::Mathematical {
269 destination_register,
270 operation: _,
271 source_register,
272 }
273 | Instruction::DateRelative {
274 destination_register,
275 kind: _,
276 source_register,
277 }
278 | Instruction::DateCalendar {
279 destination_register,
280 kind: _,
281 unit: _,
282 source_register,
283 }
284 | Instruction::PastFutureRange {
285 destination_register,
286 kind: _,
287 source_register,
288 }
289 | Instruction::ResultIsVeto {
290 destination_register,
291 source_register,
292 }
293 | Instruction::MoveRegister {
294 destination_register,
295 source_register,
296 } => {
297 check_register(index, "destination", *destination_register)?;
298 check_register(index, "source", *source_register)?;
299 }
300 Instruction::RangeContainment {
301 destination_register,
302 value_register,
303 range_register,
304 } => {
305 check_register(index, "destination", *destination_register)?;
306 check_register(index, "value", *value_register)?;
307 check_register(index, "range", *range_register)?;
308 }
309 Instruction::UserVeto {
310 destination_register,
311 message_index,
312 } => {
313 check_register(index, "destination", *destination_register)?;
314 if (*message_index as usize) >= veto_message_count {
315 return Err(format!(
316 "instruction {index} veto message index {message_index} is out of bounds (veto message count {veto_message_count})"
317 ));
318 }
319 }
320 Instruction::JumpIfFalse {
321 condition_register,
322 target_instruction: _,
323 veto_semantics: _,
324 } => {
325 check_register(index, "condition", *condition_register)?;
326 }
327 Instruction::Jump {
328 target_instruction: _,
329 } => {}
330 Instruction::Return { source_register } => {
331 check_register(index, "source", *source_register)?;
332 }
333 }
334 }
335
336 match instructions.code.last() {
337 Some(Instruction::Return { .. }) => {}
338 Some(other) => {
339 return Err(format!(
340 "instruction stream must end with Return, found {other:?}"
341 ))
342 }
343 None => return Err("instruction stream is empty".to_string()),
344 }
345
346 let code_len = instructions.code.len();
347 for tag in &instructions.arm_tags {
348 if (tag.pc as usize) >= code_len {
349 return Err(format!(
350 "arm tag pc {} is out of bounds (code length {code_len})",
351 tag.pc
352 ));
353 }
354 let tagged = &instructions.code[tag.pc as usize];
355 let valid = match tag.role {
356 ArmRole::Condition => matches!(tagged, Instruction::JumpIfFalse { .. }),
357 ArmRole::Result => matches!(tagged, Instruction::Return { .. }),
358 };
359 if !valid {
360 return Err(format!(
361 "arm tag at pc {} does not match its instruction {tagged:?}",
362 tag.pc
363 ));
364 }
365 }
366 for tag in &instructions.conversion_tags {
367 if (tag.pc as usize) >= code_len {
368 return Err(format!(
369 "conversion tag pc {} is out of bounds (code length {code_len})",
370 tag.pc
371 ));
372 }
373 if !matches!(
374 instructions.code[tag.pc as usize],
375 Instruction::UnitConversion { .. }
376 ) {
377 return Err(format!(
378 "conversion tag at pc {} does not reference a UnitConversion instruction",
379 tag.pc
380 ));
381 }
382 }
383
384 validate_register_consumption(&instructions.code)?;
385
386 Ok(())
387}
388
389struct InstructionRegisterEffect {
390 uses: Vec<u16>,
391 kills: Vec<u16>,
392 defines: Option<u16>,
393}
394
395fn instruction_register_effect(instruction: &Instruction) -> InstructionRegisterEffect {
396 match instruction {
397 Instruction::LoadConstant {
398 destination_register,
399 ..
400 }
401 | Instruction::LoadData {
402 destination_register,
403 ..
404 }
405 | Instruction::LoadNow {
406 destination_register,
407 }
408 | Instruction::UserVeto {
409 destination_register,
410 ..
411 } => InstructionRegisterEffect {
412 uses: Vec::new(),
413 kills: Vec::new(),
414 defines: Some(*destination_register),
415 },
416 Instruction::Arithmetic {
417 destination_register,
418 left_register,
419 right_register,
420 ..
421 }
422 | Instruction::Comparison {
423 destination_register,
424 left_register,
425 right_register,
426 ..
427 }
428 | Instruction::RangeLiteral {
429 destination_register,
430 left_register,
431 right_register,
432 } => InstructionRegisterEffect {
433 uses: vec![*left_register, *right_register],
434 kills: vec![*left_register, *right_register],
435 defines: Some(*destination_register),
436 },
437 Instruction::UnitConversion {
438 destination_register,
439 source_register,
440 ..
441 }
442 | Instruction::Mathematical {
443 destination_register,
444 source_register,
445 ..
446 }
447 | Instruction::DateRelative {
448 destination_register,
449 source_register,
450 ..
451 }
452 | Instruction::DateCalendar {
453 destination_register,
454 source_register,
455 ..
456 }
457 | Instruction::PastFutureRange {
458 destination_register,
459 source_register,
460 ..
461 } => InstructionRegisterEffect {
462 uses: vec![*source_register],
463 kills: vec![*source_register],
464 defines: Some(*destination_register),
465 },
466 Instruction::RangeContainment {
467 destination_register,
468 value_register,
469 range_register,
470 } => InstructionRegisterEffect {
471 uses: vec![*value_register, *range_register],
472 kills: vec![*value_register, *range_register],
473 defines: Some(*destination_register),
474 },
475 Instruction::ResultIsVeto {
476 destination_register,
477 source_register,
478 } => InstructionRegisterEffect {
479 uses: vec![*source_register],
480 kills: Vec::new(),
481 defines: Some(*destination_register),
482 },
483 Instruction::MoveRegister {
484 destination_register,
485 source_register,
486 } => InstructionRegisterEffect {
487 uses: vec![*source_register],
488 kills: vec![*source_register],
489 defines: Some(*destination_register),
490 },
491 Instruction::JumpIfFalse {
492 condition_register, ..
493 } => InstructionRegisterEffect {
494 uses: vec![*condition_register],
495 kills: Vec::new(),
496 defines: None,
497 },
498 Instruction::Return { source_register } => InstructionRegisterEffect {
499 uses: vec![*source_register],
500 kills: vec![*source_register],
501 defines: None,
502 },
503 Instruction::Jump { .. } => InstructionRegisterEffect {
504 uses: Vec::new(),
505 kills: Vec::new(),
506 defines: None,
507 },
508 }
509}
510
511fn validate_register_consumption(code: &[Instruction]) -> Result<(), String> {
514 let len = code.len();
515 if len == 0 {
516 return Ok(());
517 }
518
519 let mut successors: Vec<Vec<usize>> = vec![Vec::new(); len];
520 for (pc, instruction) in code.iter().enumerate() {
521 match instruction {
522 Instruction::Jump { target_instruction } => {
523 successors[pc].push(*target_instruction as usize);
524 }
525 Instruction::JumpIfFalse {
526 target_instruction, ..
527 } => {
528 successors[pc].push(*target_instruction as usize);
529 if pc + 1 < len {
530 successors[pc].push(pc + 1);
531 }
532 }
533 Instruction::Return { .. } => {}
534 _ => {
535 if pc + 1 < len {
536 successors[pc].push(pc + 1);
537 }
538 }
539 }
540 }
541
542 let mut live_in: Vec<std::collections::HashSet<u16>> =
543 vec![std::collections::HashSet::new(); len];
544 let mut changed = true;
545 while changed {
546 changed = false;
547 for pc in (0..len).rev() {
548 let mut live_out = std::collections::HashSet::new();
549 for succ in &successors[pc] {
550 if *succ < len {
551 live_out.extend(live_in[*succ].iter().copied());
552 }
553 }
554
555 let effect = instruction_register_effect(&code[pc]);
556 for register in &effect.kills {
557 if live_out.contains(register) {
558 return Err(format!(
559 "instruction {pc} consumes register r{register} but r{register} is live on a successor path"
560 ));
561 }
562 }
563
564 let mut next_live_in =
565 std::collections::HashSet::from_iter(effect.uses.iter().copied());
566 if let Some(def) = effect.defines {
567 for register in live_out {
568 if register != def {
569 next_live_in.insert(register);
570 }
571 }
572 } else {
573 next_live_in.extend(live_out);
574 }
575
576 if live_in[pc] != next_live_in {
577 live_in[pc] = next_live_in;
578 changed = true;
579 }
580 }
581 }
582
583 Ok(())
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct Instructions {
589 pub version: u32,
590 pub register_count: u16,
591 #[serde(with = "register_types_serde")]
592 pub register_types: Vec<Arc<LemmaType>>,
593 #[serde(with = "constants_serde")]
594 pub constants: Vec<Arc<LiteralValue>>,
595 pub data_manifest: Vec<DataPath>,
596 pub veto_messages: Vec<String>,
597 pub code: Vec<Instruction>,
598 #[serde(default, skip_serializing_if = "Vec::is_empty")]
603 pub arm_tags: Vec<ArmTag>,
604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
609 pub conversion_tags: Vec<ConversionTag>,
610}
611
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
614#[serde(rename_all = "snake_case")]
615pub enum ArmRole {
616 Condition,
618 Result,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
627pub struct ArmTag {
628 pub pc: u32,
629 pub arm: u16,
630 pub role: ArmRole,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636pub struct ConversionTag {
637 pub pc: u32,
638 pub source: Source,
639}
640
641mod register_types_serde {
642 use super::LemmaType;
643 use serde::{Deserialize, Deserializer, Serialize, Serializer};
644 use std::sync::Arc;
645
646 pub fn serialize<S>(values: &[Arc<LemmaType>], serializer: S) -> Result<S::Ok, S::Error>
647 where
648 S: Serializer,
649 {
650 let refs: Vec<&LemmaType> = values.iter().map(|v| v.as_ref()).collect();
651 refs.serialize(serializer)
652 }
653
654 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Arc<LemmaType>>, D::Error>
655 where
656 D: Deserializer<'de>,
657 {
658 let values: Vec<LemmaType> = Vec::deserialize(deserializer)?;
659 Ok(values.into_iter().map(Arc::new).collect())
660 }
661}
662
663mod constants_serde {
664 use super::LiteralValue;
665 use serde::{Deserialize, Deserializer, Serialize, Serializer};
666 use std::sync::Arc;
667
668 pub fn serialize<S>(values: &[Arc<LiteralValue>], serializer: S) -> Result<S::Ok, S::Error>
669 where
670 S: Serializer,
671 {
672 let refs: Vec<&LiteralValue> = values.iter().map(|v| v.as_ref()).collect();
673 refs.serialize(serializer)
674 }
675
676 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Arc<LiteralValue>>, D::Error>
677 where
678 D: Deserializer<'de>,
679 {
680 let values: Vec<LiteralValue> = Vec::deserialize(deserializer)?;
681 Ok(values.into_iter().map(Arc::new).collect())
682 }
683}
684
685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
687#[serde(rename_all = "snake_case")]
688pub enum Instruction {
689 LoadConstant {
690 destination_register: u16,
691 constant_index: u16,
692 },
693 LoadData {
694 destination_register: u16,
695 data_index: u16,
696 },
697 LoadNow {
698 destination_register: u16,
699 },
700 Arithmetic {
701 destination_register: u16,
702 operation: ArithmeticComputation,
703 left_register: u16,
704 right_register: u16,
705 },
706 Comparison {
707 destination_register: u16,
708 operation: ComparisonComputation,
709 left_register: u16,
710 right_register: u16,
711 },
712 UnitConversion {
713 destination_register: u16,
714 source_register: u16,
715 target: SemanticConversionTarget,
716 },
717 Mathematical {
718 destination_register: u16,
719 operation: MathematicalComputation,
720 source_register: u16,
721 },
722 DateRelative {
723 destination_register: u16,
724 kind: DateRelativeKind,
725 source_register: u16,
726 },
727 DateCalendar {
728 destination_register: u16,
729 kind: DateCalendarKind,
730 unit: CalendarPeriodUnit,
731 source_register: u16,
732 },
733 RangeLiteral {
734 destination_register: u16,
735 left_register: u16,
736 right_register: u16,
737 },
738 PastFutureRange {
739 destination_register: u16,
740 kind: DateRelativeKind,
741 source_register: u16,
742 },
743 RangeContainment {
744 destination_register: u16,
745 value_register: u16,
746 range_register: u16,
747 },
748 ResultIsVeto {
749 destination_register: u16,
750 source_register: u16,
751 },
752 MoveRegister {
753 destination_register: u16,
754 source_register: u16,
755 },
756 UserVeto {
757 destination_register: u16,
758 message_index: u16,
759 },
760 JumpIfFalse {
761 condition_register: u16,
762 target_instruction: u32,
763 #[serde(default)]
764 veto_semantics: JumpVetoSemantics,
765 },
766 Jump {
767 target_instruction: u32,
768 },
769 Return {
770 source_register: u16,
771 },
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
778pub struct ExecutableRule {
779 pub path: RulePath,
781
782 pub name: String,
784
785 pub branches: Vec<Branch>,
787
788 pub instructions: Instructions,
790
791 pub source_instructions: Instructions,
796
797 pub source: Source,
799
800 #[serde(with = "arc_lemma_type")]
803 pub rule_type: Arc<LemmaType>,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize)]
808pub struct Branch {
809 pub condition: Option<Expression>,
811
812 pub result: Expression,
814
815 pub source: Source,
817}
818
819mod arc_lemma_type {
820 use super::LemmaType;
821 use serde::{Deserialize, Deserializer, Serialize, Serializer};
822 use std::sync::Arc;
823
824 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
825 where
826 S: Serializer,
827 {
828 value.as_ref().serialize(serializer)
829 }
830
831 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
832 where
833 D: Deserializer<'de>,
834 {
835 LemmaType::deserialize(deserializer).map(Arc::new)
836 }
837}
838
839pub(crate) fn build_execution_plan(
842 graph: &Graph,
843 resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
844 effective: &EffectiveDate,
845 limits: &crate::limits::ResourceLimits,
846) -> Result<ExecutionPlan, Vec<Error>> {
847 let execution_order = graph.execution_order();
848
849 let main_spec = graph.main_spec();
850 let main_idx = resolved_types
851 .iter()
852 .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
853
854 let mut sources: SpecSources = Vec::new();
855 for (repo, spec, _) in resolved_types.iter() {
856 if !sources.iter().any(|e| {
857 e.repository == repo.name
858 && e.name == spec.name
859 && e.effective_from == spec.effective_from
860 }) {
861 sources.push(SpecSource {
862 repository: repo.name.clone(),
863 name: spec.name.clone(),
864 effective_from: spec.effective_from.clone(),
865 source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
866 });
867 }
868 }
869
870 let main_resolved_types = main_idx
871 .map(|idx| resolved_types.remove(idx).2)
872 .unwrap_or_default();
873 let data = graph.build_data(&main_resolved_types.resolved)?;
874
875 let undetermined_errors: Vec<Error> = data
882 .iter()
883 .filter_map(|(path, definition)| {
884 let (resolved_type, source) = match definition {
885 DataDefinition::TypeDeclaration {
886 resolved_type,
887 source,
888 ..
889 } => (resolved_type, source),
890 DataDefinition::Reference {
891 target: ReferenceTarget::Data(_),
892 resolved_type,
893 source,
894 ..
895 } => (resolved_type, source),
896 DataDefinition::Reference {
897 target: ReferenceTarget::Rule(_),
898 ..
899 }
900 | DataDefinition::Value { .. }
901 | DataDefinition::Import { .. } => return None,
902 };
903 if resolved_type.is_undetermined() {
904 Some(Error::validation(
905 format!("could not determine the type of '{path}'"),
906 Some(source.clone()),
907 None::<String>,
908 ))
909 } else {
910 None
911 }
912 })
913 .collect();
914 if !undetermined_errors.is_empty() {
915 return Err(undetermined_errors);
916 }
917
918 let signature_index = crate::planning::graph::build_signature_index(
919 &main_spec.name,
920 &main_resolved_types.unit_index,
921 )
922 .expect("BUG: signature_index build already validated during resolve_and_validate");
923
924 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
925 let mut max_register_count: u16 = 0;
926 let plan_rule_paths: HashSet<RulePath> = graph.rules().keys().cloned().collect();
927 let mut completed_rules: HashMap<RulePath, Arc<Expression>> = HashMap::new();
928
929 for rule_path in execution_order {
930 let rule_node = graph.rules().get(rule_path).expect(
931 "bug: rule from topological sort not in graph - validation should have caught this",
932 );
933
934 let mut executable_branches = Vec::new();
935 for (condition, result) in &rule_node.branches {
936 executable_branches.push(Branch {
937 condition: condition.clone(),
938 result: result.clone(),
939 source: rule_node.source.clone(),
940 });
941 }
942
943 let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
944 let compiled = build_normalized_rule_instructions(
945 &rule_node.branches,
946 &completed_rules,
947 &plan_rule_paths,
948 &data,
949 &unit_ctx,
950 Some(rule_node.source.clone()),
951 &rule_node.rule_type,
952 limits.max_normalized_expression_nodes,
953 )
954 .map_err(|error| vec![error])?;
958 let CompiledRule {
959 instructions,
960 source_instructions,
961 inlined_expression,
962 } = compiled;
963 max_register_count = max_register_count
964 .max(instructions.register_count)
965 .max(source_instructions.register_count);
966 completed_rules.insert(rule_path.clone(), inlined_expression);
967
968 executable_rules.push(ExecutableRule {
969 path: rule_path.clone(),
970 name: rule_path.rule.clone(),
971 branches: executable_branches,
972 instructions,
973 source_instructions,
974 source: rule_node.source.clone(),
975 rule_type: Arc::clone(&rule_node.rule_type),
976 });
977 }
978
979 Ok(ExecutionPlan {
980 spec_name: main_spec.name.clone(),
981 commentary: main_spec.commentary.clone(),
982 data,
983 rules: executable_rules,
984 max_register_count,
985 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
986 meta: main_spec
987 .meta_fields
988 .iter()
989 .map(|f| (f.key.clone(), f.value.clone()))
990 .collect(),
991 resolved_types: main_resolved_types,
992 signature_index,
993 effective: effective.clone(),
994 sources,
995 })
996}
997
998#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1023pub struct DataEntry {
1024 #[serde(rename = "type")]
1025 pub lemma_type: LemmaType,
1026 #[serde(
1027 skip_serializing_if = "Option::is_none",
1028 default,
1029 serialize_with = "crate::planning::semantics::api_wire_literal::serialize_option",
1030 deserialize_with = "crate::planning::semantics::api_wire_literal::deserialize_option"
1031 )]
1032 pub prefilled: Option<LiteralValue>,
1033 #[serde(
1034 skip_serializing_if = "Option::is_none",
1035 default,
1036 serialize_with = "crate::planning::semantics::api_wire_literal::serialize_option",
1037 deserialize_with = "crate::planning::semantics::api_wire_literal::deserialize_option"
1038 )]
1039 pub supplied: Option<LiteralValue>,
1040 #[serde(
1041 skip_serializing_if = "Option::is_none",
1042 default,
1043 serialize_with = "crate::planning::semantics::api_wire_literal::serialize_option",
1044 deserialize_with = "crate::planning::semantics::api_wire_literal::deserialize_option"
1045 )]
1046 pub default: Option<LiteralValue>,
1047}
1048
1049#[derive(Debug, Clone, Default)]
1055pub struct DataOverlay {
1056 pub values: HashMap<DataPath, Arc<LiteralValue>>,
1058 pub violated: HashMap<DataPath, String>,
1061}
1062
1063impl DataOverlay {
1064 pub fn resolve(
1070 plan: &ExecutionPlan,
1071 raw_values: HashMap<String, DataValueInput>,
1072 limits: &ResourceLimits,
1073 ) -> Result<Self, Error> {
1074 let mut overlay = Self::default();
1075
1076 for (name, raw_value) in raw_values {
1077 let data_path = plan.get_data_path_by_str(&name).ok_or_else(|| {
1078 let available: Vec<String> = plan.data.keys().map(|p| p.input_key()).collect();
1079 Error::request(
1080 format!(
1081 "Data '{}' not found. Available data: {}",
1082 name,
1083 available.join(", ")
1084 ),
1085 None::<String>,
1086 )
1087 })?;
1088 let data_path = data_path.clone();
1089
1090 let data_definition = plan
1091 .data
1092 .get(&data_path)
1093 .expect("BUG: data_path was just resolved from plan.data, must exist");
1094
1095 let data_source = data_definition.source().clone();
1096 let type_arc = match data_definition {
1097 DataDefinition::TypeDeclaration { resolved_type, .. }
1098 | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
1099 DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
1100 DataDefinition::Import { .. } => {
1101 return Err(Error::request(
1102 format!(
1103 "Data '{}' is a spec reference; cannot provide a value.",
1104 name
1105 ),
1106 None::<String>,
1107 ));
1108 }
1109 };
1110
1111 let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
1112 Ok(value) => value,
1113 Err(error) => {
1114 overlay
1115 .violated
1116 .insert(data_path, error.message().to_string());
1117 continue;
1118 }
1119 };
1120
1121 let size = literal_value.byte_size();
1122 if size > limits.max_data_value_bytes {
1123 return Err(Error::resource_limit_exceeded(
1124 "max_data_value_bytes",
1125 limits.max_data_value_bytes.to_string(),
1126 size.to_string(),
1127 format!(
1128 "Reduce the size of data values to {} bytes or less",
1129 limits.max_data_value_bytes
1130 ),
1131 Some(data_source.clone()),
1132 None,
1133 None,
1134 )
1135 .with_related_data(&name));
1136 }
1137
1138 if let Err(message) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
1139 overlay.violated.insert(data_path, message);
1140 continue;
1141 }
1142
1143 overlay.values.insert(data_path, Arc::new(literal_value));
1144 }
1145
1146 Ok(overlay)
1147 }
1148
1149 pub fn is_empty(&self) -> bool {
1150 self.values.is_empty() && self.violated.is_empty()
1151 }
1152
1153 pub(crate) fn supplied_value(&self, data_path: &DataPath) -> Option<&LiteralValue> {
1157 if self.violated.contains_key(data_path) {
1158 None
1159 } else {
1160 self.values.get(data_path).map(|value| value.as_ref())
1161 }
1162 }
1163}
1164
1165fn schema_prefilled(data: &DataDefinition) -> Option<LiteralValue> {
1166 data.prefilled_value().cloned()
1167}
1168
1169fn schema_supplied(path: &DataPath, overlay: &DataOverlay) -> Option<LiteralValue> {
1170 overlay.supplied_value(path).cloned()
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1174pub struct SpecSchema {
1175 pub spec: String,
1177 #[serde(skip_serializing_if = "Option::is_none", default)]
1179 pub commentary: Option<String>,
1180 #[serde(skip_serializing_if = "Option::is_none", default)]
1182 pub effective: Option<DateTimeValue>,
1183 #[serde(skip_serializing_if = "Vec::is_empty", default)]
1186 pub versions: Vec<DateTimeValue>,
1187 pub data: indexmap::IndexMap<String, DataEntry>,
1189 pub rules: indexmap::IndexMap<String, LemmaType>,
1191 pub meta: HashMap<String, MetaValue>,
1193}
1194
1195impl std::fmt::Display for SpecSchema {
1196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1197 write!(f, "Spec: {}", self.spec)?;
1198
1199 if let Some(commentary) = &self.commentary {
1200 write!(f, "\n {}", commentary)?;
1201 }
1202
1203 if !self.meta.is_empty() {
1204 write!(f, "\n\nMeta:")?;
1205 let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
1207 entries.sort_by_key(|(k, _)| *k);
1208 for (key, value) in entries {
1209 write!(f, "\n {}: {}", key, value)?;
1210 }
1211 }
1212
1213 if !self.data.is_empty() {
1214 write!(f, "\n\nData:")?;
1215 for (name, entry) in &self.data {
1216 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
1217 for line in type_detail_lines(&entry.lemma_type.specifications) {
1218 write!(f, "\n {}", line)?;
1219 }
1220 let help = entry.lemma_type.specifications.help();
1221 if !help.is_empty() {
1222 write!(f, "\n help: {}", help)?;
1223 }
1224 if let Some(val) = &entry.prefilled {
1225 write!(f, "\n prefilled: {}", val)?;
1226 }
1227 if let Some(val) = &entry.supplied {
1228 write!(f, "\n supplied: {}", val)?;
1229 }
1230 if let Some(val) = &entry.default {
1231 write!(f, "\n default: {}", val)?;
1232 }
1233 }
1234 }
1235
1236 if !self.rules.is_empty() {
1237 write!(f, "\n\nRules:")?;
1238 for (name, rule_type) in &self.rules {
1239 write!(f, "\n {} ({})", name, rule_type.specifications)?;
1240 }
1241 }
1242
1243 if self.data.is_empty() && self.rules.is_empty() {
1244 write!(f, "\n (no data or rules)")?;
1245 }
1246
1247 Ok(())
1248 }
1249}
1250
1251pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1257 use crate::computation::rational::rational_to_display_str;
1258 let mut lines = Vec::new();
1259 match spec {
1260 TypeSpecification::Measure {
1261 minimum,
1262 maximum,
1263 decimals,
1264 units,
1265 ..
1266 } => {
1267 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1268 if !unit_names.is_empty() {
1269 lines.push(format!("units: {}", unit_names.join(", ")));
1270 }
1271 if let Some(d) = decimals {
1272 lines.push(format!("decimals: {}", d));
1273 }
1274 if let Some((magnitude, unit_name)) = minimum {
1275 lines.push(format!(
1276 "minimum: {} {}",
1277 rational_to_display_str(magnitude),
1278 unit_name
1279 ));
1280 }
1281 if let Some((magnitude, unit_name)) = maximum {
1282 lines.push(format!(
1283 "maximum: {} {}",
1284 rational_to_display_str(magnitude),
1285 unit_name
1286 ));
1287 }
1288 }
1289 TypeSpecification::Number {
1290 minimum,
1291 maximum,
1292 decimals,
1293 ..
1294 } => {
1295 if let Some(d) = decimals {
1296 lines.push(format!("decimals: {}", d));
1297 }
1298 if let Some(v) = minimum {
1299 lines.push(format!("minimum: {}", rational_to_display_str(v)));
1300 }
1301 if let Some(v) = maximum {
1302 lines.push(format!("maximum: {}", rational_to_display_str(v)));
1303 }
1304 }
1305 TypeSpecification::Ratio {
1306 minimum,
1307 maximum,
1308 decimals,
1309 units,
1310 ..
1311 } => {
1312 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1313 if !unit_names.is_empty() {
1314 lines.push(format!("units: {}", unit_names.join(", ")));
1315 }
1316 if let Some(d) = decimals {
1317 lines.push(format!("decimals: {}", d));
1318 }
1319 if let Some(v) = minimum {
1320 lines.push(format!("minimum: {}", rational_to_display_str(v)));
1321 }
1322 if let Some(v) = maximum {
1323 lines.push(format!("maximum: {}", rational_to_display_str(v)));
1324 }
1325 }
1326 TypeSpecification::Text {
1327 options, length, ..
1328 } => {
1329 if let Some(l) = length {
1330 lines.push(format!("length: {}", l));
1331 }
1332 if !options.is_empty() {
1333 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
1334 lines.push(format!("options: {}", quoted.join(", ")));
1335 }
1336 }
1337 TypeSpecification::Date {
1338 minimum, maximum, ..
1339 } => {
1340 if let Some(v) = minimum {
1341 lines.push(format!("minimum: {}", v));
1342 }
1343 if let Some(v) = maximum {
1344 lines.push(format!("maximum: {}", v));
1345 }
1346 }
1347 TypeSpecification::Time {
1348 minimum, maximum, ..
1349 } => {
1350 if let Some(v) = minimum {
1351 lines.push(format!("minimum: {}", v));
1352 }
1353 if let Some(v) = maximum {
1354 lines.push(format!("maximum: {}", v));
1355 }
1356 }
1357 TypeSpecification::MeasureRange { units, .. } => {
1358 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1359 if !unit_names.is_empty() {
1360 lines.push(format!("units: {}", unit_names.join(", ")));
1361 }
1362 }
1363 TypeSpecification::RatioRange { units, .. } => {
1364 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1365 if !unit_names.is_empty() {
1366 lines.push(format!("units: {}", unit_names.join(", ")));
1367 }
1368 }
1369 TypeSpecification::Boolean { .. }
1370 | TypeSpecification::NumberRange { .. }
1371 | TypeSpecification::DateRange { .. }
1372 | TypeSpecification::TimeRange { .. }
1373 | TypeSpecification::Veto { .. }
1374 | TypeSpecification::Undetermined => {}
1375 }
1376 lines
1377}
1378
1379impl ExecutionPlan {
1380 pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
1384 &self.resolved_types.unit_index
1385 }
1386
1387 pub fn local_rule_names(&self) -> Vec<String> {
1399 self.rules
1400 .iter()
1401 .filter(|r| r.path.segments.is_empty())
1402 .map(|r| r.name.clone())
1403 .collect()
1404 }
1405
1406 pub fn schema(&self, overlay: &DataOverlay) -> SpecSchema {
1407 let all_local_rules = self.local_rule_names();
1408 self.schema_for_rules(&all_local_rules, overlay)
1409 .expect("BUG: all_local_rules sourced from self.rules")
1410 }
1411
1412 pub fn interface_schema(&self, overlay: &DataOverlay) -> SpecSchema {
1417 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1418 .data
1419 .iter()
1420 .filter(|(_, data)| {
1421 data.schema_type().is_some() && !matches!(data, DataDefinition::Reference { .. })
1422 })
1423 .map(|(path, data)| {
1424 let lemma_type = data
1425 .schema_type()
1426 .expect("BUG: filter above ensured schema_type is Some")
1427 .clone();
1428 let prefilled = schema_prefilled(data);
1429 let supplied = schema_supplied(path, overlay);
1430 let default = data.default_suggestion();
1431 (
1432 path.segments.len(),
1433 data.source().span.start,
1434 path.input_key(),
1435 DataEntry {
1436 lemma_type,
1437 prefilled,
1438 supplied,
1439 default,
1440 },
1441 )
1442 })
1443 .collect();
1444 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1445
1446 let rule_entries: Vec<(String, LemmaType)> = self
1447 .rules
1448 .iter()
1449 .filter(|r| r.path.segments.is_empty())
1450 .map(|r| (r.name.clone(), (*r.rule_type).clone()))
1451 .collect();
1452
1453 SpecSchema {
1454 spec: self.spec_name.clone(),
1455 commentary: self.commentary.clone(),
1456 effective: self.effective.as_ref().cloned(),
1457 versions: Vec::new(),
1458 data: data_entries
1459 .into_iter()
1460 .map(|(_, _, name, data)| (name, data))
1461 .collect(),
1462 rules: rule_entries.into_iter().collect(),
1463 meta: self.meta.clone(),
1464 }
1465 }
1466
1467 pub fn schema_for_rules(
1483 &self,
1484 rule_names: &[String],
1485 overlay: &DataOverlay,
1486 ) -> Result<SpecSchema, Error> {
1487 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
1488 for rule_name in rule_names {
1489 let rule = self.get_rule(rule_name).ok_or_else(|| {
1490 Error::request(
1491 format!(
1492 "Rule '{}' not found in spec '{}'",
1493 rule_name, self.spec_name
1494 ),
1495 None::<String>,
1496 )
1497 })?;
1498 rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
1499 }
1500
1501 let needed_data = self.collect_needed_data_paths(rule_names, overlay)?;
1502
1503 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1504 .data
1505 .iter()
1506 .filter(|(path, _)| needed_data.contains(path))
1507 .filter(|(_, data)| !matches!(data, DataDefinition::Reference { .. }))
1508 .filter_map(|(path, data)| {
1509 let lemma_type = data.schema_type()?.clone();
1510 let prefilled = schema_prefilled(data);
1511 let supplied = schema_supplied(path, overlay);
1512 let default = data.default_suggestion();
1513 Some((
1514 path.segments.len(),
1515 data.source().span.start,
1516 path.input_key(),
1517 DataEntry {
1518 lemma_type,
1519 prefilled,
1520 supplied,
1521 default,
1522 },
1523 ))
1524 })
1525 .collect();
1526 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1527 let data_entries: Vec<(String, DataEntry)> = data_entries
1528 .into_iter()
1529 .map(|(_, _, name, data)| (name, data))
1530 .collect();
1531
1532 Ok(SpecSchema {
1533 spec: self.spec_name.clone(),
1534 commentary: self.commentary.clone(),
1535 effective: self.effective.as_ref().cloned(),
1536 versions: Vec::new(),
1537 data: data_entries.into_iter().collect(),
1538 rules: rule_entries.into_iter().collect(),
1539 meta: self.meta.clone(),
1540 })
1541 }
1542
1543 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
1545 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1546 self.data
1547 .keys()
1548 .find(|path| path.input_key() == canonical_name)
1549 }
1550
1551 pub fn validated_response_rule_names(
1555 &self,
1556 rules: Option<&[String]>,
1557 ) -> Result<std::collections::HashSet<String>, Error> {
1558 let Some(rules) = rules else {
1559 return Ok(self.local_rule_names().into_iter().collect());
1560 };
1561 if rules.is_empty() {
1562 return Err(Error::request(
1563 "at least one rule required".to_string(),
1564 None::<String>,
1565 ));
1566 }
1567 let mut names = std::collections::HashSet::new();
1568 for rule_name in rules {
1569 let rule = self.get_rule(rule_name).ok_or_else(|| {
1570 Error::request(
1571 format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
1572 None::<String>,
1573 )
1574 })?;
1575 names.insert(rule.name.clone());
1576 }
1577 Ok(names)
1578 }
1579
1580 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
1582 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1583 self.rules
1584 .iter()
1585 .find(|r| r.name == canonical_name && r.path.segments.is_empty())
1586 }
1587
1588 pub fn collect_needed_data_paths(
1597 &self,
1598 rule_names: &[String],
1599 overlay: &DataOverlay,
1600 ) -> Result<HashSet<DataPath>, Error> {
1601 let mut needed_data: HashSet<DataPath> = HashSet::new();
1602 let mut visited_rules: HashSet<RulePath> = HashSet::new();
1603 let mut rule_worklist: Vec<RulePath> = Vec::new();
1604
1605 for rule_name in rule_names {
1607 let rule = self.get_rule(rule_name).ok_or_else(|| {
1608 Error::request(
1609 format!(
1610 "Rule '{}' not found in spec '{}'",
1611 rule_name, self.spec_name
1612 ),
1613 None::<String>,
1614 )
1615 })?;
1616 rule_worklist.push(rule.path.clone());
1617 }
1618
1619 while let Some(rule_path) = rule_worklist.pop() {
1622 if !visited_rules.insert(rule_path.clone()) {
1623 continue;
1624 }
1625
1626 let rule = self.get_rule_by_path(&rule_path).unwrap_or_else(|| {
1627 panic!(
1628 "BUG: rule path '{}' placed on worklist but not found in plan '{}'",
1629 rule_path.rule, self.spec_name
1630 )
1631 });
1632
1633 let live_branch_indices = live_piecewise_branch_indices(rule, self, overlay);
1634
1635 for branch_index in live_branch_indices {
1636 let branch = &rule.branches[branch_index];
1637
1638 let mut branch_data: HashSet<DataPath> = HashSet::new();
1639 if let Some(condition) = &branch.condition {
1640 condition.collect_data_paths(&mut branch_data);
1641 }
1642 branch.result.collect_data_paths(&mut branch_data);
1643
1644 let mut branch_rules: HashSet<RulePath> = HashSet::new();
1645 if let Some(condition) = &branch.condition {
1646 condition.collect_rule_paths(&mut branch_rules);
1647 }
1648 branch.result.collect_rule_paths(&mut branch_rules);
1649
1650 for data_path in &branch_data {
1651 if let Some(DataDefinition::Reference {
1652 target: ReferenceTarget::Rule(target_rule),
1653 ..
1654 }) = self.data.get(data_path)
1655 {
1656 branch_rules.insert(target_rule.clone());
1657 }
1658 }
1659
1660 needed_data.extend(branch_data);
1661 rule_worklist.extend(branch_rules);
1662 }
1663 }
1664
1665 Ok(needed_data)
1666 }
1667
1668 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
1670 self.rules.iter().find(|r| &r.path == rule_path)
1671 }
1672
1673 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
1675 self.data.get(path).and_then(|d| d.value())
1676 }
1677}
1678
1679fn live_piecewise_branch_indices(
1681 rule: &ExecutableRule,
1682 plan: &ExecutionPlan,
1683 overlay: &DataOverlay,
1684) -> Vec<usize> {
1685 if rule.branches.len() <= 1 {
1686 return vec![0];
1687 }
1688
1689 let mut indices = Vec::new();
1690
1691 for branch_index in (1..rule.branches.len()).rev() {
1692 let condition = rule.branches[branch_index]
1693 .condition
1694 .as_ref()
1695 .expect("BUG: unless branch missing condition");
1696 match crate::evaluation::partial::unless_condition_truth(condition, plan, overlay) {
1697 Some(true) => {
1698 indices.push(branch_index);
1699 return indices;
1700 }
1701 Some(false) => continue,
1702 None => indices.push(branch_index),
1703 }
1704 }
1705
1706 indices.push(0);
1707 indices
1708}
1709
1710pub(crate) fn validate_value_against_type(
1711 expected_type: &LemmaType,
1712 value: &LiteralValue,
1713) -> Result<(), String> {
1714 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
1715 use crate::planning::semantics::TypeSpecification;
1716
1717 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
1718 match commit_rational_to_decimal(magnitude) {
1719 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
1720 Err(_) => true,
1721 }
1722 }
1723
1724 fn format_rational_for_validation_message(
1725 expected_type: &crate::planning::semantics::LemmaType,
1726 magnitude: &RationalInteger,
1727 ) -> String {
1728 use crate::computation::rational::rational_to_display_str;
1729 expected_type
1730 .try_materialize_rational_as_decimal_string(magnitude)
1731 .unwrap_or_else(|_| rational_to_display_str(magnitude))
1732 }
1733
1734 match (&expected_type.specifications, &value.value) {
1735 (
1736 TypeSpecification::Number {
1737 minimum,
1738 maximum,
1739 decimals,
1740 ..
1741 },
1742 ValueKind::Number(n),
1743 ) => {
1744 if commit_rational_to_decimal(n).is_err() {
1745 return Err("Calculated result exceeds decimal value limit".to_string());
1746 }
1747 if let Some(d) = decimals {
1748 if exceeds_decimal_places(n, *d) {
1749 return Err(format!(
1750 "{} exceeds decimals constraint {d}",
1751 format_rational_for_validation_message(expected_type, n)
1752 ));
1753 }
1754 }
1755 if let Some(min) = minimum {
1756 if n < min {
1757 return Err(format!(
1758 "{} is below minimum {}",
1759 format_rational_for_validation_message(expected_type, n),
1760 format_rational_for_validation_message(expected_type, min)
1761 ));
1762 }
1763 }
1764 if let Some(max) = maximum {
1765 if n > max {
1766 return Err(format!(
1767 "{} is above maximum {}",
1768 format_rational_for_validation_message(expected_type, n),
1769 format_rational_for_validation_message(expected_type, max)
1770 ));
1771 }
1772 }
1773 Ok(())
1774 }
1775 (
1776 TypeSpecification::Measure {
1777 minimum,
1778 maximum,
1779 decimals,
1780 units,
1781 ..
1782 },
1783 ValueKind::Measure(magnitude, signature),
1784 ) => {
1785 if commit_rational_to_decimal(magnitude).is_err() {
1786 return Err("Calculated result exceeds decimal value limit".to_string());
1787 }
1788 use crate::computation::rational::checked_div;
1789 use crate::planning::semantics::measure_declared_bound_canonical;
1790 let unit = signature
1791 .first()
1792 .map(|(n, _)| n.as_str())
1793 .expect("BUG: Measure value has empty signature in execution plan validation");
1794 let measure_unit = units.get(unit)?;
1795 let factor = &measure_unit.factor;
1796 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1797 format!("cannot de-canonicalize measure for validation: {failure}")
1798 })?;
1799 if let Some(d) = decimals {
1800 if exceeds_decimal_places(&in_unit, *d) {
1801 return Err(format!(
1802 "{} {unit} exceeds decimals constraint {d}",
1803 format_rational_for_validation_message(expected_type, &in_unit)
1804 ));
1805 }
1806 }
1807 if let Some(bound) = minimum {
1808 let canonical_min = measure_declared_bound_canonical(
1809 bound,
1810 units,
1811 expected_type.name().as_str(),
1812 "minimum",
1813 )?;
1814 if magnitude < &canonical_min {
1815 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1816 format!("cannot de-canonicalize minimum for validation: {failure}")
1817 })?;
1818 let value_display = format!(
1819 "{} {}",
1820 format_rational_for_validation_message(expected_type, &in_unit),
1821 unit
1822 );
1823 let bound_display = format!(
1824 "{} {}",
1825 format_rational_for_validation_message(expected_type, &min_in_unit),
1826 measure_unit.name
1827 );
1828 return Err(format!("{value_display} is below minimum {bound_display}"));
1829 }
1830 }
1831 if let Some(bound) = maximum {
1832 let canonical_max = measure_declared_bound_canonical(
1833 bound,
1834 units,
1835 expected_type.name().as_str(),
1836 "maximum",
1837 )?;
1838 if magnitude > &canonical_max {
1839 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1840 format!("cannot de-canonicalize maximum for validation: {failure}")
1841 })?;
1842 let value_display = format!(
1843 "{} {}",
1844 format_rational_for_validation_message(expected_type, &in_unit),
1845 unit
1846 );
1847 let bound_display = format!(
1848 "{} {}",
1849 format_rational_for_validation_message(expected_type, &max_in_unit),
1850 measure_unit.name
1851 );
1852 return Err(format!("{value_display} is above maximum {bound_display}"));
1853 }
1854 }
1855 Ok(())
1856 }
1857 (
1858 TypeSpecification::Text {
1859 length, options, ..
1860 },
1861 ValueKind::Text(s),
1862 ) => {
1863 let len = s.chars().count();
1864 if let Some(exact) = length {
1865 if len != *exact {
1866 return Err(format!(
1867 "'{}' has length {} but required length is {}",
1868 s, len, exact
1869 ));
1870 }
1871 }
1872 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1873 return Err(format!(
1874 "'{}' is not in allowed options: {}",
1875 s,
1876 options.join(", ")
1877 ));
1878 }
1879 Ok(())
1880 }
1881 (
1882 TypeSpecification::Ratio {
1883 minimum,
1884 maximum,
1885 decimals,
1886 units,
1887 ..
1888 },
1889 ValueKind::Ratio(r, unit_name),
1890 ) => {
1891 if commit_rational_to_decimal(r).is_err() {
1892 return Err("Calculated result exceeds decimal value limit".to_string());
1893 }
1894 use crate::computation::rational::checked_mul;
1895
1896 if let Some(d) = decimals {
1897 if exceeds_decimal_places(r, *d) {
1898 return Err(format!(
1899 "{} exceeds decimals constraint {d}",
1900 format_rational_for_validation_message(expected_type, r)
1901 ));
1902 }
1903 }
1904 if let Some(type_minimum) = minimum {
1905 if r < type_minimum {
1906 let message = match unit_name.as_deref() {
1907 Some(unit) => {
1908 let ratio_unit = units.get(unit)?;
1909 let value_per_unit = checked_mul(r, &ratio_unit.value)
1910 .map_err(|failure| failure.to_string())?;
1911 let bound_per_unit = ratio_unit.minimum.clone().expect(
1912 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1913 );
1914 format!(
1915 "{} {unit} is below minimum {} {unit}",
1916 format_rational_for_validation_message(
1917 expected_type,
1918 &value_per_unit
1919 ),
1920 format_rational_for_validation_message(
1921 expected_type,
1922 &bound_per_unit.clone()
1923 ),
1924 )
1925 }
1926 None => format!(
1927 "{} is below minimum {}",
1928 format_rational_for_validation_message(expected_type, r),
1929 format_rational_for_validation_message(expected_type, type_minimum),
1930 ),
1931 };
1932 return Err(message);
1933 }
1934 }
1935 if let Some(type_maximum) = maximum {
1936 if r > type_maximum {
1937 let message = match unit_name.as_deref() {
1938 Some(unit) => {
1939 let ratio_unit = units.get(unit)?;
1940 let value_per_unit = checked_mul(r, &ratio_unit.value)
1941 .map_err(|failure| failure.to_string())?;
1942 let bound_per_unit = ratio_unit.maximum.clone().expect(
1943 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1944 );
1945 format!(
1946 "{} {unit} is above maximum {} {unit}",
1947 format_rational_for_validation_message(
1948 expected_type,
1949 &value_per_unit
1950 ),
1951 format_rational_for_validation_message(
1952 expected_type,
1953 &bound_per_unit.clone()
1954 ),
1955 )
1956 }
1957 None => format!(
1958 "{} is above maximum {}",
1959 format_rational_for_validation_message(expected_type, r),
1960 format_rational_for_validation_message(expected_type, type_maximum),
1961 ),
1962 };
1963 return Err(message);
1964 }
1965 }
1966 Ok(())
1967 }
1968 (
1969 TypeSpecification::Date {
1970 minimum, maximum, ..
1971 },
1972 ValueKind::Date(dt),
1973 ) => {
1974 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1975 use std::cmp::Ordering;
1976 if let Some(min) = minimum {
1977 let min_sem = date_time_to_semantic(min);
1978 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1979 return Err(format!("{} is below minimum {}", dt, min));
1980 }
1981 }
1982 if let Some(max) = maximum {
1983 let max_sem = date_time_to_semantic(max);
1984 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1985 return Err(format!("{} is above maximum {}", dt, max));
1986 }
1987 }
1988 Ok(())
1989 }
1990 (
1991 TypeSpecification::Time {
1992 minimum, maximum, ..
1993 },
1994 ValueKind::Time(t),
1995 ) => {
1996 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1997 use std::cmp::Ordering;
1998 if let Some(min) = minimum {
1999 let min_sem = time_to_semantic(min);
2000 if compare_semantic_times(t, &min_sem) == Ordering::Less {
2001 return Err(format!("{} is below minimum {}", t, min));
2002 }
2003 }
2004 if let Some(max) = maximum {
2005 let max_sem = time_to_semantic(max);
2006 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
2007 return Err(format!("{} is above maximum {}", t, max));
2008 }
2009 }
2010 Ok(())
2011 }
2012 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
2013 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
2014 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
2015 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
2016 | (TypeSpecification::MeasureRange { .. }, ValueKind::Range(_, _))
2017 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
2018 | (TypeSpecification::Veto { .. }, _)
2019 | (TypeSpecification::Undetermined, _) => Ok(()),
2020 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
2021 "BUG: validate_value_against_type called with mismatched type/value: \
2022 spec={:?}, value={:?} — typing must be enforced before validation",
2023 spec, value_kind
2024 ),
2025 (_, _) => Ok(()),
2026 }
2027}
2028
2029pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
2030 let mut errors = Vec::new();
2031
2032 for (data_path, data_definition) in &plan.data {
2033 let (expected_type, lit) = match data_definition {
2034 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
2035 DataDefinition::TypeDeclaration { .. }
2036 | DataDefinition::Import { .. }
2037 | DataDefinition::Reference { .. } => continue,
2038 };
2039
2040 if let Err(msg) = validate_value_against_type(expected_type, lit) {
2041 let source = data_definition.source().clone();
2042 errors.push(Error::validation(
2043 format!(
2044 "Invalid value for data {} (expected {}): {}",
2045 data_path,
2046 expected_type.name().as_str(),
2047 msg
2048 ),
2049 Some(source),
2050 None::<String>,
2051 ));
2052 }
2053 }
2054
2055 errors
2056}
2057
2058pub(crate) fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
2059 for rule in &plan.rules {
2060 for instructions in [&rule.instructions, &rule.source_instructions] {
2061 for insn in &instructions.code {
2062 let Instruction::UnitConversion { target, .. } = insn else {
2063 continue;
2064 };
2065 let Some((unit_name, owning_type)) =
2066 crate::computation::units::conversion_target_declares_unit(target)
2067 else {
2068 continue;
2069 };
2070 if crate::computation::units::owning_type_declares_unit_name(
2071 owning_type.as_ref(),
2072 unit_name,
2073 ) {
2074 continue;
2075 }
2076 return Err(Error::validation(
2077 format!(
2078 "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
2079 owning_type.name()
2080 ),
2081 None::<Source>,
2082 Some(plan.spec_name.clone()),
2083 ));
2084 }
2085 }
2086 }
2087 Ok(())
2088}
2089
2090pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
2091 validate_unit_conversion_targets(plan)
2092}
2093
2094#[derive(Debug, Clone, Serialize, Deserialize)]
2105pub struct ExecutionPlanSerialized {
2106 pub spec_name: String,
2107 #[serde(skip_serializing_if = "Option::is_none", default)]
2108 pub commentary: Option<String>,
2109 #[serde(
2110 serialize_with = "serialize_resolved_data_value_map",
2111 deserialize_with = "deserialize_resolved_data_value_map"
2112 )]
2113 pub data: IndexMap<DataPath, DataDefinition>,
2114 #[serde(default)]
2115 pub rules: Vec<ExecutableRule>,
2116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2117 pub reference_evaluation_order: Vec<DataPath>,
2118 #[serde(default)]
2119 pub meta: HashMap<String, MetaValue>,
2120 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
2123 pub unit_index: HashMap<String, Arc<LemmaType>>,
2124 pub effective: EffectiveDate,
2125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2126 pub sources: SpecSources,
2127}
2128
2129impl From<&ExecutionPlan> for ExecutionPlanSerialized {
2130 fn from(plan: &ExecutionPlan) -> Self {
2131 Self {
2132 spec_name: plan.spec_name.clone(),
2133 commentary: plan.commentary.clone(),
2134 data: plan.data.clone(),
2135 rules: plan.rules.clone(),
2136 reference_evaluation_order: plan.reference_evaluation_order.clone(),
2137 meta: plan.meta.clone(),
2138 unit_index: plan.resolved_types.unit_index.clone(),
2139 effective: plan.effective.clone(),
2140 sources: plan.sources.clone(),
2141 }
2142 }
2143}
2144
2145impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
2146 type Error = crate::Error;
2147
2148 fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
2149 let signature_index = crate::planning::graph::build_signature_index(
2150 &serialized.spec_name,
2151 &serialized.unit_index,
2152 )?;
2153 for rule in &serialized.rules {
2157 validate_instructions(&rule.instructions).map_err(|message| {
2158 crate::Error::request(
2159 format!(
2160 "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
2161 serialized.spec_name, rule.name
2162 ),
2163 None::<String>,
2164 )
2165 })?;
2166 validate_instructions(&rule.source_instructions).map_err(|message| {
2167 crate::Error::request(
2168 format!(
2169 "Serialized execution plan for spec '{}' contains invalid source instructions for rule '{}': {message}",
2170 serialized.spec_name, rule.name
2171 ),
2172 None::<String>,
2173 )
2174 })?;
2175 }
2176 let max_register_count = serialized
2177 .rules
2178 .iter()
2179 .map(|rule| rule.instructions.register_count)
2180 .max()
2181 .unwrap_or(0);
2182 let plan = Self {
2183 spec_name: serialized.spec_name,
2184 commentary: serialized.commentary,
2185 data: serialized.data,
2186 rules: serialized.rules,
2187 max_register_count,
2188 reference_evaluation_order: serialized.reference_evaluation_order,
2189 meta: serialized.meta,
2190 resolved_types: ResolvedSpecTypes {
2191 unit_index: serialized.unit_index,
2192 ..ResolvedSpecTypes::default()
2193 },
2194 signature_index,
2195 effective: serialized.effective,
2196 sources: serialized.sources,
2197 };
2198 validate_unit_index_references(&plan).map_err(|error| {
2199 crate::Error::request(
2200 format!(
2201 "Serialized execution plan for spec '{}' is invalid: {}",
2202 plan.spec_name,
2203 error.message()
2204 ),
2205 None::<String>,
2206 )
2207 })?;
2208 Ok(plan)
2209 }
2210}
2211
2212fn serialize_resolved_data_value_map<S>(
2213 map: &IndexMap<DataPath, DataDefinition>,
2214 serializer: S,
2215) -> Result<S::Ok, S::Error>
2216where
2217 S: Serializer,
2218{
2219 let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
2220 entries.serialize(serializer)
2221}
2222
2223fn deserialize_resolved_data_value_map<'de, D>(
2224 deserializer: D,
2225) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
2226where
2227 D: Deserializer<'de>,
2228{
2229 let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
2230 Ok(entries.into_iter().collect())
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235 use super::*;
2236 use crate::computation::rational::{rational_new, rational_zero};
2237 use crate::literals::DateGranularity;
2238 use crate::parsing::ast::DateTimeValue;
2239 use crate::planning::semantics::{
2240 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
2241 };
2242 use crate::Engine;
2243 use serde_json;
2244 use std::str::FromStr;
2245 use std::sync::Arc;
2246
2247 fn default_limits() -> ResourceLimits {
2248 ResourceLimits::default()
2249 }
2250
2251 fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
2252 let serialized = ExecutionPlanSerialized::from(plan);
2253 let json = serde_json::to_string(&serialized).expect("Should serialize");
2254 let back: ExecutionPlanSerialized =
2255 serde_json::from_str(&json).expect("Should deserialize");
2256 ExecutionPlan::try_from(back).expect("Should reconstruct")
2257 }
2258
2259 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
2260 pairs
2261 .iter()
2262 .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
2263 .collect()
2264 }
2265
2266 #[test]
2267 fn test_with_raw_values() {
2268 let mut engine = Engine::new();
2269 engine
2270 .load(
2271 r#"
2272 spec test
2273 data age: number -> default 25
2274 "#,
2275 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2276 "test.lemma",
2277 ))),
2278 )
2279 .unwrap();
2280
2281 let now = DateTimeValue::now();
2282 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2283 let data_path = DataPath::new(vec![], "age".to_string());
2284
2285 let values = input_data(&[("age", "30")]);
2286
2287 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2288 let updated_value = overlay.values.get(&data_path).unwrap().as_ref();
2289 match &updated_value.value {
2290 crate::planning::semantics::ValueKind::Number(n) => {
2291 assert_eq!(n, &rational_new(30, 1));
2292 }
2293 other => panic!("Expected number literal, got {:?}", other),
2294 }
2295 }
2296
2297 #[test]
2298 fn test_with_raw_values_type_mismatch() {
2299 let mut engine = Engine::new();
2300 engine
2301 .load(
2302 r#"
2303 spec test
2304 data age: number
2305 "#,
2306 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2307 "test.lemma",
2308 ))),
2309 )
2310 .unwrap();
2311
2312 let now = DateTimeValue::now();
2313 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2314
2315 let values = input_data(&[("age", "thirty")]);
2316
2317 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2318 let data_path = DataPath::new(vec![], "age".to_string());
2319 match overlay.violated.get(&data_path) {
2320 Some(reason) => {
2321 assert!(
2322 reason.contains("number"),
2323 "type mismatch must record violation reason, got: {reason}"
2324 );
2325 }
2326 None => panic!("expected violated data for age=thirty"),
2327 }
2328 }
2329
2330 #[test]
2331 fn test_with_raw_values_unknown_data() {
2332 let mut engine = Engine::new();
2333 engine
2334 .load(
2335 r#"
2336 spec test
2337 data known: number
2338 "#,
2339 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2340 "test.lemma",
2341 ))),
2342 )
2343 .unwrap();
2344
2345 let now = DateTimeValue::now();
2346 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2347
2348 let values = input_data(&[("unknown", "30")]);
2349
2350 assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
2351 }
2352
2353 #[test]
2354 fn test_with_raw_values_nested() {
2355 let mut engine = Engine::new();
2356 engine
2357 .load(
2358 r#"
2359 spec private
2360 data base_price: number
2361
2362 spec test
2363 uses rules: private
2364 "#,
2365 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2366 "test.lemma",
2367 ))),
2368 )
2369 .unwrap();
2370
2371 let now = DateTimeValue::now();
2372 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2373
2374 let values = input_data(&[("rules.base_price", "100")]);
2375
2376 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2377 let data_path = DataPath {
2378 segments: vec![PathSegment {
2379 data: "rules".to_string(),
2380 spec: "private".to_string(),
2381 }],
2382 data: "base_price".to_string(),
2383 };
2384 let updated_value = overlay.values.get(&data_path).unwrap().as_ref();
2385 match &updated_value.value {
2386 crate::planning::semantics::ValueKind::Number(n) => {
2387 assert_eq!(n, &rational_new(100, 1));
2388 }
2389 other => panic!("Expected number literal, got {:?}", other),
2390 }
2391 }
2392
2393 fn test_source() -> Source {
2394 use crate::parsing::ast::Span;
2395 Source::new(
2396 crate::parsing::source::SourceType::Volatile,
2397 Span {
2398 start: 0,
2399 end: 0,
2400 line: 1,
2401 col: 0,
2402 },
2403 )
2404 }
2405
2406 fn create_literal_expr(value: LiteralValue) -> Expression {
2407 Expression::new(
2408 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2409 test_source(),
2410 )
2411 }
2412
2413 fn create_data_path_expr(path: DataPath) -> Expression {
2414 Expression::new(
2415 crate::planning::semantics::ExpressionKind::DataPath(path),
2416 test_source(),
2417 )
2418 }
2419
2420 fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2421 Instructions {
2422 version: INSTRUCTIONS_VERSION,
2423 register_count: 1,
2424 register_types: vec![Arc::clone(&literal.lemma_type)],
2425 constants: vec![Arc::new(literal)],
2426 data_manifest: Vec::new(),
2427 veto_messages: Vec::new(),
2428 arm_tags: Vec::new(),
2429 conversion_tags: Vec::new(),
2430 code: vec![
2431 Instruction::LoadConstant {
2432 destination_register: 0,
2433 constant_index: 0,
2434 },
2435 Instruction::Return { source_register: 0 },
2436 ],
2437 }
2438 }
2439
2440 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2441 LiteralValue::number_from_decimal(n)
2442 }
2443
2444 fn create_boolean_literal(b: bool) -> LiteralValue {
2445 LiteralValue::from_bool(b)
2446 }
2447
2448 fn create_text_literal(s: String) -> LiteralValue {
2449 LiteralValue::text(s)
2450 }
2451
2452 #[test]
2453 fn with_values_should_enforce_number_maximum_constraint() {
2454 let data_path = DataPath::new(vec![], "x".to_string());
2457
2458 let max10 = crate::planning::semantics::LemmaType::primitive(
2459 crate::planning::semantics::TypeSpecification::Number {
2460 minimum: None,
2461 maximum: Some(rational_new(10, 1)),
2462 decimals: None,
2463 help: String::new(),
2464 },
2465 );
2466 let source = Source::new(
2467 crate::parsing::source::SourceType::Volatile,
2468 crate::parsing::ast::Span {
2469 start: 0,
2470 end: 0,
2471 line: 1,
2472 col: 0,
2473 },
2474 );
2475 let mut data = IndexMap::new();
2476 data.insert(
2477 data_path.clone(),
2478 crate::planning::semantics::DataDefinition::Value {
2479 value: crate::planning::semantics::LiteralValue::number_with_type(
2480 rational_new(0, 1),
2481 Arc::new(max10.clone()),
2482 ),
2483 source: source.clone(),
2484 },
2485 );
2486
2487 let plan = ExecutionPlan {
2488 spec_name: "test".to_string(),
2489 commentary: None,
2490 data,
2491 rules: Vec::new(),
2492 max_register_count: 0,
2493 reference_evaluation_order: Vec::new(),
2494 meta: HashMap::new(),
2495 resolved_types: ResolvedSpecTypes::default(),
2496 signature_index: HashMap::new(),
2497 effective: EffectiveDate::Origin,
2498 sources: Vec::new(),
2499 };
2500
2501 let values = input_data(&[("x", "11")]);
2502
2503 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2504 match overlay.violated.get(&data_path) {
2505 Some(reason) => {
2506 assert!(
2507 reason.contains("maximum") || reason.contains("10"),
2508 "x=11 must violate maximum 10, got: {reason}"
2509 );
2510 }
2511 None => panic!("expected violated data for x=11"),
2512 }
2513 }
2514
2515 #[test]
2516 fn with_values_should_enforce_text_enum_options() {
2517 let data_path = DataPath::new(vec![], "tier".to_string());
2519
2520 let tier = crate::planning::semantics::LemmaType::primitive(
2521 crate::planning::semantics::TypeSpecification::Text {
2522 length: None,
2523 options: vec!["silver".to_string(), "gold".to_string()],
2524 help: String::new(),
2525 },
2526 );
2527 let source = Source::new(
2528 crate::parsing::source::SourceType::Volatile,
2529 crate::parsing::ast::Span {
2530 start: 0,
2531 end: 0,
2532 line: 1,
2533 col: 0,
2534 },
2535 );
2536 let mut data = IndexMap::new();
2537 data.insert(
2538 data_path.clone(),
2539 crate::planning::semantics::DataDefinition::Value {
2540 value: crate::planning::semantics::LiteralValue::text_with_type(
2541 "silver".to_string(),
2542 Arc::new(tier.clone()),
2543 ),
2544 source,
2545 },
2546 );
2547
2548 let plan = ExecutionPlan {
2549 spec_name: "test".to_string(),
2550 commentary: None,
2551 data,
2552 rules: Vec::new(),
2553 max_register_count: 0,
2554 reference_evaluation_order: Vec::new(),
2555 meta: HashMap::new(),
2556 resolved_types: ResolvedSpecTypes::default(),
2557 signature_index: HashMap::new(),
2558 effective: EffectiveDate::Origin,
2559 sources: Vec::new(),
2560 };
2561
2562 let values = input_data(&[("tier", "platinum")]);
2563
2564 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2565 match overlay.violated.get(&data_path) {
2566 Some(reason) => {
2567 assert!(
2568 reason.contains("allowed options") || reason.contains("platinum"),
2569 "invalid enum must record violation, got: {reason}"
2570 );
2571 }
2572 None => panic!("expected violated data for tier=platinum"),
2573 }
2574 }
2575
2576 #[test]
2577 fn with_values_should_enforce_measure_decimals() {
2578 let data_path = DataPath::new(vec![], "price".to_string());
2581
2582 let money = crate::planning::semantics::LemmaType::primitive(
2583 crate::planning::semantics::TypeSpecification::Measure {
2584 minimum: None,
2585 maximum: None,
2586 decimals: Some(2),
2587 units: crate::planning::semantics::MeasureUnits::from(vec![
2588 crate::planning::semantics::MeasureUnit::from_decimal_factor(
2589 "eur".to_string(),
2590 rust_decimal::Decimal::from_str("1.0").unwrap(),
2591 Vec::new(),
2592 )
2593 .expect("eur unit factor must be exact decimal"),
2594 ]),
2595 traits: Vec::new(),
2596 decomposition: None,
2597 help: String::new(),
2598 },
2599 );
2600 let source = Source::new(
2601 crate::parsing::source::SourceType::Volatile,
2602 crate::parsing::ast::Span {
2603 start: 0,
2604 end: 0,
2605 line: 1,
2606 col: 0,
2607 },
2608 );
2609 let mut data = IndexMap::new();
2610 data.insert(
2611 data_path.clone(),
2612 crate::planning::semantics::DataDefinition::Value {
2613 value: crate::planning::semantics::LiteralValue::measure_with_type(
2614 rational_zero(),
2615 "eur".to_string(),
2616 Arc::new(money.clone()),
2617 ),
2618 source,
2619 },
2620 );
2621
2622 let plan = ExecutionPlan {
2623 spec_name: "test".to_string(),
2624 commentary: None,
2625 data,
2626 rules: Vec::new(),
2627 max_register_count: 0,
2628 reference_evaluation_order: Vec::new(),
2629 meta: HashMap::new(),
2630 resolved_types: ResolvedSpecTypes::default(),
2631 signature_index: HashMap::new(),
2632 effective: EffectiveDate::Origin,
2633 sources: Vec::new(),
2634 };
2635
2636 let values = input_data(&[("price", "1.234 eur")]);
2637
2638 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2639 match overlay.violated.get(&data_path) {
2640 Some(reason) => {
2641 assert!(
2642 reason.contains("decimals") || reason.contains("decimal"),
2643 "1.234 eur must violate decimals=2, got: {reason}"
2644 );
2645 }
2646 None => panic!("expected violated data for price=1.234 eur"),
2647 }
2648 }
2649
2650 #[test]
2651 fn test_serialize_deserialize_execution_plan() {
2652 let data_path = DataPath {
2653 segments: vec![],
2654 data: "age".to_string(),
2655 };
2656 let mut data = IndexMap::new();
2657 data.insert(
2658 data_path.clone(),
2659 crate::planning::semantics::DataDefinition::Value {
2660 value: create_number_literal(0.into()),
2661 source: test_source(),
2662 },
2663 );
2664 let plan = ExecutionPlan {
2665 spec_name: "test".to_string(),
2666 commentary: None,
2667 data,
2668 rules: Vec::new(),
2669 max_register_count: 0,
2670 reference_evaluation_order: Vec::new(),
2671 meta: HashMap::new(),
2672 resolved_types: ResolvedSpecTypes::default(),
2673 signature_index: HashMap::new(),
2674 effective: EffectiveDate::Origin,
2675 sources: Vec::new(),
2676 };
2677
2678 let deserialized = roundtrip_execution_plan(&plan);
2679
2680 assert_eq!(deserialized.spec_name, plan.spec_name);
2681 assert_eq!(deserialized.data.len(), plan.data.len());
2682 assert_eq!(deserialized.rules.len(), plan.rules.len());
2683 }
2684
2685 #[test]
2686 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2687 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2688 let imported_type = crate::planning::semantics::LemmaType::new(
2689 "salary".to_string(),
2690 TypeSpecification::measure(),
2691 crate::planning::semantics::TypeExtends::Custom {
2692 parent: "money".to_string(),
2693 family: "money".to_string(),
2694 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2695 spec: Arc::clone(&dep_spec),
2696 },
2697 },
2698 );
2699
2700 let salary_path = DataPath::new(vec![], "salary".to_string());
2701 let mut data = IndexMap::new();
2702 data.insert(
2703 salary_path,
2704 crate::planning::semantics::DataDefinition::TypeDeclaration {
2705 resolved_type: Arc::new(imported_type),
2706 declared_default: None,
2707 source: test_source(),
2708 },
2709 );
2710
2711 let plan = ExecutionPlan {
2712 spec_name: "test".to_string(),
2713 commentary: None,
2714 data,
2715 rules: Vec::new(),
2716 max_register_count: 0,
2717 reference_evaluation_order: Vec::new(),
2718 meta: HashMap::new(),
2719 resolved_types: ResolvedSpecTypes::default(),
2720 signature_index: HashMap::new(),
2721 effective: EffectiveDate::Origin,
2722 sources: Vec::new(),
2723 };
2724
2725 let deserialized = roundtrip_execution_plan(&plan);
2726
2727 let recovered = deserialized
2728 .data
2729 .get(&DataPath::new(vec![], "salary".to_string()))
2730 .and_then(|d| d.schema_type())
2731 .expect("salary type should be present in plan.data");
2732 match &recovered.extends {
2733 crate::planning::semantics::TypeExtends::Custom {
2734 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2735 ..
2736 } => {
2737 assert_eq!(spec.name, "examples");
2738 }
2739 other => panic!(
2740 "Expected imported defining_spec after round-trip, got {:?}",
2741 other
2742 ),
2743 }
2744 }
2745
2746 #[test]
2747 fn test_serialize_deserialize_plan_with_rules() {
2748 use crate::planning::semantics::ExpressionKind;
2749
2750 let age_path = DataPath::new(vec![], "age".to_string());
2751 let mut data = IndexMap::new();
2752 data.insert(
2753 age_path.clone(),
2754 crate::planning::semantics::DataDefinition::Value {
2755 value: create_number_literal(0.into()),
2756 source: test_source(),
2757 },
2758 );
2759 let mut plan = ExecutionPlan {
2760 spec_name: "test".to_string(),
2761 commentary: None,
2762 data,
2763 rules: Vec::new(),
2764 max_register_count: 0,
2765 reference_evaluation_order: Vec::new(),
2766 meta: HashMap::new(),
2767 resolved_types: ResolvedSpecTypes::default(),
2768 signature_index: HashMap::new(),
2769 effective: EffectiveDate::Origin,
2770 sources: Vec::new(),
2771 };
2772
2773 let rule = ExecutableRule {
2774 path: RulePath::new(vec![], "can_drive".to_string()),
2775 name: "can_drive".to_string(),
2776 branches: vec![{
2777 let result = create_literal_expr(create_boolean_literal(true));
2778 let condition = Expression::new(
2779 ExpressionKind::Comparison(
2780 Arc::new(create_data_path_expr(age_path.clone())),
2781 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2782 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2783 ),
2784 test_source(),
2785 );
2786 Branch {
2787 condition: Some(condition.clone()),
2788 result: result.clone(),
2789 source: test_source(),
2790 }
2791 }],
2792 instructions: constant_return_instructions(create_boolean_literal(true)),
2793 source_instructions: constant_return_instructions(create_boolean_literal(true)),
2794 source: test_source(),
2795 rule_type: Arc::new(primitive_boolean().clone()),
2796 };
2797
2798 plan.rules.push(rule);
2799 plan.max_register_count = plan.rules[0].instructions.register_count;
2800
2801 let deserialized = roundtrip_execution_plan(&plan);
2802
2803 assert_eq!(deserialized.spec_name, plan.spec_name);
2804 assert_eq!(deserialized.data.len(), plan.data.len());
2805 assert_eq!(deserialized.rules.len(), plan.rules.len());
2806 assert_eq!(deserialized.rules[0].name, "can_drive");
2807 assert_eq!(deserialized.rules[0].branches.len(), 1);
2808 }
2809
2810 #[test]
2811 fn test_serialize_deserialize_plan_with_nested_data_paths() {
2812 use crate::planning::semantics::PathSegment;
2813 let data_path = DataPath {
2814 segments: vec![PathSegment {
2815 data: "employee".to_string(),
2816 spec: "private".to_string(),
2817 }],
2818 data: "salary".to_string(),
2819 };
2820
2821 let mut data = IndexMap::new();
2822 data.insert(
2823 data_path.clone(),
2824 crate::planning::semantics::DataDefinition::Value {
2825 value: create_number_literal(0.into()),
2826 source: test_source(),
2827 },
2828 );
2829 let plan = ExecutionPlan {
2830 spec_name: "test".to_string(),
2831 commentary: None,
2832 data,
2833 rules: Vec::new(),
2834 max_register_count: 0,
2835 reference_evaluation_order: Vec::new(),
2836 meta: HashMap::new(),
2837 resolved_types: ResolvedSpecTypes::default(),
2838 signature_index: HashMap::new(),
2839 effective: EffectiveDate::Origin,
2840 sources: Vec::new(),
2841 };
2842
2843 let deserialized = roundtrip_execution_plan(&plan);
2844
2845 assert_eq!(deserialized.data.len(), 1);
2846 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2847 assert_eq!(deserialized_path.segments.len(), 1);
2848 assert_eq!(deserialized_path.segments[0].data, "employee");
2849 assert_eq!(deserialized_path.data, "salary");
2850 }
2851
2852 #[test]
2853 fn test_serialize_deserialize_plan_with_multiple_data_types() {
2854 let name_path = DataPath::new(vec![], "name".to_string());
2855 let age_path = DataPath::new(vec![], "age".to_string());
2856 let active_path = DataPath::new(vec![], "active".to_string());
2857
2858 let mut data = IndexMap::new();
2859 data.insert(
2860 name_path.clone(),
2861 crate::planning::semantics::DataDefinition::Value {
2862 value: create_text_literal("Alice".to_string()),
2863 source: test_source(),
2864 },
2865 );
2866 data.insert(
2867 age_path.clone(),
2868 crate::planning::semantics::DataDefinition::Value {
2869 value: create_number_literal(30.into()),
2870 source: test_source(),
2871 },
2872 );
2873 data.insert(
2874 active_path.clone(),
2875 crate::planning::semantics::DataDefinition::Value {
2876 value: create_boolean_literal(true),
2877 source: test_source(),
2878 },
2879 );
2880
2881 let plan = ExecutionPlan {
2882 spec_name: "test".to_string(),
2883 commentary: None,
2884 data,
2885 rules: Vec::new(),
2886 max_register_count: 0,
2887 reference_evaluation_order: Vec::new(),
2888 meta: HashMap::new(),
2889 resolved_types: ResolvedSpecTypes::default(),
2890 signature_index: HashMap::new(),
2891 effective: EffectiveDate::Origin,
2892 sources: Vec::new(),
2893 };
2894
2895 let deserialized = roundtrip_execution_plan(&plan);
2896
2897 assert_eq!(deserialized.data.len(), 3);
2898
2899 assert_eq!(
2900 deserialized.get_data_value(&name_path).unwrap().value,
2901 crate::planning::semantics::ValueKind::Text("Alice".to_string())
2902 );
2903 assert_eq!(
2904 deserialized.get_data_value(&age_path).unwrap().value,
2905 crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2906 );
2907 assert_eq!(
2908 deserialized.get_data_value(&active_path).unwrap().value,
2909 crate::planning::semantics::ValueKind::Boolean(true)
2910 );
2911 }
2912
2913 #[test]
2914 fn test_serialize_deserialize_plan_with_multiple_branches() {
2915 use crate::planning::semantics::ExpressionKind;
2916
2917 let points_path = DataPath::new(vec![], "points".to_string());
2918 let mut data = IndexMap::new();
2919 data.insert(
2920 points_path.clone(),
2921 crate::planning::semantics::DataDefinition::Value {
2922 value: create_number_literal(0.into()),
2923 source: test_source(),
2924 },
2925 );
2926 let mut plan = ExecutionPlan {
2927 spec_name: "test".to_string(),
2928 commentary: None,
2929 data,
2930 rules: Vec::new(),
2931 max_register_count: 0,
2932 reference_evaluation_order: Vec::new(),
2933 meta: HashMap::new(),
2934 resolved_types: ResolvedSpecTypes::default(),
2935 signature_index: HashMap::new(),
2936 effective: EffectiveDate::Origin,
2937 sources: Vec::new(),
2938 };
2939
2940 let rule = ExecutableRule {
2941 path: RulePath::new(vec![], "tier".to_string()),
2942 name: "tier".to_string(),
2943 branches: vec![
2944 {
2945 let result = create_literal_expr(create_text_literal("bronze".to_string()));
2946 Branch {
2947 condition: None,
2948 result: result.clone(),
2949 source: test_source(),
2950 }
2951 },
2952 {
2953 let result = create_literal_expr(create_text_literal("silver".to_string()));
2954 Branch {
2955 condition: Some(Expression::new(
2956 ExpressionKind::Comparison(
2957 Arc::new(create_data_path_expr(points_path.clone())),
2958 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2959 Arc::new(create_literal_expr(create_number_literal(100.into()))),
2960 ),
2961 test_source(),
2962 )),
2963 result: result.clone(),
2964 source: test_source(),
2965 }
2966 },
2967 {
2968 let result = create_literal_expr(create_text_literal("gold".to_string()));
2969 Branch {
2970 condition: Some(Expression::new(
2971 ExpressionKind::Comparison(
2972 Arc::new(create_data_path_expr(points_path.clone())),
2973 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2974 Arc::new(create_literal_expr(create_number_literal(500.into()))),
2975 ),
2976 test_source(),
2977 )),
2978 result: result.clone(),
2979 source: test_source(),
2980 }
2981 },
2982 ],
2983 instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2984 source_instructions: constant_return_instructions(create_text_literal(
2985 "bronze".to_string(),
2986 )),
2987 source: test_source(),
2988 rule_type: Arc::new(primitive_text().clone()),
2989 };
2990
2991 plan.rules.push(rule);
2992 plan.max_register_count = plan.rules[0].instructions.register_count;
2993
2994 let deserialized = roundtrip_execution_plan(&plan);
2995
2996 assert_eq!(deserialized.rules.len(), 1);
2997 assert_eq!(deserialized.rules[0].branches.len(), 3);
2998 assert!(deserialized.rules[0].branches[0].condition.is_none());
2999 assert!(deserialized.rules[0].branches[1].condition.is_some());
3000 assert!(deserialized.rules[0].branches[2].condition.is_some());
3001 }
3002
3003 #[test]
3004 fn test_serialize_deserialize_empty_plan() {
3005 let plan = ExecutionPlan {
3006 spec_name: "empty".to_string(),
3007 commentary: None,
3008 data: IndexMap::new(),
3009 rules: Vec::new(),
3010 max_register_count: 0,
3011 reference_evaluation_order: Vec::new(),
3012 meta: HashMap::new(),
3013 resolved_types: ResolvedSpecTypes::default(),
3014 signature_index: HashMap::new(),
3015 effective: EffectiveDate::Origin,
3016 sources: Vec::new(),
3017 };
3018
3019 let deserialized = roundtrip_execution_plan(&plan);
3020
3021 assert_eq!(deserialized.spec_name, "empty");
3022 assert_eq!(deserialized.data.len(), 0);
3023 assert_eq!(deserialized.rules.len(), 0);
3024 }
3025
3026 #[test]
3027 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
3028 use crate::planning::semantics::ExpressionKind;
3029
3030 let x_path = DataPath::new(vec![], "x".to_string());
3031 let mut data = IndexMap::new();
3032 data.insert(
3033 x_path.clone(),
3034 crate::planning::semantics::DataDefinition::Value {
3035 value: create_number_literal(0.into()),
3036 source: test_source(),
3037 },
3038 );
3039 let mut plan = ExecutionPlan {
3040 spec_name: "test".to_string(),
3041 commentary: None,
3042 data,
3043 rules: Vec::new(),
3044 max_register_count: 0,
3045 reference_evaluation_order: Vec::new(),
3046 meta: HashMap::new(),
3047 resolved_types: ResolvedSpecTypes::default(),
3048 signature_index: HashMap::new(),
3049 effective: EffectiveDate::Origin,
3050 sources: Vec::new(),
3051 };
3052
3053 let rule = ExecutableRule {
3054 path: RulePath::new(vec![], "doubled".to_string()),
3055 name: "doubled".to_string(),
3056 branches: vec![{
3057 let result = Expression::new(
3058 ExpressionKind::Arithmetic(
3059 Arc::new(create_data_path_expr(x_path.clone())),
3060 crate::parsing::ast::ArithmeticComputation::Multiply,
3061 Arc::new(create_literal_expr(create_number_literal(2.into()))),
3062 ),
3063 test_source(),
3064 );
3065 Branch {
3066 condition: None,
3067 result: result.clone(),
3068 source: test_source(),
3069 }
3070 }],
3071 instructions: constant_return_instructions(create_number_literal(0.into())),
3072 source_instructions: constant_return_instructions(create_number_literal(0.into())),
3073 source: test_source(),
3074 rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
3075 };
3076
3077 plan.rules.push(rule);
3078 plan.max_register_count = plan.rules[0].instructions.register_count;
3079
3080 let deserialized = roundtrip_execution_plan(&plan);
3081
3082 assert_eq!(deserialized.rules.len(), 1);
3083 match &deserialized.rules[0].branches[0].result.kind {
3084 ExpressionKind::Arithmetic(left, op, right) => {
3085 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
3086 match &left.kind {
3087 ExpressionKind::DataPath(_) => {}
3088 _ => panic!("Expected DataPath in left operand"),
3089 }
3090 match &right.kind {
3091 ExpressionKind::Literal(_) => {}
3092 _ => panic!("Expected Literal in right operand"),
3093 }
3094 }
3095 _ => panic!("Expected Arithmetic expression"),
3096 }
3097 }
3098
3099 #[test]
3100 fn test_serialize_deserialize_round_trip_equality() {
3101 use crate::planning::semantics::ExpressionKind;
3102
3103 let age_path = DataPath::new(vec![], "age".to_string());
3104 let mut data = IndexMap::new();
3105 data.insert(
3106 age_path.clone(),
3107 crate::planning::semantics::DataDefinition::Value {
3108 value: create_number_literal(0.into()),
3109 source: test_source(),
3110 },
3111 );
3112 let mut plan = ExecutionPlan {
3113 spec_name: "test".to_string(),
3114 commentary: None,
3115 data,
3116 rules: Vec::new(),
3117 max_register_count: 0,
3118 reference_evaluation_order: Vec::new(),
3119 meta: HashMap::new(),
3120 resolved_types: ResolvedSpecTypes::default(),
3121 signature_index: HashMap::new(),
3122 effective: EffectiveDate::Origin,
3123 sources: Vec::new(),
3124 };
3125
3126 let rule = ExecutableRule {
3127 path: RulePath::new(vec![], "is_adult".to_string()),
3128 name: "is_adult".to_string(),
3129 branches: vec![{
3130 let result = create_literal_expr(create_boolean_literal(true));
3131 let condition = Expression::new(
3132 ExpressionKind::Comparison(
3133 Arc::new(create_data_path_expr(age_path.clone())),
3134 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
3135 Arc::new(create_literal_expr(create_number_literal(18.into()))),
3136 ),
3137 test_source(),
3138 );
3139 Branch {
3140 condition: Some(condition.clone()),
3141 result: result.clone(),
3142 source: test_source(),
3143 }
3144 }],
3145 instructions: constant_return_instructions(create_boolean_literal(true)),
3146 source_instructions: constant_return_instructions(create_boolean_literal(true)),
3147 source: test_source(),
3148 rule_type: Arc::new(primitive_boolean().clone()),
3149 };
3150
3151 plan.rules.push(rule);
3152 plan.max_register_count = plan.rules[0].instructions.register_count;
3153
3154 let deserialized = roundtrip_execution_plan(&plan);
3155 let deserialized2 = roundtrip_execution_plan(&deserialized);
3156
3157 assert_eq!(deserialized2.spec_name, plan.spec_name);
3158 assert_eq!(deserialized2.data.len(), plan.data.len());
3159 assert_eq!(deserialized2.rules.len(), plan.rules.len());
3160 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
3161 assert_eq!(
3162 deserialized2.rules[0].branches.len(),
3163 plan.rules[0].branches.len()
3164 );
3165 }
3166
3167 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
3168 ExecutionPlan {
3169 spec_name: "s".into(),
3170 commentary: None,
3171 data: IndexMap::new(),
3172 rules: Vec::new(),
3173 max_register_count: 0,
3174 reference_evaluation_order: Vec::new(),
3175 meta: HashMap::new(),
3176 resolved_types: ResolvedSpecTypes::default(),
3177 signature_index: HashMap::new(),
3178 effective,
3179 sources: Vec::new(),
3180 }
3181 }
3182
3183 #[test]
3184 fn plan_at_exact_boundary_selects_later_slice() {
3185 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3186
3187 let june = DateTimeValue {
3188 year: 2025,
3189 month: 6,
3190 day: 1,
3191 hour: 0,
3192 minute: 0,
3193 second: 0,
3194 microsecond: 0,
3195 timezone: None,
3196
3197 granularity: DateGranularity::Full,
3198 };
3199 let dec = DateTimeValue {
3200 year: 2025,
3201 month: 12,
3202 day: 1,
3203 hour: 0,
3204 minute: 0,
3205 second: 0,
3206 microsecond: 0,
3207 timezone: None,
3208
3209 granularity: DateGranularity::Full,
3210 };
3211
3212 let set = ExecutionPlanSet {
3213 spec_name: "s".into(),
3214 plans: vec![
3215 empty_plan(EffectiveDate::Origin),
3216 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
3217 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
3218 ],
3219 };
3220
3221 assert!(std::ptr::eq(
3222 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
3223 .expect("boundary instant"),
3224 &set.plans[1]
3225 ));
3226 assert!(std::ptr::eq(
3227 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
3228 .expect("dec boundary"),
3229 &set.plans[2]
3230 ));
3231 }
3232
3233 #[test]
3234 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
3235 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3236
3237 let june = DateTimeValue {
3238 year: 2025,
3239 month: 6,
3240 day: 1,
3241 hour: 0,
3242 minute: 0,
3243 second: 0,
3244 microsecond: 0,
3245 timezone: None,
3246
3247 granularity: DateGranularity::Full,
3248 };
3249 let may_end = DateTimeValue {
3250 year: 2025,
3251 month: 5,
3252 day: 31,
3253 hour: 23,
3254 minute: 59,
3255 second: 59,
3256 microsecond: 0,
3257 timezone: None,
3258
3259 granularity: DateGranularity::DateTime,
3260 };
3261
3262 let set = ExecutionPlanSet {
3263 spec_name: "s".into(),
3264 plans: vec![
3265 empty_plan(EffectiveDate::Origin),
3266 empty_plan(EffectiveDate::DateTimeValue(june)),
3267 ],
3268 };
3269
3270 assert!(std::ptr::eq(
3271 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
3272 .expect("may 31"),
3273 &set.plans[0]
3274 ));
3275 }
3276
3277 #[test]
3278 fn plan_at_single_plan_matches_any_instant_after_start() {
3279 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3280
3281 let t = DateTimeValue {
3282 year: 2025,
3283 month: 3,
3284 day: 1,
3285 hour: 0,
3286 minute: 0,
3287 second: 0,
3288 microsecond: 0,
3289 timezone: None,
3290
3291 granularity: DateGranularity::Full,
3292 };
3293 let set = ExecutionPlanSet {
3294 spec_name: "s".into(),
3295 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
3296 year: 2025,
3297 month: 1,
3298 day: 1,
3299 hour: 0,
3300 minute: 0,
3301 second: 0,
3302 microsecond: 0,
3303 timezone: None,
3304
3305 granularity: DateGranularity::Full,
3306 }))],
3307 };
3308 assert!(std::ptr::eq(
3309 set.plan_at(&EffectiveDate::DateTimeValue(t))
3310 .expect("inside single slice"),
3311 &set.plans[0]
3312 ));
3313 }
3314
3315 #[test]
3318 fn schema_json_shape_contract() {
3319 let mut engine = Engine::new();
3320 engine
3321 .load(
3322 r#"
3323 spec pricing
3324 data bridge_height: measure
3325 -> unit meter 1
3326 -> default 100 meter
3327 data quantity: number -> minimum 0
3328 rule cost: bridge_height * quantity
3329 "#,
3330 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3331 "test.lemma",
3332 ))),
3333 )
3334 .unwrap();
3335 let now = DateTimeValue::now();
3336 let schema = engine
3337 .get_plan(None, "pricing", Some(&now))
3338 .unwrap()
3339 .schema(&DataOverlay::default());
3340
3341 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3342
3343 let bh = &value["data"]["bridge_height"];
3344 assert!(
3345 bh.is_object(),
3346 "data entry must be a named object, not tuple"
3347 );
3348 assert!(
3349 bh.get("type").is_some(),
3350 "data entry must expose `type` field"
3351 );
3352 assert!(
3353 bh.get("default").is_some(),
3354 "bridge_height exposes `-> default` as schema default suggestion"
3355 );
3356 assert!(
3357 bh.get("prefilled").is_none(),
3358 "bridge_height is not prefilled from spec"
3359 );
3360 assert!(
3361 bh.get("supplied").is_none(),
3362 "bridge_height has no caller overlay"
3363 );
3364
3365 let ty = &bh["type"];
3366 assert_eq!(
3367 ty["kind"], "measure",
3368 "kind tag sits on the type object itself"
3369 );
3370 assert!(
3371 ty["units"].is_array(),
3372 "measure-only fields flatten up to top level"
3373 );
3374 assert!(
3375 ty.get("options").is_none(),
3376 "text-only fields must not leak"
3377 );
3378
3379 let quantity = &value["data"]["quantity"];
3380 assert_eq!(quantity["type"]["kind"], "number");
3381 assert!(
3382 quantity.get("default").is_none(),
3383 "quantity has no default suggestion"
3384 );
3385 assert!(
3386 quantity.get("prefilled").is_none(),
3387 "quantity has no prefilled literal"
3388 );
3389 assert!(
3390 quantity.get("supplied").is_none(),
3391 "quantity has no caller overlay"
3392 );
3393
3394 let cost = &value["rules"]["cost"];
3395 assert_eq!(
3396 cost["kind"], "measure",
3397 "rule types use the same flat shape"
3398 );
3399 assert!(
3400 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3401 "measure rule result types expose declared units"
3402 );
3403 assert!(
3404 cost["units"][0].get("factor").is_some(),
3405 "measure rule units use factor field"
3406 );
3407 }
3408
3409 #[test]
3410 fn schema_rule_result_units_contract() {
3411 let mut engine = Engine::new();
3412 engine
3413 .load(
3414 r#"
3415 spec units_contract
3416 data money: measure
3417 -> unit eur 1
3418 -> unit usd 0.91
3419 data rate: ratio
3420 -> unit basis_points 10000
3421 -> unit percent 100
3422 -> default 500 basis_points
3423 rule total: money
3424 rule rate_out: rate
3425 "#,
3426 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3427 "units_contract.lemma",
3428 ))),
3429 )
3430 .unwrap();
3431 let now = DateTimeValue::now();
3432 let schema = engine
3433 .get_plan(None, "units_contract", Some(&now))
3434 .unwrap()
3435 .schema(&DataOverlay::default());
3436 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3437
3438 let money_units = &value["data"]["money"]["type"]["units"];
3439 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3440 assert!(money_units[0].get("name").is_some());
3441 assert!(money_units[0].get("factor").is_some());
3442 assert!(money_units[0]["factor"].get("numer").is_some());
3443 assert!(money_units[0]["factor"].get("denom").is_some());
3444
3445 let rate_units = &value["data"]["rate"]["type"]["units"];
3446 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3447 assert!(rate_units[0].get("name").is_some());
3448 assert!(rate_units[0].get("value").is_some());
3449 assert!(rate_units[0]["value"].get("numer").is_some());
3450 assert!(rate_units[0]["value"].get("denom").is_some());
3451
3452 let total_rule_units = &value["rules"]["total"]["units"];
3453 let money_unit_names: Vec<_> = money_units
3454 .as_array()
3455 .unwrap()
3456 .iter()
3457 .map(|u| u["name"].as_str().unwrap())
3458 .collect();
3459 let total_rule_unit_names: Vec<_> = total_rule_units
3460 .as_array()
3461 .unwrap()
3462 .iter()
3463 .map(|u| u["name"].as_str().unwrap())
3464 .collect();
3465 assert_eq!(total_rule_unit_names, money_unit_names);
3466
3467 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3468 let rate_unit_names: Vec<_> = rate_units
3469 .as_array()
3470 .unwrap()
3471 .iter()
3472 .map(|u| u["name"].as_str().unwrap())
3473 .collect();
3474 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3475 .as_array()
3476 .unwrap()
3477 .iter()
3478 .map(|u| u["name"].as_str().unwrap())
3479 .collect();
3480 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3481 }
3482
3483 #[test]
3484 fn schema_json_round_trip_preserves_shape() {
3485 let mut engine = Engine::new();
3486 engine
3487 .load(
3488 r#"
3489 spec s
3490 data age: number -> minimum 0 -> default 18
3491 data grade: text -> options "A" "B" "C"
3492 rule adult: age >= 18
3493 "#,
3494 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3495 )
3496 .unwrap();
3497 let now = DateTimeValue::now();
3498 let schema = engine
3499 .get_plan(None, "s", Some(&now))
3500 .unwrap()
3501 .schema(&DataOverlay::default());
3502
3503 let json = serde_json::to_string(&schema).unwrap();
3504 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3505 assert_eq!(schema, round_tripped);
3506 }
3507}
3508
3509