1use std::cmp::Ordering;
18use std::sync::Arc;
19
20use chrono::NaiveDateTime;
21
22use crate::computation::datetime::{semantic_datetime_to_chrono, semantic_time_to_chrono_datetime};
23use crate::computation::rational::{NumericFailure, RationalInteger};
24use crate::computation::VetoType;
25use crate::parsing::ast::PrimitiveKind;
26use crate::planning::semantics::{ComparisonComputation, LemmaType, LiteralValue, ValueKind};
27
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub enum DispatchKey {
41 Text(Arc<str>),
42 Rational(RationalInteger),
45 Date(NaiveDateTime),
46 Time(NaiveDateTime),
47}
48
49#[derive(Clone, Copy, Debug)]
55pub(crate) enum DispatchProbe<'a> {
56 Text(&'a str),
57 Rational(&'a RationalInteger),
58 Date(NaiveDateTime),
59 Time(NaiveDateTime),
60}
61
62impl DispatchKey {
63 pub(crate) fn as_probe(&self) -> DispatchProbe<'_> {
64 match self {
65 DispatchKey::Text(text) => DispatchProbe::Text(text.as_ref()),
66 DispatchKey::Rational(magnitude) => DispatchProbe::Rational(magnitude),
67 DispatchKey::Date(moment) => DispatchProbe::Date(*moment),
68 DispatchKey::Time(moment) => DispatchProbe::Time(*moment),
69 }
70 }
71
72 pub(crate) fn try_compare(
77 &self,
78 other: &DispatchProbe<'_>,
79 ) -> Result<Ordering, NumericFailure> {
80 match (self, other) {
81 (DispatchKey::Text(left), DispatchProbe::Text(right)) => Ok((**left).cmp(right)),
82 (DispatchKey::Rational(left), DispatchProbe::Rational(right)) => left.try_cmp(right),
83 (DispatchKey::Date(left), DispatchProbe::Date(right)) => Ok(left.cmp(right)),
84 (DispatchKey::Time(left), DispatchProbe::Time(right)) => Ok(left.cmp(right)),
85 (left, right) => panic!(
86 "BUG: OrderedDispatch compared keys of different classes: {left:?} vs {right:?}"
87 ),
88 }
89 }
90}
91
92#[derive(Debug)]
94pub(crate) enum DispatchKeyBuildError {
95 CalendarFailure(String),
98 Unsupported,
100}
101
102pub(crate) fn dispatch_key_of_literal(
104 value: &ValueKind,
105) -> Result<DispatchKey, DispatchKeyBuildError> {
106 match value {
107 ValueKind::Text(text) => Ok(DispatchKey::Text(Arc::clone(text))),
108 ValueKind::Number(magnitude)
109 | ValueKind::Measure(magnitude)
110 | ValueKind::Ratio(magnitude) => Ok(DispatchKey::Rational(magnitude.clone())),
111 ValueKind::Date(date) => match semantic_datetime_to_chrono(date) {
112 Ok(moment) => Ok(DispatchKey::Date(moment.naive_utc())),
113 Err(message) => Err(DispatchKeyBuildError::CalendarFailure(message)),
114 },
115 ValueKind::Time(time) => match semantic_time_to_chrono_datetime(time) {
116 Ok(moment) => Ok(DispatchKey::Time(moment.naive_utc())),
117 Err(message) => Err(DispatchKeyBuildError::CalendarFailure(message)),
118 },
119 ValueKind::Boolean(_)
120 | ValueKind::Range(_, _)
121 | ValueKind::Spec(_)
122 | ValueKind::List(_)
123 | ValueKind::UnscaledMeasure { .. } => Err(DispatchKeyBuildError::Unsupported),
124 }
125}
126
127#[derive(Debug)]
129pub(crate) enum DispatchProbeOutcome<'a> {
130 Probe(DispatchProbe<'a>),
131 CalendarFailure(String),
134 Unsupported,
136}
137
138pub(crate) fn dispatch_probe_of(value: &ValueKind) -> DispatchProbeOutcome<'_> {
139 match value {
140 ValueKind::Text(text) => DispatchProbeOutcome::Probe(DispatchProbe::Text(text.as_ref())),
141 ValueKind::Number(magnitude)
142 | ValueKind::Measure(magnitude)
143 | ValueKind::Ratio(magnitude) => {
144 DispatchProbeOutcome::Probe(DispatchProbe::Rational(magnitude))
145 }
146 ValueKind::Date(date) => match semantic_datetime_to_chrono(date) {
147 Ok(moment) => DispatchProbeOutcome::Probe(DispatchProbe::Date(moment.naive_utc())),
148 Err(message) => DispatchProbeOutcome::CalendarFailure(message),
149 },
150 ValueKind::Time(time) => match semantic_time_to_chrono_datetime(time) {
151 Ok(moment) => DispatchProbeOutcome::Probe(DispatchProbe::Time(moment.naive_utc())),
152 Err(message) => DispatchProbeOutcome::CalendarFailure(message),
153 },
154 ValueKind::Boolean(_)
155 | ValueKind::Range(_, _)
156 | ValueKind::Spec(_)
157 | ValueKind::List(_)
158 | ValueKind::UnscaledMeasure { .. } => DispatchProbeOutcome::Unsupported,
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub(crate) enum DispatchClass {
170 Text,
173 Rational,
174 Date,
175 Time,
176}
177
178pub(crate) fn classify_dispatch(
181 scrutinee_type: &LemmaType,
182 keys: &[&LiteralValue],
183) -> Option<DispatchClass> {
184 if keys.is_empty() {
185 return None;
186 }
187 if scrutinee_type.is_undetermined() || scrutinee_type.is_range() || scrutinee_type.is_boolean()
188 {
189 return None;
190 }
191 let mut class: Option<DispatchClass> = None;
192 for key in keys {
193 let key_class = class_for_pair(scrutinee_type, key)?;
194 match class {
195 None => class = Some(key_class),
196 Some(established) if established == key_class => {}
197 Some(_) => return None,
198 }
199 }
200 class
201}
202
203fn class_for_pair(scrutinee_type: &LemmaType, key: &LiteralValue) -> Option<DispatchClass> {
206 if matches!(key.value, ValueKind::Range(_, _)) {
208 return None;
209 }
210 match &key.value {
211 ValueKind::Text(_) => scrutinee_type
212 .matches_primitive_kind(PrimitiveKind::Text)
213 .then_some(DispatchClass::Text),
214 ValueKind::Date(_) => scrutinee_type
215 .matches_primitive_kind(PrimitiveKind::Date)
216 .then_some(DispatchClass::Date),
217 ValueKind::Time(_) => scrutinee_type
218 .matches_primitive_kind(PrimitiveKind::Time)
219 .then_some(DispatchClass::Time),
220 ValueKind::Ratio(_) => scrutinee_type
221 .matches_primitive_kind(PrimitiveKind::Ratio)
222 .then_some(DispatchClass::Rational),
223 ValueKind::Number(_) => {
224 if scrutinee_type.matches_primitive_kind(PrimitiveKind::Number) {
225 return Some(DispatchClass::Rational);
226 }
227 (scrutinee_type.is_duration_like_measure() || scrutinee_type.is_calendar_like())
230 .then_some(DispatchClass::Rational)
231 }
232 ValueKind::Measure(_) => {
233 if scrutinee_type.matches_primitive_kind(PrimitiveKind::Number) {
235 return None;
236 }
237 if !scrutinee_type.is_measure() {
238 return None;
239 }
240 Some(DispatchClass::Rational)
243 }
244 ValueKind::Boolean(_)
245 | ValueKind::Range(_, _)
246 | ValueKind::Spec(_)
247 | ValueKind::List(_)
248 | ValueKind::UnscaledMeasure { .. } => None,
249 }
250}
251
252pub fn region_count(boundary_count: usize) -> usize {
254 2 * boundary_count + 1
255}
256
257pub(crate) fn is_exclusive_point_table<T: Copy + PartialEq>(
261 regions: &[T],
262 point_is_nested: impl Fn(T) -> bool,
263) -> bool {
264 assert!(
265 !regions.is_empty(),
266 "BUG: empty OrderedDispatch region table"
267 );
268 let default = regions[0];
269 for (index, &body) in regions.iter().enumerate() {
270 if index % 2 == 0 {
271 if body != default {
272 return false;
273 }
274 } else if point_is_nested(body) {
275 return false;
276 }
277 }
278 true
279}
280
281pub(crate) fn regions_matching(
287 operator: &ComparisonComputation,
288 boundary_index: usize,
289 boundary_count: usize,
290) -> [Option<(usize, usize)>; 2] {
291 let point = 2 * boundary_index + 1;
292 let last = 2 * boundary_count;
293 match operator {
294 ComparisonComputation::Is => [Some((point, point)), None],
295 ComparisonComputation::IsNot => [Some((0, point - 1)), Some((point + 1, last))],
296 ComparisonComputation::GreaterThan => [Some((point + 1, last)), None],
297 ComparisonComputation::GreaterThanOrEqual => [Some((point, last)), None],
298 ComparisonComputation::LessThan => [Some((0, point - 1)), None],
299 ComparisonComputation::LessThanOrEqual => [Some((0, point)), None],
300 }
301}
302
303pub(crate) fn region_for_value(
308 boundaries: &[DispatchKey],
309 value: &DispatchProbe<'_>,
310) -> Result<usize, NumericFailure> {
311 let mut low = 0usize;
312 let mut high = boundaries.len();
313 while low < high {
314 let middle = low + (high - low) / 2;
315 match boundaries[middle].try_compare(value)? {
316 Ordering::Less => low = middle + 1,
317 Ordering::Equal | Ordering::Greater => high = middle,
318 }
319 }
320 if low < boundaries.len() && boundaries[low].try_compare(value)? == Ordering::Equal {
321 Ok(2 * low + 1)
322 } else {
323 Ok(2 * low)
324 }
325}
326
327pub fn region_of_scrutinee(
332 boundaries: &[DispatchKey],
333 value: &ValueKind,
334) -> Result<usize, VetoType> {
335 let probe = match dispatch_probe_of(value) {
336 DispatchProbeOutcome::Probe(probe) => probe,
337 DispatchProbeOutcome::CalendarFailure(message) => {
338 return Err(VetoType::computation(message));
339 }
340 DispatchProbeOutcome::Unsupported => panic!(
341 "BUG: OrderedDispatch scrutinee evaluated to {value:?}, a kind the fold excludes"
342 ),
343 };
344 region_for_value(boundaries, &probe)
345 .map_err(|failure| VetoType::computation(failure.to_string()))
346}
347
348pub(crate) fn sorted_unique_boundaries(
353 mut keys: Vec<DispatchKey>,
354) -> Result<Vec<DispatchKey>, NumericFailure> {
355 let mut failure: Option<NumericFailure> = None;
356 keys.sort_by(|left, right| match left.try_compare(&right.as_probe()) {
357 Ok(ordering) => ordering,
358 Err(numeric) => {
359 failure.get_or_insert(numeric);
360 Ordering::Equal
361 }
362 });
363 match failure {
364 Some(numeric) => Err(numeric),
365 None => {
366 keys.dedup();
367 Ok(keys)
368 }
369 }
370}
371
372pub(crate) fn paint_dispatch_regions<T: Copy>(
381 boundary_count: usize,
382 default_body: T,
383 arms: &[(ComparisonComputation, usize, T)],
384) -> Vec<T> {
385 let n = region_count(boundary_count);
386 let mut regions = vec![default_body; n];
387 let mut next: Vec<usize> = (0..=n).collect();
390
391 fn find(next: &mut [usize], mut index: usize) -> usize {
392 let mut root = index;
393 while next[root] != root {
394 root = next[root];
395 }
396 while index != root {
397 let parent = next[index];
398 next[index] = root;
399 index = parent;
400 }
401 root
402 }
403
404 for (operator, boundary_index, body) in arms.iter().rev() {
405 for (start, end) in regions_matching(operator, *boundary_index, boundary_count)
406 .into_iter()
407 .flatten()
408 {
409 let mut index = find(&mut next, start);
410 while index <= end {
411 regions[index] = *body;
412 let after = find(&mut next, index + 1);
413 next[index] = after;
414 index = after;
415 }
416 }
417 }
418 regions
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use crate::computation::rational::rational_new;
425
426 fn rational_key(numerator: i64) -> DispatchKey {
427 DispatchKey::Rational(rational_new(numerator, 1))
428 }
429
430 fn text_key(text: &str) -> DispatchKey {
431 DispatchKey::Text(Arc::from(text))
432 }
433
434 fn predicate_holds(
436 scrutinee: &DispatchKey,
437 operator: &ComparisonComputation,
438 boundary: &DispatchKey,
439 ) -> bool {
440 let ordering = scrutinee
441 .try_compare(&boundary.as_probe())
442 .expect("test keys compare without failure");
443 match operator {
444 ComparisonComputation::Is => ordering == Ordering::Equal,
445 ComparisonComputation::IsNot => ordering != Ordering::Equal,
446 ComparisonComputation::GreaterThan => ordering == Ordering::Greater,
447 ComparisonComputation::GreaterThanOrEqual => ordering != Ordering::Less,
448 ComparisonComputation::LessThan => ordering == Ordering::Less,
449 ComparisonComputation::LessThanOrEqual => ordering != Ordering::Greater,
450 }
451 }
452
453 fn all_operators() -> Vec<ComparisonComputation> {
454 vec![
455 ComparisonComputation::Is,
456 ComparisonComputation::IsNot,
457 ComparisonComputation::GreaterThan,
458 ComparisonComputation::GreaterThanOrEqual,
459 ComparisonComputation::LessThan,
460 ComparisonComputation::LessThanOrEqual,
461 ]
462 }
463
464 #[test]
465 fn region_lookup_places_values_below_at_and_above_each_boundary() {
466 let boundaries = vec![rational_key(0), rational_key(5), rational_key(10)];
467 let cases = [
468 (-1, 0usize),
469 (0, 1),
470 (3, 2),
471 (5, 3),
472 (7, 4),
473 (10, 5),
474 (11, 6),
475 ];
476 for (value, expected) in cases {
477 assert_eq!(
478 region_for_value(&boundaries, &rational_key(value).as_probe()).expect("comparable"),
479 expected,
480 "value {value} must land in region {expected}"
481 );
482 }
483 }
484
485 #[test]
486 fn empty_boundary_list_has_one_region() {
487 assert_eq!(region_count(0), 1);
488 assert_eq!(
489 region_for_value(&[], &rational_key(42).as_probe()).expect("comparable"),
490 0
491 );
492 }
493
494 #[test]
497 fn claimed_regions_agree_with_the_predicate_at_every_region() {
498 let boundaries = vec![rational_key(0), rational_key(5), rational_key(10)];
499 let representatives = [-5, 0, 2, 5, 7, 10, 15];
501 assert_eq!(representatives.len(), region_count(boundaries.len()));
502
503 for operator in all_operators() {
504 for (boundary_index, boundary) in boundaries.iter().enumerate() {
505 let mut claimed = vec![false; region_count(boundaries.len())];
506 for span in regions_matching(&operator, boundary_index, boundaries.len())
507 .into_iter()
508 .flatten()
509 {
510 let (start, end) = span;
511 for region in claimed.iter_mut().take(end + 1).skip(start) {
512 *region = true;
513 }
514 }
515 for (region, value) in representatives.iter().enumerate() {
516 let scrutinee = rational_key(*value);
517 assert_eq!(
518 region_for_value(&boundaries, &scrutinee.as_probe()).expect("comparable"),
519 region,
520 "representative {value} must sit in region {region}"
521 );
522 assert_eq!(
523 claimed[region],
524 predicate_holds(&scrutinee, &operator, boundary),
525 "operator {operator} against boundary index {boundary_index} \
526 disagrees at region {region} (value {value})"
527 );
528 }
529 }
530 }
531 }
532
533 #[test]
534 fn greater_than_and_greater_or_equal_differ_only_at_the_point_region() {
535 let strictly = regions_matching(&ComparisonComputation::GreaterThan, 1, 3);
536 let inclusive = regions_matching(&ComparisonComputation::GreaterThanOrEqual, 1, 3);
537 assert_eq!(strictly, [Some((4, 6)), None]);
538 assert_eq!(inclusive, [Some((3, 6)), None]);
539 }
540
541 #[test]
542 fn less_than_and_less_or_equal_differ_only_at_the_point_region() {
543 let strictly = regions_matching(&ComparisonComputation::LessThan, 1, 3);
544 let inclusive = regions_matching(&ComparisonComputation::LessThanOrEqual, 1, 3);
545 assert_eq!(strictly, [Some((0, 2)), None]);
546 assert_eq!(inclusive, [Some((0, 3)), None]);
547 }
548
549 #[test]
550 fn is_not_claims_everything_except_its_point() {
551 assert_eq!(
552 regions_matching(&ComparisonComputation::IsNot, 1, 3),
553 [Some((0, 2)), Some((4, 6))]
554 );
555 }
556
557 #[test]
558 fn boundaries_are_sorted_and_deduplicated() {
559 let sorted = sorted_unique_boundaries(vec![
560 rational_key(5),
561 rational_key(-2),
562 rational_key(5),
563 rational_key(0),
564 ])
565 .expect("comparable");
566 assert_eq!(
567 sorted,
568 vec![rational_key(-2), rational_key(0), rational_key(5)]
569 );
570 }
571
572 #[test]
573 fn text_boundaries_sort_lexicographically() {
574 let sorted = sorted_unique_boundaries(vec![text_key("NL"), text_key("AD"), text_key("ZW")])
575 .expect("comparable");
576 assert_eq!(sorted, vec![text_key("AD"), text_key("NL"), text_key("ZW")]);
577 assert_eq!(
578 region_for_value(&sorted, &text_key("NL").as_probe()).expect("comparable"),
579 3
580 );
581 assert_eq!(
582 region_for_value(&sorted, &text_key("BE").as_probe()).expect("comparable"),
583 2
584 );
585 }
586
587 #[test]
588 fn exclusive_point_table_requires_interval_default_and_flat_points() {
589 assert!(is_exclusive_point_table(&[0, 1, 0, 2, 0], |_| false));
591 assert!(!is_exclusive_point_table(&[0, 1, 3, 2, 0], |_| false));
592 assert!(!is_exclusive_point_table(&[0, 1, 0, 2, 0], |body| body == 2));
593 }
594
595 #[test]
596 #[should_panic(expected = "BUG: OrderedDispatch compared keys of different classes")]
597 fn comparing_keys_of_different_classes_is_a_bug() {
598 text_key("AD")
599 .try_compare(&rational_key(1).as_probe())
600 .expect("BUG: comparison must panic before returning a failure");
601 }
602
603 #[test]
604 fn paint_last_wins_matches_forward_overwrite_on_overlapping_spans() {
605 let forward = {
608 let mut regions = vec![0u8; 5];
609 for (start, end) in regions_matching(&ComparisonComputation::GreaterThan, 0, 2)
610 .into_iter()
611 .flatten()
612 {
613 regions[start..=end].fill(1);
614 }
615 for (start, end) in regions_matching(&ComparisonComputation::GreaterThan, 1, 2)
616 .into_iter()
617 .flatten()
618 {
619 regions[start..=end].fill(2);
620 }
621 regions
622 };
623 let painted = paint_dispatch_regions(
624 2,
625 0u8,
626 &[
627 (ComparisonComputation::GreaterThan, 0, 1u8),
628 (ComparisonComputation::GreaterThan, 1, 2u8),
629 ],
630 );
631 assert_eq!(painted, forward);
632 assert_eq!(painted, vec![0, 0, 1, 1, 2]);
633 }
634
635 #[test]
636 fn paint_is_not_and_duplicate_keys_agree_with_forward_fill() {
637 let arms = [
638 (ComparisonComputation::IsNot, 0usize, 1u8),
639 (ComparisonComputation::Is, 0, 2u8),
640 (ComparisonComputation::Is, 1, 3u8),
641 ];
642 let boundary_count = 2;
643 let mut forward = vec![0u8; region_count(boundary_count)];
644 for (operator, boundary_index, body) in &arms {
645 for (start, end) in regions_matching(operator, *boundary_index, boundary_count)
646 .into_iter()
647 .flatten()
648 {
649 forward[start..=end].fill(*body);
650 }
651 }
652 assert_eq!(paint_dispatch_regions(boundary_count, 0u8, &arms), forward);
653 }
654
655 #[test]
656 fn probe_of_text_and_rational_borrows_without_clone() {
657 let text = ValueKind::Text(Arc::from("NL"));
658 let number = ValueKind::Number(rational_new(3, 1));
659 match dispatch_probe_of(&text) {
660 DispatchProbeOutcome::Probe(DispatchProbe::Text(borrowed)) => {
661 assert_eq!(borrowed, "NL");
662 }
663 other => panic!("expected text probe, got {other:?}"),
664 }
665 match dispatch_probe_of(&number) {
666 DispatchProbeOutcome::Probe(DispatchProbe::Rational(borrowed)) => {
667 assert_eq!(*borrowed, rational_new(3, 1));
668 }
669 other => panic!("expected rational probe, got {other:?}"),
670 }
671 }
672
673 #[test]
675 fn classify_dispatch_pairs_are_accepted_by_comparison_operation() {
676 use crate::computation::comparison::comparison_operation;
677 use crate::computation::operation_result::OperationResult;
678 use crate::planning::semantics::LiteralValue;
679
680 let cases: Vec<(LiteralValue, LiteralValue, ComparisonComputation)> = vec![
681 (
682 LiteralValue::number(rational_new(1, 1)),
683 LiteralValue::number(rational_new(2, 1)),
684 ComparisonComputation::LessThan,
685 ),
686 (
687 LiteralValue::number(rational_new(3, 1)),
688 LiteralValue::number(rational_new(3, 1)),
689 ComparisonComputation::Is,
690 ),
691 (
692 LiteralValue::text("NL"),
693 LiteralValue::text("NL"),
694 ComparisonComputation::Is,
695 ),
696 (
697 LiteralValue::text("NL"),
698 LiteralValue::text("BE"),
699 ComparisonComputation::IsNot,
700 ),
701 (
702 LiteralValue::ratio(rational_new(1, 2)),
703 LiteralValue::ratio(rational_new(3, 4)),
704 ComparisonComputation::LessThan,
705 ),
706 ];
707
708 for (scrutinee, key, operator) in &cases {
709 let scrutinee_type = match &scrutinee.value {
710 ValueKind::Number(_) => crate::planning::semantics::primitive_number_arc(),
711 ValueKind::Text(_) => crate::planning::semantics::primitive_text_arc(),
712 ValueKind::Ratio(_) => crate::planning::semantics::primitive_ratio_arc(),
713 other => panic!("unexpected scrutinee kind {other:?}"),
714 };
715 let class = classify_dispatch(scrutinee_type.as_ref(), &[key]).unwrap_or_else(|| {
716 panic!(
717 "classify_dispatch must accept {:?} vs {:?}",
718 scrutinee.value, key.value
719 )
720 });
721 if class == DispatchClass::Text {
722 assert!(
723 matches!(
724 operator,
725 ComparisonComputation::Is | ComparisonComputation::IsNot
726 ),
727 "text class only uses is / is not"
728 );
729 }
730 match comparison_operation(
731 &scrutinee.value,
732 scrutinee_type,
733 operator,
734 &key.value,
735 scrutinee_type,
736 ) {
737 OperationResult::Value(result) => match &result.value {
738 ValueKind::Boolean(_) => {}
739 other => panic!("expected boolean comparison result, got {other:?}"),
740 },
741 OperationResult::Veto(veto) => {
742 panic!("comparison_operation vetoed a classify-accepted pair: {veto:?}");
743 }
744 }
745 }
746
747 let boolean = LiteralValue::from_bool(true);
748 assert!(
749 classify_dispatch(
750 crate::planning::semantics::primitive_boolean_arc().as_ref(),
751 &[&boolean],
752 )
753 .is_none(),
754 "boolean scrutinees are not a dispatch class"
755 );
756 }
757}