1use crate::computation::operation_result::{OperationResult, VetoType};
4use crate::computation::rational::{
5 rational_operation, rational_operation_with_fallback, NumericFailure, NumericOperation,
6 RationalInteger,
7};
8use crate::planning::semantics::{
9 ArithmeticComputation, BoundValueKind, LemmaType, LiteralValue, TypeFamily, TypedLiteral,
10 ValueKind,
11};
12use indexmap::IndexMap;
13use serde::Serialize;
14use std::sync::Arc;
15
16#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
18pub struct SignatureFactor {
19 pub name: String,
20 pub family: TypeFamily,
21 pub exponent: i32,
22}
23
24pub type SignatureIndex = IndexMap<Vec<SignatureFactor>, (String, Arc<LemmaType>)>;
26
27pub(crate) fn find_by_names<'a>(
30 index: &'a SignatureIndex,
31 names: &[(String, i32)],
32) -> Option<&'a (String, Arc<LemmaType>)> {
33 let mut matched = None;
34 for (factors, value) in index.iter() {
35 if factors.len() != names.len() {
36 continue;
37 }
38 let mut left: Vec<(&str, i32)> = factors
39 .iter()
40 .map(|factor| (factor.name.as_str(), factor.exponent))
41 .collect();
42 let mut right: Vec<(&str, i32)> = names
43 .iter()
44 .map(|(name, exponent)| (name.as_str(), *exponent))
45 .collect();
46 left.sort();
47 right.sort();
48 if left != right {
49 continue;
50 }
51 if matched.is_some() {
52 return None;
53 }
54 matched = Some(value);
55 }
56 matched
57}
58
59struct CalendarRangeShiftIndexes<'a> {
60 signature_index: &'a SignatureIndex,
61 endpoint_type: &'a Arc<LemmaType>,
62 calendar_type: &'a Arc<LemmaType>,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
75enum SignatureFamily {
76 Calendar,
77 Measure(crate::planning::semantics::TypeFamily),
78}
79
80fn measure_family_key(
81 unit_name: &str,
82 unit_index: &crate::planning::unit_index::UnitIndex,
83 typed_owners: &[&LemmaType],
84) -> Option<SignatureFamily> {
85 use crate::planning::semantics::calendar_unit_factor;
86 if calendar_unit_factor(unit_name).is_some() {
87 return Some(SignatureFamily::Calendar);
88 }
89 unit_index
90 .owning_type_for_signature_factor(unit_name, typed_owners)
91 .and_then(|t| t.measure_family().cloned())
92 .map(SignatureFamily::Measure)
93}
94
95fn canonical_unit_in_family(
97 family: &SignatureFamily,
98 unit_index: &crate::planning::unit_index::UnitIndex,
99) -> Option<String> {
100 use crate::planning::semantics::TypeSpecification;
101 let SignatureFamily::Measure(family) = family else {
102 return Some("month".to_string());
103 };
104 let lemma_type = unit_index.family_root(family)?;
105 match &lemma_type.specifications {
106 TypeSpecification::Measure { units, .. } => units
107 .iter()
108 .find(|unit| unit.is_canonical_factor())
109 .map(|canonical| canonical.name.clone()),
110 _ => None,
111 }
112}
113
114pub(crate) fn expand_signature_to_base_units(
115 raw: &[(String, i32)],
116 unit_index: &crate::planning::unit_index::UnitIndex,
117 typed_owners: &[&LemmaType],
118) -> Vec<(String, i32)> {
119 use crate::planning::semantics::canonicalize_signature;
120 use crate::planning::semantics::TypeSpecification;
121 use std::collections::BTreeMap;
122 let mut expanded: Vec<(String, i32)> = Vec::new();
123 for (unit_name, exp) in raw {
124 if let Some(owning_type) =
125 unit_index.owning_type_for_signature_factor(unit_name, typed_owners)
126 {
127 if let TypeSpecification::Measure { units, .. } = &owning_type.specifications {
128 if let Ok(unit) = units.get(unit_name) {
129 if !unit.derived_measure_factors.is_empty() {
130 for (base_name, base_exp) in &unit.derived_measure_factors {
131 expanded.push((
132 base_name.clone(),
133 base_exp.checked_mul(*exp).expect(
134 "BUG: planning bounds declared and signature unit exponents",
135 ),
136 ));
137 }
138 continue;
139 }
140 }
141 }
142 }
143 expanded.push((unit_name.clone(), *exp));
144 }
145 expanded = canonicalize_signature(&expanded);
146
147 let mut by_family: BTreeMap<SignatureFamily, Vec<(String, i32)>> = BTreeMap::new();
148 let mut ungrouped: Vec<(String, i32)> = Vec::new();
149 for (unit_name, exp) in expanded {
150 if let Some(family) = measure_family_key(&unit_name, unit_index, typed_owners) {
151 by_family.entry(family).or_default().push((unit_name, exp));
152 } else {
153 ungrouped.push((unit_name, exp));
154 }
155 }
156
157 let mut normalized = ungrouped;
158 for (family, entries) in by_family {
159 let distinct: std::collections::BTreeSet<&str> =
160 entries.iter().map(|(name, _)| name.as_str()).collect();
161 let canonical = (distinct.len() > 1)
162 .then(|| canonical_unit_in_family(&family, unit_index))
163 .flatten();
164 match canonical {
165 Some(canonical) => {
166 let net_exp: i32 = entries
167 .iter()
168 .try_fold(0i32, |sum, (_, exp)| sum.checked_add(*exp))
169 .expect("BUG: planning bounds family unit exponents");
170 if net_exp != 0 {
171 normalized.push((canonical, net_exp));
172 }
173 }
174 None => normalized.extend(entries),
175 }
176 }
177 canonicalize_signature(&normalized)
178}
179
180fn number_op_on_stored_rationals(
181 left: &RationalInteger,
182 operator: &ArithmeticComputation,
183 right: &RationalInteger,
184) -> OperationResult {
185 match number_arithmetic(left, operator, right) {
186 Ok(rational) => OperationResult::from_literal(LiteralValue::number(rational)),
187 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
188 }
189}
190
191fn number_ratio_arithmetic(
192 left: &RationalInteger,
193 op: &ArithmeticComputation,
194 right: &RationalInteger,
195) -> Result<RationalInteger, NumberArithmeticFailure> {
196 match op {
197 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
198 rational_operation(left, numeric_operation_from_arithmetic(op), right)
199 .map_err(map_numeric_failure)
200 }
201 ArithmeticComputation::Power => {
202 rational_operation_with_fallback(left, NumericOperation::Power, right)
203 .map_err(map_numeric_failure)
204 }
205 ArithmeticComputation::Modulo => rational_operation(left, NumericOperation::Modulo, right)
206 .map_err(|f| {
207 if matches!(f, NumericFailure::DivisionByZero) {
208 NumberArithmeticFailure::ModuloByZero
209 } else {
210 map_numeric_failure(f)
211 }
212 }),
213 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
214 unreachable!("BUG: number/ratio add-subtract rejected during planning")
215 }
216 }
217}
218
219fn calendar_from_months_arithmetic(
220 left: &RationalInteger,
221 operator: &ArithmeticComputation,
222 right: &RationalInteger,
223) -> OperationResult {
224 let result_months = match operator {
225 ArithmeticComputation::Add => {
226 match rational_operation(left, NumericOperation::Add, right) {
227 Ok(r) => r,
228 Err(failure) => {
229 return OperationResult::Veto(VetoType::computation(
230 map_numeric_failure(failure).message(),
231 ))
232 }
233 }
234 }
235 ArithmeticComputation::Subtract => {
236 match rational_operation(left, NumericOperation::Subtract, right) {
237 Ok(r) => r,
238 Err(failure) => {
239 return OperationResult::Veto(VetoType::computation(
240 map_numeric_failure(failure).message(),
241 ))
242 }
243 }
244 }
245 _ => unreachable!(
246 "BUG: calendar_from_months_arithmetic called with {:?}; planning rejects this",
247 operator
248 ),
249 };
250
251 OperationResult::from_literal(LiteralValue::measure(result_months))
252}
253
254pub fn arithmetic_operation(
260 left: &crate::planning::semantics::BoundValueKind,
261 left_type: &Arc<LemmaType>,
262 op: &ArithmeticComputation,
263 right: &crate::planning::semantics::BoundValueKind,
264 right_type: &Arc<LemmaType>,
265 signature_index: &SignatureIndex,
266) -> OperationResult {
267 match (&left.value, &right.value) {
268 (ValueKind::Range(range_left, range_right), ValueKind::Measure(value))
269 if left_type.is_calendar_like_range()
270 && right_type.is_calendar_like()
271 && matches!(
272 op,
273 ArithmeticComputation::Add | ArithmeticComputation::Subtract
274 ) =>
275 {
276 let endpoint_type = left_type
277 .specifications
278 .element_from_range()
279 .map(|element| Arc::new(LemmaType::primitive(element)))
280 .unwrap_or_else(|| Arc::clone(left_type));
281 shift_calendar_range_right_endpoint(
282 range_left.as_ref(),
283 range_right.as_ref(),
284 value,
285 matches!(op, ArithmeticComputation::Add),
286 &CalendarRangeShiftIndexes {
287 signature_index,
288 endpoint_type: &endpoint_type,
289 calendar_type: right_type,
290 },
291 )
292 }
293
294 (ValueKind::Measure(value), ValueKind::Range(range_left, range_right))
295 if right_type.is_calendar_like_range()
296 && left_type.is_calendar_like()
297 && matches!(
298 op,
299 ArithmeticComputation::Add | ArithmeticComputation::Subtract
300 ) =>
301 {
302 let endpoint_type = right_type
303 .specifications
304 .element_from_range()
305 .map(|element| Arc::new(LemmaType::primitive(element)))
306 .unwrap_or_else(|| Arc::clone(right_type));
307 shift_calendar_range_right_endpoint(
308 range_left.as_ref(),
309 range_right.as_ref(),
310 value,
311 matches!(op, ArithmeticComputation::Add),
312 &CalendarRangeShiftIndexes {
313 signature_index,
314 endpoint_type: &endpoint_type,
315 calendar_type: left_type,
316 },
317 )
318 }
319
320 (ValueKind::Range(range_left, range_right), ValueKind::Measure(value))
321 if left_type.is_date_range()
322 && right_type.is_calendar_like()
323 && matches!(
324 op,
325 ArithmeticComputation::Add | ArithmeticComputation::Subtract
326 ) =>
327 {
328 shift_date_range_right_endpoint(
329 range_left.as_ref(),
330 range_right.as_ref(),
331 value,
332 matches!(op, ArithmeticComputation::Add),
333 Arc::clone(right_type),
334 )
335 }
336
337 (ValueKind::Measure(value), ValueKind::Range(range_left, range_right))
338 if right_type.is_date_range()
339 && left_type.is_calendar_like()
340 && matches!(
341 op,
342 ArithmeticComputation::Add | ArithmeticComputation::Subtract
343 ) =>
344 {
345 shift_date_range_right_endpoint(
346 range_left.as_ref(),
347 range_right.as_ref(),
348 value,
349 matches!(op, ArithmeticComputation::Add),
350 Arc::clone(left_type),
351 )
352 }
353
354 (
355 ValueKind::Range(left_range_left, left_range_right),
356 ValueKind::Range(right_range_left, right_range_right),
357 ) if matches!(
358 op,
359 ArithmeticComputation::Add | ArithmeticComputation::Subtract
360 ) =>
361 {
362 let left_measure =
363 super::range::compute_span(left_range_left.as_ref(), left_range_right.as_ref());
364 let right_measure =
365 super::range::compute_span(right_range_left.as_ref(), right_range_right.as_ref());
366 let left_span_ty = super::range::span_result_type(left_type);
367 let right_span_ty = super::range::span_result_type(right_type);
368 operate_on_operation_results(
369 left_measure,
370 &left_span_ty,
371 op,
372 right_measure,
373 &right_span_ty,
374 signature_index,
375 )
376 }
377
378 (ValueKind::Range(range_left, range_right), _)
379 if matches!(
380 op,
381 ArithmeticComputation::Add | ArithmeticComputation::Subtract
382 ) =>
383 {
384 let measure = super::range::compute_span(range_left.as_ref(), range_right.as_ref());
385 let span_ty = super::range::span_result_type(left_type);
386 operate_with_left_result(measure, &span_ty, op, right, right_type, signature_index)
387 }
388
389 (_, ValueKind::Range(range_left, range_right))
390 if matches!(
391 op,
392 ArithmeticComputation::Add | ArithmeticComputation::Subtract
393 ) =>
394 {
395 let measure = super::range::compute_span(range_left.as_ref(), range_right.as_ref());
396 let span_ty = super::range::span_result_type(right_type);
397 operate_with_right_result(left, left_type, op, measure, &span_ty, signature_index)
398 }
399
400 _ => scalar_arithmetic(left, left_type, op, right, right_type, signature_index),
401 }
402}
403
404pub(crate) fn scalar_arithmetic(
407 left: &crate::planning::semantics::BoundValueKind,
408 left_type: &Arc<LemmaType>,
409 op: &ArithmeticComputation,
410 right: &crate::planning::semantics::BoundValueKind,
411 right_type: &Arc<LemmaType>,
412 signature_index: &SignatureIndex,
413) -> OperationResult {
414 match (&left.value, &right.value) {
415 (ValueKind::Number(l), ValueKind::Number(r)) => number_op_on_stored_rationals(l, op, r),
416
417 (ValueKind::Date(_), _) | (_, ValueKind::Date(_)) => super::datetime::datetime_arithmetic(
418 &left.to_literal(),
419 left_type,
420 op,
421 &right.to_literal(),
422 right_type,
423 ),
424
425 (ValueKind::Time(_), _) | (_, ValueKind::Time(_)) => super::datetime::time_arithmetic(
426 &left.to_literal(),
427 left_type,
428 op,
429 &right.to_literal(),
430 right_type,
431 ),
432
433 (ValueKind::Number(n), ValueKind::Ratio(r)) => {
435 if matches!(op, ArithmeticComputation::Modulo) {
436 unreachable!("BUG: number modulo ratio is rejected during planning");
437 }
438 match number_ratio_arithmetic(n, op, r) {
439 Ok(rational) => OperationResult::from_literal(LiteralValue::number(rational)),
440 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
441 }
442 }
443 (ValueKind::Ratio(r), ValueKind::Number(n)) => match op {
445 ArithmeticComputation::Multiply => match number_ratio_arithmetic(r, op, n) {
446 Ok(rational) => OperationResult::from_literal(LiteralValue::number(rational)),
447 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
448 },
449 ArithmeticComputation::Modulo => {
450 unreachable!("BUG: ratio modulo number is rejected during planning");
451 }
452 ArithmeticComputation::Divide | ArithmeticComputation::Power => {
453 match number_ratio_arithmetic(r, op, n) {
454 Ok(rational) => OperationResult::from_bound(BoundValueKind {
455 value: ValueKind::Ratio(rational),
456 unit: left.unit.clone(),
457 }),
458 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
459 }
460 }
461 _ => unreachable!(
462 "BUG: ratio {:?} number; add/subtract rejected during planning",
463 op
464 ),
465 },
466
467 (ValueKind::Ratio(l), ValueKind::Ratio(r)) => {
469 if matches!(op, ArithmeticComputation::Modulo) {
470 unreachable!("BUG: ratio modulo ratio is rejected during planning");
471 }
472 match number_arithmetic(l, op, r) {
473 Ok(rational) => OperationResult::from_bound(BoundValueKind {
474 value: ValueKind::Ratio(rational),
475 unit: BoundValueKind::agree_or_left_binding(
476 left.unit.as_ref(),
477 right.unit.as_ref(),
478 ),
479 }),
480 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
481 }
482 }
483 (ValueKind::Measure(l_val), ValueKind::Measure(r_val)) => {
485 if left_type.is_calendar_like() != right_type.is_calendar_like() {
486 let is_measure_left = left_type.is_measure() && !left_type.is_calendar_like();
487 let is_multiply = matches!(op, ArithmeticComputation::Multiply);
488 let is_divide = matches!(op, ArithmeticComputation::Divide);
489 if !is_multiply && !is_divide {
490 unreachable!("BUG: measure {:?} calendar is rejected during planning", op);
491 }
492 let (l_val_ref, r_val_ref) = if is_measure_left {
493 (l_val, r_val)
494 } else {
495 (r_val, l_val)
496 };
497 if is_divide && crate::computation::rational::rational_is_zero(r_val_ref) {
498 return OperationResult::Veto(VetoType::computation(
499 "Division by zero is not allowed.",
500 ));
501 }
502 let raw_result = match rational_operation(
503 l_val_ref,
504 if is_multiply {
505 NumericOperation::Multiply
506 } else {
507 NumericOperation::Divide
508 },
509 r_val_ref,
510 ) {
511 Ok(p) => p,
512 Err(failure) => {
513 return OperationResult::Veto(VetoType::computation(
514 map_numeric_failure(failure).message(),
515 ))
516 }
517 };
518 let q_decomp = if is_measure_left {
519 left_type.measure_type_decomposition()
520 } else {
521 right_type.measure_type_decomposition()
522 }
523 .expect("BUG: decomposition must be resolved after planning");
524 let c_decomp = crate::planning::semantics::calendar_decomposition();
525 let combined = crate::planning::semantics::combine_decompositions(
526 q_decomp,
527 &c_decomp,
528 is_multiply,
529 )
530 .expect("BUG: planning bounds combined unit exponents");
531 if combined.is_empty() {
532 return OperationResult::from_literal(LiteralValue::number(raw_result));
533 }
534 return OperationResult::from_bound(BoundValueKind::unbound(ValueKind::Measure(
535 raw_result,
536 )));
537 }
538 if left_type.is_calendar_like() && right_type.is_calendar_like() {
539 return match op {
540 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
541 calendar_from_months_arithmetic(l_val, op, r_val)
542 }
543 ArithmeticComputation::Divide => {
544 number_op_on_stored_rationals(l_val, &ArithmeticComputation::Divide, r_val)
545 }
546 _ => unreachable!(
547 "BUG: calendar * calendar with op {:?}; planning should have rejected this",
548 op
549 ),
550 };
551 }
552 if left_type.is_calendar_like() && !right_type.is_calendar_like() {
553 if let ValueKind::Number(n) = &right.value {
554 return match number_arithmetic(l_val, op, n) {
555 Ok(rational) => {
556 OperationResult::from_literal(LiteralValue::measure(rational))
557 }
558 Err(failure) => {
559 OperationResult::Veto(VetoType::computation(failure.message()))
560 }
561 };
562 }
563 if let ValueKind::Ratio(r) = &right.value {
564 return match number_ratio_arithmetic(l_val, op, r) {
565 Ok(rational) => {
566 OperationResult::from_literal(LiteralValue::measure(rational))
567 }
568 Err(failure) => {
569 OperationResult::Veto(VetoType::computation(failure.message()))
570 }
571 };
572 }
573 }
574 if right_type.is_calendar_like() && !left_type.is_calendar_like() {
575 if let (ValueKind::Number(n), ArithmeticComputation::Multiply) = (&left.value, op) {
576 return match number_arithmetic(n, op, r_val) {
577 Ok(rational) => {
578 OperationResult::from_literal(LiteralValue::measure(rational))
579 }
580 Err(failure) => {
581 OperationResult::Veto(VetoType::computation(failure.message()))
582 }
583 };
584 }
585 if let ValueKind::Number(n) = &left.value {
586 return match number_arithmetic(n, op, r_val) {
587 Ok(rational) => {
588 OperationResult::from_literal(LiteralValue::number(rational))
589 }
590 Err(failure) => {
591 OperationResult::Veto(VetoType::computation(failure.message()))
592 }
593 };
594 }
595 if let ValueKind::Ratio(r) = &left.value {
596 return match number_ratio_arithmetic(r_val, op, r) {
597 Ok(rational) => {
598 OperationResult::from_literal(LiteralValue::measure(rational))
599 }
600 Err(failure) => {
601 OperationResult::Veto(VetoType::computation(failure.message()))
602 }
603 };
604 }
605 }
606 match op {
607 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
608 let same_family = left_type.same_measure_family(right_type);
609 let anonymous_compatible =
610 left_type.compatible_with_anonymous_measure(right_type);
611 let identical_measure = left_type.as_ref() == right_type.as_ref()
612 || (left_type
613 .specifications
614 .equal_ignoring_unit(&right_type.specifications)
615 && left_type.name == right_type.name
616 && left_type.extends == right_type.extends);
617 if !identical_measure && !same_family && !anonymous_compatible {
618 unreachable!(
619 "BUG: measure add/subtract with incompatible types ({} vs {}); should be rejected during planning",
620 left_type.name(),
621 right_type.name()
622 );
623 }
624 match measure_add_subtract(l_val, op, r_val) {
625 Ok(rational) => OperationResult::from_bound(BoundValueKind {
626 value: ValueKind::Measure(rational),
627 unit: BoundValueKind::agree_or_left_binding(
628 left.unit.as_ref(),
629 right.unit.as_ref(),
630 ),
631 }),
632 Err(failure) => {
633 OperationResult::Veto(VetoType::computation(failure.message()))
634 }
635 }
636 }
637 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
638 if matches!(op, ArithmeticComputation::Divide)
639 && crate::computation::rational::rational_is_zero(r_val)
640 {
641 return OperationResult::Veto(VetoType::computation(
642 "Division by zero is not allowed.",
643 ));
644 }
645 let numeric_op = match op {
646 ArithmeticComputation::Multiply => NumericOperation::Multiply,
647 ArithmeticComputation::Divide => NumericOperation::Divide,
648 _ => unreachable!("BUG: matched multiply/divide arm with other op"),
649 };
650 let raw_result = match rational_operation(l_val, numeric_op, r_val) {
651 Ok(p) => p,
652 Err(failure) => {
653 return OperationResult::Veto(VetoType::computation(
654 map_numeric_failure(failure).message(),
655 ))
656 }
657 };
658 let l_decomp = left_type
659 .measure_type_decomposition()
660 .expect("BUG: decomposition must be resolved after planning");
661 let r_decomp = right_type
662 .measure_type_decomposition()
663 .expect("BUG: decomposition must be resolved after planning");
664 let combined_decomposition =
665 crate::planning::semantics::combine_decompositions(
666 l_decomp,
667 r_decomp,
668 matches!(op, ArithmeticComputation::Multiply),
669 )
670 .expect("BUG: planning bounds combined unit exponents");
671 if combined_decomposition.is_empty() {
672 OperationResult::from_literal(LiteralValue::number(raw_result))
673 } else {
674 OperationResult::from_bound(BoundValueKind::unbound(ValueKind::Measure(
675 raw_result,
676 )))
677 }
678 }
679 _ => unreachable!("BUG: measure {:?} measure is rejected during planning", op),
680 }
681 }
682 (ValueKind::Measure(q_val), ValueKind::Ratio(r)) => {
684 match measure_ratio_arithmetic(q_val.clone(), op, r.clone()) {
685 Ok(rational) => OperationResult::from_bound(BoundValueKind {
686 value: ValueKind::Measure(rational),
687 unit: left.unit.clone(),
688 }),
689 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
690 }
691 }
692 (ValueKind::Ratio(r), ValueKind::Measure(q_val)) => match op {
694 ArithmeticComputation::Multiply => {
695 match measure_ratio_arithmetic(q_val.clone(), op, r.clone()) {
696 Ok(rational) => OperationResult::from_bound(BoundValueKind {
697 value: ValueKind::Measure(rational),
698 unit: right.unit.clone(),
699 }),
700 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
701 }
702 }
703 _ => unreachable!(
704 "BUG: ratio {:?} measure; planning should have rejected this",
705 op
706 ),
707 },
708
709 (ValueKind::Measure(measure_val), ValueKind::Number(n)) => match op {
713 ArithmeticComputation::Multiply
714 | ArithmeticComputation::Divide
715 | ArithmeticComputation::Add
716 | ArithmeticComputation::Subtract => match number_arithmetic(measure_val, op, n) {
717 Ok(rational) => OperationResult::from_bound(BoundValueKind {
718 value: ValueKind::Measure(rational),
719 unit: left.unit.clone(),
720 }),
721 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
722 },
723 ArithmeticComputation::Power => {
724 if !n.is_integer() {
725 unreachable!(
726 "BUG: measure power with a non-integer exponent is rejected during planning"
727 );
728 }
729 match number_arithmetic(measure_val, op, n) {
730 Ok(rational) if n.numer_is_zero() => {
731 OperationResult::from_literal(LiteralValue::number(rational))
732 }
733 Ok(rational) => OperationResult::from_bound(BoundValueKind::unbound(
734 ValueKind::Measure(rational),
735 )),
736 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
737 }
738 }
739 ArithmeticComputation::Modulo => {
740 unreachable!("BUG: measure modulo number is rejected during planning")
741 }
742 },
743 (ValueKind::Number(n), ValueKind::Measure(measure_val)) => match op {
746 ArithmeticComputation::Multiply => match number_arithmetic(n, op, measure_val) {
747 Ok(rational) => OperationResult::from_bound(BoundValueKind {
748 value: ValueKind::Measure(rational),
749 unit: right.unit.clone(),
750 }),
751 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
752 },
753 ArithmeticComputation::Divide => {
754 let measure_decomp = right_type
755 .measure_type_decomposition()
756 .expect("BUG: decomposition must be resolved after planning");
757 if measure_decomp.is_empty() {
758 match number_arithmetic(n, op, measure_val) {
759 Ok(rational) => {
760 OperationResult::from_literal(LiteralValue::number(rational))
761 }
762 Err(failure) => {
763 OperationResult::Veto(VetoType::computation(failure.message()))
764 }
765 }
766 } else {
767 let negated_signature = crate::planning::semantics::negate_signature(
768 &right_type.measure_runtime_signature(),
769 );
770 match number_arithmetic(n, op, measure_val) {
771 Ok(rational) => {
772 if let Some((unit_name, owning_type)) =
773 find_by_names(signature_index, &negated_signature)
774 {
775 let target_factor =
776 owning_type.measure_unit_factor(unit_name).clone();
777 match crate::computation::rational::checked_div(
778 &rational,
779 &target_factor,
780 ) {
781 Ok(magnitude) => OperationResult::from_bound(
782 BoundValueKind::unbound(ValueKind::Measure(magnitude)),
783 ),
784 Err(failure) => OperationResult::Veto(VetoType::computation(
785 failure.to_string(),
786 )),
787 }
788 } else {
789 OperationResult::from_bound(BoundValueKind::unbound(
790 ValueKind::Measure(rational),
791 ))
792 }
793 }
794 Err(failure) => {
795 OperationResult::Veto(VetoType::computation(failure.message()))
796 }
797 }
798 }
799 }
800 ArithmeticComputation::Modulo
801 | ArithmeticComputation::Add
802 | ArithmeticComputation::Subtract
803 | ArithmeticComputation::Power => {
804 unreachable!(
805 "BUG: Number {:?} Measure should be rejected during planning",
806 op
807 )
808 }
809 },
810 _ => unreachable!(
811 "BUG: arithmetic {:?} for {:?} and {:?}; planning should have rejected this",
812 op,
813 type_name(left_type),
814 type_name(right_type)
815 ),
816 }
817}
818
819#[derive(Debug, Clone, PartialEq, Eq)]
820pub enum NumberArithmeticFailure {
821 DivisionByZero,
822 ModuloByZero,
823 Computation(String),
824}
825
826impl NumberArithmeticFailure {
827 pub fn message(self) -> String {
828 match self {
829 Self::DivisionByZero => "Division by zero is not allowed.".to_string(),
830 Self::ModuloByZero => "Modulo by zero is not allowed.".to_string(),
831 Self::Computation(message) => message,
832 }
833 }
834}
835
836fn numeric_operation_from_arithmetic(operator: &ArithmeticComputation) -> NumericOperation {
837 match operator {
838 ArithmeticComputation::Add => NumericOperation::Add,
839 ArithmeticComputation::Subtract => NumericOperation::Subtract,
840 ArithmeticComputation::Multiply => NumericOperation::Multiply,
841 ArithmeticComputation::Divide => NumericOperation::Divide,
842 ArithmeticComputation::Modulo => NumericOperation::Modulo,
843 ArithmeticComputation::Power => NumericOperation::Power,
844 }
845}
846
847fn map_numeric_failure(failure: NumericFailure) -> NumberArithmeticFailure {
848 match failure {
849 NumericFailure::DivisionByZero => NumberArithmeticFailure::DivisionByZero,
850 other => NumberArithmeticFailure::Computation(other.to_string()),
851 }
852}
853
854fn map_numeric_failure_modulo(failure: NumericFailure) -> NumberArithmeticFailure {
855 match failure {
856 NumericFailure::DivisionByZero => NumberArithmeticFailure::ModuloByZero,
857 other => map_numeric_failure(other),
858 }
859}
860
861fn measure_scale_magnitude_by_rational(
862 magnitude: RationalInteger,
863 factor: &RationalInteger,
864) -> Result<RationalInteger, NumberArithmeticFailure> {
865 rational_operation(&magnitude, NumericOperation::Multiply, factor).map_err(map_numeric_failure)
866}
867
868fn measure_ratio_arithmetic(
869 measure_value: RationalInteger,
870 operator: &ArithmeticComputation,
871 ratio_value: RationalInteger,
872) -> Result<RationalInteger, NumberArithmeticFailure> {
873 match operator {
874 ArithmeticComputation::Multiply => {
875 measure_scale_magnitude_by_rational(measure_value, &ratio_value)
876 }
877 ArithmeticComputation::Divide => {
878 rational_operation(&measure_value, NumericOperation::Divide, &ratio_value)
879 .map_err(map_numeric_failure)
880 }
881 _ => unreachable!(
882 "BUG: measure {:?} ratio is rejected during planning",
883 operator
884 ),
885 }
886}
887
888fn measure_add_subtract(
889 left_value: &RationalInteger,
890 operator: &ArithmeticComputation,
891 right_value: &RationalInteger,
892) -> Result<RationalInteger, NumberArithmeticFailure> {
893 rational_operation(
894 left_value,
895 numeric_operation_from_arithmetic(operator),
896 right_value,
897 )
898 .map_err(map_numeric_failure)
899}
900
901pub fn number_arithmetic(
902 left: &RationalInteger,
903 operator: &ArithmeticComputation,
904 right: &RationalInteger,
905) -> Result<RationalInteger, NumberArithmeticFailure> {
906 rational_operation_with_fallback(left, numeric_operation_from_arithmetic(operator), right)
907 .map_err(|failure| {
908 if matches!(operator, ArithmeticComputation::Modulo) {
909 map_numeric_failure_modulo(failure)
910 } else {
911 map_numeric_failure(failure)
912 }
913 })
914}
915
916fn operate_on_operation_results(
917 left_result: OperationResult,
918 left_type: &Arc<LemmaType>,
919 op: &ArithmeticComputation,
920 right_result: OperationResult,
921 right_type: &Arc<LemmaType>,
922 signature_index: &SignatureIndex,
923) -> OperationResult {
924 let left_value = match left_result {
925 OperationResult::Value(value) => value,
926 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
927 };
928 let right_value = match right_result {
929 OperationResult::Value(value) => value,
930 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
931 };
932 scalar_arithmetic(
933 &left_value,
934 left_type,
935 op,
936 &right_value,
937 right_type,
938 signature_index,
939 )
940}
941
942pub(crate) fn divisive_operation_results(
946 left: &OperationResult,
947 left_type: &Arc<LemmaType>,
948 op: &ArithmeticComputation,
949 right: &OperationResult,
950 right_type: &Arc<LemmaType>,
951 signature_index: &SignatureIndex,
952) -> OperationResult {
953 assert!(
954 matches!(
955 op,
956 ArithmeticComputation::Divide | ArithmeticComputation::Modulo
957 ),
958 "BUG: divisive_operation_results only for Divide/Modulo, got {op:?}"
959 );
960 let right_value = match right {
961 OperationResult::Value(value) => value,
962 OperationResult::Veto(_) => return right.clone(),
963 };
964 if let Some(veto) = computation_if_zero_divisor(&right_value.value, op) {
965 return veto;
966 }
967 match left {
968 OperationResult::Value(left_value) => scalar_arithmetic(
969 left_value,
970 left_type,
971 op,
972 right_value,
973 right_type,
974 signature_index,
975 ),
976 OperationResult::Veto(_) => left.clone(),
977 }
978}
979
980fn computation_if_zero_divisor(
981 right: &ValueKind,
982 op: &ArithmeticComputation,
983) -> Option<OperationResult> {
984 let magnitude = match right {
985 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => n,
986 _ => return None,
987 };
988 if !crate::computation::rational::rational_is_zero(magnitude) {
989 return None;
990 }
991 let failure = match op {
992 ArithmeticComputation::Divide => NumberArithmeticFailure::DivisionByZero,
993 ArithmeticComputation::Modulo => NumberArithmeticFailure::ModuloByZero,
994 _ => return None,
995 };
996 Some(OperationResult::Veto(VetoType::computation(
997 failure.message(),
998 )))
999}
1000
1001fn operate_with_left_result(
1002 left_result: OperationResult,
1003 left_type: &Arc<LemmaType>,
1004 op: &ArithmeticComputation,
1005 right: &BoundValueKind,
1006 right_type: &Arc<LemmaType>,
1007 signature_index: &SignatureIndex,
1008) -> OperationResult {
1009 let left_value = match left_result {
1010 OperationResult::Value(value) => value,
1011 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1012 };
1013 scalar_arithmetic(
1014 &left_value,
1015 left_type,
1016 op,
1017 right,
1018 right_type,
1019 signature_index,
1020 )
1021}
1022
1023fn operate_with_right_result(
1024 left: &BoundValueKind,
1025 left_type: &Arc<LemmaType>,
1026 op: &ArithmeticComputation,
1027 right_result: OperationResult,
1028 right_type: &Arc<LemmaType>,
1029 signature_index: &SignatureIndex,
1030) -> OperationResult {
1031 let right_value = match right_result {
1032 OperationResult::Value(value) => value,
1033 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1034 };
1035 scalar_arithmetic(
1036 left,
1037 left_type,
1038 op,
1039 &right_value,
1040 right_type,
1041 signature_index,
1042 )
1043}
1044
1045fn shift_date_range_right_endpoint(
1046 range_left: &TypedLiteral,
1047 range_right: &TypedLiteral,
1048 calendar_value: &RationalInteger,
1049 add: bool,
1050 calendar_lemma_type: Arc<LemmaType>,
1051) -> OperationResult {
1052 let (ValueKind::Date(_), ValueKind::Date(_)) = (&range_left.value, &range_right.value) else {
1053 unreachable!(
1054 "BUG: date range calendar arithmetic received non-date endpoints; planning should have rejected this"
1055 );
1056 };
1057
1058 let date_type = crate::planning::semantics::primitive_date_arc();
1059 let calendar_literal = LiteralValue::measure(calendar_value.clone());
1060 let op = if add {
1061 ArithmeticComputation::Add
1062 } else {
1063 ArithmeticComputation::Subtract
1064 };
1065 let shifted_right = match super::datetime::datetime_arithmetic(
1066 &range_right.to_literal(),
1067 date_type,
1068 &op,
1069 &calendar_literal,
1070 &calendar_lemma_type,
1071 ) {
1072 OperationResult::Value(value) => value.to_literal(),
1073 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1074 };
1075
1076 OperationResult::from_literal(LiteralValue::range(
1077 range_left.clone(),
1078 TypedLiteral {
1079 value: shifted_right.value,
1080 lemma_type: Arc::clone(&range_right.lemma_type),
1081 },
1082 ))
1083}
1084
1085fn shift_calendar_range_right_endpoint(
1086 range_left: &TypedLiteral,
1087 range_right: &TypedLiteral,
1088 calendar_value: &RationalInteger,
1089 add: bool,
1090 indexes: &CalendarRangeShiftIndexes<'_>,
1091) -> OperationResult {
1092 let (ValueKind::Measure(_), ValueKind::Measure(_)) = (&range_left.value, &range_right.value)
1093 else {
1094 unreachable!(
1095 "BUG: calendar range calendar arithmetic received non-calendar endpoints; planning should have rejected this"
1096 );
1097 };
1098 let calendar_bound = BoundValueKind::unbound(ValueKind::Measure(calendar_value.clone()));
1099 let range_right_bound = BoundValueKind::unbound(range_right.value.clone());
1100 let op = if add {
1101 ArithmeticComputation::Add
1102 } else {
1103 ArithmeticComputation::Subtract
1104 };
1105 let shifted_right = match scalar_arithmetic(
1106 &range_right_bound,
1107 indexes.endpoint_type,
1108 &op,
1109 &calendar_bound,
1110 indexes.calendar_type,
1111 indexes.signature_index,
1112 ) {
1113 OperationResult::Value(value) => value.to_literal(),
1114 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1115 };
1116
1117 OperationResult::from_literal(LiteralValue::range(
1118 range_left.clone(),
1119 TypedLiteral {
1120 value: shifted_right.value,
1121 lemma_type: Arc::clone(&range_right.lemma_type),
1122 },
1123 ))
1124}
1125
1126fn type_name(lemma_type: &LemmaType) -> String {
1127 lemma_type.name().to_string()
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133 use crate::computation::rational::rational_new;
1134 use crate::planning::semantics::{ArithmeticComputation, LiteralValue, ValueKind};
1135 use rust_decimal::Decimal;
1136
1137 #[test]
1138 fn number_arithmetic_add_on_stored_decimals() {
1139 let left = rational_new(1, 1);
1140 let right = rational_new(2, 1);
1141 let sum = number_arithmetic(&left, &ArithmeticComputation::Add, &right).unwrap();
1142 assert_eq!(sum, rational_new(3, 1));
1143 }
1144
1145 #[test]
1146 fn number_arithmetic_divide_ten_by_three_returns_value_decimal() {
1147 let left = rational_new(10, 1);
1148 let right = rational_new(3, 1);
1149 let quotient = number_arithmetic(&left, &ArithmeticComputation::Divide, &right).unwrap();
1150 let decimal = quotient.try_to_decimal().unwrap();
1151 assert!(decimal > Decimal::from(3));
1152 assert!(decimal < Decimal::from(4));
1153 }
1154
1155 #[test]
1156 fn number_arithmetic_division_by_zero_returns_failure() {
1157 let left = rational_new(10, 1);
1158 let right = rational_new(0, 1);
1159 let failure = number_arithmetic(&left, &ArithmeticComputation::Divide, &right).unwrap_err();
1160 assert_eq!(failure, NumberArithmeticFailure::DivisionByZero);
1161 }
1162
1163 #[test]
1164 fn arithmetic_operation_adds_primitive_numbers() {
1165 use crate::computation::rational::decimal_to_rational;
1166 use crate::planning::semantics::primitive_number_arc;
1167 use rust_decimal::Decimal;
1168 let left = BoundValueKind::unbound(
1169 LiteralValue::number(decimal_to_rational(Decimal::new(11, 1)).unwrap()).value,
1170 );
1171 let right = BoundValueKind::unbound(
1172 LiteralValue::number(decimal_to_rational(Decimal::new(9, 1)).unwrap()).value,
1173 );
1174 let number_ty = primitive_number_arc();
1175 let signature_index = SignatureIndex::new();
1176 let OperationResult::Value(lit) = arithmetic_operation(
1177 &left,
1178 number_ty,
1179 &ArithmeticComputation::Add,
1180 &right,
1181 number_ty,
1182 &signature_index,
1183 ) else {
1184 panic!("expected value");
1185 };
1186 match &lit.value {
1187 ValueKind::Number(n) => {
1188 assert_eq!(n, &decimal_to_rational(Decimal::new(2, 0)).unwrap());
1189 }
1190 other => panic!("expected number, got {other:?}"),
1191 }
1192 }
1193
1194 #[test]
1195 fn arithmetic_operation_propagates_veto_from_left() {
1196 use crate::planning::semantics::primitive_number_arc;
1197 let left = OperationResult::Veto(VetoType::computation("left failed"));
1198 let right = LiteralValue::number(rational_new(1, 1));
1199 let number_ty = primitive_number_arc();
1200 let signature_index = SignatureIndex::new();
1201 let result = operate_on_operation_results(
1202 left,
1203 number_ty,
1204 &ArithmeticComputation::Add,
1205 OperationResult::from_literal(right),
1206 number_ty,
1207 &signature_index,
1208 );
1209 assert!(matches!(result, OperationResult::Veto(_)));
1210 }
1211
1212 fn build_engine_and_get_value(code: &str, spec: &str, rule: &str) -> (LiteralValue, String) {
1223 use crate::engine::Engine;
1224 use std::path::PathBuf;
1225 use std::sync::Arc;
1226 let mut engine = Engine::new();
1227 engine
1228 .load([(
1229 crate::parsing::source::SourceType::Path(Arc::new(PathBuf::from("t.lemma"))),
1230 code.to_string(),
1231 )])
1232 .expect("spec must load");
1233 let response = engine
1234 .evaluate(None, spec, None, IndexMap::new(), None, false)
1235 .expect("spec must evaluate");
1236 let rule_result = response
1237 .results
1238 .get(rule)
1239 .unwrap_or_else(|| panic!("rule '{}' missing", rule));
1240 if rule_result.vetoed {
1241 panic!("rule '{}' vetoed", rule);
1242 }
1243 (rule_result.to_literal(), rule_result.rule_type.clone())
1244 }
1245
1246 #[test]
1251 fn q_times_q_signature_hit_emits_named_lemma_type() {
1252 let code = r#"spec t
1253data money: measure
1254 -> unit eur: 1
1255data rate: measure
1256 -> unit eur_per_hour: eur/hour
1257data hour: measure
1258 -> unit hour: 1
1259data r: 30 eur_per_hour
1260data h: 2 hour
1261rule pay: r * h
1262"#;
1263 let (value, _) = build_engine_and_get_value(code, "t", "pay");
1264 match &value.value {
1267 ValueKind::Measure(_) => {}
1268 other => panic!("expected Measure result, got {:?}", other),
1269 }
1270 assert!(
1272 matches!(&value.value, ValueKind::Measure(_)),
1273 "result must be a Measure promoted via signature hit"
1274 );
1275 }
1276
1277 #[test]
1281 fn q_times_q_signature_miss_emits_anonymous_lemma_type() {
1282 let code = r#"spec t
1283data money: measure
1284 -> unit eur: 1
1285data rate: measure
1286 -> unit eur_per_minute: eur/minute
1287data hour: measure
1288 -> unit hour: 1
1289data r: 40 eur_per_minute
1290data h: 2 hour
1291rule weird: r * h
1292"#;
1293 use crate::engine::Engine;
1298 use std::path::PathBuf;
1299 use std::sync::Arc;
1300 let mut engine = Engine::new();
1301 let result = engine.load([(
1302 crate::parsing::source::SourceType::Path(Arc::new(PathBuf::from("t.lemma"))),
1303 code.to_string(),
1304 )]);
1305 assert!(
1306 result.is_err(),
1307 "rule producing anonymous Q*Q with no signature_index match must be rejected"
1308 );
1309 }
1310
1311 #[test]
1313 fn q_plus_q_same_signature() {
1314 let code = r#"spec t
1315data money: measure
1316 -> unit eur: 1
1317data a: 100 eur
1318data b: 50 eur
1319rule total: a + b
1320"#;
1321 let (value, _) = build_engine_and_get_value(code, "t", "total");
1322 match &value.value {
1323 ValueKind::Measure(n) => {
1324 let decimal = n.try_to_decimal().unwrap();
1325 assert_eq!(decimal, Decimal::from(150));
1326 }
1327 other => panic!("expected Measure, got {:?}", other),
1328 }
1329 }
1330
1331 #[test]
1335 fn q_plus_q_different_signature() {
1336 let code = r#"spec t
1337uses lemma units
1338data money: measure
1339 -> unit eur: 1
1340data rate: measure
1341 -> unit eur_per_second: eur/second
1342 -> unit eur_per_minute: eur/minute
1343data a: 10 eur_per_second
1344data b: 20 eur_per_minute
1345rule total_rate: (a + b) as eur_per_second
1346"#;
1347 let (value, _) = build_engine_and_get_value(code, "t", "total_rate");
1348 match &value.value {
1349 ValueKind::Measure(n) => {
1350 let decimal = n.try_to_decimal().unwrap();
1351 let expected_low = Decimal::new(10_333, 3);
1353 let expected_high = Decimal::new(10_334, 3);
1354 assert!(
1355 decimal >= expected_low && decimal <= expected_high,
1356 "expected ~10.333, got {}",
1357 decimal
1358 );
1359 }
1360 other => panic!("expected Measure, got {:?}", other),
1361 }
1362 }
1363
1364 #[test]
1368 fn number_divided_by_q_reciprocates_signature() {
1369 let code = r#"spec t
1370data duration_t: measure
1371 -> unit second: 1
1372data freq: measure
1373 -> unit per_second: second^-1
1374data d: 2 second
1375rule f: 1 / d
1376"#;
1377 let (value, rule_type) = build_engine_and_get_value(code, "t", "f");
1379 match &value.value {
1380 ValueKind::Measure(_) => {}
1381 other => panic!("expected Measure, got {:?}", other),
1382 }
1383 assert_eq!(
1384 rule_type, "freq",
1385 "1/second must promote to 'freq' (signature [(second,-1)] -> per_second)"
1386 );
1387 }
1388}