1use crate::parsing::ast::{
13 CalendarUnit, DateCalendarKind, DateRelativeKind, DateTimeValue, DurationUnit, TimeValue,
14};
15use crate::planning::execution_plan::{Branch, ExecutableRule, ExecutionPlan, SpecId};
16use crate::planning::semantics::{
17 ArithmeticComputation, ComparisonComputation, Expression, ExpressionKind, FactData, FactPath,
18 LemmaType, LiteralValue, MathematicalComputation, NegationType, RatioUnit, RatioUnits,
19 RulePath, ScaleUnit, ScaleUnits, SemanticConversionTarget, TypeDefiningSpec, TypeExtends,
20 TypeSpecification, ValueKind, VetoExpression,
21};
22use rust_decimal::Decimal;
23use serde::Serialize;
24use sha2::{Digest, Sha256};
25use std::collections::BTreeMap;
26
27pub const FINGERPRINT_FORMAT_VERSION: u32 = 1;
29
30const FINGERPRINT_MAGIC: &[u8; 4] = b"LMFP";
31
32#[derive(Debug, Clone, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum TypeDefiningSpecFingerprint {
35 Local,
36 Import {
37 spec_id: String,
39 effective_from: Option<DateTimeValue>,
40 },
41}
42
43#[derive(Debug, Clone, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum TypeExtendsFingerprint {
46 Primitive,
47 Custom {
48 parent: String,
49 family: String,
50 defining_spec: TypeDefiningSpecFingerprint,
51 },
52}
53
54#[derive(Debug, Clone, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum TypeSpecificationFingerprint {
58 Boolean {
59 help: String,
60 default: Option<bool>,
61 },
62 Scale {
63 minimum: Option<Decimal>,
64 maximum: Option<Decimal>,
65 decimals: Option<u8>,
66 precision: Option<Decimal>,
67 units: ScaleUnits,
68 help: String,
69 default: Option<(Decimal, String)>,
70 },
71 Number {
72 minimum: Option<Decimal>,
73 maximum: Option<Decimal>,
74 decimals: Option<u8>,
75 precision: Option<Decimal>,
76 help: String,
77 default: Option<Decimal>,
78 },
79 Ratio {
80 minimum: Option<Decimal>,
81 maximum: Option<Decimal>,
82 decimals: Option<u8>,
83 units: RatioUnits,
84 help: String,
85 default: Option<Decimal>,
86 },
87 Text {
88 minimum: Option<usize>,
89 maximum: Option<usize>,
90 length: Option<usize>,
91 options: Vec<String>,
92 help: String,
93 default: Option<String>,
94 },
95 Date {
96 minimum: Option<DateTimeValue>,
97 maximum: Option<DateTimeValue>,
98 help: String,
99 default: Option<DateTimeValue>,
100 },
101 Time {
102 minimum: Option<TimeValue>,
103 maximum: Option<TimeValue>,
104 help: String,
105 default: Option<TimeValue>,
106 },
107 Duration {
108 help: String,
109 default: Option<(Decimal, DurationUnit)>,
110 },
111 Veto {
112 message: Option<String>,
113 },
114}
115
116#[derive(Debug, Clone, Serialize)]
117pub struct LemmaTypeFingerprint {
118 pub name: Option<String>,
119 pub specifications: TypeSpecificationFingerprint,
120 pub extends: TypeExtendsFingerprint,
121}
122
123#[derive(Debug, Clone, Serialize)]
124pub struct LiteralValueFingerprint {
125 pub value: ValueKind,
126 pub lemma_type: LemmaTypeFingerprint,
127}
128
129#[derive(Debug, Clone, Serialize)]
131pub struct PlanFingerprint {
132 pub spec_name: String,
133 pub valid_from: Option<DateTimeValue>,
134 pub facts: BTreeMap<FactPath, FactFingerprint>,
135 pub rules: BTreeMap<RulePath, RuleFingerprint>,
136 pub named_types: BTreeMap<String, LemmaTypeFingerprint>,
137}
138
139#[derive(Debug, Clone, Serialize)]
140#[serde(rename_all = "snake_case")]
141pub enum FactFingerprint {
142 Value {
143 value: LiteralValueFingerprint,
144 is_default: bool,
145 },
146 TypeDeclaration {
147 resolved_type: LemmaTypeFingerprint,
148 },
149 SpecRef {
150 spec_id: String,
152 effective_from: Option<DateTimeValue>,
153 },
154}
155
156#[derive(Debug, Clone, Serialize)]
157pub struct RuleFingerprint {
158 pub path: RulePath,
159 pub branches: Vec<BranchFingerprint>,
160 pub needs_facts: Vec<FactPath>,
161 pub rule_type: LemmaTypeFingerprint,
162}
163
164#[derive(Debug, Clone, Serialize)]
165pub struct BranchFingerprint {
166 pub condition: Option<ExpressionFingerprint>,
167 pub result: ExpressionFingerprint,
168}
169
170#[derive(Debug, Clone, Serialize)]
171pub struct ExpressionFingerprint {
172 pub kind: ExpressionKindFingerprint,
173}
174
175#[derive(Debug, Clone, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub enum ExpressionKindFingerprint {
178 Literal(Box<LiteralValueFingerprint>),
179 FactPath(FactPath),
180 RulePath(RulePath),
181 LogicalAnd(Box<ExpressionFingerprint>, Box<ExpressionFingerprint>),
182 Arithmetic(
183 Box<ExpressionFingerprint>,
184 ArithmeticComputation,
185 Box<ExpressionFingerprint>,
186 ),
187 Comparison(
188 Box<ExpressionFingerprint>,
189 ComparisonComputation,
190 Box<ExpressionFingerprint>,
191 ),
192 UnitConversion(Box<ExpressionFingerprint>, SemanticConversionTarget),
193 LogicalNegation(Box<ExpressionFingerprint>, NegationType),
194 MathematicalComputation(MathematicalComputation, Box<ExpressionFingerprint>),
195 Veto(VetoExpression),
196 Now,
197 DateRelative(
198 DateRelativeKind,
199 Box<ExpressionFingerprint>,
200 Option<Box<ExpressionFingerprint>>,
201 ),
202 DateCalendar(DateCalendarKind, CalendarUnit, Box<ExpressionFingerprint>),
203}
204
205fn type_spec_fingerprint(ts: &TypeSpecification) -> TypeSpecificationFingerprint {
206 match ts {
207 TypeSpecification::Boolean { help, default } => TypeSpecificationFingerprint::Boolean {
208 help: help.clone(),
209 default: *default,
210 },
211 TypeSpecification::Scale {
212 minimum,
213 maximum,
214 decimals,
215 precision,
216 units,
217 help,
218 default,
219 } => {
220 let mut sorted: Vec<ScaleUnit> = units.iter().cloned().collect();
221 sorted.sort_by(|a, b| a.name.cmp(&b.name));
222 TypeSpecificationFingerprint::Scale {
223 minimum: *minimum,
224 maximum: *maximum,
225 decimals: *decimals,
226 precision: *precision,
227 units: ScaleUnits::from(sorted),
228 help: help.clone(),
229 default: default.clone(),
230 }
231 }
232 TypeSpecification::Number {
233 minimum,
234 maximum,
235 decimals,
236 precision,
237 help,
238 default,
239 } => TypeSpecificationFingerprint::Number {
240 minimum: *minimum,
241 maximum: *maximum,
242 decimals: *decimals,
243 precision: *precision,
244 help: help.clone(),
245 default: *default,
246 },
247 TypeSpecification::Ratio {
248 minimum,
249 maximum,
250 decimals,
251 units,
252 help,
253 default,
254 } => {
255 let mut sorted: Vec<RatioUnit> = units.iter().cloned().collect();
256 sorted.sort_by(|a, b| a.name.cmp(&b.name));
257 TypeSpecificationFingerprint::Ratio {
258 minimum: *minimum,
259 maximum: *maximum,
260 decimals: *decimals,
261 units: RatioUnits::from(sorted),
262 help: help.clone(),
263 default: *default,
264 }
265 }
266 TypeSpecification::Text {
267 minimum,
268 maximum,
269 length,
270 options,
271 help,
272 default,
273 } => {
274 let mut sorted_opts = options.clone();
275 sorted_opts.sort();
276 TypeSpecificationFingerprint::Text {
277 minimum: *minimum,
278 maximum: *maximum,
279 length: *length,
280 options: sorted_opts,
281 help: help.clone(),
282 default: default.clone(),
283 }
284 }
285 TypeSpecification::Date {
286 minimum,
287 maximum,
288 help,
289 default,
290 } => TypeSpecificationFingerprint::Date {
291 minimum: minimum.clone(),
292 maximum: maximum.clone(),
293 help: help.clone(),
294 default: default.clone(),
295 },
296 TypeSpecification::Time {
297 minimum,
298 maximum,
299 help,
300 default,
301 } => TypeSpecificationFingerprint::Time {
302 minimum: minimum.clone(),
303 maximum: maximum.clone(),
304 help: help.clone(),
305 default: default.clone(),
306 },
307 TypeSpecification::Duration { help, default } => TypeSpecificationFingerprint::Duration {
308 help: help.clone(),
309 default: default.clone(),
310 },
311 TypeSpecification::Veto { message } => TypeSpecificationFingerprint::Veto {
312 message: message.clone(),
313 },
314 TypeSpecification::Undetermined => {
315 unreachable!("fingerprint: Undetermined must not appear in a validated execution plan")
316 }
317 }
318}
319
320fn type_defining_spec_fingerprint(ds: &TypeDefiningSpec) -> TypeDefiningSpecFingerprint {
321 match ds {
322 TypeDefiningSpec::Local => TypeDefiningSpecFingerprint::Local,
323 TypeDefiningSpec::Import {
324 spec,
325 resolved_plan_hash,
326 } => TypeDefiningSpecFingerprint::Import {
327 spec_id: SpecId::new(spec.name.clone(), resolved_plan_hash.clone()).to_string(),
328 effective_from: spec.effective_from.clone(),
329 },
330 }
331}
332
333fn type_extends_fingerprint(e: &TypeExtends) -> TypeExtendsFingerprint {
334 match e {
335 TypeExtends::Primitive => TypeExtendsFingerprint::Primitive,
336 TypeExtends::Custom {
337 parent,
338 family,
339 defining_spec,
340 } => TypeExtendsFingerprint::Custom {
341 parent: parent.clone(),
342 family: family.clone(),
343 defining_spec: type_defining_spec_fingerprint(defining_spec),
344 },
345 }
346}
347
348fn lemma_type_fingerprint(lt: &LemmaType) -> LemmaTypeFingerprint {
349 LemmaTypeFingerprint {
350 name: lt.name.clone(),
351 specifications: type_spec_fingerprint(<.specifications),
352 extends: type_extends_fingerprint(<.extends),
353 }
354}
355
356fn literal_value_fingerprint(lv: &LiteralValue) -> LiteralValueFingerprint {
357 LiteralValueFingerprint {
358 value: lv.value.clone(),
359 lemma_type: lemma_type_fingerprint(&lv.lemma_type),
360 }
361}
362
363pub fn from_plan(plan: &ExecutionPlan) -> PlanFingerprint {
365 let facts: BTreeMap<FactPath, FactFingerprint> = plan
366 .facts
367 .iter()
368 .map(|(path, data)| (path.clone(), fact_fingerprint(data)))
369 .collect();
370
371 let rules: BTreeMap<RulePath, RuleFingerprint> = plan
372 .rules
373 .iter()
374 .map(|rule| (rule.path.clone(), rule_fingerprint(rule)))
375 .collect();
376
377 let named_types: BTreeMap<String, LemmaTypeFingerprint> = plan
378 .named_types
379 .iter()
380 .map(|(k, v)| (k.clone(), lemma_type_fingerprint(v)))
381 .collect();
382
383 PlanFingerprint {
384 spec_name: plan.spec_name.clone(),
385 valid_from: plan.valid_from.clone(),
386 facts,
387 rules,
388 named_types,
389 }
390}
391
392fn fact_fingerprint(data: &FactData) -> FactFingerprint {
393 match data {
394 FactData::Value {
395 value, is_default, ..
396 } => FactFingerprint::Value {
397 value: literal_value_fingerprint(value),
398 is_default: *is_default,
399 },
400 FactData::TypeDeclaration { resolved_type, .. } => FactFingerprint::TypeDeclaration {
401 resolved_type: lemma_type_fingerprint(resolved_type),
402 },
403 FactData::SpecRef {
404 spec,
405 resolved_plan_hash,
406 ..
407 } => FactFingerprint::SpecRef {
408 spec_id: SpecId::new(spec.name.clone(), resolved_plan_hash.clone()).to_string(),
409 effective_from: spec.effective_from.clone(),
410 },
411 }
412}
413
414fn rule_fingerprint(rule: &ExecutableRule) -> RuleFingerprint {
415 RuleFingerprint {
416 path: rule.path.clone(),
417 branches: rule.branches.iter().map(branch_fingerprint).collect(),
418 needs_facts: rule.needs_facts.iter().cloned().collect(),
419 rule_type: lemma_type_fingerprint(&rule.rule_type),
420 }
421}
422
423fn branch_fingerprint(branch: &Branch) -> BranchFingerprint {
424 BranchFingerprint {
425 condition: branch.condition.as_ref().map(expression_fingerprint),
426 result: expression_fingerprint(&branch.result),
427 }
428}
429
430fn expression_fingerprint(expr: &Expression) -> ExpressionFingerprint {
431 ExpressionFingerprint {
432 kind: expression_kind_fingerprint(&expr.kind),
433 }
434}
435
436fn expression_kind_fingerprint(kind: &ExpressionKind) -> ExpressionKindFingerprint {
437 match kind {
438 ExpressionKind::Literal(lv) => {
439 ExpressionKindFingerprint::Literal(Box::new(literal_value_fingerprint(lv)))
440 }
441 ExpressionKind::FactPath(fp) => ExpressionKindFingerprint::FactPath(fp.clone()),
442 ExpressionKind::RulePath(rp) => ExpressionKindFingerprint::RulePath(rp.clone()),
443 ExpressionKind::LogicalAnd(l, r) => ExpressionKindFingerprint::LogicalAnd(
444 Box::new(expression_fingerprint(l)),
445 Box::new(expression_fingerprint(r)),
446 ),
447 ExpressionKind::Arithmetic(l, op, r) => ExpressionKindFingerprint::Arithmetic(
448 Box::new(expression_fingerprint(l)),
449 op.clone(),
450 Box::new(expression_fingerprint(r)),
451 ),
452 ExpressionKind::Comparison(l, op, r) => ExpressionKindFingerprint::Comparison(
453 Box::new(expression_fingerprint(l)),
454 op.clone(),
455 Box::new(expression_fingerprint(r)),
456 ),
457 ExpressionKind::UnitConversion(inner, target) => ExpressionKindFingerprint::UnitConversion(
458 Box::new(expression_fingerprint(inner)),
459 target.clone(),
460 ),
461 ExpressionKind::LogicalNegation(inner, nt) => ExpressionKindFingerprint::LogicalNegation(
462 Box::new(expression_fingerprint(inner)),
463 nt.clone(),
464 ),
465 ExpressionKind::MathematicalComputation(mc, inner) => {
466 ExpressionKindFingerprint::MathematicalComputation(
467 mc.clone(),
468 Box::new(expression_fingerprint(inner)),
469 )
470 }
471 ExpressionKind::Veto(ve) => ExpressionKindFingerprint::Veto(ve.clone()),
472 ExpressionKind::Now => ExpressionKindFingerprint::Now,
473 ExpressionKind::DateRelative(kind, date_expr, tol) => {
474 ExpressionKindFingerprint::DateRelative(
475 *kind,
476 Box::new(expression_fingerprint(date_expr)),
477 tol.as_ref().map(|t| Box::new(expression_fingerprint(t))),
478 )
479 }
480 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
481 ExpressionKindFingerprint::DateCalendar(
482 *kind,
483 *unit,
484 Box::new(expression_fingerprint(date_expr)),
485 )
486 }
487 }
488}
489
490pub fn fingerprint_hash(fp: &PlanFingerprint) -> String {
492 let payload = postcard::to_allocvec(fp).expect("PlanFingerprint serialization");
493 let mut prefixed = Vec::with_capacity(FINGERPRINT_MAGIC.len() + 4 + payload.len());
494 prefixed.extend_from_slice(FINGERPRINT_MAGIC.as_slice());
495 prefixed.extend_from_slice(&FINGERPRINT_FORMAT_VERSION.to_be_bytes());
496 prefixed.extend_from_slice(&payload);
497 let digest = Sha256::digest(&prefixed);
498 let n = (u32::from(digest[0]) << 24)
499 | (u32::from(digest[1]) << 16)
500 | (u32::from(digest[2]) << 8)
501 | u32::from(digest[3]);
502 format!("{:08x}", n)
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::parsing::ast::Span;
509 use crate::parsing::source::Source;
510 use crate::planning::semantics::primitive_number;
511 use indexmap::IndexMap;
512 use std::collections::{BTreeSet, HashMap};
513
514 fn empty_plan(spec_name: &str) -> ExecutionPlan {
515 ExecutionPlan {
516 spec_name: spec_name.to_string(),
517 facts: IndexMap::new(),
518 rules: vec![],
519 meta: HashMap::new(),
520 named_types: BTreeMap::new(),
521 valid_from: None,
522 valid_to: None,
523 sources: IndexMap::new(),
524 }
525 }
526
527 fn dummy_source() -> Source {
528 Source::new(
529 "test.lemma",
530 Span {
531 start: 0,
532 end: 0,
533 line: 1,
534 col: 0,
535 },
536 )
537 }
538
539 fn literal_expr_one() -> Expression {
540 Expression::with_source(
541 ExpressionKind::Literal(Box::new(LiteralValue::number(Decimal::ONE))),
542 None,
543 )
544 }
545
546 fn simple_rule(path: RulePath, name: &str) -> ExecutableRule {
547 ExecutableRule {
548 path,
549 name: name.to_string(),
550 branches: vec![Branch {
551 condition: None,
552 result: literal_expr_one(),
553 source: dummy_source(),
554 }],
555 needs_facts: BTreeSet::new(),
556 source: dummy_source(),
557 rule_type: primitive_number().clone(),
558 }
559 }
560
561 #[test]
562 fn same_plan_same_fingerprint() {
563 let plan = empty_plan("test");
564 let fp1 = from_plan(&plan);
565 let fp2 = from_plan(&plan);
566 assert_eq!(fp1.spec_name, fp2.spec_name);
567 }
568
569 #[test]
570 fn same_plan_same_hash() {
571 let plan = empty_plan("test");
572 let h1 = fingerprint_hash(&from_plan(&plan));
573 let h2 = fingerprint_hash(&from_plan(&plan));
574 assert_eq!(h1, h2);
575 }
576
577 #[test]
578 fn different_spec_name_different_hash() {
579 let h1 = fingerprint_hash(&from_plan(&empty_plan("a")));
580 let h2 = fingerprint_hash(&from_plan(&empty_plan("b")));
581 assert_ne!(h1, h2);
582 }
583
584 #[test]
586 fn golden_plan_hash_empty_spec_names() {
587 assert_eq!(
588 fingerprint_hash(&from_plan(&empty_plan("golden_empty"))),
589 "fc4c852f"
590 );
591 assert_eq!(fingerprint_hash(&from_plan(&empty_plan("x"))), "e97e410c");
592 let mut p = empty_plan("golden_valid_from");
593 p.valid_from = Some(DateTimeValue {
594 year: 2024,
595 month: 6,
596 day: 15,
597 hour: 0,
598 minute: 0,
599 second: 0,
600 microsecond: 0,
601 timezone: None,
602 });
603 assert_eq!(fingerprint_hash(&from_plan(&p)), "b301d0c3");
604 }
605
606 #[test]
607 fn fingerprint_independent_of_fact_rule_and_type_order() {
608 let fa = FactPath::local("a".to_string());
609 let fb = FactPath::local("b".to_string());
610 let fact_a = FactData::Value {
611 value: LiteralValue::number(Decimal::ONE),
612 source: dummy_source(),
613 is_default: false,
614 };
615 let fact_b = FactData::Value {
616 value: LiteralValue::number(Decimal::from(2)),
617 source: dummy_source(),
618 is_default: false,
619 };
620
621 let type_x = LemmaType::new(
622 "age".to_string(),
623 TypeSpecification::number(),
624 TypeExtends::Primitive,
625 );
626 let type_y = LemmaType::new(
627 "weight".to_string(),
628 TypeSpecification::number(),
629 TypeExtends::Primitive,
630 );
631
632 let mut plan1 = empty_plan("order_test");
633 plan1.facts.insert(fa.clone(), fact_a.clone());
634 plan1.facts.insert(fb.clone(), fact_b.clone());
635 plan1.named_types.insert("age".to_string(), type_x.clone());
636 plan1
637 .named_types
638 .insert("weight".to_string(), type_y.clone());
639
640 let mut plan2 = empty_plan("order_test");
641 plan2.facts.insert(fb, fact_b);
642 plan2.facts.insert(fa, fact_a);
643 plan2.named_types.insert("weight".to_string(), type_y);
644 plan2.named_types.insert("age".to_string(), type_x);
645
646 let r1 = RulePath::new(vec![], "r1".to_string());
647 let r2 = RulePath::new(vec![], "r2".to_string());
648 plan1.rules = vec![simple_rule(r1.clone(), "r1"), simple_rule(r2.clone(), "r2")];
649 plan2.rules = vec![simple_rule(r2, "r2"), simple_rule(r1, "r1")];
650
651 assert_eq!(
652 fingerprint_hash(&from_plan(&plan1)),
653 fingerprint_hash(&from_plan(&plan2))
654 );
655 }
656}