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                        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        // Ratio op Ratio → Ratio
469        (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        // Measure operations with Measure
486        (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        // Measure op Ratio → Measure (multiply/divide; add/subtract rejected at planning)
705        (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        // Ratio op Measure → Measure only for multiply (commutative); others rejected at planning
716        (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        // Measure op Number. Multiply and divide keep the operand unit. Power scales
734        // the composition, so the magnitude is unbound and the stamped type names the unit.
735        // Exponent 0 is the number 1. Modulo of a measure is a planning error.
736        (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        // Number op Measure → Measure for multiply; for divide, keep inverse dimensions
771        // as an unbound measure until a later cancel or signature hit.
772        (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
970/// Divide / modulo with OperationResult operands: validate the divisor first so a
971/// present definitive Computation factor settles intake even when the dividend is
972/// still MissingData (keys that cannot un-veto leave `missing_data`).
973pub(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    // ---------------------------------------------------------------------------
1241    // Phase 0 — Q*Q signature behaviour for the rewritten arithmetic arm.
1242    //
1243    // These tests use ValueKind::Measure with the OLD (RationalInteger, String,
1244    // BaseMeasureVector) shape today. After rewrite_measure_value_kind_shape lands,
1245    // they will be updated to (RationalInteger, Vec<(String,i32)>). They will fail today
1246    // because the Q*Q arm uses canonical magnitudes (not direct multiply), and the result
1247    // emission paths differ from the rewrite spec.
1248    // ---------------------------------------------------------------------------
1249
1250    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    /// Phase 0 — Q*Q producing a signature hit must emit a named lemma_type.
1275    /// Today the arithmetic arm uses canonical pre-conversion (not direct multiply),
1276    /// so the magnitude may differ. After the rewrite, the magnitude must equal the
1277    /// direct product of operand magnitudes.
1278    #[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        // Result must be a Measure. The signature [(eur,1),(hour,-1),(hour,1)] -> [(eur,1)]
1293        // must hit signature_index and emit lemma_type = money.
1294        match &value.value {
1295            ValueKind::Measure(_) => {}
1296            other => panic!("expected Measure result, got {:?}", other),
1297        }
1298        // Type lives on the rule / NormalForm after Change 5, not on LiteralValue.
1299        assert!(
1300            matches!(&value.value, ValueKind::Measure(_)),
1301            "result must be a Measure promoted via signature hit"
1302        );
1303    }
1304
1305    /// Phase 0 — Q*Q producing a signature miss must emit anonymous lemma_type.
1306    /// e.g., `eur_per_minute * hour` does not cancel; signature is
1307    /// [(eur,1),(hour,1),(minute,-1)] which has no signature_index entry.
1308    #[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        // The full evaluation will be rejected today by the rule boundary check (anonymous
1322        // intermediate). After the rewrite, planning either accepts (no matching named type
1323        // -> rejected with precise message) or accepts when the signature combines to a known
1324        // type. For this test, the expectation is that the rule is rejected today.
1325        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    /// Phase 0 — Q + Q with identical signatures sums magnitudes directly.
1340    #[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    /// Phase 0 — Q + Q with different but dimensionally-compatible signatures converts
1360    /// via signature_factor before summing.
1361    /// 10 eur_per_second + 20 eur_per_minute = 10 + (20/60) eur_per_second
1362    #[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                // 10 + 1/3 ≈ 10.333...
1380                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    /// Phase 0 — `number / measure` must produce a Measure whose signature is the
1393    /// negation of the rhs signature, and lemma_type from signature_index lookup or
1394    /// anonymous marker.
1395    #[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        // 1 / second = 1 per_second; signature_index hit yields named 'freq' type.
1406        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}