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 instance_factors: None,
458 }),
459 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
460 }
461 }
462 _ => unreachable!(
463 "BUG: ratio {:?} number; add/subtract rejected during planning",
464 op
465 ),
466 },
467
468 (ValueKind::Ratio(l), ValueKind::Ratio(r)) => {
470 if matches!(op, ArithmeticComputation::Modulo) {
471 unreachable!("BUG: ratio modulo ratio is rejected during planning");
472 }
473 match number_arithmetic(l, op, r) {
474 Ok(rational) => OperationResult::from_bound(BoundValueKind {
475 value: ValueKind::Ratio(rational),
476 unit: BoundValueKind::agree_or_left_binding(
477 left.unit.as_ref(),
478 right.unit.as_ref(),
479 ),
480 instance_factors: None,
481 }),
482 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
483 }
484 }
485 (ValueKind::Measure(l_val), ValueKind::Measure(r_val)) => {
487 if left_type.is_calendar_like() != right_type.is_calendar_like() {
488 let is_measure_left = left_type.is_measure() && !left_type.is_calendar_like();
489 let is_multiply = matches!(op, ArithmeticComputation::Multiply);
490 let is_divide = matches!(op, ArithmeticComputation::Divide);
491 if !is_multiply && !is_divide {
492 unreachable!("BUG: measure {:?} calendar is rejected during planning", op);
493 }
494 let (l_val_ref, r_val_ref) = if is_measure_left {
495 (l_val, r_val)
496 } else {
497 (r_val, l_val)
498 };
499 if is_divide && crate::computation::rational::rational_is_zero(r_val_ref) {
500 return OperationResult::Veto(VetoType::computation(
501 "Division by zero is not allowed.",
502 ));
503 }
504 let raw_result = match rational_operation(
505 l_val_ref,
506 if is_multiply {
507 NumericOperation::Multiply
508 } else {
509 NumericOperation::Divide
510 },
511 r_val_ref,
512 ) {
513 Ok(p) => p,
514 Err(failure) => {
515 return OperationResult::Veto(VetoType::computation(
516 map_numeric_failure(failure).message(),
517 ))
518 }
519 };
520 let q_decomp = if is_measure_left {
521 left_type.measure_type_decomposition()
522 } else {
523 right_type.measure_type_decomposition()
524 }
525 .expect("BUG: decomposition must be resolved after planning");
526 let c_decomp = crate::planning::semantics::calendar_decomposition();
527 let combined = crate::planning::semantics::combine_decompositions(
528 q_decomp,
529 &c_decomp,
530 is_multiply,
531 )
532 .expect("BUG: planning bounds combined unit exponents");
533 if combined.is_empty() {
534 return OperationResult::from_literal(LiteralValue::number(raw_result));
535 }
536 return OperationResult::from_bound(BoundValueKind::unbound(ValueKind::Measure(
537 raw_result,
538 )));
539 }
540 if left_type.is_calendar_like() && right_type.is_calendar_like() {
541 return match op {
542 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
543 calendar_from_months_arithmetic(l_val, op, r_val)
544 }
545 ArithmeticComputation::Divide => {
546 number_op_on_stored_rationals(l_val, &ArithmeticComputation::Divide, r_val)
547 }
548 _ => unreachable!(
549 "BUG: calendar * calendar with op {:?}; planning should have rejected this",
550 op
551 ),
552 };
553 }
554 if left_type.is_calendar_like() && !right_type.is_calendar_like() {
555 if let ValueKind::Number(n) = &right.value {
556 return match number_arithmetic(l_val, op, n) {
557 Ok(rational) => {
558 OperationResult::from_literal(LiteralValue::measure(rational))
559 }
560 Err(failure) => {
561 OperationResult::Veto(VetoType::computation(failure.message()))
562 }
563 };
564 }
565 if let ValueKind::Ratio(r) = &right.value {
566 return match number_ratio_arithmetic(l_val, op, r) {
567 Ok(rational) => {
568 OperationResult::from_literal(LiteralValue::measure(rational))
569 }
570 Err(failure) => {
571 OperationResult::Veto(VetoType::computation(failure.message()))
572 }
573 };
574 }
575 }
576 if right_type.is_calendar_like() && !left_type.is_calendar_like() {
577 if let (ValueKind::Number(n), ArithmeticComputation::Multiply) = (&left.value, op) {
578 return match number_arithmetic(n, op, r_val) {
579 Ok(rational) => {
580 OperationResult::from_literal(LiteralValue::measure(rational))
581 }
582 Err(failure) => {
583 OperationResult::Veto(VetoType::computation(failure.message()))
584 }
585 };
586 }
587 if let ValueKind::Number(n) = &left.value {
588 return match number_arithmetic(n, op, r_val) {
589 Ok(rational) => {
590 OperationResult::from_literal(LiteralValue::number(rational))
591 }
592 Err(failure) => {
593 OperationResult::Veto(VetoType::computation(failure.message()))
594 }
595 };
596 }
597 if let ValueKind::Ratio(r) = &left.value {
598 return match number_ratio_arithmetic(r_val, op, r) {
599 Ok(rational) => {
600 OperationResult::from_literal(LiteralValue::measure(rational))
601 }
602 Err(failure) => {
603 OperationResult::Veto(VetoType::computation(failure.message()))
604 }
605 };
606 }
607 }
608 match op {
609 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
610 let same_family = left_type.same_measure_family(right_type);
611 let anonymous_compatible =
612 left_type.compatible_with_anonymous_measure(right_type);
613 let identical_measure = left_type.as_ref() == right_type.as_ref()
614 || (left_type
615 .specifications
616 .equal_ignoring_unit(&right_type.specifications)
617 && left_type.name == right_type.name
618 && left_type.extends == right_type.extends);
619 if !identical_measure && !same_family && !anonymous_compatible {
620 unreachable!(
621 "BUG: measure add/subtract with incompatible types ({} vs {}); should be rejected during planning",
622 left_type.name(),
623 right_type.name()
624 );
625 }
626 match measure_add_subtract(l_val, op, r_val) {
627 Ok(rational) => {
628 let instance_factors = BoundValueKind::agree_instance_factors(
629 left.instance_factors.as_ref(),
630 right.instance_factors.as_ref(),
631 );
632 let unit = BoundValueKind::binding_with_agreed_factors(
633 BoundValueKind::agree_or_left_binding(
634 left.unit.as_ref(),
635 right.unit.as_ref(),
636 ),
637 &instance_factors,
638 );
639 OperationResult::from_bound(BoundValueKind {
640 value: ValueKind::Measure(rational),
641 unit,
642 instance_factors,
643 })
644 }
645 Err(failure) => {
646 OperationResult::Veto(VetoType::computation(failure.message()))
647 }
648 }
649 }
650 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
651 if matches!(op, ArithmeticComputation::Divide)
652 && crate::computation::rational::rational_is_zero(r_val)
653 {
654 return OperationResult::Veto(VetoType::computation(
655 "Division by zero is not allowed.",
656 ));
657 }
658 let numeric_op = match op {
659 ArithmeticComputation::Multiply => NumericOperation::Multiply,
660 ArithmeticComputation::Divide => NumericOperation::Divide,
661 _ => unreachable!("BUG: matched multiply/divide arm with other op"),
662 };
663 let raw_result = match rational_operation(l_val, numeric_op, r_val) {
664 Ok(p) => p,
665 Err(failure) => {
666 return OperationResult::Veto(VetoType::computation(
667 map_numeric_failure(failure).message(),
668 ))
669 }
670 };
671 let l_decomp = left_type
672 .measure_type_decomposition()
673 .expect("BUG: decomposition must be resolved after planning");
674 let r_decomp = right_type
675 .measure_type_decomposition()
676 .expect("BUG: decomposition must be resolved after planning");
677 let combined_decomposition =
678 crate::planning::semantics::combine_decompositions(
679 l_decomp,
680 r_decomp,
681 matches!(op, ArithmeticComputation::Multiply),
682 )
683 .expect("BUG: planning bounds combined unit exponents");
684 if combined_decomposition.is_empty() {
685 OperationResult::from_literal(LiteralValue::number(raw_result))
686 } else if left_type.same_measure_family(right_type) {
687 OperationResult::from_bound(BoundValueKind {
688 value: ValueKind::Measure(raw_result),
689 unit: None,
690 instance_factors: BoundValueKind::agree_instance_factors(
691 left.instance_factors.as_ref(),
692 right.instance_factors.as_ref(),
693 ),
694 })
695 } else {
696 OperationResult::from_bound(BoundValueKind::unbound(ValueKind::Measure(
697 raw_result,
698 )))
699 }
700 }
701 _ => unreachable!("BUG: measure {:?} measure is rejected during planning", op),
702 }
703 }
704 (ValueKind::Measure(q_val), ValueKind::Ratio(r)) => {
706 match measure_ratio_arithmetic(q_val.clone(), op, r.clone()) {
707 Ok(rational) => OperationResult::from_bound(BoundValueKind {
708 value: ValueKind::Measure(rational),
709 unit: left.unit.clone(),
710 instance_factors: left.instance_factors.clone(),
711 }),
712 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
713 }
714 }
715 (ValueKind::Ratio(r), ValueKind::Measure(q_val)) => match op {
717 ArithmeticComputation::Multiply => {
718 match measure_ratio_arithmetic(q_val.clone(), op, r.clone()) {
719 Ok(rational) => OperationResult::from_bound(BoundValueKind {
720 value: ValueKind::Measure(rational),
721 unit: right.unit.clone(),
722 instance_factors: right.instance_factors.clone(),
723 }),
724 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
725 }
726 }
727 _ => unreachable!(
728 "BUG: ratio {:?} measure; planning should have rejected this",
729 op
730 ),
731 },
732
733 (ValueKind::Measure(measure_val), ValueKind::Number(n)) => match op {
737 ArithmeticComputation::Multiply
738 | ArithmeticComputation::Divide
739 | ArithmeticComputation::Add
740 | ArithmeticComputation::Subtract => match number_arithmetic(measure_val, op, n) {
741 Ok(rational) => OperationResult::from_bound(BoundValueKind {
742 value: ValueKind::Measure(rational),
743 unit: left.unit.clone(),
744 instance_factors: left.instance_factors.clone(),
745 }),
746 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
747 },
748 ArithmeticComputation::Power => {
749 if !n.is_integer() {
750 unreachable!(
751 "BUG: measure power with a non-integer exponent is rejected during planning"
752 );
753 }
754 match number_arithmetic(measure_val, op, n) {
755 Ok(rational) if n.numer_is_zero() => {
756 OperationResult::from_literal(LiteralValue::number(rational))
757 }
758 Ok(rational) => OperationResult::from_bound(BoundValueKind {
759 value: ValueKind::Measure(rational),
760 unit: None,
761 instance_factors: left.instance_factors.clone(),
762 }),
763 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
764 }
765 }
766 ArithmeticComputation::Modulo => {
767 unreachable!("BUG: measure modulo number is rejected during planning")
768 }
769 },
770 (ValueKind::Number(n), ValueKind::Measure(measure_val)) => match op {
773 ArithmeticComputation::Multiply => match number_arithmetic(n, op, measure_val) {
774 Ok(rational) => OperationResult::from_bound(BoundValueKind {
775 value: ValueKind::Measure(rational),
776 unit: right.unit.clone(),
777 instance_factors: right.instance_factors.clone(),
778 }),
779 Err(failure) => OperationResult::Veto(VetoType::computation(failure.message())),
780 },
781 ArithmeticComputation::Divide => {
782 let measure_decomp = right_type
783 .measure_type_decomposition()
784 .expect("BUG: decomposition must be resolved after planning");
785 if measure_decomp.is_empty() {
786 match number_arithmetic(n, op, measure_val) {
787 Ok(rational) => {
788 OperationResult::from_literal(LiteralValue::number(rational))
789 }
790 Err(failure) => {
791 OperationResult::Veto(VetoType::computation(failure.message()))
792 }
793 }
794 } else {
795 let negated_signature = crate::planning::semantics::negate_signature(
796 &right_type.measure_runtime_signature(),
797 );
798 match number_arithmetic(n, op, measure_val) {
799 Ok(rational) => {
800 if let Some((unit_name, owning_type)) =
801 find_by_names(signature_index, &negated_signature)
802 {
803 let target_factor =
804 owning_type.measure_unit_factor(unit_name).clone();
805 match crate::computation::rational::checked_div(
806 &rational,
807 &target_factor,
808 ) {
809 Ok(magnitude) => OperationResult::from_bound(
810 BoundValueKind::unbound(ValueKind::Measure(magnitude)),
811 ),
812 Err(failure) => OperationResult::Veto(VetoType::computation(
813 failure.to_string(),
814 )),
815 }
816 } else {
817 OperationResult::from_bound(BoundValueKind::unbound(
818 ValueKind::Measure(rational),
819 ))
820 }
821 }
822 Err(failure) => {
823 OperationResult::Veto(VetoType::computation(failure.message()))
824 }
825 }
826 }
827 }
828 ArithmeticComputation::Modulo
829 | ArithmeticComputation::Add
830 | ArithmeticComputation::Subtract
831 | ArithmeticComputation::Power => {
832 unreachable!(
833 "BUG: Number {:?} Measure should be rejected during planning",
834 op
835 )
836 }
837 },
838 _ => unreachable!(
839 "BUG: arithmetic {:?} for {:?} and {:?}; planning should have rejected this",
840 op,
841 type_name(left_type),
842 type_name(right_type)
843 ),
844 }
845}
846
847#[derive(Debug, Clone, PartialEq, Eq)]
848pub enum NumberArithmeticFailure {
849 DivisionByZero,
850 ModuloByZero,
851 Computation(String),
852}
853
854impl NumberArithmeticFailure {
855 pub fn message(self) -> String {
856 match self {
857 Self::DivisionByZero => "Division by zero is not allowed.".to_string(),
858 Self::ModuloByZero => "Modulo by zero is not allowed.".to_string(),
859 Self::Computation(message) => message,
860 }
861 }
862}
863
864fn numeric_operation_from_arithmetic(operator: &ArithmeticComputation) -> NumericOperation {
865 match operator {
866 ArithmeticComputation::Add => NumericOperation::Add,
867 ArithmeticComputation::Subtract => NumericOperation::Subtract,
868 ArithmeticComputation::Multiply => NumericOperation::Multiply,
869 ArithmeticComputation::Divide => NumericOperation::Divide,
870 ArithmeticComputation::Modulo => NumericOperation::Modulo,
871 ArithmeticComputation::Power => NumericOperation::Power,
872 }
873}
874
875fn map_numeric_failure(failure: NumericFailure) -> NumberArithmeticFailure {
876 match failure {
877 NumericFailure::DivisionByZero => NumberArithmeticFailure::DivisionByZero,
878 other => NumberArithmeticFailure::Computation(other.to_string()),
879 }
880}
881
882fn map_numeric_failure_modulo(failure: NumericFailure) -> NumberArithmeticFailure {
883 match failure {
884 NumericFailure::DivisionByZero => NumberArithmeticFailure::ModuloByZero,
885 other => map_numeric_failure(other),
886 }
887}
888
889fn measure_scale_magnitude_by_rational(
890 magnitude: RationalInteger,
891 factor: &RationalInteger,
892) -> Result<RationalInteger, NumberArithmeticFailure> {
893 rational_operation(&magnitude, NumericOperation::Multiply, factor).map_err(map_numeric_failure)
894}
895
896fn measure_ratio_arithmetic(
897 measure_value: RationalInteger,
898 operator: &ArithmeticComputation,
899 ratio_value: RationalInteger,
900) -> Result<RationalInteger, NumberArithmeticFailure> {
901 match operator {
902 ArithmeticComputation::Multiply => {
903 measure_scale_magnitude_by_rational(measure_value, &ratio_value)
904 }
905 ArithmeticComputation::Divide => {
906 rational_operation(&measure_value, NumericOperation::Divide, &ratio_value)
907 .map_err(map_numeric_failure)
908 }
909 _ => unreachable!(
910 "BUG: measure {:?} ratio is rejected during planning",
911 operator
912 ),
913 }
914}
915
916fn measure_add_subtract(
917 left_value: &RationalInteger,
918 operator: &ArithmeticComputation,
919 right_value: &RationalInteger,
920) -> Result<RationalInteger, NumberArithmeticFailure> {
921 rational_operation(
922 left_value,
923 numeric_operation_from_arithmetic(operator),
924 right_value,
925 )
926 .map_err(map_numeric_failure)
927}
928
929pub fn number_arithmetic(
930 left: &RationalInteger,
931 operator: &ArithmeticComputation,
932 right: &RationalInteger,
933) -> Result<RationalInteger, NumberArithmeticFailure> {
934 rational_operation_with_fallback(left, numeric_operation_from_arithmetic(operator), right)
935 .map_err(|failure| {
936 if matches!(operator, ArithmeticComputation::Modulo) {
937 map_numeric_failure_modulo(failure)
938 } else {
939 map_numeric_failure(failure)
940 }
941 })
942}
943
944fn operate_on_operation_results(
945 left_result: OperationResult,
946 left_type: &Arc<LemmaType>,
947 op: &ArithmeticComputation,
948 right_result: OperationResult,
949 right_type: &Arc<LemmaType>,
950 signature_index: &SignatureIndex,
951) -> OperationResult {
952 let left_value = match left_result {
953 OperationResult::Value(value) => value,
954 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
955 };
956 let right_value = match right_result {
957 OperationResult::Value(value) => value,
958 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
959 };
960 scalar_arithmetic(
961 &left_value,
962 left_type,
963 op,
964 &right_value,
965 right_type,
966 signature_index,
967 )
968}
969
970pub(crate) fn divisive_operation_results(
974 left: &OperationResult,
975 left_type: &Arc<LemmaType>,
976 op: &ArithmeticComputation,
977 right: &OperationResult,
978 right_type: &Arc<LemmaType>,
979 signature_index: &SignatureIndex,
980) -> OperationResult {
981 assert!(
982 matches!(
983 op,
984 ArithmeticComputation::Divide | ArithmeticComputation::Modulo
985 ),
986 "BUG: divisive_operation_results only for Divide/Modulo, got {op:?}"
987 );
988 let right_value = match right {
989 OperationResult::Value(value) => value,
990 OperationResult::Veto(_) => return right.clone(),
991 };
992 if let Some(veto) = computation_if_zero_divisor(&right_value.value, op) {
993 return veto;
994 }
995 match left {
996 OperationResult::Value(left_value) => scalar_arithmetic(
997 left_value,
998 left_type,
999 op,
1000 right_value,
1001 right_type,
1002 signature_index,
1003 ),
1004 OperationResult::Veto(_) => left.clone(),
1005 }
1006}
1007
1008fn computation_if_zero_divisor(
1009 right: &ValueKind,
1010 op: &ArithmeticComputation,
1011) -> Option<OperationResult> {
1012 let magnitude = match right {
1013 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => n,
1014 _ => return None,
1015 };
1016 if !crate::computation::rational::rational_is_zero(magnitude) {
1017 return None;
1018 }
1019 let failure = match op {
1020 ArithmeticComputation::Divide => NumberArithmeticFailure::DivisionByZero,
1021 ArithmeticComputation::Modulo => NumberArithmeticFailure::ModuloByZero,
1022 _ => return None,
1023 };
1024 Some(OperationResult::Veto(VetoType::computation(
1025 failure.message(),
1026 )))
1027}
1028
1029fn operate_with_left_result(
1030 left_result: OperationResult,
1031 left_type: &Arc<LemmaType>,
1032 op: &ArithmeticComputation,
1033 right: &BoundValueKind,
1034 right_type: &Arc<LemmaType>,
1035 signature_index: &SignatureIndex,
1036) -> OperationResult {
1037 let left_value = match left_result {
1038 OperationResult::Value(value) => value,
1039 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1040 };
1041 scalar_arithmetic(
1042 &left_value,
1043 left_type,
1044 op,
1045 right,
1046 right_type,
1047 signature_index,
1048 )
1049}
1050
1051fn operate_with_right_result(
1052 left: &BoundValueKind,
1053 left_type: &Arc<LemmaType>,
1054 op: &ArithmeticComputation,
1055 right_result: OperationResult,
1056 right_type: &Arc<LemmaType>,
1057 signature_index: &SignatureIndex,
1058) -> OperationResult {
1059 let right_value = match right_result {
1060 OperationResult::Value(value) => value,
1061 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1062 };
1063 scalar_arithmetic(
1064 left,
1065 left_type,
1066 op,
1067 &right_value,
1068 right_type,
1069 signature_index,
1070 )
1071}
1072
1073fn shift_date_range_right_endpoint(
1074 range_left: &TypedLiteral,
1075 range_right: &TypedLiteral,
1076 calendar_value: &RationalInteger,
1077 add: bool,
1078 calendar_lemma_type: Arc<LemmaType>,
1079) -> OperationResult {
1080 let (ValueKind::Date(_), ValueKind::Date(_)) = (&range_left.value, &range_right.value) else {
1081 unreachable!(
1082 "BUG: date range calendar arithmetic received non-date endpoints; planning should have rejected this"
1083 );
1084 };
1085
1086 let date_type = crate::planning::semantics::primitive_date_arc();
1087 let calendar_literal = LiteralValue::measure(calendar_value.clone());
1088 let op = if add {
1089 ArithmeticComputation::Add
1090 } else {
1091 ArithmeticComputation::Subtract
1092 };
1093 let shifted_right = match super::datetime::datetime_arithmetic(
1094 &range_right.to_literal(),
1095 date_type,
1096 &op,
1097 &calendar_literal,
1098 &calendar_lemma_type,
1099 ) {
1100 OperationResult::Value(value) => value.to_literal(),
1101 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1102 };
1103
1104 OperationResult::from_literal(LiteralValue::range(
1105 range_left.clone(),
1106 TypedLiteral {
1107 value: shifted_right.value,
1108 lemma_type: Arc::clone(&range_right.lemma_type),
1109 },
1110 ))
1111}
1112
1113fn shift_calendar_range_right_endpoint(
1114 range_left: &TypedLiteral,
1115 range_right: &TypedLiteral,
1116 calendar_value: &RationalInteger,
1117 add: bool,
1118 indexes: &CalendarRangeShiftIndexes<'_>,
1119) -> OperationResult {
1120 let (ValueKind::Measure(_), ValueKind::Measure(_)) = (&range_left.value, &range_right.value)
1121 else {
1122 unreachable!(
1123 "BUG: calendar range calendar arithmetic received non-calendar endpoints; planning should have rejected this"
1124 );
1125 };
1126 let calendar_bound = BoundValueKind::unbound(ValueKind::Measure(calendar_value.clone()));
1127 let range_right_bound = BoundValueKind::unbound(range_right.value.clone());
1128 let op = if add {
1129 ArithmeticComputation::Add
1130 } else {
1131 ArithmeticComputation::Subtract
1132 };
1133 let shifted_right = match scalar_arithmetic(
1134 &range_right_bound,
1135 indexes.endpoint_type,
1136 &op,
1137 &calendar_bound,
1138 indexes.calendar_type,
1139 indexes.signature_index,
1140 ) {
1141 OperationResult::Value(value) => value.to_literal(),
1142 OperationResult::Veto(reason) => return OperationResult::Veto(reason),
1143 };
1144
1145 OperationResult::from_literal(LiteralValue::range(
1146 range_left.clone(),
1147 TypedLiteral {
1148 value: shifted_right.value,
1149 lemma_type: Arc::clone(&range_right.lemma_type),
1150 },
1151 ))
1152}
1153
1154fn type_name(lemma_type: &LemmaType) -> String {
1155 lemma_type.name().to_string()
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161 use crate::computation::rational::rational_new;
1162 use crate::planning::semantics::{ArithmeticComputation, LiteralValue, ValueKind};
1163 use rust_decimal::Decimal;
1164
1165 #[test]
1166 fn number_arithmetic_add_on_stored_decimals() {
1167 let left = rational_new(1, 1);
1168 let right = rational_new(2, 1);
1169 let sum = number_arithmetic(&left, &ArithmeticComputation::Add, &right).unwrap();
1170 assert_eq!(sum, rational_new(3, 1));
1171 }
1172
1173 #[test]
1174 fn number_arithmetic_divide_ten_by_three_returns_value_decimal() {
1175 let left = rational_new(10, 1);
1176 let right = rational_new(3, 1);
1177 let quotient = number_arithmetic(&left, &ArithmeticComputation::Divide, &right).unwrap();
1178 let decimal = quotient.try_to_decimal().unwrap();
1179 assert!(decimal > Decimal::from(3));
1180 assert!(decimal < Decimal::from(4));
1181 }
1182
1183 #[test]
1184 fn number_arithmetic_division_by_zero_returns_failure() {
1185 let left = rational_new(10, 1);
1186 let right = rational_new(0, 1);
1187 let failure = number_arithmetic(&left, &ArithmeticComputation::Divide, &right).unwrap_err();
1188 assert_eq!(failure, NumberArithmeticFailure::DivisionByZero);
1189 }
1190
1191 #[test]
1192 fn arithmetic_operation_adds_primitive_numbers() {
1193 use crate::computation::rational::decimal_to_rational;
1194 use crate::planning::semantics::primitive_number_arc;
1195 use rust_decimal::Decimal;
1196 let left = BoundValueKind::unbound(
1197 LiteralValue::number(decimal_to_rational(Decimal::new(11, 1)).unwrap()).value,
1198 );
1199 let right = BoundValueKind::unbound(
1200 LiteralValue::number(decimal_to_rational(Decimal::new(9, 1)).unwrap()).value,
1201 );
1202 let number_ty = primitive_number_arc();
1203 let signature_index = SignatureIndex::new();
1204 let OperationResult::Value(lit) = arithmetic_operation(
1205 &left,
1206 number_ty,
1207 &ArithmeticComputation::Add,
1208 &right,
1209 number_ty,
1210 &signature_index,
1211 ) else {
1212 panic!("expected value");
1213 };
1214 match &lit.value {
1215 ValueKind::Number(n) => {
1216 assert_eq!(n, &decimal_to_rational(Decimal::new(2, 0)).unwrap());
1217 }
1218 other => panic!("expected number, got {other:?}"),
1219 }
1220 }
1221
1222 #[test]
1223 fn arithmetic_operation_propagates_veto_from_left() {
1224 use crate::planning::semantics::primitive_number_arc;
1225 let left = OperationResult::Veto(VetoType::computation("left failed"));
1226 let right = LiteralValue::number(rational_new(1, 1));
1227 let number_ty = primitive_number_arc();
1228 let signature_index = SignatureIndex::new();
1229 let result = operate_on_operation_results(
1230 left,
1231 number_ty,
1232 &ArithmeticComputation::Add,
1233 OperationResult::from_literal(right),
1234 number_ty,
1235 &signature_index,
1236 );
1237 assert!(matches!(result, OperationResult::Veto(_)));
1238 }
1239
1240 fn build_engine_and_get_value(code: &str, spec: &str, rule: &str) -> (LiteralValue, String) {
1251 use crate::engine::Engine;
1252 use std::path::PathBuf;
1253 use std::sync::Arc;
1254 let mut engine = Engine::new();
1255 engine
1256 .load([(
1257 crate::parsing::source::SourceType::Path(Arc::new(PathBuf::from("t.lemma"))),
1258 code.to_string(),
1259 )])
1260 .expect("spec must load");
1261 let response = engine
1262 .evaluate(None, spec, None, IndexMap::new(), None, false)
1263 .expect("spec must evaluate");
1264 let rule_result = response
1265 .results
1266 .get(rule)
1267 .unwrap_or_else(|| panic!("rule '{}' missing", rule));
1268 if rule_result.vetoed {
1269 panic!("rule '{}' vetoed", rule);
1270 }
1271 (rule_result.to_literal(), rule_result.rule_type.clone())
1272 }
1273
1274 #[test]
1279 fn q_times_q_signature_hit_emits_named_lemma_type() {
1280 let code = r#"spec t
1281data money: measure
1282 -> unit eur: 1
1283data rate: measure
1284 -> unit eur_per_hour: eur/hour
1285data hour: measure
1286 -> unit hour: 1
1287data r: 30 eur_per_hour
1288data h: 2 hour
1289rule pay: r * h
1290"#;
1291 let (value, _) = build_engine_and_get_value(code, "t", "pay");
1292 match &value.value {
1295 ValueKind::Measure(_) => {}
1296 other => panic!("expected Measure result, got {:?}", other),
1297 }
1298 assert!(
1300 matches!(&value.value, ValueKind::Measure(_)),
1301 "result must be a Measure promoted via signature hit"
1302 );
1303 }
1304
1305 #[test]
1309 fn q_times_q_signature_miss_emits_anonymous_lemma_type() {
1310 let code = r#"spec t
1311data money: measure
1312 -> unit eur: 1
1313data rate: measure
1314 -> unit eur_per_minute: eur/minute
1315data hour: measure
1316 -> unit hour: 1
1317data r: 40 eur_per_minute
1318data h: 2 hour
1319rule weird: r * h
1320"#;
1321 use crate::engine::Engine;
1326 use std::path::PathBuf;
1327 use std::sync::Arc;
1328 let mut engine = Engine::new();
1329 let result = engine.load([(
1330 crate::parsing::source::SourceType::Path(Arc::new(PathBuf::from("t.lemma"))),
1331 code.to_string(),
1332 )]);
1333 assert!(
1334 result.is_err(),
1335 "rule producing anonymous Q*Q with no signature_index match must be rejected"
1336 );
1337 }
1338
1339 #[test]
1341 fn q_plus_q_same_signature() {
1342 let code = r#"spec t
1343data money: measure
1344 -> unit eur: 1
1345data a: 100 eur
1346data b: 50 eur
1347rule total: a + b
1348"#;
1349 let (value, _) = build_engine_and_get_value(code, "t", "total");
1350 match &value.value {
1351 ValueKind::Measure(n) => {
1352 let decimal = n.try_to_decimal().unwrap();
1353 assert_eq!(decimal, Decimal::from(150));
1354 }
1355 other => panic!("expected Measure, got {:?}", other),
1356 }
1357 }
1358
1359 #[test]
1363 fn q_plus_q_different_signature() {
1364 let code = r#"spec t
1365uses lemma units
1366data money: measure
1367 -> unit eur: 1
1368data rate: measure
1369 -> unit eur_per_second: eur/second
1370 -> unit eur_per_minute: eur/minute
1371data a: 10 eur_per_second
1372data b: 20 eur_per_minute
1373rule total_rate: (a + b) as eur_per_second
1374"#;
1375 let (value, _) = build_engine_and_get_value(code, "t", "total_rate");
1376 match &value.value {
1377 ValueKind::Measure(n) => {
1378 let decimal = n.try_to_decimal().unwrap();
1379 let expected_low = Decimal::new(10_333, 3);
1381 let expected_high = Decimal::new(10_334, 3);
1382 assert!(
1383 decimal >= expected_low && decimal <= expected_high,
1384 "expected ~10.333, got {}",
1385 decimal
1386 );
1387 }
1388 other => panic!("expected Measure, got {:?}", other),
1389 }
1390 }
1391
1392 #[test]
1396 fn number_divided_by_q_reciprocates_signature() {
1397 let code = r#"spec t
1398data duration_t: measure
1399 -> unit second: 1
1400data freq: measure
1401 -> unit per_second: second^-1
1402data d: 2 second
1403rule f: 1 / d
1404"#;
1405 let (value, rule_type) = build_engine_and_get_value(code, "t", "f");
1407 match &value.value {
1408 ValueKind::Measure(_) => {}
1409 other => panic!("expected Measure, got {:?}", other),
1410 }
1411 assert_eq!(
1412 rule_type, "freq",
1413 "1/second must promote to 'freq' (signature [(second,-1)] -> per_second)"
1414 );
1415 }
1416}