Skip to main content

lemma/computation/
arithmetic.rs

1//! Type-aware arithmetic operations
2
3use 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/// One resolved unit in a signature: declared name, owning family, exponent.
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
18pub struct SignatureFactor {
19    pub name: String,
20    pub family: TypeFamily,
21    pub exponent: i32,
22}
23
24/// Reverse index mapping a resolved unit signature to the unit (by name) and its owning measure type.
25pub type SignatureIndex = IndexMap<Vec<SignatureFactor>, (String, Arc<LemmaType>)>;
26
27/// Find the one indexed unit whose factor names and exponents match `names`.
28/// Two families with the same names are not one unit.
29pub(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/// Expand a raw signature (literal unit names) to derived base units, so that signature_index
66/// (which is keyed by `derived_measure_factors`) can be searched.
67///
68/// For each `(unit_name, exp)`:
69/// - Look up `unit_name`'s owning type in `unit_index`.
70/// - Replace it with its `derived_measure_factors`, scaled by `exp`.
71/// - When multiple units from the same family appear, normalize them to the canonical
72///   unit name so exponents can cancel (e.g. `hour` and `minute` both become `second`).
73/// - Single remaining units keep their declared name (e.g. `minute` stays `minute`).
74#[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
95/// Factor-1 unit of `family`; `None` when the family declares no such unit.
96fn 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
254/// Perform type-aware arithmetic operation, returning OperationResult (Veto for runtime errors).
255///
256/// `expression_units` and `signature_index` come from the plan's resolved types and are
257/// used during expression evaluation to resolve combined unit signatures back to a named
258/// unit and owning type.
259pub 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
404/// Arithmetic on operands that are not ranges. Never re-enters [`arithmetic_operation`], so
405/// the range helpers that call it are not part of a cycle.
406pub(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        // Number op Ratio → Number (multiply is symmetric; divide/power preserve order)
434        (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        // Ratio op Number → result type depends on operator
444        (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        // Ratio op Ratio → Ratio
468        (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        // Measure operations with Measure
484        (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        // Measure op Ratio → Measure (multiply/divide; add/subtract rejected at planning)
683        (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        // Ratio op Measure → Measure only for multiply (commutative); others rejected at planning
693        (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        // Measure op Number. Multiply and divide keep the operand unit. Power scales
710        // the composition, so the magnitude is unbound and the stamped type names the unit.
711        // Exponent 0 is the number 1. Modulo of a measure is a planning error.
712        (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        // Number op Measure → Measure for multiply; for divide, keep inverse dimensions
744        // as an unbound measure until a later cancel or signature hit.
745        (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
942/// Divide / modulo with OperationResult operands: validate the divisor first so a
943/// present definitive Computation factor settles intake even when the dividend is
944/// still MissingData (keys that cannot un-veto leave `missing_data`).
945pub(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    // ---------------------------------------------------------------------------
1213    // Phase 0 — Q*Q signature behaviour for the rewritten arithmetic arm.
1214    //
1215    // These tests use ValueKind::Measure with the OLD (RationalInteger, String,
1216    // BaseMeasureVector) shape today. After rewrite_measure_value_kind_shape lands,
1217    // they will be updated to (RationalInteger, Vec<(String,i32)>). They will fail today
1218    // because the Q*Q arm uses canonical magnitudes (not direct multiply), and the result
1219    // emission paths differ from the rewrite spec.
1220    // ---------------------------------------------------------------------------
1221
1222    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    /// Phase 0 — Q*Q producing a signature hit must emit a named lemma_type.
1247    /// Today the arithmetic arm uses canonical pre-conversion (not direct multiply),
1248    /// so the magnitude may differ. After the rewrite, the magnitude must equal the
1249    /// direct product of operand magnitudes.
1250    #[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        // Result must be a Measure. The signature [(eur,1),(hour,-1),(hour,1)] -> [(eur,1)]
1265        // must hit signature_index and emit lemma_type = money.
1266        match &value.value {
1267            ValueKind::Measure(_) => {}
1268            other => panic!("expected Measure result, got {:?}", other),
1269        }
1270        // Type lives on the rule / NormalForm after Change 5, not on LiteralValue.
1271        assert!(
1272            matches!(&value.value, ValueKind::Measure(_)),
1273            "result must be a Measure promoted via signature hit"
1274        );
1275    }
1276
1277    /// Phase 0 — Q*Q producing a signature miss must emit anonymous lemma_type.
1278    /// e.g., `eur_per_minute * hour` does not cancel; signature is
1279    /// [(eur,1),(hour,1),(minute,-1)] which has no signature_index entry.
1280    #[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        // The full evaluation will be rejected today by the rule boundary check (anonymous
1294        // intermediate). After the rewrite, planning either accepts (no matching named type
1295        // -> rejected with precise message) or accepts when the signature combines to a known
1296        // type. For this test, the expectation is that the rule is rejected today.
1297        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    /// Phase 0 — Q + Q with identical signatures sums magnitudes directly.
1312    #[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    /// Phase 0 — Q + Q with different but dimensionally-compatible signatures converts
1332    /// via signature_factor before summing.
1333    /// 10 eur_per_second + 20 eur_per_minute = 10 + (20/60) eur_per_second
1334    #[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                // 10 + 1/3 ≈ 10.333...
1352                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    /// Phase 0 — `number / measure` must produce a Measure whose signature is the
1365    /// negation of the rhs signature, and lemma_type from signature_index lookup or
1366    /// anonymous marker.
1367    #[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        // 1 / second = 1 per_second; signature_index hit yields named 'freq' type.
1378        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}