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;
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::{BTreeSet, 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 = 1;
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 { .. }) => Ok(()),
338 Some(other) => Err(format!(
339 "instruction stream must end with Return, found {other:?}"
340 )),
341 None => Err("instruction stream is empty".to_string()),
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct Instructions {
348 pub version: u32,
349 pub register_count: u16,
350 #[serde(with = "register_types_serde")]
351 pub register_types: Vec<Arc<LemmaType>>,
352 pub constants: Vec<LiteralValue>,
353 pub data_manifest: Vec<DataPath>,
354 pub veto_messages: Vec<String>,
355 pub code: Vec<Instruction>,
356}
357
358mod register_types_serde {
359 use super::LemmaType;
360 use serde::{Deserialize, Deserializer, Serialize, Serializer};
361 use std::sync::Arc;
362
363 pub fn serialize<S>(values: &[Arc<LemmaType>], serializer: S) -> Result<S::Ok, S::Error>
364 where
365 S: Serializer,
366 {
367 let refs: Vec<&LemmaType> = values.iter().map(|v| v.as_ref()).collect();
368 refs.serialize(serializer)
369 }
370
371 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Arc<LemmaType>>, D::Error>
372 where
373 D: Deserializer<'de>,
374 {
375 let values: Vec<LemmaType> = Vec::deserialize(deserializer)?;
376 Ok(values.into_iter().map(Arc::new).collect())
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382#[serde(rename_all = "snake_case")]
383pub enum Instruction {
384 LoadConstant {
385 destination_register: u16,
386 constant_index: u16,
387 },
388 LoadData {
389 destination_register: u16,
390 data_index: u16,
391 },
392 LoadNow {
393 destination_register: u16,
394 },
395 Arithmetic {
396 destination_register: u16,
397 operation: ArithmeticComputation,
398 left_register: u16,
399 right_register: u16,
400 },
401 Comparison {
402 destination_register: u16,
403 operation: ComparisonComputation,
404 left_register: u16,
405 right_register: u16,
406 },
407 UnitConversion {
408 destination_register: u16,
409 source_register: u16,
410 target: SemanticConversionTarget,
411 },
412 Mathematical {
413 destination_register: u16,
414 operation: MathematicalComputation,
415 source_register: u16,
416 },
417 DateRelative {
418 destination_register: u16,
419 kind: DateRelativeKind,
420 source_register: u16,
421 },
422 DateCalendar {
423 destination_register: u16,
424 kind: DateCalendarKind,
425 unit: CalendarPeriodUnit,
426 source_register: u16,
427 },
428 RangeLiteral {
429 destination_register: u16,
430 left_register: u16,
431 right_register: u16,
432 },
433 PastFutureRange {
434 destination_register: u16,
435 kind: DateRelativeKind,
436 source_register: u16,
437 },
438 RangeContainment {
439 destination_register: u16,
440 value_register: u16,
441 range_register: u16,
442 },
443 ResultIsVeto {
444 destination_register: u16,
445 source_register: u16,
446 },
447 MoveRegister {
448 destination_register: u16,
449 source_register: u16,
450 },
451 UserVeto {
452 destination_register: u16,
453 message_index: u16,
454 },
455 JumpIfFalse {
456 condition_register: u16,
457 target_instruction: u32,
458 #[serde(default)]
459 veto_semantics: JumpVetoSemantics,
460 },
461 Jump {
462 target_instruction: u32,
463 },
464 Return {
465 source_register: u16,
466 },
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct ExecutableRule {
474 pub path: RulePath,
476
477 pub name: String,
479
480 pub branches: Vec<Branch>,
482
483 pub instructions: Instructions,
485
486 pub source: Source,
488
489 #[serde(with = "arc_lemma_type")]
492 pub rule_type: Arc<LemmaType>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct Branch {
498 pub condition: Option<Expression>,
500
501 pub result: Expression,
503
504 pub source: Source,
506}
507
508mod arc_lemma_type {
509 use super::LemmaType;
510 use serde::{Deserialize, Deserializer, Serialize, Serializer};
511 use std::sync::Arc;
512
513 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
514 where
515 S: Serializer,
516 {
517 value.as_ref().serialize(serializer)
518 }
519
520 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
521 where
522 D: Deserializer<'de>,
523 {
524 LemmaType::deserialize(deserializer).map(Arc::new)
525 }
526}
527
528pub(crate) fn build_execution_plan(
531 graph: &Graph,
532 resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
533 effective: &EffectiveDate,
534 limits: &crate::limits::ResourceLimits,
535) -> Result<ExecutionPlan, Vec<Error>> {
536 let execution_order = graph.execution_order();
537
538 let main_spec = graph.main_spec();
539 let main_idx = resolved_types
540 .iter()
541 .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
542
543 let mut sources: SpecSources = Vec::new();
544 for (repo, spec, _) in resolved_types.iter() {
545 if !sources.iter().any(|e| {
546 e.repository == repo.name
547 && e.name == spec.name
548 && e.effective_from == spec.effective_from
549 }) {
550 sources.push(SpecSource {
551 repository: repo.name.clone(),
552 name: spec.name.clone(),
553 effective_from: spec.effective_from.clone(),
554 source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
555 });
556 }
557 }
558
559 let main_resolved_types = main_idx
560 .map(|idx| resolved_types.remove(idx).2)
561 .unwrap_or_default();
562 let data = graph.build_data(&main_resolved_types.resolved);
563
564 let undetermined_errors: Vec<Error> = data
571 .iter()
572 .filter_map(|(path, definition)| {
573 let (resolved_type, source) = match definition {
574 DataDefinition::TypeDeclaration {
575 resolved_type,
576 source,
577 ..
578 } => (resolved_type, source),
579 DataDefinition::Reference {
580 target: ReferenceTarget::Data(_),
581 resolved_type,
582 source,
583 ..
584 } => (resolved_type, source),
585 DataDefinition::Reference {
586 target: ReferenceTarget::Rule(_),
587 ..
588 }
589 | DataDefinition::Value { .. }
590 | DataDefinition::Import { .. } => return None,
591 };
592 if resolved_type.is_undetermined() {
593 Some(Error::validation(
594 format!("could not determine the type of '{path}'"),
595 Some(source.clone()),
596 None::<String>,
597 ))
598 } else {
599 None
600 }
601 })
602 .collect();
603 if !undetermined_errors.is_empty() {
604 return Err(undetermined_errors);
605 }
606
607 let signature_index = crate::planning::graph::build_signature_index(
608 &main_spec.name,
609 &main_resolved_types.unit_index,
610 )
611 .expect("BUG: signature_index build already validated during resolve_and_validate");
612
613 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
614 let mut max_register_count: u16 = 0;
615 let plan_rule_paths: HashSet<RulePath> = graph.rules().keys().cloned().collect();
616 let mut completed_rules: HashMap<RulePath, Arc<Expression>> = HashMap::new();
617
618 for rule_path in execution_order {
619 let rule_node = graph.rules().get(rule_path).expect(
620 "bug: rule from topological sort not in graph - validation should have caught this",
621 );
622
623 let mut executable_branches = Vec::new();
624 for (condition, result) in &rule_node.branches {
625 executable_branches.push(Branch {
626 condition: condition.clone(),
627 result: result.clone(),
628 source: rule_node.source.clone(),
629 });
630 }
631
632 let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
633 let (instructions, inlined_expression) = build_normalized_rule_instructions(
634 &rule_node.branches,
635 &completed_rules,
636 &plan_rule_paths,
637 &data,
638 &unit_ctx,
639 Some(rule_node.source.clone()),
640 &rule_node.rule_type,
641 limits.max_normalized_expression_nodes,
642 )
643 .map_err(|error| vec![error])?;
647 max_register_count = max_register_count.max(instructions.register_count);
648 completed_rules.insert(rule_path.clone(), inlined_expression);
649
650 executable_rules.push(ExecutableRule {
651 path: rule_path.clone(),
652 name: rule_path.rule.clone(),
653 branches: executable_branches,
654 instructions,
655 source: rule_node.source.clone(),
656 rule_type: Arc::clone(&rule_node.rule_type),
657 });
658 }
659
660 Ok(ExecutionPlan {
661 spec_name: main_spec.name.clone(),
662 commentary: main_spec.commentary.clone(),
663 data,
664 rules: executable_rules,
665 max_register_count,
666 reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
667 meta: main_spec
668 .meta_fields
669 .iter()
670 .map(|f| (f.key.clone(), f.value.clone()))
671 .collect(),
672 resolved_types: main_resolved_types,
673 signature_index,
674 effective: effective.clone(),
675 sources,
676 })
677}
678
679#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
703pub struct DataEntry {
704 #[serde(rename = "type")]
705 pub lemma_type: LemmaType,
706 #[serde(skip_serializing_if = "Option::is_none", default)]
707 pub bound_value: Option<LiteralValue>,
708 #[serde(skip_serializing_if = "Option::is_none", default)]
709 pub default: Option<LiteralValue>,
710}
711
712#[derive(Debug, Clone, Default)]
718pub struct DataOverlay {
719 pub values: HashMap<DataPath, LiteralValue>,
721 pub violated: HashMap<DataPath, String>,
724}
725
726impl DataOverlay {
727 pub fn resolve(
733 plan: &ExecutionPlan,
734 raw_values: HashMap<String, DataValueInput>,
735 limits: &ResourceLimits,
736 ) -> Result<Self, Error> {
737 let mut overlay = Self::default();
738
739 for (name, raw_value) in raw_values {
740 let data_path = plan.get_data_path_by_str(&name).ok_or_else(|| {
741 let available: Vec<String> = plan.data.keys().map(|p| p.input_key()).collect();
742 Error::request(
743 format!(
744 "Data '{}' not found. Available data: {}",
745 name,
746 available.join(", ")
747 ),
748 None::<String>,
749 )
750 })?;
751 let data_path = data_path.clone();
752
753 let data_definition = plan
754 .data
755 .get(&data_path)
756 .expect("BUG: data_path was just resolved from plan.data, must exist");
757
758 let data_source = data_definition.source().clone();
759 let type_arc = match data_definition {
760 DataDefinition::TypeDeclaration { resolved_type, .. }
761 | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
762 DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
763 DataDefinition::Import { .. } => {
764 return Err(Error::request(
765 format!(
766 "Data '{}' is a spec reference; cannot provide a value.",
767 name
768 ),
769 None::<String>,
770 ));
771 }
772 };
773
774 let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
775 Ok(value) => value,
776 Err(error) => {
777 overlay
778 .violated
779 .insert(data_path, error.message().to_string());
780 continue;
781 }
782 };
783
784 let size = literal_value.byte_size();
785 if size > limits.max_data_value_bytes {
786 return Err(Error::resource_limit_exceeded(
787 "max_data_value_bytes",
788 limits.max_data_value_bytes.to_string(),
789 size.to_string(),
790 format!(
791 "Reduce the size of data values to {} bytes or less",
792 limits.max_data_value_bytes
793 ),
794 Some(data_source.clone()),
795 None,
796 None,
797 )
798 .with_related_data(&name));
799 }
800
801 if let Err(message) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
802 overlay.violated.insert(data_path, message);
803 continue;
804 }
805
806 overlay.values.insert(data_path, literal_value);
807 }
808
809 Ok(overlay)
810 }
811
812 pub fn is_empty(&self) -> bool {
813 self.values.is_empty() && self.violated.is_empty()
814 }
815}
816
817pub(crate) fn build_known_values(
825 plan: &ExecutionPlan,
826 overlay: &DataOverlay,
827) -> HashMap<DataPath, LiteralValue> {
828 let mut known_values: HashMap<DataPath, LiteralValue> = plan
829 .data
830 .iter()
831 .filter_map(|(path, definition)| {
832 if overlay.violated.contains_key(path) {
833 return None;
834 }
835 definition
836 .value()
837 .map(|value| (path.clone(), value.clone()))
838 })
839 .collect();
840
841 for (path, value) in &overlay.values {
842 known_values.insert(path.clone(), value.clone());
843 }
844
845 known_values
846}
847
848fn schema_bound_value(
849 path: &DataPath,
850 data: &DataDefinition,
851 overlay: &DataOverlay,
852) -> Option<LiteralValue> {
853 if let Some(value) = overlay.values.get(path) {
854 return Some(value.clone());
855 }
856 data.bound_value().cloned()
857}
858
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
860pub struct SpecSchema {
861 pub spec: String,
863 #[serde(skip_serializing_if = "Option::is_none", default)]
865 pub commentary: Option<String>,
866 #[serde(skip_serializing_if = "Option::is_none", default)]
868 pub effective: Option<DateTimeValue>,
869 #[serde(skip_serializing_if = "Vec::is_empty", default)]
872 pub versions: Vec<DateTimeValue>,
873 pub data: indexmap::IndexMap<String, DataEntry>,
875 pub rules: indexmap::IndexMap<String, LemmaType>,
877 pub meta: HashMap<String, MetaValue>,
879}
880
881impl std::fmt::Display for SpecSchema {
882 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
883 write!(f, "Spec: {}", self.spec)?;
884
885 if let Some(commentary) = &self.commentary {
886 write!(f, "\n {}", commentary)?;
887 }
888
889 if !self.meta.is_empty() {
890 write!(f, "\n\nMeta:")?;
891 let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
893 entries.sort_by_key(|(k, _)| *k);
894 for (key, value) in entries {
895 write!(f, "\n {}: {}", key, value)?;
896 }
897 }
898
899 if !self.data.is_empty() {
900 write!(f, "\n\nData:")?;
901 for (name, entry) in &self.data {
902 write!(f, "\n {} ({})", name, entry.lemma_type.specifications)?;
903 for line in type_detail_lines(&entry.lemma_type.specifications) {
904 write!(f, "\n {}", line)?;
905 }
906 let help = entry.lemma_type.specifications.help();
907 if !help.is_empty() {
908 write!(f, "\n help: {}", help)?;
909 }
910 if let Some(val) = &entry.bound_value {
911 write!(f, "\n value: {}", val)?;
912 }
913 if let Some(val) = &entry.default {
914 write!(f, "\n default: {}", val)?;
915 }
916 }
917 }
918
919 if !self.rules.is_empty() {
920 write!(f, "\n\nRules:")?;
921 for (name, rule_type) in &self.rules {
922 write!(f, "\n {} ({})", name, rule_type.specifications)?;
923 }
924 }
925
926 if self.data.is_empty() && self.rules.is_empty() {
927 write!(f, "\n (no data or rules)")?;
928 }
929
930 Ok(())
931 }
932}
933
934pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
940 use crate::computation::rational::rational_to_display_str;
941 let mut lines = Vec::new();
942 match spec {
943 TypeSpecification::Quantity {
944 minimum,
945 maximum,
946 decimals,
947 units,
948 ..
949 } => {
950 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
951 if !unit_names.is_empty() {
952 lines.push(format!("units: {}", unit_names.join(", ")));
953 }
954 if let Some(d) = decimals {
955 lines.push(format!("decimals: {}", d));
956 }
957 if let Some((magnitude, unit_name)) = minimum {
958 lines.push(format!(
959 "minimum: {} {}",
960 rational_to_display_str(magnitude),
961 unit_name
962 ));
963 }
964 if let Some((magnitude, unit_name)) = maximum {
965 lines.push(format!(
966 "maximum: {} {}",
967 rational_to_display_str(magnitude),
968 unit_name
969 ));
970 }
971 }
972 TypeSpecification::Number {
973 minimum,
974 maximum,
975 decimals,
976 ..
977 } => {
978 if let Some(d) = decimals {
979 lines.push(format!("decimals: {}", d));
980 }
981 if let Some(v) = minimum {
982 lines.push(format!("minimum: {}", rational_to_display_str(v)));
983 }
984 if let Some(v) = maximum {
985 lines.push(format!("maximum: {}", rational_to_display_str(v)));
986 }
987 }
988 TypeSpecification::Ratio {
989 minimum,
990 maximum,
991 decimals,
992 units,
993 ..
994 } => {
995 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
996 if !unit_names.is_empty() {
997 lines.push(format!("units: {}", unit_names.join(", ")));
998 }
999 if let Some(d) = decimals {
1000 lines.push(format!("decimals: {}", d));
1001 }
1002 if let Some(v) = minimum {
1003 lines.push(format!("minimum: {}", rational_to_display_str(v)));
1004 }
1005 if let Some(v) = maximum {
1006 lines.push(format!("maximum: {}", rational_to_display_str(v)));
1007 }
1008 }
1009 TypeSpecification::Text {
1010 options, length, ..
1011 } => {
1012 if let Some(l) = length {
1013 lines.push(format!("length: {}", l));
1014 }
1015 if !options.is_empty() {
1016 let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
1017 lines.push(format!("options: {}", quoted.join(", ")));
1018 }
1019 }
1020 TypeSpecification::Date {
1021 minimum, maximum, ..
1022 } => {
1023 if let Some(v) = minimum {
1024 lines.push(format!("minimum: {}", v));
1025 }
1026 if let Some(v) = maximum {
1027 lines.push(format!("maximum: {}", v));
1028 }
1029 }
1030 TypeSpecification::Time {
1031 minimum, maximum, ..
1032 } => {
1033 if let Some(v) = minimum {
1034 lines.push(format!("minimum: {}", v));
1035 }
1036 if let Some(v) = maximum {
1037 lines.push(format!("maximum: {}", v));
1038 }
1039 }
1040 TypeSpecification::QuantityRange { units, .. } => {
1041 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1042 if !unit_names.is_empty() {
1043 lines.push(format!("units: {}", unit_names.join(", ")));
1044 }
1045 }
1046 TypeSpecification::RatioRange { units, .. } => {
1047 let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1048 if !unit_names.is_empty() {
1049 lines.push(format!("units: {}", unit_names.join(", ")));
1050 }
1051 }
1052 TypeSpecification::Boolean { .. }
1053 | TypeSpecification::NumberRange { .. }
1054 | TypeSpecification::DateRange { .. }
1055 | TypeSpecification::TimeRange { .. }
1056 | TypeSpecification::Veto { .. }
1057 | TypeSpecification::Undetermined => {}
1058 }
1059 lines
1060}
1061
1062impl ExecutionPlan {
1063 pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
1067 &self.resolved_types.unit_index
1068 }
1069
1070 pub fn local_rule_names(&self) -> Vec<String> {
1082 self.rules
1083 .iter()
1084 .filter(|r| r.path.segments.is_empty())
1085 .map(|r| r.name.clone())
1086 .collect()
1087 }
1088
1089 pub fn schema(&self, overlay: &DataOverlay) -> SpecSchema {
1090 let all_local_rules = self.local_rule_names();
1091 self.schema_for_rules(&all_local_rules, overlay)
1092 .expect("BUG: all_local_rules sourced from self.rules")
1093 }
1094
1095 pub fn interface_schema(&self, overlay: &DataOverlay) -> SpecSchema {
1100 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1101 .data
1102 .iter()
1103 .filter(|(_, data)| {
1104 data.schema_type().is_some() && !matches!(data, DataDefinition::Reference { .. })
1105 })
1106 .map(|(path, data)| {
1107 let lemma_type = data
1108 .schema_type()
1109 .expect("BUG: filter above ensured schema_type is Some")
1110 .clone();
1111 let bound_value = schema_bound_value(path, data, overlay);
1112 let default = data.default_suggestion();
1113 (
1114 path.segments.len(),
1115 data.source().span.start,
1116 path.input_key(),
1117 DataEntry {
1118 lemma_type,
1119 bound_value,
1120 default,
1121 },
1122 )
1123 })
1124 .collect();
1125 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1126
1127 let rule_entries: Vec<(String, LemmaType)> = self
1128 .rules
1129 .iter()
1130 .filter(|r| r.path.segments.is_empty())
1131 .map(|r| (r.name.clone(), (*r.rule_type).clone()))
1132 .collect();
1133
1134 SpecSchema {
1135 spec: self.spec_name.clone(),
1136 commentary: self.commentary.clone(),
1137 effective: self.effective.as_ref().cloned(),
1138 versions: Vec::new(),
1139 data: data_entries
1140 .into_iter()
1141 .map(|(_, _, name, data)| (name, data))
1142 .collect(),
1143 rules: rule_entries.into_iter().collect(),
1144 meta: self.meta.clone(),
1145 }
1146 }
1147
1148 pub fn schema_for_rules(
1164 &self,
1165 rule_names: &[String],
1166 overlay: &DataOverlay,
1167 ) -> Result<SpecSchema, Error> {
1168 let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
1169 for rule_name in rule_names {
1170 let rule = self.get_rule(rule_name).ok_or_else(|| {
1171 Error::request(
1172 format!(
1173 "Rule '{}' not found in spec '{}'",
1174 rule_name, self.spec_name
1175 ),
1176 None::<String>,
1177 )
1178 })?;
1179 rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
1180 }
1181
1182 let needed_data = self.collect_needed_data_paths(rule_names, overlay)?;
1183
1184 let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1185 .data
1186 .iter()
1187 .filter(|(path, _)| needed_data.contains(path))
1188 .filter(|(_, data)| !matches!(data, DataDefinition::Reference { .. }))
1189 .filter_map(|(path, data)| {
1190 let lemma_type = data.schema_type()?.clone();
1191 let bound_value = schema_bound_value(path, data, overlay);
1192 let default = data.default_suggestion();
1193 Some((
1194 path.segments.len(),
1195 data.source().span.start,
1196 path.input_key(),
1197 DataEntry {
1198 lemma_type,
1199 bound_value,
1200 default,
1201 },
1202 ))
1203 })
1204 .collect();
1205 data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1206 let data_entries: Vec<(String, DataEntry)> = data_entries
1207 .into_iter()
1208 .map(|(_, _, name, data)| (name, data))
1209 .collect();
1210
1211 Ok(SpecSchema {
1212 spec: self.spec_name.clone(),
1213 commentary: self.commentary.clone(),
1214 effective: self.effective.as_ref().cloned(),
1215 versions: Vec::new(),
1216 data: data_entries.into_iter().collect(),
1217 rules: rule_entries.into_iter().collect(),
1218 meta: self.meta.clone(),
1219 })
1220 }
1221
1222 pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
1224 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1225 self.data
1226 .keys()
1227 .find(|path| path.input_key() == canonical_name)
1228 }
1229
1230 pub fn validated_response_rule_names(
1234 &self,
1235 rules: Option<&[String]>,
1236 ) -> Result<std::collections::HashSet<String>, Error> {
1237 let Some(rules) = rules else {
1238 return Ok(self.local_rule_names().into_iter().collect());
1239 };
1240 if rules.is_empty() {
1241 return Err(Error::request(
1242 "at least one rule required".to_string(),
1243 None::<String>,
1244 ));
1245 }
1246 let mut names = std::collections::HashSet::new();
1247 for rule_name in rules {
1248 let rule = self.get_rule(rule_name).ok_or_else(|| {
1249 Error::request(
1250 format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
1251 None::<String>,
1252 )
1253 })?;
1254 names.insert(rule.name.clone());
1255 }
1256 Ok(names)
1257 }
1258
1259 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
1261 let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1262 self.rules
1263 .iter()
1264 .find(|r| r.name == canonical_name && r.path.segments.is_empty())
1265 }
1266
1267 pub fn collect_needed_data_paths(
1276 &self,
1277 rule_names: &[String],
1278 overlay: &DataOverlay,
1279 ) -> Result<HashSet<DataPath>, Error> {
1280 let known_values = build_known_values(self, overlay);
1281
1282 let mut needed_data: HashSet<DataPath> = HashSet::new();
1283 let mut visited_rules: HashSet<RulePath> = HashSet::new();
1284 let mut rule_worklist: Vec<RulePath> = Vec::new();
1285
1286 for rule_name in rule_names {
1288 let rule = self.get_rule(rule_name).ok_or_else(|| {
1289 Error::request(
1290 format!(
1291 "Rule '{}' not found in spec '{}'",
1292 rule_name, self.spec_name
1293 ),
1294 None::<String>,
1295 )
1296 })?;
1297 rule_worklist.push(rule.path.clone());
1298 }
1299
1300 while let Some(rule_path) = rule_worklist.pop() {
1303 if !visited_rules.insert(rule_path.clone()) {
1304 continue;
1305 }
1306
1307 let rule = self.get_rule_by_path(&rule_path).unwrap_or_else(|| {
1308 panic!(
1309 "BUG: rule path '{}' placed on worklist but not found in plan '{}'",
1310 rule_path.rule, self.spec_name
1311 )
1312 });
1313
1314 for (branch_index, branch) in rule.branches.iter().enumerate() {
1315 if branch_index == 0 {
1316 let any_unless_definitely_true =
1317 rule.branches[1..].iter().any(|unless_branch| {
1318 let unless_condition = unless_branch
1319 .condition
1320 .as_ref()
1321 .expect("BUG: unless branch missing condition");
1322 crate::evaluation::partial::try_evaluate_condition(
1323 unless_condition,
1324 &known_values,
1325 self,
1326 ) == Some(true)
1327 });
1328 if any_unless_definitely_true {
1329 continue;
1330 }
1331 } else if let Some(condition) = &branch.condition {
1332 if crate::evaluation::partial::try_evaluate_condition(
1333 condition,
1334 &known_values,
1335 self,
1336 ) == Some(false)
1337 {
1338 continue;
1339 }
1340 }
1341
1342 let mut branch_data: HashSet<DataPath> = HashSet::new();
1343 if let Some(condition) = &branch.condition {
1344 condition.collect_data_paths(&mut branch_data);
1345 }
1346 branch.result.collect_data_paths(&mut branch_data);
1347
1348 let mut branch_rules: HashSet<RulePath> = HashSet::new();
1349 if let Some(condition) = &branch.condition {
1350 condition.collect_rule_paths(&mut branch_rules);
1351 }
1352 branch.result.collect_rule_paths(&mut branch_rules);
1353
1354 for data_path in &branch_data {
1355 if let Some(DataDefinition::Reference {
1356 target: ReferenceTarget::Rule(target_rule),
1357 ..
1358 }) = self.data.get(data_path)
1359 {
1360 branch_rules.insert(target_rule.clone());
1361 }
1362 }
1363
1364 needed_data.extend(branch_data);
1365 rule_worklist.extend(branch_rules);
1366 }
1367 }
1368
1369 Ok(needed_data)
1370 }
1371
1372 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
1374 self.rules.iter().find(|r| &r.path == rule_path)
1375 }
1376
1377 pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
1379 self.data.get(path).and_then(|d| d.value())
1380 }
1381}
1382
1383pub(crate) fn validate_value_against_type(
1384 expected_type: &LemmaType,
1385 value: &LiteralValue,
1386) -> Result<(), String> {
1387 use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
1388 use crate::planning::semantics::TypeSpecification;
1389
1390 fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
1391 match commit_rational_to_decimal(magnitude) {
1392 Ok(decimal) => decimal.scale() > u32::from(max_decimals),
1393 Err(_) => true,
1394 }
1395 }
1396
1397 fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
1398 use crate::computation::rational::rational_to_display_str;
1399 match commit_rational_to_decimal(r) {
1400 Ok(decimal) => match decimals {
1401 Some(dp) => {
1402 let rounded = decimal.round_dp(u32::from(dp));
1403 format!("{:.prec$}", rounded, prec = dp as usize)
1404 }
1405 None => decimal.normalize().to_string(),
1406 },
1407 Err(_) => rational_to_display_str(r),
1408 }
1409 }
1410
1411 match (&expected_type.specifications, &value.value) {
1412 (
1413 TypeSpecification::Number {
1414 minimum,
1415 maximum,
1416 decimals,
1417 ..
1418 },
1419 ValueKind::Number(n),
1420 ) => {
1421 if let Some(d) = decimals {
1422 if exceeds_decimal_places(n, *d) {
1423 return Err(format!(
1424 "{} exceeds decimals constraint {d}",
1425 format_rational(n, *decimals)
1426 ));
1427 }
1428 }
1429 if let Some(min) = minimum {
1430 if n < min {
1431 return Err(format!(
1432 "{} is below minimum {}",
1433 format_rational(n, *decimals),
1434 format_rational(min, *decimals)
1435 ));
1436 }
1437 }
1438 if let Some(max) = maximum {
1439 if n > max {
1440 return Err(format!(
1441 "{} is above maximum {}",
1442 format_rational(n, *decimals),
1443 format_rational(max, *decimals)
1444 ));
1445 }
1446 }
1447 Ok(())
1448 }
1449 (
1450 TypeSpecification::Quantity {
1451 minimum,
1452 maximum,
1453 decimals,
1454 units,
1455 ..
1456 },
1457 ValueKind::Quantity(magnitude, signature),
1458 ) => {
1459 use crate::computation::rational::checked_div;
1460 use crate::planning::semantics::quantity_declared_bound_canonical;
1461 let unit = signature
1462 .first()
1463 .map(|(n, _)| n.as_str())
1464 .expect("BUG: Quantity value has empty signature in execution plan validation");
1465 let quantity_unit = units.get(unit)?;
1466 let factor = &quantity_unit.factor;
1467 let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1468 format!("cannot de-canonicalize quantity for validation: {failure}")
1469 })?;
1470 if let Some(d) = decimals {
1471 if exceeds_decimal_places(&in_unit, *d) {
1472 return Err(format!(
1473 "{} {unit} exceeds decimals constraint {d}",
1474 format_rational(&in_unit, *decimals)
1475 ));
1476 }
1477 }
1478 if let Some(bound) = minimum {
1479 let canonical_min = quantity_declared_bound_canonical(
1480 bound,
1481 units,
1482 expected_type.name().as_str(),
1483 "minimum",
1484 )?;
1485 if magnitude < &canonical_min {
1486 let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1487 format!("cannot de-canonicalize minimum for validation: {failure}")
1488 })?;
1489 let value_display =
1490 format!("{} {}", format_rational(&in_unit, *decimals), unit);
1491 let bound_display = format!(
1492 "{} {}",
1493 format_rational(&min_in_unit, *decimals),
1494 quantity_unit.name
1495 );
1496 return Err(format!("{value_display} is below minimum {bound_display}"));
1497 }
1498 }
1499 if let Some(bound) = maximum {
1500 let canonical_max = quantity_declared_bound_canonical(
1501 bound,
1502 units,
1503 expected_type.name().as_str(),
1504 "maximum",
1505 )?;
1506 if magnitude > &canonical_max {
1507 let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1508 format!("cannot de-canonicalize maximum for validation: {failure}")
1509 })?;
1510 let value_display =
1511 format!("{} {}", format_rational(&in_unit, *decimals), unit);
1512 let bound_display = format!(
1513 "{} {}",
1514 format_rational(&max_in_unit, *decimals),
1515 quantity_unit.name
1516 );
1517 return Err(format!("{value_display} is above maximum {bound_display}"));
1518 }
1519 }
1520 Ok(())
1521 }
1522 (
1523 TypeSpecification::Text {
1524 length, options, ..
1525 },
1526 ValueKind::Text(s),
1527 ) => {
1528 let len = s.chars().count();
1529 if let Some(exact) = length {
1530 if len != *exact {
1531 return Err(format!(
1532 "'{}' has length {} but required length is {}",
1533 s, len, exact
1534 ));
1535 }
1536 }
1537 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1538 return Err(format!(
1539 "'{}' is not in allowed options: {}",
1540 s,
1541 options.join(", ")
1542 ));
1543 }
1544 Ok(())
1545 }
1546 (
1547 TypeSpecification::Ratio {
1548 minimum,
1549 maximum,
1550 decimals,
1551 units,
1552 ..
1553 },
1554 ValueKind::Ratio(r, unit_name),
1555 ) => {
1556 use crate::computation::rational::checked_mul;
1557
1558 if let Some(d) = decimals {
1559 if exceeds_decimal_places(r, *d) {
1560 return Err(format!(
1561 "{} exceeds decimals constraint {d}",
1562 format_rational(r, *decimals)
1563 ));
1564 }
1565 }
1566 if let Some(type_minimum) = minimum {
1567 if r < type_minimum {
1568 let message = match unit_name.as_deref() {
1569 Some(unit) => {
1570 let ratio_unit = units.get(unit)?;
1571 let value_per_unit = checked_mul(r, &ratio_unit.value)
1572 .map_err(|failure| failure.to_string())?;
1573 let bound_per_unit = ratio_unit.minimum.clone().expect(
1574 "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1575 );
1576 format!(
1577 "{} {unit} is below minimum {} {unit}",
1578 format_rational(&value_per_unit, *decimals),
1579 format_rational(&bound_per_unit.clone(), *decimals),
1580 )
1581 }
1582 None => format!(
1583 "{} is below minimum {}",
1584 format_rational(r, *decimals),
1585 format_rational(type_minimum, *decimals),
1586 ),
1587 };
1588 return Err(message);
1589 }
1590 }
1591 if let Some(type_maximum) = maximum {
1592 if r > type_maximum {
1593 let message = match unit_name.as_deref() {
1594 Some(unit) => {
1595 let ratio_unit = units.get(unit)?;
1596 let value_per_unit = checked_mul(r, &ratio_unit.value)
1597 .map_err(|failure| failure.to_string())?;
1598 let bound_per_unit = ratio_unit.maximum.clone().expect(
1599 "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1600 );
1601 format!(
1602 "{} {unit} is above maximum {} {unit}",
1603 format_rational(&value_per_unit, *decimals),
1604 format_rational(&bound_per_unit.clone(), *decimals),
1605 )
1606 }
1607 None => format!(
1608 "{} is above maximum {}",
1609 format_rational(r, *decimals),
1610 format_rational(type_maximum, *decimals),
1611 ),
1612 };
1613 return Err(message);
1614 }
1615 }
1616 Ok(())
1617 }
1618 (
1619 TypeSpecification::Date {
1620 minimum, maximum, ..
1621 },
1622 ValueKind::Date(dt),
1623 ) => {
1624 use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1625 use std::cmp::Ordering;
1626 if let Some(min) = minimum {
1627 let min_sem = date_time_to_semantic(min);
1628 if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1629 return Err(format!("{} is below minimum {}", dt, min));
1630 }
1631 }
1632 if let Some(max) = maximum {
1633 let max_sem = date_time_to_semantic(max);
1634 if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1635 return Err(format!("{} is above maximum {}", dt, max));
1636 }
1637 }
1638 Ok(())
1639 }
1640 (
1641 TypeSpecification::Time {
1642 minimum, maximum, ..
1643 },
1644 ValueKind::Time(t),
1645 ) => {
1646 use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1647 use std::cmp::Ordering;
1648 if let Some(min) = minimum {
1649 let min_sem = time_to_semantic(min);
1650 if compare_semantic_times(t, &min_sem) == Ordering::Less {
1651 return Err(format!("{} is below minimum {}", t, min));
1652 }
1653 }
1654 if let Some(max) = maximum {
1655 let max_sem = time_to_semantic(max);
1656 if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1657 return Err(format!("{} is above maximum {}", t, max));
1658 }
1659 }
1660 Ok(())
1661 }
1662 (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1663 | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1664 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1665 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1666 | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1667 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1668 | (TypeSpecification::Veto { .. }, _)
1669 | (TypeSpecification::Undetermined, _) => Ok(()),
1670 (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1671 "BUG: validate_value_against_type called with mismatched type/value: \
1672 spec={:?}, value={:?} — typing must be enforced before validation",
1673 spec, value_kind
1674 ),
1675 (_, _) => Ok(()),
1676 }
1677}
1678
1679pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1680 let mut errors = Vec::new();
1681
1682 for (data_path, data_definition) in &plan.data {
1683 let (expected_type, lit) = match data_definition {
1684 DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1685 DataDefinition::TypeDeclaration { .. }
1686 | DataDefinition::Import { .. }
1687 | DataDefinition::Reference { .. } => continue,
1688 };
1689
1690 if let Err(msg) = validate_value_against_type(expected_type, lit) {
1691 let source = data_definition.source().clone();
1692 errors.push(Error::validation(
1693 format!(
1694 "Invalid value for data {} (expected {}): {}",
1695 data_path,
1696 expected_type.name().as_str(),
1697 msg
1698 ),
1699 Some(source),
1700 None::<String>,
1701 ));
1702 }
1703 }
1704
1705 errors
1706}
1707
1708fn collect_unit_conversion_targets_from_instructions(
1709 instructions: &Instructions,
1710 units: &mut BTreeSet<String>,
1711) {
1712 for insn in &instructions.code {
1713 if let Instruction::UnitConversion {
1714 target: SemanticConversionTarget::Unit { unit_name },
1715 ..
1716 } = insn
1717 {
1718 units.insert(unit_name.clone());
1719 }
1720 }
1721}
1722
1723pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1724 let mut required_units = BTreeSet::new();
1725 for rule in &plan.rules {
1726 collect_unit_conversion_targets_from_instructions(&rule.instructions, &mut required_units);
1727 }
1728 for unit_name in required_units {
1729 if plan.resolved_types.unit_index.contains_key(&unit_name) {
1730 continue;
1731 }
1732 return Err(Error::validation(
1733 format!("Unknown unit '{unit_name}' in execution plan unit index."),
1734 None::<Source>,
1735 Some(plan.spec_name.clone()),
1736 ));
1737 }
1738 Ok(())
1739}
1740
1741#[derive(Debug, Clone, Serialize, Deserialize)]
1752pub struct ExecutionPlanSerialized {
1753 pub spec_name: String,
1754 #[serde(skip_serializing_if = "Option::is_none", default)]
1755 pub commentary: Option<String>,
1756 #[serde(
1757 serialize_with = "serialize_resolved_data_value_map",
1758 deserialize_with = "deserialize_resolved_data_value_map"
1759 )]
1760 pub data: IndexMap<DataPath, DataDefinition>,
1761 #[serde(default)]
1762 pub rules: Vec<ExecutableRule>,
1763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1764 pub reference_evaluation_order: Vec<DataPath>,
1765 #[serde(default)]
1766 pub meta: HashMap<String, MetaValue>,
1767 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1770 pub unit_index: HashMap<String, Arc<LemmaType>>,
1771 pub effective: EffectiveDate,
1772 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1773 pub sources: SpecSources,
1774}
1775
1776impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1777 fn from(plan: &ExecutionPlan) -> Self {
1778 Self {
1779 spec_name: plan.spec_name.clone(),
1780 commentary: plan.commentary.clone(),
1781 data: plan.data.clone(),
1782 rules: plan.rules.clone(),
1783 reference_evaluation_order: plan.reference_evaluation_order.clone(),
1784 meta: plan.meta.clone(),
1785 unit_index: plan.resolved_types.unit_index.clone(),
1786 effective: plan.effective.clone(),
1787 sources: plan.sources.clone(),
1788 }
1789 }
1790}
1791
1792impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1793 type Error = crate::Error;
1794
1795 fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1796 let signature_index = crate::planning::graph::build_signature_index(
1797 &serialized.spec_name,
1798 &serialized.unit_index,
1799 )?;
1800 for rule in &serialized.rules {
1804 validate_instructions(&rule.instructions).map_err(|message| {
1805 crate::Error::request(
1806 format!(
1807 "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
1808 serialized.spec_name, rule.name
1809 ),
1810 None::<String>,
1811 )
1812 })?;
1813 }
1814 let max_register_count = serialized
1815 .rules
1816 .iter()
1817 .map(|rule| rule.instructions.register_count)
1818 .max()
1819 .unwrap_or(0);
1820 Ok(Self {
1821 spec_name: serialized.spec_name,
1822 commentary: serialized.commentary,
1823 data: serialized.data,
1824 rules: serialized.rules,
1825 max_register_count,
1826 reference_evaluation_order: serialized.reference_evaluation_order,
1827 meta: serialized.meta,
1828 resolved_types: ResolvedSpecTypes {
1829 unit_index: serialized.unit_index,
1830 ..ResolvedSpecTypes::default()
1831 },
1832 signature_index,
1833 effective: serialized.effective,
1834 sources: serialized.sources,
1835 })
1836 }
1837}
1838
1839fn serialize_resolved_data_value_map<S>(
1840 map: &IndexMap<DataPath, DataDefinition>,
1841 serializer: S,
1842) -> Result<S::Ok, S::Error>
1843where
1844 S: Serializer,
1845{
1846 let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1847 entries.serialize(serializer)
1848}
1849
1850fn deserialize_resolved_data_value_map<'de, D>(
1851 deserializer: D,
1852) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1853where
1854 D: Deserializer<'de>,
1855{
1856 let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1857 Ok(entries.into_iter().collect())
1858}
1859
1860#[cfg(test)]
1861mod tests {
1862 use super::*;
1863 use crate::computation::rational::{rational_new, rational_zero};
1864 use crate::literals::DateGranularity;
1865 use crate::parsing::ast::DateTimeValue;
1866 use crate::planning::semantics::{
1867 primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1868 };
1869 use crate::Engine;
1870 use serde_json;
1871 use std::str::FromStr;
1872 use std::sync::Arc;
1873
1874 fn default_limits() -> ResourceLimits {
1875 ResourceLimits::default()
1876 }
1877
1878 fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
1879 let serialized = ExecutionPlanSerialized::from(plan);
1880 let json = serde_json::to_string(&serialized).expect("Should serialize");
1881 let back: ExecutionPlanSerialized =
1882 serde_json::from_str(&json).expect("Should deserialize");
1883 ExecutionPlan::try_from(back).expect("Should reconstruct")
1884 }
1885
1886 fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
1887 pairs
1888 .iter()
1889 .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
1890 .collect()
1891 }
1892
1893 #[test]
1894 fn test_with_raw_values() {
1895 let mut engine = Engine::new();
1896 engine
1897 .load(
1898 r#"
1899 spec test
1900 data age: number -> default 25
1901 "#,
1902 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1903 "test.lemma",
1904 ))),
1905 )
1906 .unwrap();
1907
1908 let now = DateTimeValue::now();
1909 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
1910 let data_path = DataPath::new(vec![], "age".to_string());
1911
1912 let values = input_data(&[("age", "30")]);
1913
1914 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
1915 let updated_value = overlay.values.get(&data_path).unwrap();
1916 match &updated_value.value {
1917 crate::planning::semantics::ValueKind::Number(n) => {
1918 assert_eq!(n, &rational_new(30, 1));
1919 }
1920 other => panic!("Expected number literal, got {:?}", other),
1921 }
1922 }
1923
1924 #[test]
1925 fn test_with_raw_values_type_mismatch() {
1926 let mut engine = Engine::new();
1927 engine
1928 .load(
1929 r#"
1930 spec test
1931 data age: number
1932 "#,
1933 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1934 "test.lemma",
1935 ))),
1936 )
1937 .unwrap();
1938
1939 let now = DateTimeValue::now();
1940 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
1941
1942 let values = input_data(&[("age", "thirty")]);
1943
1944 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
1945 let data_path = DataPath::new(vec![], "age".to_string());
1946 match overlay.violated.get(&data_path) {
1947 Some(reason) => {
1948 assert!(
1949 reason.contains("number"),
1950 "type mismatch must record violation reason, got: {reason}"
1951 );
1952 }
1953 None => panic!("expected violated data for age=thirty"),
1954 }
1955 }
1956
1957 #[test]
1958 fn test_with_raw_values_unknown_data() {
1959 let mut engine = Engine::new();
1960 engine
1961 .load(
1962 r#"
1963 spec test
1964 data known: number
1965 "#,
1966 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1967 "test.lemma",
1968 ))),
1969 )
1970 .unwrap();
1971
1972 let now = DateTimeValue::now();
1973 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
1974
1975 let values = input_data(&[("unknown", "30")]);
1976
1977 assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
1978 }
1979
1980 #[test]
1981 fn test_with_raw_values_nested() {
1982 let mut engine = Engine::new();
1983 engine
1984 .load(
1985 r#"
1986 spec private
1987 data base_price: number
1988
1989 spec test
1990 uses rules: private
1991 "#,
1992 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1993 "test.lemma",
1994 ))),
1995 )
1996 .unwrap();
1997
1998 let now = DateTimeValue::now();
1999 let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2000
2001 let values = input_data(&[("rules.base_price", "100")]);
2002
2003 let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2004 let data_path = DataPath {
2005 segments: vec![PathSegment {
2006 data: "rules".to_string(),
2007 spec: "private".to_string(),
2008 }],
2009 data: "base_price".to_string(),
2010 };
2011 let updated_value = overlay.values.get(&data_path).unwrap();
2012 match &updated_value.value {
2013 crate::planning::semantics::ValueKind::Number(n) => {
2014 assert_eq!(n, &rational_new(100, 1));
2015 }
2016 other => panic!("Expected number literal, got {:?}", other),
2017 }
2018 }
2019
2020 fn test_source() -> Source {
2021 use crate::parsing::ast::Span;
2022 Source::new(
2023 crate::parsing::source::SourceType::Volatile,
2024 Span {
2025 start: 0,
2026 end: 0,
2027 line: 1,
2028 col: 0,
2029 },
2030 )
2031 }
2032
2033 fn create_literal_expr(value: LiteralValue) -> Expression {
2034 Expression::new(
2035 crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2036 test_source(),
2037 )
2038 }
2039
2040 fn create_data_path_expr(path: DataPath) -> Expression {
2041 Expression::new(
2042 crate::planning::semantics::ExpressionKind::DataPath(path),
2043 test_source(),
2044 )
2045 }
2046
2047 fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2048 Instructions {
2049 version: INSTRUCTIONS_VERSION,
2050 register_count: 1,
2051 register_types: vec![Arc::clone(&literal.lemma_type)],
2052 constants: vec![literal],
2053 data_manifest: Vec::new(),
2054 veto_messages: Vec::new(),
2055 code: vec![
2056 Instruction::LoadConstant {
2057 destination_register: 0,
2058 constant_index: 0,
2059 },
2060 Instruction::Return { source_register: 0 },
2061 ],
2062 }
2063 }
2064
2065 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2066 LiteralValue::number_from_decimal(n)
2067 }
2068
2069 fn create_boolean_literal(b: bool) -> LiteralValue {
2070 LiteralValue::from_bool(b)
2071 }
2072
2073 fn create_text_literal(s: String) -> LiteralValue {
2074 LiteralValue::text(s)
2075 }
2076
2077 #[test]
2078 fn with_values_should_enforce_number_maximum_constraint() {
2079 let data_path = DataPath::new(vec![], "x".to_string());
2082
2083 let max10 = crate::planning::semantics::LemmaType::primitive(
2084 crate::planning::semantics::TypeSpecification::Number {
2085 minimum: None,
2086 maximum: Some(rational_new(10, 1)),
2087 decimals: None,
2088 help: String::new(),
2089 },
2090 );
2091 let source = Source::new(
2092 crate::parsing::source::SourceType::Volatile,
2093 crate::parsing::ast::Span {
2094 start: 0,
2095 end: 0,
2096 line: 1,
2097 col: 0,
2098 },
2099 );
2100 let mut data = IndexMap::new();
2101 data.insert(
2102 data_path.clone(),
2103 crate::planning::semantics::DataDefinition::Value {
2104 value: crate::planning::semantics::LiteralValue::number_with_type(
2105 rational_new(0, 1),
2106 Arc::new(max10.clone()),
2107 ),
2108 source: source.clone(),
2109 },
2110 );
2111
2112 let plan = ExecutionPlan {
2113 spec_name: "test".to_string(),
2114 commentary: None,
2115 data,
2116 rules: Vec::new(),
2117 max_register_count: 0,
2118 reference_evaluation_order: Vec::new(),
2119 meta: HashMap::new(),
2120 resolved_types: ResolvedSpecTypes::default(),
2121 signature_index: HashMap::new(),
2122 effective: EffectiveDate::Origin,
2123 sources: Vec::new(),
2124 };
2125
2126 let values = input_data(&[("x", "11")]);
2127
2128 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2129 match overlay.violated.get(&data_path) {
2130 Some(reason) => {
2131 assert!(
2132 reason.contains("maximum") || reason.contains("10"),
2133 "x=11 must violate maximum 10, got: {reason}"
2134 );
2135 }
2136 None => panic!("expected violated data for x=11"),
2137 }
2138 }
2139
2140 #[test]
2141 fn with_values_should_enforce_text_enum_options() {
2142 let data_path = DataPath::new(vec![], "tier".to_string());
2144
2145 let tier = crate::planning::semantics::LemmaType::primitive(
2146 crate::planning::semantics::TypeSpecification::Text {
2147 length: None,
2148 options: vec!["silver".to_string(), "gold".to_string()],
2149 help: String::new(),
2150 },
2151 );
2152 let source = Source::new(
2153 crate::parsing::source::SourceType::Volatile,
2154 crate::parsing::ast::Span {
2155 start: 0,
2156 end: 0,
2157 line: 1,
2158 col: 0,
2159 },
2160 );
2161 let mut data = IndexMap::new();
2162 data.insert(
2163 data_path.clone(),
2164 crate::planning::semantics::DataDefinition::Value {
2165 value: crate::planning::semantics::LiteralValue::text_with_type(
2166 "silver".to_string(),
2167 Arc::new(tier.clone()),
2168 ),
2169 source,
2170 },
2171 );
2172
2173 let plan = ExecutionPlan {
2174 spec_name: "test".to_string(),
2175 commentary: None,
2176 data,
2177 rules: Vec::new(),
2178 max_register_count: 0,
2179 reference_evaluation_order: Vec::new(),
2180 meta: HashMap::new(),
2181 resolved_types: ResolvedSpecTypes::default(),
2182 signature_index: HashMap::new(),
2183 effective: EffectiveDate::Origin,
2184 sources: Vec::new(),
2185 };
2186
2187 let values = input_data(&[("tier", "platinum")]);
2188
2189 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2190 match overlay.violated.get(&data_path) {
2191 Some(reason) => {
2192 assert!(
2193 reason.contains("allowed options") || reason.contains("platinum"),
2194 "invalid enum must record violation, got: {reason}"
2195 );
2196 }
2197 None => panic!("expected violated data for tier=platinum"),
2198 }
2199 }
2200
2201 #[test]
2202 fn with_values_should_enforce_quantity_decimals() {
2203 let data_path = DataPath::new(vec![], "price".to_string());
2206
2207 let money = crate::planning::semantics::LemmaType::primitive(
2208 crate::planning::semantics::TypeSpecification::Quantity {
2209 minimum: None,
2210 maximum: None,
2211 decimals: Some(2),
2212 units: crate::planning::semantics::QuantityUnits::from(vec![
2213 crate::planning::semantics::QuantityUnit::from_decimal_factor(
2214 "eur".to_string(),
2215 rust_decimal::Decimal::from_str("1.0").unwrap(),
2216 Vec::new(),
2217 )
2218 .expect("eur unit factor must be exact decimal"),
2219 ]),
2220 traits: Vec::new(),
2221 decomposition: None,
2222 help: String::new(),
2223 },
2224 );
2225 let source = Source::new(
2226 crate::parsing::source::SourceType::Volatile,
2227 crate::parsing::ast::Span {
2228 start: 0,
2229 end: 0,
2230 line: 1,
2231 col: 0,
2232 },
2233 );
2234 let mut data = IndexMap::new();
2235 data.insert(
2236 data_path.clone(),
2237 crate::planning::semantics::DataDefinition::Value {
2238 value: crate::planning::semantics::LiteralValue::quantity_with_type(
2239 rational_zero(),
2240 "eur".to_string(),
2241 Arc::new(money.clone()),
2242 ),
2243 source,
2244 },
2245 );
2246
2247 let plan = ExecutionPlan {
2248 spec_name: "test".to_string(),
2249 commentary: None,
2250 data,
2251 rules: Vec::new(),
2252 max_register_count: 0,
2253 reference_evaluation_order: Vec::new(),
2254 meta: HashMap::new(),
2255 resolved_types: ResolvedSpecTypes::default(),
2256 signature_index: HashMap::new(),
2257 effective: EffectiveDate::Origin,
2258 sources: Vec::new(),
2259 };
2260
2261 let values = input_data(&[("price", "1.234 eur")]);
2262
2263 let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2264 match overlay.violated.get(&data_path) {
2265 Some(reason) => {
2266 assert!(
2267 reason.contains("decimals") || reason.contains("decimal"),
2268 "1.234 eur must violate decimals=2, got: {reason}"
2269 );
2270 }
2271 None => panic!("expected violated data for price=1.234 eur"),
2272 }
2273 }
2274
2275 #[test]
2276 fn test_serialize_deserialize_execution_plan() {
2277 let data_path = DataPath {
2278 segments: vec![],
2279 data: "age".to_string(),
2280 };
2281 let mut data = IndexMap::new();
2282 data.insert(
2283 data_path.clone(),
2284 crate::planning::semantics::DataDefinition::Value {
2285 value: create_number_literal(0.into()),
2286 source: test_source(),
2287 },
2288 );
2289 let plan = ExecutionPlan {
2290 spec_name: "test".to_string(),
2291 commentary: None,
2292 data,
2293 rules: Vec::new(),
2294 max_register_count: 0,
2295 reference_evaluation_order: Vec::new(),
2296 meta: HashMap::new(),
2297 resolved_types: ResolvedSpecTypes::default(),
2298 signature_index: HashMap::new(),
2299 effective: EffectiveDate::Origin,
2300 sources: Vec::new(),
2301 };
2302
2303 let deserialized = roundtrip_execution_plan(&plan);
2304
2305 assert_eq!(deserialized.spec_name, plan.spec_name);
2306 assert_eq!(deserialized.data.len(), plan.data.len());
2307 assert_eq!(deserialized.rules.len(), plan.rules.len());
2308 }
2309
2310 #[test]
2311 fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2312 let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2313 let imported_type = crate::planning::semantics::LemmaType::new(
2314 "salary".to_string(),
2315 TypeSpecification::quantity(),
2316 crate::planning::semantics::TypeExtends::Custom {
2317 parent: "money".to_string(),
2318 family: "money".to_string(),
2319 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2320 spec: Arc::clone(&dep_spec),
2321 },
2322 },
2323 );
2324
2325 let salary_path = DataPath::new(vec![], "salary".to_string());
2326 let mut data = IndexMap::new();
2327 data.insert(
2328 salary_path,
2329 crate::planning::semantics::DataDefinition::TypeDeclaration {
2330 resolved_type: Arc::new(imported_type),
2331 declared_default: None,
2332 source: test_source(),
2333 },
2334 );
2335
2336 let plan = ExecutionPlan {
2337 spec_name: "test".to_string(),
2338 commentary: None,
2339 data,
2340 rules: Vec::new(),
2341 max_register_count: 0,
2342 reference_evaluation_order: Vec::new(),
2343 meta: HashMap::new(),
2344 resolved_types: ResolvedSpecTypes::default(),
2345 signature_index: HashMap::new(),
2346 effective: EffectiveDate::Origin,
2347 sources: Vec::new(),
2348 };
2349
2350 let deserialized = roundtrip_execution_plan(&plan);
2351
2352 let recovered = deserialized
2353 .data
2354 .get(&DataPath::new(vec![], "salary".to_string()))
2355 .and_then(|d| d.schema_type())
2356 .expect("salary type should be present in plan.data");
2357 match &recovered.extends {
2358 crate::planning::semantics::TypeExtends::Custom {
2359 defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2360 ..
2361 } => {
2362 assert_eq!(spec.name, "examples");
2363 }
2364 other => panic!(
2365 "Expected imported defining_spec after round-trip, got {:?}",
2366 other
2367 ),
2368 }
2369 }
2370
2371 #[test]
2372 fn test_serialize_deserialize_plan_with_rules() {
2373 use crate::planning::semantics::ExpressionKind;
2374
2375 let age_path = DataPath::new(vec![], "age".to_string());
2376 let mut data = IndexMap::new();
2377 data.insert(
2378 age_path.clone(),
2379 crate::planning::semantics::DataDefinition::Value {
2380 value: create_number_literal(0.into()),
2381 source: test_source(),
2382 },
2383 );
2384 let mut plan = ExecutionPlan {
2385 spec_name: "test".to_string(),
2386 commentary: None,
2387 data,
2388 rules: Vec::new(),
2389 max_register_count: 0,
2390 reference_evaluation_order: Vec::new(),
2391 meta: HashMap::new(),
2392 resolved_types: ResolvedSpecTypes::default(),
2393 signature_index: HashMap::new(),
2394 effective: EffectiveDate::Origin,
2395 sources: Vec::new(),
2396 };
2397
2398 let rule = ExecutableRule {
2399 path: RulePath::new(vec![], "can_drive".to_string()),
2400 name: "can_drive".to_string(),
2401 branches: vec![{
2402 let result = create_literal_expr(create_boolean_literal(true));
2403 let condition = Expression::new(
2404 ExpressionKind::Comparison(
2405 Arc::new(create_data_path_expr(age_path.clone())),
2406 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2407 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2408 ),
2409 test_source(),
2410 );
2411 Branch {
2412 condition: Some(condition.clone()),
2413 result: result.clone(),
2414 source: test_source(),
2415 }
2416 }],
2417 instructions: constant_return_instructions(create_boolean_literal(true)),
2418 source: test_source(),
2419 rule_type: Arc::new(primitive_boolean().clone()),
2420 };
2421
2422 plan.rules.push(rule);
2423 plan.max_register_count = plan.rules[0].instructions.register_count;
2424
2425 let deserialized = roundtrip_execution_plan(&plan);
2426
2427 assert_eq!(deserialized.spec_name, plan.spec_name);
2428 assert_eq!(deserialized.data.len(), plan.data.len());
2429 assert_eq!(deserialized.rules.len(), plan.rules.len());
2430 assert_eq!(deserialized.rules[0].name, "can_drive");
2431 assert_eq!(deserialized.rules[0].branches.len(), 1);
2432 }
2433
2434 #[test]
2435 fn test_serialize_deserialize_plan_with_nested_data_paths() {
2436 use crate::planning::semantics::PathSegment;
2437 let data_path = DataPath {
2438 segments: vec![PathSegment {
2439 data: "employee".to_string(),
2440 spec: "private".to_string(),
2441 }],
2442 data: "salary".to_string(),
2443 };
2444
2445 let mut data = IndexMap::new();
2446 data.insert(
2447 data_path.clone(),
2448 crate::planning::semantics::DataDefinition::Value {
2449 value: create_number_literal(0.into()),
2450 source: test_source(),
2451 },
2452 );
2453 let plan = ExecutionPlan {
2454 spec_name: "test".to_string(),
2455 commentary: None,
2456 data,
2457 rules: Vec::new(),
2458 max_register_count: 0,
2459 reference_evaluation_order: Vec::new(),
2460 meta: HashMap::new(),
2461 resolved_types: ResolvedSpecTypes::default(),
2462 signature_index: HashMap::new(),
2463 effective: EffectiveDate::Origin,
2464 sources: Vec::new(),
2465 };
2466
2467 let deserialized = roundtrip_execution_plan(&plan);
2468
2469 assert_eq!(deserialized.data.len(), 1);
2470 let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2471 assert_eq!(deserialized_path.segments.len(), 1);
2472 assert_eq!(deserialized_path.segments[0].data, "employee");
2473 assert_eq!(deserialized_path.data, "salary");
2474 }
2475
2476 #[test]
2477 fn test_serialize_deserialize_plan_with_multiple_data_types() {
2478 let name_path = DataPath::new(vec![], "name".to_string());
2479 let age_path = DataPath::new(vec![], "age".to_string());
2480 let active_path = DataPath::new(vec![], "active".to_string());
2481
2482 let mut data = IndexMap::new();
2483 data.insert(
2484 name_path.clone(),
2485 crate::planning::semantics::DataDefinition::Value {
2486 value: create_text_literal("Alice".to_string()),
2487 source: test_source(),
2488 },
2489 );
2490 data.insert(
2491 age_path.clone(),
2492 crate::planning::semantics::DataDefinition::Value {
2493 value: create_number_literal(30.into()),
2494 source: test_source(),
2495 },
2496 );
2497 data.insert(
2498 active_path.clone(),
2499 crate::planning::semantics::DataDefinition::Value {
2500 value: create_boolean_literal(true),
2501 source: test_source(),
2502 },
2503 );
2504
2505 let plan = ExecutionPlan {
2506 spec_name: "test".to_string(),
2507 commentary: None,
2508 data,
2509 rules: Vec::new(),
2510 max_register_count: 0,
2511 reference_evaluation_order: Vec::new(),
2512 meta: HashMap::new(),
2513 resolved_types: ResolvedSpecTypes::default(),
2514 signature_index: HashMap::new(),
2515 effective: EffectiveDate::Origin,
2516 sources: Vec::new(),
2517 };
2518
2519 let deserialized = roundtrip_execution_plan(&plan);
2520
2521 assert_eq!(deserialized.data.len(), 3);
2522
2523 assert_eq!(
2524 deserialized.get_data_value(&name_path).unwrap().value,
2525 crate::planning::semantics::ValueKind::Text("Alice".to_string())
2526 );
2527 assert_eq!(
2528 deserialized.get_data_value(&age_path).unwrap().value,
2529 crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2530 );
2531 assert_eq!(
2532 deserialized.get_data_value(&active_path).unwrap().value,
2533 crate::planning::semantics::ValueKind::Boolean(true)
2534 );
2535 }
2536
2537 #[test]
2538 fn test_serialize_deserialize_plan_with_multiple_branches() {
2539 use crate::planning::semantics::ExpressionKind;
2540
2541 let points_path = DataPath::new(vec![], "points".to_string());
2542 let mut data = IndexMap::new();
2543 data.insert(
2544 points_path.clone(),
2545 crate::planning::semantics::DataDefinition::Value {
2546 value: create_number_literal(0.into()),
2547 source: test_source(),
2548 },
2549 );
2550 let mut plan = ExecutionPlan {
2551 spec_name: "test".to_string(),
2552 commentary: None,
2553 data,
2554 rules: Vec::new(),
2555 max_register_count: 0,
2556 reference_evaluation_order: Vec::new(),
2557 meta: HashMap::new(),
2558 resolved_types: ResolvedSpecTypes::default(),
2559 signature_index: HashMap::new(),
2560 effective: EffectiveDate::Origin,
2561 sources: Vec::new(),
2562 };
2563
2564 let rule = ExecutableRule {
2565 path: RulePath::new(vec![], "tier".to_string()),
2566 name: "tier".to_string(),
2567 branches: vec![
2568 {
2569 let result = create_literal_expr(create_text_literal("bronze".to_string()));
2570 Branch {
2571 condition: None,
2572 result: result.clone(),
2573 source: test_source(),
2574 }
2575 },
2576 {
2577 let result = create_literal_expr(create_text_literal("silver".to_string()));
2578 Branch {
2579 condition: Some(Expression::new(
2580 ExpressionKind::Comparison(
2581 Arc::new(create_data_path_expr(points_path.clone())),
2582 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2583 Arc::new(create_literal_expr(create_number_literal(100.into()))),
2584 ),
2585 test_source(),
2586 )),
2587 result: result.clone(),
2588 source: test_source(),
2589 }
2590 },
2591 {
2592 let result = create_literal_expr(create_text_literal("gold".to_string()));
2593 Branch {
2594 condition: Some(Expression::new(
2595 ExpressionKind::Comparison(
2596 Arc::new(create_data_path_expr(points_path.clone())),
2597 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2598 Arc::new(create_literal_expr(create_number_literal(500.into()))),
2599 ),
2600 test_source(),
2601 )),
2602 result: result.clone(),
2603 source: test_source(),
2604 }
2605 },
2606 ],
2607 instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2608 source: test_source(),
2609 rule_type: Arc::new(primitive_text().clone()),
2610 };
2611
2612 plan.rules.push(rule);
2613 plan.max_register_count = plan.rules[0].instructions.register_count;
2614
2615 let deserialized = roundtrip_execution_plan(&plan);
2616
2617 assert_eq!(deserialized.rules.len(), 1);
2618 assert_eq!(deserialized.rules[0].branches.len(), 3);
2619 assert!(deserialized.rules[0].branches[0].condition.is_none());
2620 assert!(deserialized.rules[0].branches[1].condition.is_some());
2621 assert!(deserialized.rules[0].branches[2].condition.is_some());
2622 }
2623
2624 #[test]
2625 fn test_serialize_deserialize_empty_plan() {
2626 let plan = ExecutionPlan {
2627 spec_name: "empty".to_string(),
2628 commentary: None,
2629 data: IndexMap::new(),
2630 rules: Vec::new(),
2631 max_register_count: 0,
2632 reference_evaluation_order: Vec::new(),
2633 meta: HashMap::new(),
2634 resolved_types: ResolvedSpecTypes::default(),
2635 signature_index: HashMap::new(),
2636 effective: EffectiveDate::Origin,
2637 sources: Vec::new(),
2638 };
2639
2640 let deserialized = roundtrip_execution_plan(&plan);
2641
2642 assert_eq!(deserialized.spec_name, "empty");
2643 assert_eq!(deserialized.data.len(), 0);
2644 assert_eq!(deserialized.rules.len(), 0);
2645 }
2646
2647 #[test]
2648 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2649 use crate::planning::semantics::ExpressionKind;
2650
2651 let x_path = DataPath::new(vec![], "x".to_string());
2652 let mut data = IndexMap::new();
2653 data.insert(
2654 x_path.clone(),
2655 crate::planning::semantics::DataDefinition::Value {
2656 value: create_number_literal(0.into()),
2657 source: test_source(),
2658 },
2659 );
2660 let mut plan = ExecutionPlan {
2661 spec_name: "test".to_string(),
2662 commentary: None,
2663 data,
2664 rules: Vec::new(),
2665 max_register_count: 0,
2666 reference_evaluation_order: Vec::new(),
2667 meta: HashMap::new(),
2668 resolved_types: ResolvedSpecTypes::default(),
2669 signature_index: HashMap::new(),
2670 effective: EffectiveDate::Origin,
2671 sources: Vec::new(),
2672 };
2673
2674 let rule = ExecutableRule {
2675 path: RulePath::new(vec![], "doubled".to_string()),
2676 name: "doubled".to_string(),
2677 branches: vec![{
2678 let result = Expression::new(
2679 ExpressionKind::Arithmetic(
2680 Arc::new(create_data_path_expr(x_path.clone())),
2681 crate::parsing::ast::ArithmeticComputation::Multiply,
2682 Arc::new(create_literal_expr(create_number_literal(2.into()))),
2683 ),
2684 test_source(),
2685 );
2686 Branch {
2687 condition: None,
2688 result: result.clone(),
2689 source: test_source(),
2690 }
2691 }],
2692 instructions: constant_return_instructions(create_number_literal(0.into())),
2693 source: test_source(),
2694 rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2695 };
2696
2697 plan.rules.push(rule);
2698 plan.max_register_count = plan.rules[0].instructions.register_count;
2699
2700 let deserialized = roundtrip_execution_plan(&plan);
2701
2702 assert_eq!(deserialized.rules.len(), 1);
2703 match &deserialized.rules[0].branches[0].result.kind {
2704 ExpressionKind::Arithmetic(left, op, right) => {
2705 assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2706 match &left.kind {
2707 ExpressionKind::DataPath(_) => {}
2708 _ => panic!("Expected DataPath in left operand"),
2709 }
2710 match &right.kind {
2711 ExpressionKind::Literal(_) => {}
2712 _ => panic!("Expected Literal in right operand"),
2713 }
2714 }
2715 _ => panic!("Expected Arithmetic expression"),
2716 }
2717 }
2718
2719 #[test]
2720 fn test_serialize_deserialize_round_trip_equality() {
2721 use crate::planning::semantics::ExpressionKind;
2722
2723 let age_path = DataPath::new(vec![], "age".to_string());
2724 let mut data = IndexMap::new();
2725 data.insert(
2726 age_path.clone(),
2727 crate::planning::semantics::DataDefinition::Value {
2728 value: create_number_literal(0.into()),
2729 source: test_source(),
2730 },
2731 );
2732 let mut plan = ExecutionPlan {
2733 spec_name: "test".to_string(),
2734 commentary: None,
2735 data,
2736 rules: Vec::new(),
2737 max_register_count: 0,
2738 reference_evaluation_order: Vec::new(),
2739 meta: HashMap::new(),
2740 resolved_types: ResolvedSpecTypes::default(),
2741 signature_index: HashMap::new(),
2742 effective: EffectiveDate::Origin,
2743 sources: Vec::new(),
2744 };
2745
2746 let rule = ExecutableRule {
2747 path: RulePath::new(vec![], "is_adult".to_string()),
2748 name: "is_adult".to_string(),
2749 branches: vec![{
2750 let result = create_literal_expr(create_boolean_literal(true));
2751 let condition = Expression::new(
2752 ExpressionKind::Comparison(
2753 Arc::new(create_data_path_expr(age_path.clone())),
2754 crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2755 Arc::new(create_literal_expr(create_number_literal(18.into()))),
2756 ),
2757 test_source(),
2758 );
2759 Branch {
2760 condition: Some(condition.clone()),
2761 result: result.clone(),
2762 source: test_source(),
2763 }
2764 }],
2765 instructions: constant_return_instructions(create_boolean_literal(true)),
2766 source: test_source(),
2767 rule_type: Arc::new(primitive_boolean().clone()),
2768 };
2769
2770 plan.rules.push(rule);
2771 plan.max_register_count = plan.rules[0].instructions.register_count;
2772
2773 let deserialized = roundtrip_execution_plan(&plan);
2774 let deserialized2 = roundtrip_execution_plan(&deserialized);
2775
2776 assert_eq!(deserialized2.spec_name, plan.spec_name);
2777 assert_eq!(deserialized2.data.len(), plan.data.len());
2778 assert_eq!(deserialized2.rules.len(), plan.rules.len());
2779 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2780 assert_eq!(
2781 deserialized2.rules[0].branches.len(),
2782 plan.rules[0].branches.len()
2783 );
2784 }
2785
2786 fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2787 ExecutionPlan {
2788 spec_name: "s".into(),
2789 commentary: None,
2790 data: IndexMap::new(),
2791 rules: Vec::new(),
2792 max_register_count: 0,
2793 reference_evaluation_order: Vec::new(),
2794 meta: HashMap::new(),
2795 resolved_types: ResolvedSpecTypes::default(),
2796 signature_index: HashMap::new(),
2797 effective,
2798 sources: Vec::new(),
2799 }
2800 }
2801
2802 #[test]
2803 fn plan_at_exact_boundary_selects_later_slice() {
2804 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2805
2806 let june = DateTimeValue {
2807 year: 2025,
2808 month: 6,
2809 day: 1,
2810 hour: 0,
2811 minute: 0,
2812 second: 0,
2813 microsecond: 0,
2814 timezone: None,
2815
2816 granularity: DateGranularity::Full,
2817 };
2818 let dec = DateTimeValue {
2819 year: 2025,
2820 month: 12,
2821 day: 1,
2822 hour: 0,
2823 minute: 0,
2824 second: 0,
2825 microsecond: 0,
2826 timezone: None,
2827
2828 granularity: DateGranularity::Full,
2829 };
2830
2831 let set = ExecutionPlanSet {
2832 spec_name: "s".into(),
2833 plans: vec![
2834 empty_plan(EffectiveDate::Origin),
2835 empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2836 empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2837 ],
2838 };
2839
2840 assert!(std::ptr::eq(
2841 set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2842 .expect("boundary instant"),
2843 &set.plans[1]
2844 ));
2845 assert!(std::ptr::eq(
2846 set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2847 .expect("dec boundary"),
2848 &set.plans[2]
2849 ));
2850 }
2851
2852 #[test]
2853 fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2854 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2855
2856 let june = DateTimeValue {
2857 year: 2025,
2858 month: 6,
2859 day: 1,
2860 hour: 0,
2861 minute: 0,
2862 second: 0,
2863 microsecond: 0,
2864 timezone: None,
2865
2866 granularity: DateGranularity::Full,
2867 };
2868 let may_end = DateTimeValue {
2869 year: 2025,
2870 month: 5,
2871 day: 31,
2872 hour: 23,
2873 minute: 59,
2874 second: 59,
2875 microsecond: 0,
2876 timezone: None,
2877
2878 granularity: DateGranularity::DateTime,
2879 };
2880
2881 let set = ExecutionPlanSet {
2882 spec_name: "s".into(),
2883 plans: vec![
2884 empty_plan(EffectiveDate::Origin),
2885 empty_plan(EffectiveDate::DateTimeValue(june)),
2886 ],
2887 };
2888
2889 assert!(std::ptr::eq(
2890 set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2891 .expect("may 31"),
2892 &set.plans[0]
2893 ));
2894 }
2895
2896 #[test]
2897 fn plan_at_single_plan_matches_any_instant_after_start() {
2898 use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2899
2900 let t = DateTimeValue {
2901 year: 2025,
2902 month: 3,
2903 day: 1,
2904 hour: 0,
2905 minute: 0,
2906 second: 0,
2907 microsecond: 0,
2908 timezone: None,
2909
2910 granularity: DateGranularity::Full,
2911 };
2912 let set = ExecutionPlanSet {
2913 spec_name: "s".into(),
2914 plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2915 year: 2025,
2916 month: 1,
2917 day: 1,
2918 hour: 0,
2919 minute: 0,
2920 second: 0,
2921 microsecond: 0,
2922 timezone: None,
2923
2924 granularity: DateGranularity::Full,
2925 }))],
2926 };
2927 assert!(std::ptr::eq(
2928 set.plan_at(&EffectiveDate::DateTimeValue(t))
2929 .expect("inside single slice"),
2930 &set.plans[0]
2931 ));
2932 }
2933
2934 #[test]
2937 fn schema_json_shape_contract() {
2938 let mut engine = Engine::new();
2939 engine
2940 .load(
2941 r#"
2942 spec pricing
2943 data bridge_height: quantity
2944 -> unit meter 1
2945 -> default 100 meter
2946 data quantity: number -> minimum 0
2947 rule cost: bridge_height * quantity
2948 "#,
2949 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2950 "test.lemma",
2951 ))),
2952 )
2953 .unwrap();
2954 let now = DateTimeValue::now();
2955 let schema = engine
2956 .get_plan(None, "pricing", Some(&now))
2957 .unwrap()
2958 .schema(&DataOverlay::default());
2959
2960 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2961
2962 let bh = &value["data"]["bridge_height"];
2963 assert!(
2964 bh.is_object(),
2965 "data entry must be a named object, not tuple"
2966 );
2967 assert!(
2968 bh.get("type").is_some(),
2969 "data entry must expose `type` field"
2970 );
2971 assert!(
2972 bh.get("default").is_some(),
2973 "bridge_height exposes `-> default` as schema default suggestion"
2974 );
2975 assert!(
2976 bh.get("bound_value").is_none(),
2977 "bridge_height is not a spec-bound literal"
2978 );
2979
2980 let ty = &bh["type"];
2981 assert_eq!(
2982 ty["kind"], "quantity",
2983 "kind tag sits on the type object itself"
2984 );
2985 assert!(
2986 ty["units"].is_array(),
2987 "quantity-only fields flatten up to top level"
2988 );
2989 assert!(
2990 ty.get("options").is_none(),
2991 "text-only fields must not leak"
2992 );
2993
2994 let qty = &value["data"]["quantity"];
2995 assert_eq!(qty["type"]["kind"], "number");
2996 assert!(
2997 qty.get("default").is_none(),
2998 "quantity has no default suggestion"
2999 );
3000 assert!(
3001 qty.get("bound_value").is_none(),
3002 "quantity has no bound literal"
3003 );
3004
3005 let cost = &value["rules"]["cost"];
3006 assert_eq!(
3007 cost["kind"], "quantity",
3008 "rule types use the same flat shape"
3009 );
3010 assert!(
3011 cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3012 "quantity rule result types expose declared units"
3013 );
3014 assert!(
3015 cost["units"][0].get("factor").is_some(),
3016 "quantity rule units use factor field"
3017 );
3018 }
3019
3020 #[test]
3021 fn schema_rule_result_units_contract() {
3022 let mut engine = Engine::new();
3023 engine
3024 .load(
3025 r#"
3026 spec units_contract
3027 data money: quantity
3028 -> unit eur 1
3029 -> unit usd 0.91
3030 data rate: ratio
3031 -> unit basis_points 10000
3032 -> unit percent 100
3033 -> default 500 basis_points
3034 rule total: money
3035 rule rate_out: rate
3036 "#,
3037 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3038 "units_contract.lemma",
3039 ))),
3040 )
3041 .unwrap();
3042 let now = DateTimeValue::now();
3043 let schema = engine
3044 .get_plan(None, "units_contract", Some(&now))
3045 .unwrap()
3046 .schema(&DataOverlay::default());
3047 let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3048
3049 let money_units = &value["data"]["money"]["type"]["units"];
3050 assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3051 assert!(money_units[0].get("name").is_some());
3052 assert!(money_units[0].get("factor").is_some());
3053 assert!(money_units[0]["factor"].get("numer").is_some());
3054 assert!(money_units[0]["factor"].get("denom").is_some());
3055
3056 let rate_units = &value["data"]["rate"]["type"]["units"];
3057 assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3058 assert!(rate_units[0].get("name").is_some());
3059 assert!(rate_units[0].get("value").is_some());
3060 assert!(rate_units[0]["value"].get("numer").is_some());
3061 assert!(rate_units[0]["value"].get("denom").is_some());
3062
3063 let total_rule_units = &value["rules"]["total"]["units"];
3064 let money_unit_names: Vec<_> = money_units
3065 .as_array()
3066 .unwrap()
3067 .iter()
3068 .map(|u| u["name"].as_str().unwrap())
3069 .collect();
3070 let total_rule_unit_names: Vec<_> = total_rule_units
3071 .as_array()
3072 .unwrap()
3073 .iter()
3074 .map(|u| u["name"].as_str().unwrap())
3075 .collect();
3076 assert_eq!(total_rule_unit_names, money_unit_names);
3077
3078 let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3079 let rate_unit_names: Vec<_> = rate_units
3080 .as_array()
3081 .unwrap()
3082 .iter()
3083 .map(|u| u["name"].as_str().unwrap())
3084 .collect();
3085 let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3086 .as_array()
3087 .unwrap()
3088 .iter()
3089 .map(|u| u["name"].as_str().unwrap())
3090 .collect();
3091 assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3092 }
3093
3094 #[test]
3095 fn schema_json_round_trip_preserves_shape() {
3096 let mut engine = Engine::new();
3097 engine
3098 .load(
3099 r#"
3100 spec s
3101 data age: number -> minimum 0 -> default 18
3102 data grade: text -> options "A" "B" "C"
3103 rule adult: age >= 18
3104 "#,
3105 crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3106 )
3107 .unwrap();
3108 let now = DateTimeValue::now();
3109 let schema = engine
3110 .get_plan(None, "s", Some(&now))
3111 .unwrap()
3112 .schema(&DataOverlay::default());
3113
3114 let json = serde_json::to_string(&schema).unwrap();
3115 let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3116 assert_eq!(schema, round_tripped);
3117 }
3118}
3119
3120