1use crate::evaluation::Evaluator;
2use crate::parsing::ast::{DateTimeValue, LemmaSpec};
3use crate::{parse, Error, ResourceLimits, Response};
4use std::collections::{BTreeSet, HashMap};
5use std::sync::Arc;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
13pub(crate) enum TemporalBound {
14 NegInf,
15 At(DateTimeValue),
16 PosInf,
17}
18
19impl PartialOrd for TemporalBound {
20 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
21 Some(self.cmp(other))
22 }
23}
24
25impl Ord for TemporalBound {
26 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
27 use std::cmp::Ordering;
28 match (self, other) {
29 (TemporalBound::NegInf, TemporalBound::NegInf) => Ordering::Equal,
30 (TemporalBound::NegInf, _) => Ordering::Less,
31 (_, TemporalBound::NegInf) => Ordering::Greater,
32 (TemporalBound::PosInf, TemporalBound::PosInf) => Ordering::Equal,
33 (TemporalBound::PosInf, _) => Ordering::Greater,
34 (_, TemporalBound::PosInf) => Ordering::Less,
35 (TemporalBound::At(a), TemporalBound::At(b)) => a.cmp(b),
36 }
37 }
38}
39
40impl TemporalBound {
41 pub(crate) fn from_start(opt: Option<&DateTimeValue>) -> Self {
43 match opt {
44 None => TemporalBound::NegInf,
45 Some(d) => TemporalBound::At(d.clone()),
46 }
47 }
48
49 pub(crate) fn from_end(opt: Option<&DateTimeValue>) -> Self {
51 match opt {
52 None => TemporalBound::PosInf,
53 Some(d) => TemporalBound::At(d.clone()),
54 }
55 }
56
57 pub(crate) fn to_start(&self) -> Option<DateTimeValue> {
59 match self {
60 TemporalBound::NegInf => None,
61 TemporalBound::At(d) => Some(d.clone()),
62 TemporalBound::PosInf => {
63 unreachable!("BUG: PosInf cannot represent a start bound")
64 }
65 }
66 }
67
68 pub(crate) fn to_end(&self) -> Option<DateTimeValue> {
70 match self {
71 TemporalBound::NegInf => {
72 unreachable!("BUG: NegInf cannot represent an end bound")
73 }
74 TemporalBound::At(d) => Some(d.clone()),
75 TemporalBound::PosInf => None,
76 }
77 }
78}
79
80#[derive(Debug, Default)]
87pub struct Context {
88 specs: BTreeSet<Arc<LemmaSpec>>,
89}
90
91impl Context {
92 pub fn new() -> Self {
93 Self {
94 specs: BTreeSet::new(),
95 }
96 }
97
98 pub(crate) fn specs_for_name(&self, name: &str) -> Vec<Arc<LemmaSpec>> {
99 self.specs
100 .iter()
101 .filter(|a| a.name == name)
102 .cloned()
103 .collect()
104 }
105
106 pub fn get_spec_effective_from(
111 &self,
112 name: &str,
113 effective_from: Option<&DateTimeValue>,
114 ) -> Option<Arc<LemmaSpec>> {
115 self.specs_for_name(name)
116 .into_iter()
117 .find(|s| s.effective_from() == effective_from)
118 }
119
120 pub fn get_spec(&self, name: &str, effective: &DateTimeValue) -> Option<Arc<LemmaSpec>> {
126 let versions = self.specs_for_name(name);
127 if versions.is_empty() {
128 return None;
129 }
130
131 for (i, spec) in versions.iter().enumerate() {
132 let from_ok = spec
133 .effective_from()
134 .map(|f| *effective >= *f)
135 .unwrap_or(true);
136 if !from_ok {
137 continue;
138 }
139
140 let effective_to: Option<&DateTimeValue> =
141 versions.get(i + 1).and_then(|next| next.effective_from());
142 let to_ok = effective_to.map(|end| *effective < *end).unwrap_or(true);
143
144 if to_ok {
145 return Some(spec.clone());
146 }
147 }
148
149 None
150 }
151
152 pub fn iter(&self) -> impl Iterator<Item = Arc<LemmaSpec>> + '_ {
153 self.specs.iter().cloned()
154 }
155
156 pub fn insert_spec(&mut self, spec: Arc<LemmaSpec>) -> Result<(), Error> {
158 let existing = self.specs_for_name(&spec.name);
159
160 if existing
161 .iter()
162 .any(|o| o.effective_from() == spec.effective_from())
163 {
164 return Err(Error::validation(
165 format!(
166 "Duplicate spec '{}' (same name and effective_from already in context)",
167 spec.name
168 ),
169 None,
170 None::<String>,
171 ));
172 }
173
174 self.specs.insert(spec);
175 Ok(())
176 }
177
178 pub fn remove_spec(&mut self, spec: &Arc<LemmaSpec>) -> bool {
179 self.specs.remove(spec)
180 }
181
182 #[cfg(test)]
183 pub(crate) fn len(&self) -> usize {
184 self.specs.len()
185 }
186
187 pub fn effective_range(
194 &self,
195 spec: &Arc<LemmaSpec>,
196 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
197 let from = spec.effective_from().cloned();
198 let versions = self.specs_for_name(&spec.name);
199 let pos = versions
200 .iter()
201 .position(|v| Arc::ptr_eq(v, spec))
202 .unwrap_or_else(|| {
203 unreachable!(
204 "BUG: effective_range called with spec '{}' not in context",
205 spec.name
206 )
207 });
208 let to = versions
209 .get(pos + 1)
210 .and_then(|next| next.effective_from().cloned());
211 (from, to)
212 }
213
214 pub fn version_boundaries(&self, name: &str) -> Vec<DateTimeValue> {
217 self.specs_for_name(name)
218 .iter()
219 .filter_map(|s| s.effective_from().cloned())
220 .collect()
221 }
222
223 pub fn dep_coverage_gaps(
229 &self,
230 dep_name: &str,
231 required_from: Option<&DateTimeValue>,
232 required_to: Option<&DateTimeValue>,
233 ) -> Vec<(Option<DateTimeValue>, Option<DateTimeValue>)> {
234 let versions = self.specs_for_name(dep_name);
235 if versions.is_empty() {
236 return vec![(required_from.cloned(), required_to.cloned())];
237 }
238
239 let req_start = TemporalBound::from_start(required_from);
240 let req_end = TemporalBound::from_end(required_to);
241
242 let intervals: Vec<(TemporalBound, TemporalBound)> = versions
243 .iter()
244 .enumerate()
245 .map(|(i, v)| {
246 let start = TemporalBound::from_start(v.effective_from());
247 let end = match versions.get(i + 1).and_then(|next| next.effective_from()) {
248 Some(next_from) => TemporalBound::At(next_from.clone()),
249 None => TemporalBound::PosInf,
250 };
251 (start, end)
252 })
253 .collect();
254
255 let mut gaps = Vec::new();
256 let mut cursor = req_start.clone();
257
258 for (v_start, v_end) in &intervals {
259 if cursor >= req_end {
260 break;
261 }
262
263 if *v_end <= cursor {
264 continue;
265 }
266
267 if *v_start > cursor {
268 let gap_end = std::cmp::min(v_start.clone(), req_end.clone());
269 if cursor < gap_end {
270 gaps.push((cursor.to_start(), gap_end.to_end()));
271 }
272 }
273
274 if *v_end > cursor {
275 cursor = v_end.clone();
276 }
277 }
278
279 if cursor < req_end {
280 gaps.push((cursor.to_start(), req_end.to_end()));
281 }
282
283 gaps
284 }
285}
286
287fn find_slice_plan<'a>(
291 plans: &'a [crate::planning::ExecutionPlan],
292 effective: &DateTimeValue,
293) -> Option<&'a crate::planning::ExecutionPlan> {
294 for plan in plans {
295 let from_ok = plan
296 .valid_from
297 .as_ref()
298 .map(|f| *effective >= *f)
299 .unwrap_or(true);
300 let to_ok = plan
301 .valid_to
302 .as_ref()
303 .map(|t| *effective < *t)
304 .unwrap_or(true);
305 if from_ok && to_ok {
306 return Some(plan);
307 }
308 }
309 None
310}
311
312pub struct Engine {
324 execution_plans: HashMap<Arc<LemmaSpec>, Vec<crate::planning::ExecutionPlan>>,
325 specs: Context,
326 sources: HashMap<String, String>,
327 evaluator: Evaluator,
328 limits: ResourceLimits,
329 hash_pins: HashMap<Arc<LemmaSpec>, String>,
330 total_expression_count: usize,
331}
332
333impl Default for Engine {
334 fn default() -> Self {
335 Self {
336 execution_plans: HashMap::new(),
337 specs: Context::new(),
338 sources: HashMap::new(),
339 evaluator: Evaluator,
340 limits: ResourceLimits::default(),
341 hash_pins: HashMap::new(),
342 total_expression_count: 0,
343 }
344 }
345}
346
347impl Engine {
348 pub fn new() -> Self {
349 Self::default()
350 }
351
352 pub fn with_limits(limits: ResourceLimits) -> Self {
354 Self {
355 execution_plans: HashMap::new(),
356 specs: Context::new(),
357 sources: HashMap::new(),
358 evaluator: Evaluator,
359 limits,
360 hash_pins: HashMap::new(),
361 total_expression_count: 0,
362 }
363 }
364
365 pub fn hash_pin(&self, spec_name: &str, effective: &DateTimeValue) -> Option<&str> {
367 let spec_arc = self.get_spec(spec_name, effective)?;
368 self.hash_pin_for_spec(&spec_arc)
369 }
370
371 pub fn hash_pin_for_spec(&self, spec: &Arc<LemmaSpec>) -> Option<&str> {
373 self.hash_pins.get(spec).map(|s| s.as_str())
374 }
375
376 pub fn all_hash_pins(&self) -> Vec<(&str, Option<String>, &str)> {
378 self.hash_pins
379 .iter()
380 .map(|(spec, hash)| {
381 (
382 spec.name.as_str(),
383 spec.effective_from().map(|af| af.to_string()),
384 hash.as_str(),
385 )
386 })
387 .collect()
388 }
389
390 pub fn get_spec_by_hash_pin(&self, spec_name: &str, hash_pin: &str) -> Option<Arc<LemmaSpec>> {
393 let mut matched: Option<Arc<LemmaSpec>> = None;
394 for spec in self.specs.specs_for_name(spec_name) {
395 let computed = match self.hash_pins.get(&spec) {
396 Some(h) => h.as_str(),
397 None => continue,
398 };
399 if crate::planning::content_hash::content_hash_matches(hash_pin, computed) {
400 if matched.is_some() {
401 return None;
402 }
403 matched = Some(spec);
404 }
405 }
406 matched
407 }
408
409 pub fn add_lemma_files(&mut self, files: HashMap<String, String>) -> Result<(), Vec<Error>> {
422 let mut errors: Vec<Error> = Vec::new();
423
424 for (source_id, code) in &files {
425 match parse(code, source_id, &self.limits) {
426 Ok(result) => {
427 self.total_expression_count += result.expression_count;
428 if self.total_expression_count > self.limits.max_total_expression_count {
429 errors.push(Error::resource_limit_exceeded(
430 "max_total_expression_count",
431 self.limits.max_total_expression_count.to_string(),
432 self.total_expression_count.to_string(),
433 "Split logic across fewer files or reduce expression complexity",
434 None::<crate::Source>,
435 ));
436 return Err(errors);
437 }
438 let new_specs = result.specs;
439 let source_text: Arc<str> = Arc::from(code.as_str());
440 for spec in new_specs {
441 let attribute = spec.attribute.clone().unwrap_or_else(|| spec.name.clone());
442 let start_line = spec.start_line;
443
444 match self.specs.insert_spec(Arc::new(spec)) {
445 Ok(()) => {
446 self.sources.insert(attribute, code.clone());
447 }
448 Err(e) => {
449 let source = crate::Source::new(
450 &attribute,
451 crate::parsing::ast::Span {
452 start: 0,
453 end: 0,
454 line: start_line,
455 col: 0,
456 },
457 Arc::clone(&source_text),
458 );
459 errors.push(Error::validation(
460 e.to_string(),
461 Some(source),
462 None::<String>,
463 ));
464 }
465 }
466 }
467 }
468 Err(e) => errors.push(e),
469 }
470 }
471
472 let planning_result = crate::planning::plan(&self.specs, self.sources.clone());
473 for spec_result in &planning_result.per_spec {
474 self.execution_plans
475 .insert(Arc::clone(&spec_result.spec), spec_result.plans.clone());
476 self.hash_pins
477 .insert(Arc::clone(&spec_result.spec), spec_result.hash_pin.clone());
478 }
479 errors.extend(planning_result.global_errors);
480 for spec_result in planning_result.per_spec {
481 for err in spec_result.errors {
482 errors.push(err.with_spec_context(Arc::clone(&spec_result.spec)));
483 }
484 }
485
486 if errors.is_empty() {
487 Ok(())
488 } else {
489 Err(errors)
490 }
491 }
492
493 pub fn remove_spec(&mut self, spec: Arc<LemmaSpec>) {
494 self.execution_plans.remove(&spec);
495 self.specs.remove_spec(&spec);
496 }
497
498 pub fn list_specs(&self) -> Vec<Arc<LemmaSpec>> {
500 self.specs.iter().collect()
501 }
502
503 pub fn list_specs_effective(&self, effective: &DateTimeValue) -> Vec<Arc<LemmaSpec>> {
505 let mut seen_names = std::collections::HashSet::new();
506 let mut result = Vec::new();
507 for spec in self.specs.iter() {
508 if seen_names.contains(&spec.name) {
509 continue;
510 }
511 if let Some(active) = self.specs.get_spec(&spec.name, effective) {
512 if seen_names.insert(active.name.clone()) {
513 result.push(active);
514 }
515 }
516 }
517 result.sort_by(|a, b| a.name.cmp(&b.name));
518 result
519 }
520
521 pub fn get_spec(
523 &self,
524 spec_name: &str,
525 effective: &DateTimeValue,
526 ) -> Option<std::sync::Arc<LemmaSpec>> {
527 self.specs.get_spec(spec_name, effective)
528 }
529
530 fn spec_not_found_error(&self, spec_name: &str, effective: &DateTimeValue) -> Error {
534 let versions = self.specs.specs_for_name(spec_name);
535 let msg = if versions.is_empty() {
536 format!("Spec '{}' not found", spec_name)
537 } else {
538 let version_list: Vec<String> = versions
539 .iter()
540 .map(|s| match s.effective_from() {
541 Some(dt) => format!(" {} (effective from {})", s.name, dt),
542 None => format!(" {} (no effective_from)", s.name),
543 })
544 .collect();
545 format!(
546 "Spec '{}' not found for effective {}. Available temporal versions:\n{}",
547 spec_name,
548 effective,
549 version_list.join("\n")
550 )
551 };
552 Error::request(msg, None::<String>)
553 }
554
555 pub fn get_execution_plan(
562 &self,
563 spec_name: &str,
564 hash_pin: Option<&str>,
565 effective: &DateTimeValue,
566 ) -> Option<&crate::planning::ExecutionPlan> {
567 let arc = if let Some(pin) = hash_pin {
568 self.get_spec_by_hash_pin(spec_name, pin)?
569 } else {
570 self.get_spec(spec_name, effective)?
571 };
572 let slice_plans = self.execution_plans.get(&arc)?;
573 let plan = find_slice_plan(slice_plans, effective);
574 if plan.is_none() && !slice_plans.is_empty() {
575 unreachable!(
576 "BUG: spec '{}' has {} slice plans but none covers effective={} — slice partition is broken",
577 spec_name, slice_plans.len(), effective
578 );
579 }
580 plan
581 }
582
583 pub fn get_spec_rules(
584 &self,
585 spec_name: &str,
586 effective: &DateTimeValue,
587 ) -> Result<Vec<crate::LemmaRule>, Error> {
588 let arc = self
589 .get_spec(spec_name, effective)
590 .ok_or_else(|| self.spec_not_found_error(spec_name, effective))?;
591 Ok(arc.rules.clone())
592 }
593
594 pub fn evaluate_json(
608 &self,
609 spec_name: &str,
610 hash_pin: Option<&str>,
611 effective: &DateTimeValue,
612 rule_names: Vec<String>,
613 json: &[u8],
614 ) -> Result<Response, Error> {
615 let base_plan = self
616 .get_execution_plan(spec_name, hash_pin, effective)
617 .ok_or_else(|| self.spec_not_found_error(spec_name, effective))?;
618
619 let values = crate::serialization::from_json(json)?;
620 let plan = base_plan.clone().with_fact_values(values, &self.limits)?;
621
622 self.evaluate_plan(plan, rule_names, effective)
623 }
624
625 pub fn evaluate(
640 &self,
641 spec_name: &str,
642 hash_pin: Option<&str>,
643 effective: &DateTimeValue,
644 rule_names: Vec<String>,
645 fact_values: HashMap<String, String>,
646 ) -> Result<Response, Error> {
647 let base_plan = self
648 .get_execution_plan(spec_name, hash_pin, effective)
649 .ok_or_else(|| self.spec_not_found_error(spec_name, effective))?;
650
651 let plan = base_plan
652 .clone()
653 .with_fact_values(fact_values, &self.limits)?;
654
655 self.evaluate_plan(plan, rule_names, effective)
656 }
657
658 pub fn invert(
663 &self,
664 spec_name: &str,
665 effective: &DateTimeValue,
666 rule_name: &str,
667 target: crate::inversion::Target,
668 values: HashMap<String, String>,
669 ) -> Result<crate::InversionResponse, Error> {
670 let base_plan = self
671 .get_execution_plan(spec_name, None, effective)
672 .ok_or_else(|| self.spec_not_found_error(spec_name, effective))?;
673
674 let plan = base_plan.clone().with_fact_values(values, &self.limits)?;
675 let provided_facts: std::collections::HashSet<_> = plan
676 .facts
677 .iter()
678 .filter(|(_, d)| d.value().is_some())
679 .map(|(p, _)| p.clone())
680 .collect();
681
682 crate::inversion::invert(rule_name, target, &plan, &provided_facts)
683 }
684
685 fn evaluate_plan(
686 &self,
687 plan: crate::planning::ExecutionPlan,
688 rule_names: Vec<String>,
689 effective: &DateTimeValue,
690 ) -> Result<Response, Error> {
691 let now_semantic = crate::planning::semantics::date_time_to_semantic(effective);
692 let now_literal = crate::planning::semantics::LiteralValue {
693 value: crate::planning::semantics::ValueKind::Date(now_semantic),
694 lemma_type: crate::planning::semantics::primitive_date().clone(),
695 };
696 let mut response = self.evaluator.evaluate(&plan, now_literal);
697
698 if !rule_names.is_empty() {
699 response.filter_rules(&rule_names);
700 }
701
702 Ok(response)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use rust_decimal::Decimal;
710 use std::str::FromStr;
711
712 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
713 DateTimeValue {
714 year,
715 month,
716 day,
717 hour: 0,
718 minute: 0,
719 second: 0,
720 microsecond: 0,
721 timezone: None,
722 }
723 }
724
725 fn make_spec(name: &str) -> LemmaSpec {
726 LemmaSpec::new(name.to_string())
727 }
728
729 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
730 let mut spec = LemmaSpec::new(name.to_string());
731 spec.effective_from = effective_from;
732 spec
733 }
734
735 #[test]
738 fn effective_range_unbounded_single_version() {
739 let mut ctx = Context::new();
740 let spec = Arc::new(make_spec("a"));
741 ctx.insert_spec(Arc::clone(&spec)).unwrap();
742
743 let (from, to) = ctx.effective_range(&spec);
744 assert_eq!(from, None);
745 assert_eq!(to, None);
746 }
747
748 #[test]
749 fn effective_range_soft_end_from_next_version() {
750 let mut ctx = Context::new();
751 let v1 = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
752 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
753 ctx.insert_spec(Arc::clone(&v1)).unwrap();
754 ctx.insert_spec(Arc::clone(&v2)).unwrap();
755
756 let (from, to) = ctx.effective_range(&v1);
757 assert_eq!(from, Some(date(2025, 1, 1)));
758 assert_eq!(to, Some(date(2025, 6, 1)));
759
760 let (from, to) = ctx.effective_range(&v2);
761 assert_eq!(from, Some(date(2025, 6, 1)));
762 assert_eq!(to, None);
763 }
764
765 #[test]
766 fn effective_range_unbounded_start_with_successor() {
767 let mut ctx = Context::new();
768 let v1 = Arc::new(make_spec("a"));
769 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))));
770 ctx.insert_spec(Arc::clone(&v1)).unwrap();
771 ctx.insert_spec(Arc::clone(&v2)).unwrap();
772
773 let (from, to) = ctx.effective_range(&v1);
774 assert_eq!(from, None);
775 assert_eq!(to, Some(date(2025, 3, 1)));
776 }
777
778 #[test]
781 fn version_boundaries_single_unversioned() {
782 let mut ctx = Context::new();
783 ctx.insert_spec(Arc::new(make_spec("a"))).unwrap();
784
785 assert!(ctx.version_boundaries("a").is_empty());
786 }
787
788 #[test]
789 fn version_boundaries_multiple_versions() {
790 let mut ctx = Context::new();
791 ctx.insert_spec(Arc::new(make_spec("a"))).unwrap();
792 ctx.insert_spec(Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1)))))
793 .unwrap();
794 ctx.insert_spec(Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1)))))
795 .unwrap();
796
797 let boundaries = ctx.version_boundaries("a");
798 assert_eq!(boundaries, vec![date(2025, 3, 1), date(2025, 6, 1)]);
799 }
800
801 #[test]
802 fn version_boundaries_nonexistent_name() {
803 let ctx = Context::new();
804 assert!(ctx.version_boundaries("nope").is_empty());
805 }
806
807 #[test]
810 fn dep_coverage_no_versions_is_full_gap() {
811 let ctx = Context::new();
812 let gaps =
813 ctx.dep_coverage_gaps("missing", Some(&date(2025, 1, 1)), Some(&date(2025, 6, 1)));
814 assert_eq!(gaps, vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]);
815 }
816
817 #[test]
818 fn dep_coverage_single_unbounded_version_covers_everything() {
819 let mut ctx = Context::new();
820 ctx.insert_spec(Arc::new(make_spec("dep"))).unwrap();
821
822 let gaps = ctx.dep_coverage_gaps("dep", None, None);
823 assert!(gaps.is_empty());
824
825 let gaps = ctx.dep_coverage_gaps("dep", Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)));
826 assert!(gaps.is_empty());
827 }
828
829 #[test]
830 fn dep_coverage_single_version_with_from_leaves_leading_gap() {
831 let mut ctx = Context::new();
832 ctx.insert_spec(Arc::new(make_spec_with_range(
833 "dep",
834 Some(date(2025, 3, 1)),
835 )))
836 .unwrap();
837
838 let gaps = ctx.dep_coverage_gaps("dep", None, None);
839 assert_eq!(gaps, vec![(None, Some(date(2025, 3, 1)))]);
840 }
841
842 #[test]
843 fn dep_coverage_continuous_versions_no_gaps() {
844 let mut ctx = Context::new();
845 ctx.insert_spec(Arc::new(make_spec_with_range(
846 "dep",
847 Some(date(2025, 1, 1)),
848 )))
849 .unwrap();
850 ctx.insert_spec(Arc::new(make_spec_with_range(
851 "dep",
852 Some(date(2025, 6, 1)),
853 )))
854 .unwrap();
855
856 let gaps = ctx.dep_coverage_gaps("dep", Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)));
857 assert!(gaps.is_empty());
858 }
859
860 #[test]
861 fn dep_coverage_dep_starts_after_required_start() {
862 let mut ctx = Context::new();
863 ctx.insert_spec(Arc::new(make_spec_with_range(
864 "dep",
865 Some(date(2025, 6, 1)),
866 )))
867 .unwrap();
868
869 let gaps = ctx.dep_coverage_gaps("dep", Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)));
870 assert_eq!(gaps, vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]);
871 }
872
873 #[test]
874 fn dep_coverage_unbounded_required_range() {
875 let mut ctx = Context::new();
876 ctx.insert_spec(Arc::new(make_spec_with_range(
877 "dep",
878 Some(date(2025, 6, 1)),
879 )))
880 .unwrap();
881
882 let gaps = ctx.dep_coverage_gaps("dep", None, None);
883 assert_eq!(gaps, vec![(None, Some(date(2025, 6, 1)))]);
884 }
885
886 fn add_lemma_code_blocking(
887 engine: &mut Engine,
888 code: &str,
889 source: &str,
890 ) -> Result<(), Vec<Error>> {
891 let files: HashMap<String, String> =
892 std::iter::once((source.to_string(), code.to_string())).collect();
893 engine.add_lemma_files(files)
894 }
895
896 #[test]
897 fn test_evaluate_spec_all_rules() {
898 let mut engine = Engine::new();
899 add_lemma_code_blocking(
900 &mut engine,
901 r#"
902 spec test
903 fact x: 10
904 fact y: 5
905 rule sum: x + y
906 rule product: x * y
907 "#,
908 "test.lemma",
909 )
910 .unwrap();
911
912 let now = DateTimeValue::now();
913 let response = engine
914 .evaluate("test", None, &now, vec![], HashMap::new())
915 .unwrap();
916 assert_eq!(response.results.len(), 2);
917
918 let sum_result = response
919 .results
920 .values()
921 .find(|r| r.rule.name == "sum")
922 .unwrap();
923 assert_eq!(
924 sum_result.result,
925 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
926 Decimal::from_str("15").unwrap()
927 )))
928 );
929
930 let product_result = response
931 .results
932 .values()
933 .find(|r| r.rule.name == "product")
934 .unwrap();
935 assert_eq!(
936 product_result.result,
937 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
938 Decimal::from_str("50").unwrap()
939 )))
940 );
941 }
942
943 #[test]
944 fn test_evaluate_empty_facts() {
945 let mut engine = Engine::new();
946 add_lemma_code_blocking(
947 &mut engine,
948 r#"
949 spec test
950 fact price: 100
951 rule total: price * 2
952 "#,
953 "test.lemma",
954 )
955 .unwrap();
956
957 let now = DateTimeValue::now();
958 let response = engine
959 .evaluate("test", None, &now, vec![], HashMap::new())
960 .unwrap();
961 assert_eq!(response.results.len(), 1);
962 assert_eq!(
963 response.results.values().next().unwrap().result,
964 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
965 Decimal::from_str("200").unwrap()
966 )))
967 );
968 }
969
970 #[test]
971 fn test_evaluate_boolean_rule() {
972 let mut engine = Engine::new();
973 add_lemma_code_blocking(
974 &mut engine,
975 r#"
976 spec test
977 fact age: 25
978 rule is_adult: age >= 18
979 "#,
980 "test.lemma",
981 )
982 .unwrap();
983
984 let now = DateTimeValue::now();
985 let response = engine
986 .evaluate("test", None, &now, vec![], HashMap::new())
987 .unwrap();
988 assert_eq!(
989 response.results.values().next().unwrap().result,
990 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::from_bool(true)))
991 );
992 }
993
994 #[test]
995 fn test_evaluate_with_unless_clause() {
996 let mut engine = Engine::new();
997 add_lemma_code_blocking(
998 &mut engine,
999 r#"
1000 spec test
1001 fact quantity: 15
1002 rule discount: 0
1003 unless quantity >= 10 then 10
1004 "#,
1005 "test.lemma",
1006 )
1007 .unwrap();
1008
1009 let now = DateTimeValue::now();
1010 let response = engine
1011 .evaluate("test", None, &now, vec![], HashMap::new())
1012 .unwrap();
1013 assert_eq!(
1014 response.results.values().next().unwrap().result,
1015 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
1016 Decimal::from_str("10").unwrap()
1017 )))
1018 );
1019 }
1020
1021 #[test]
1022 fn test_spec_not_found() {
1023 let engine = Engine::new();
1024 let now = DateTimeValue::now();
1025 let result = engine.evaluate("nonexistent", None, &now, vec![], HashMap::new());
1026 assert!(result.is_err());
1027 assert!(result.unwrap_err().to_string().contains("not found"));
1028 }
1029
1030 #[test]
1031 fn test_multiple_specs() {
1032 let mut engine = Engine::new();
1033 add_lemma_code_blocking(
1034 &mut engine,
1035 r#"
1036 spec spec1
1037 fact x: 10
1038 rule result: x * 2
1039 "#,
1040 "spec 1.lemma",
1041 )
1042 .unwrap();
1043
1044 add_lemma_code_blocking(
1045 &mut engine,
1046 r#"
1047 spec spec2
1048 fact y: 5
1049 rule result: y * 3
1050 "#,
1051 "spec 2.lemma",
1052 )
1053 .unwrap();
1054
1055 let now = DateTimeValue::now();
1056 let response1 = engine
1057 .evaluate("spec1", None, &now, vec![], HashMap::new())
1058 .unwrap();
1059 assert_eq!(
1060 response1.results[0].result,
1061 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
1062 Decimal::from_str("20").unwrap()
1063 )))
1064 );
1065
1066 let response2 = engine
1067 .evaluate("spec2", None, &now, vec![], HashMap::new())
1068 .unwrap();
1069 assert_eq!(
1070 response2.results[0].result,
1071 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
1072 Decimal::from_str("15").unwrap()
1073 )))
1074 );
1075 }
1076
1077 #[test]
1078 fn test_runtime_error_mapping() {
1079 let mut engine = Engine::new();
1080 add_lemma_code_blocking(
1081 &mut engine,
1082 r#"
1083 spec test
1084 fact numerator: 10
1085 fact denominator: 0
1086 rule division: numerator / denominator
1087 "#,
1088 "test.lemma",
1089 )
1090 .unwrap();
1091
1092 let now = DateTimeValue::now();
1093 let result = engine.evaluate("test", None, &now, vec![], HashMap::new());
1094 assert!(result.is_ok(), "Evaluation should succeed");
1096 let response = result.unwrap();
1097 let division_result = response
1098 .results
1099 .values()
1100 .find(|r| r.rule.name == "division");
1101 assert!(
1102 division_result.is_some(),
1103 "Should have division rule result"
1104 );
1105 match &division_result.unwrap().result {
1106 crate::OperationResult::Veto(message) => {
1107 assert!(
1108 message
1109 .as_ref()
1110 .map(|m| m.contains("Division by zero"))
1111 .unwrap_or(false),
1112 "Veto message should mention division by zero: {:?}",
1113 message
1114 );
1115 }
1116 other => panic!("Expected Veto for division by zero, got {:?}", other),
1117 }
1118 }
1119
1120 #[test]
1121 fn test_rules_sorted_by_source_order() {
1122 let mut engine = Engine::new();
1123 add_lemma_code_blocking(
1124 &mut engine,
1125 r#"
1126 spec test
1127 fact a: 1
1128 fact b: 2
1129 rule z: a + b
1130 rule y: a * b
1131 rule x: a - b
1132 "#,
1133 "test.lemma",
1134 )
1135 .unwrap();
1136
1137 let now = DateTimeValue::now();
1138 let response = engine
1139 .evaluate("test", None, &now, vec![], HashMap::new())
1140 .unwrap();
1141 assert_eq!(response.results.len(), 3);
1142
1143 let z_pos = response
1145 .results
1146 .values()
1147 .find(|r| r.rule.name == "z")
1148 .unwrap()
1149 .rule
1150 .source_location
1151 .span
1152 .start;
1153 let y_pos = response
1154 .results
1155 .values()
1156 .find(|r| r.rule.name == "y")
1157 .unwrap()
1158 .rule
1159 .source_location
1160 .span
1161 .start;
1162 let x_pos = response
1163 .results
1164 .values()
1165 .find(|r| r.rule.name == "x")
1166 .unwrap()
1167 .rule
1168 .source_location
1169 .span
1170 .start;
1171
1172 assert!(z_pos < y_pos);
1173 assert!(y_pos < x_pos);
1174 }
1175
1176 #[test]
1177 fn test_rule_filtering_evaluates_dependencies() {
1178 let mut engine = Engine::new();
1179 add_lemma_code_blocking(
1180 &mut engine,
1181 r#"
1182 spec test
1183 fact base: 100
1184 rule subtotal: base * 2
1185 rule tax: subtotal * 10%
1186 rule total: subtotal + tax
1187 "#,
1188 "test.lemma",
1189 )
1190 .unwrap();
1191
1192 let now = DateTimeValue::now();
1194 let response = engine
1195 .evaluate(
1196 "test",
1197 None,
1198 &now,
1199 vec!["total".to_string()],
1200 HashMap::new(),
1201 )
1202 .unwrap();
1203
1204 assert_eq!(response.results.len(), 1);
1206 assert_eq!(response.results.keys().next().unwrap(), "total");
1207
1208 let total = response.results.values().next().unwrap();
1210 assert_eq!(
1211 total.result,
1212 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
1213 Decimal::from_str("220").unwrap()
1214 )))
1215 );
1216 }
1217
1218 use crate::parsing::ast::DateTimeValue;
1223
1224 #[test]
1225 fn pre_resolved_deps_in_file_map_evaluates_external_spec() {
1226 let mut engine = Engine::new();
1227 let mut files = HashMap::new();
1228 files.insert(
1229 "main.lemma".to_string(),
1230 r#"spec main_spec
1231fact external: spec @org/project/helper
1232rule value: external.quantity"#
1233 .to_string(),
1234 );
1235 files.insert(
1236 "deps/org_project_helper.lemma".to_string(),
1237 "spec @org/project/helper\nfact quantity: 42".to_string(),
1238 );
1239 engine
1240 .add_lemma_files(files)
1241 .expect("should succeed with pre-resolved deps in file map");
1242
1243 let now = DateTimeValue::now();
1244 let response = engine
1245 .evaluate("main_spec", None, &now, vec![], HashMap::new())
1246 .expect("evaluate should succeed");
1247
1248 let value_result = response
1249 .results
1250 .get("value")
1251 .expect("rule 'value' should exist");
1252 assert_eq!(
1253 value_result.result,
1254 crate::OperationResult::Value(Box::new(crate::planning::LiteralValue::number(
1255 Decimal::from_str("42").unwrap()
1256 )))
1257 );
1258 }
1259
1260 #[test]
1261 fn add_lemma_files_no_external_refs_works() {
1262 let mut engine = Engine::new();
1263
1264 add_lemma_code_blocking(
1265 &mut engine,
1266 r#"spec local_only
1267fact price: 100
1268rule doubled: price * 2"#,
1269 "local.lemma",
1270 )
1271 .expect("should succeed when there are no @... references");
1272
1273 let now = DateTimeValue::now();
1274 let response = engine
1275 .evaluate("local_only", None, &now, vec![], HashMap::new())
1276 .expect("evaluate should succeed");
1277
1278 assert!(response.results.contains_key("doubled"));
1279 }
1280
1281 #[test]
1282 fn unresolved_external_ref_without_deps_fails() {
1283 let mut engine = Engine::new();
1284
1285 let result = add_lemma_code_blocking(
1286 &mut engine,
1287 r#"spec main_spec
1288fact external: spec @org/project/missing
1289rule value: external.quantity"#,
1290 "main.lemma",
1291 );
1292
1293 assert!(
1294 result.is_err(),
1295 "Should fail when @... dep is not in file map"
1296 );
1297 }
1298
1299 #[test]
1300 fn pre_resolved_deps_with_spec_and_type_refs() {
1301 let mut engine = Engine::new();
1302 let mut files = HashMap::new();
1303 files.insert(
1304 "main.lemma".to_string(),
1305 r#"spec registry_demo
1306type money from @lemma/std/finance
1307fact unit_price: 5 eur
1308fact helper: spec @org/example/helper
1309rule helper_value: helper.value
1310rule line_total: unit_price * 2
1311rule formatted: helper_value + 0"#
1312 .to_string(),
1313 );
1314 files.insert(
1315 "deps/helper.lemma".to_string(),
1316 "spec @org/example/helper\nfact value: 42".to_string(),
1317 );
1318 files.insert(
1319 "deps/finance.lemma".to_string(),
1320 "spec @lemma/std/finance\ntype money: scale\n -> unit eur 1.00\n -> decimals 2"
1321 .to_string(),
1322 );
1323 engine
1324 .add_lemma_files(files)
1325 .expect("should succeed with pre-resolved spec and type deps");
1326
1327 let now = DateTimeValue::now();
1328 let response = engine
1329 .evaluate("registry_demo", None, &now, vec![], HashMap::new())
1330 .expect("evaluate should succeed");
1331
1332 assert!(response.results.contains_key("helper_value"));
1333 assert!(response.results.contains_key("formatted"));
1334 }
1335
1336 #[test]
1337 fn add_lemma_files_returns_all_errors_not_just_first() {
1338 let mut engine = Engine::new();
1339
1340 let result = add_lemma_code_blocking(
1341 &mut engine,
1342 r#"spec demo
1343type money from nonexistent_type_source
1344fact helper: spec nonexistent_spec
1345fact price: 10
1346rule total: helper.value + price"#,
1347 "test.lemma",
1348 );
1349
1350 assert!(result.is_err(), "Should fail with multiple errors");
1351 let errs = result.unwrap_err();
1352 assert!(
1353 errs.len() >= 2,
1354 "expected at least 2 errors (type + spec ref), got {}",
1355 errs.len()
1356 );
1357 let error_message = errs
1358 .iter()
1359 .map(ToString::to_string)
1360 .collect::<Vec<_>>()
1361 .join("; ");
1362
1363 assert!(
1364 error_message.contains("money"),
1365 "Should mention type error about 'money'. Got:\n{}",
1366 error_message
1367 );
1368 assert!(
1369 error_message.contains("nonexistent_spec"),
1370 "Should mention spec reference error about 'nonexistent_spec'. Got:\n{}",
1371 error_message
1372 );
1373 }
1374
1375 #[test]
1381 fn planning_rejects_invalid_number_default() {
1382 let mut engine = Engine::new();
1383 let result = add_lemma_code_blocking(
1384 &mut engine,
1385 "spec t\nfact x: [number -> default \"10 $$\"]\nrule r: x",
1386 "t.lemma",
1387 );
1388 assert!(
1389 result.is_err(),
1390 "must reject non-numeric default on number type"
1391 );
1392 }
1393
1394 #[test]
1395 fn planning_rejects_text_literal_as_number_default() {
1396 let mut engine = Engine::new();
1401 let result = add_lemma_code_blocking(
1402 &mut engine,
1403 "spec t\nfact x: [number -> default \"10\"]\nrule r: x",
1404 "t.lemma",
1405 );
1406 assert!(
1407 result.is_err(),
1408 "must reject text literal \"10\" as default for number type"
1409 );
1410 }
1411
1412 #[test]
1413 fn planning_rejects_invalid_boolean_default() {
1414 let mut engine = Engine::new();
1415 let result = add_lemma_code_blocking(
1416 &mut engine,
1417 "spec t\nfact x: [boolean -> default \"maybe\"]\nrule r: x",
1418 "t.lemma",
1419 );
1420 assert!(
1421 result.is_err(),
1422 "must reject non-boolean default on boolean type"
1423 );
1424 }
1425
1426 #[test]
1427 fn planning_rejects_invalid_named_type_default() {
1428 let mut engine = Engine::new();
1430 let result = add_lemma_code_blocking(
1431 &mut engine,
1432 "spec t\ntype custom: number -> minimum 0\nfact x: [custom -> default \"abc\"]\nrule r: x",
1433 "t.lemma",
1434 );
1435 assert!(
1436 result.is_err(),
1437 "must reject non-numeric default on named number type"
1438 );
1439 }
1440
1441 #[test]
1442 fn planning_accepts_valid_number_default() {
1443 let mut engine = Engine::new();
1444 let result = add_lemma_code_blocking(
1445 &mut engine,
1446 "spec t\nfact x: [number -> default 10]\nrule r: x",
1447 "t.lemma",
1448 );
1449 assert!(result.is_ok(), "must accept valid number default");
1450 }
1451
1452 #[test]
1453 fn planning_accepts_valid_boolean_default() {
1454 let mut engine = Engine::new();
1455 let result = add_lemma_code_blocking(
1456 &mut engine,
1457 "spec t\nfact x: [boolean -> default true]\nrule r: x",
1458 "t.lemma",
1459 );
1460 assert!(result.is_ok(), "must accept valid boolean default");
1461 }
1462
1463 #[test]
1464 fn planning_accepts_valid_text_default() {
1465 let mut engine = Engine::new();
1466 let result = add_lemma_code_blocking(
1467 &mut engine,
1468 "spec t\nfact x: [text -> default \"hello\"]\nrule r: x",
1469 "t.lemma",
1470 );
1471 assert!(result.is_ok(), "must accept valid text default");
1472 }
1473}