1use crate::parsing::arena::{ExprId, ExprKind, ExprTree};
9pub use crate::parsing::ast::{
10 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
11 RangeBound, RoundingMode, VetoExpression,
12};
13pub use crate::parsing::source::Source;
14
15#[must_use]
18pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
19 match op {
20 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
21 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
22 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
23 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
24 ComparisonComputation::Is => ComparisonComputation::IsNot,
25 ComparisonComputation::IsNot => ComparisonComputation::Is,
26 }
27}
28
29#[must_use]
32pub fn mirrored_comparison(op: ComparisonComputation) -> ComparisonComputation {
33 match op {
34 ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
35 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
36 ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
37 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
38 ComparisonComputation::Is => ComparisonComputation::Is,
39 ComparisonComputation::IsNot => ComparisonComputation::IsNot,
40 }
41}
42
43use crate::computation::rational::{checked_div, rational_new, RationalInteger};
45use crate::computation::OperationResult;
46use crate::parsing::ast::Constraint;
47use crate::parsing::ast::{
48 BooleanValue, CalendarPeriodUnit, ChainStep, CommandArg, ConversionTarget, ConversionTypeKind,
49 DateCalendarKind, DateRelativeKind, DateTimeValue, LogicalConnective, PrimitiveKind, TimeValue,
50 TimezoneValue, TypeConstraintCommand,
51};
52use crate::planning::normalize::NormalFormId;
53use crate::planning::unit_index::UnitIndex;
54use crate::Error;
55use indexmap::IndexMap;
56use rust_decimal::Decimal;
57use serde::{Deserialize, Deserializer, Serialize, Serializer};
58use std::collections::{BTreeMap, HashMap};
59use std::fmt;
60use std::hash::Hash;
61use std::str::FromStr;
62use std::sync::{Arc, OnceLock};
63
64pub use crate::literals::{
71 BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, UnitScale,
72};
73
74pub fn combine_decompositions(
78 left: &BaseMeasureVector,
79 right: &BaseMeasureVector,
80 is_multiply: bool,
81) -> Option<BaseMeasureVector> {
82 let mut result = left.clone();
83 for (dim, &exp) in right {
84 let delta = if is_multiply { exp } else { exp.checked_neg()? };
85 let entry = result.entry(dim.clone()).or_insert(0);
86 *entry = entry.checked_add(delta)?;
87 if entry.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
88 return None;
89 }
90 if *entry == 0 {
91 result.remove(dim);
92 }
93 }
94 Some(result)
95}
96
97pub fn scale_decomposition(base: &BaseMeasureVector, exponent: i32) -> Option<BaseMeasureVector> {
100 if exponent == 0 {
101 return Some(BaseMeasureVector::new());
102 }
103 let mut result = BaseMeasureVector::new();
104 for (dimension, &power) in base {
105 let scaled = power.checked_mul(exponent)?;
106 if scaled.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
107 return None;
108 }
109 if scaled != 0 {
110 result.insert(dimension.clone(), scaled);
111 }
112 }
113 Some(result)
114}
115
116#[cfg(test)]
123pub fn combine_signatures(
124 left: &[(String, i32)],
125 right: &[(String, i32)],
126 is_multiply: bool,
127) -> Vec<(String, i32)> {
128 use std::collections::BTreeMap;
129 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
130 for (name, exponent) in left {
131 *accumulator.entry(name.clone()).or_insert(0) += exponent;
132 }
133 for (name, exponent) in right {
134 let delta = if is_multiply { *exponent } else { -*exponent };
135 *accumulator.entry(name.clone()).or_insert(0) += delta;
136 }
137 accumulator
138 .into_iter()
139 .filter(|(_, exponent)| *exponent != 0)
140 .collect()
141}
142
143pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
156 let canonical = canonicalize_signature(signature);
157 let mut numerator: Vec<(String, i32)> = Vec::new();
158 let mut denominator: Vec<(String, i32)> = Vec::new();
159 for (name, exponent) in canonical {
160 if exponent > 0 {
161 numerator.push((name, exponent));
162 } else if exponent < 0 {
163 denominator.push((name, -exponent));
164 }
165 }
166 let render = |terms: &[(String, i32)]| -> String {
167 terms
168 .iter()
169 .map(|(name, exp)| {
170 if *exp == 1 {
171 name.clone()
172 } else {
173 format!("{name}^{exp}")
174 }
175 })
176 .collect::<Vec<_>>()
177 .join("*")
178 };
179 match (numerator.is_empty(), denominator.is_empty()) {
180 (true, true) => String::new(),
181 (false, true) => render(&numerator),
182 (true, false) => format!("1/{}", render(&denominator)),
183 (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
184 }
185}
186
187pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
194 use crate::computation::rational::rational_one;
195 match name {
196 "month" => Some(rational_one()),
197 "year" => Some(rational_new(12, 1)),
198 _ => None,
199 }
200}
201
202fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
203 use crate::computation::rational::rational_zero;
204 use std::cmp::Ordering;
205 match magnitude.try_cmp(&rational_zero()) {
206 Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
207 Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
208 Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
209 }
210}
211
212fn parse_unresolved_width_bound(
215 args: &[CommandArg],
216 cmd: &str,
217) -> Result<(RationalInteger, String), String> {
218 use crate::computation::rational::decimal_to_rational;
219 let lit = require_literal(args, cmd)?;
220 let (magnitude, unit_name) = match lit {
221 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
222 other => {
223 return Err(format!(
224 "{cmd} requires a measure literal with a unit, got {}",
225 value_kind_name(other)
226 ));
227 }
228 };
229 let magnitude_rational = decimal_to_rational(magnitude)
230 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
231 reject_negative_width_magnitude(&magnitude_rational, cmd)?;
232 Ok((magnitude_rational, unit_name))
233}
234
235pub(crate) fn check_range_bound_consistency(
240 spec: &TypeSpecification,
241 unit_index: &crate::planning::unit_index::UnitIndex,
242) -> Result<(), String> {
243 use std::cmp::Ordering;
244
245 fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
246 compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
247 != Ordering::Greater
248 }
249 fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
250 compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
251 }
252
253 match spec {
254 TypeSpecification::NumberRange {
255 lower,
256 upper,
257 minimum,
258 maximum,
259 ..
260 }
261 | TypeSpecification::RatioRange {
262 lower,
263 upper,
264 minimum,
265 maximum,
266 ..
267 } => {
268 if let (Some(lo), Some(hi)) = (lower, upper) {
269 if lo
270 .try_cmp(hi)
271 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
272 == Ordering::Greater
273 {
274 return Err(format!(
275 "invalid range: lower {} is greater than upper {}",
276 lo.display_str(),
277 hi.display_str()
278 ));
279 }
280 }
281 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
282 if min_w
283 .try_cmp(max_w)
284 .map_err(|failure| format!("range width compare failed: {failure}"))?
285 == Ordering::Greater
286 {
287 return Err(format!(
288 "invalid range: minimum width {} is greater than maximum width {}",
289 min_w.display_str(),
290 max_w.display_str()
291 ));
292 }
293 }
294 Ok(())
295 }
296 TypeSpecification::MeasureRange {
297 lower,
298 upper,
299 minimum,
300 maximum,
301 units,
302 ..
303 } => {
304 if let (Some(lo), Some(hi)) = (lower, upper) {
305 let lo_c =
306 measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
307 let hi_c =
308 measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
309 if lo_c
310 .try_cmp(&hi_c)
311 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
312 == Ordering::Greater
313 {
314 return Err(format!(
315 "invalid range: lower {} {} is greater than upper {} {}",
316 lo.0.display_str(),
317 lo.1,
318 hi.0.display_str(),
319 hi.1
320 ));
321 }
322 }
323 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
324 let min_c = measure_declared_bound_to_canonical(
325 &min_w.0, &min_w.1, units, "range", "minimum",
326 )?;
327 let max_c = measure_declared_bound_to_canonical(
328 &max_w.0, &max_w.1, units, "range", "maximum",
329 )?;
330 if min_c
331 .try_cmp(&max_c)
332 .map_err(|failure| format!("range width compare failed: {failure}"))?
333 == Ordering::Greater
334 {
335 return Err(format!(
336 "invalid range: minimum width {} {} is greater than maximum width {} {}",
337 min_w.0.display_str(),
338 min_w.1,
339 max_w.0.display_str(),
340 max_w.1
341 ));
342 }
343 }
344 Ok(())
345 }
346 TypeSpecification::DateRange {
347 lower,
348 upper,
349 minimum,
350 maximum,
351 ..
352 } => {
353 if let (Some(lo), Some(hi)) = (lower, upper) {
354 if !endpoint_order_ok_dates(lo, hi) {
355 return Err(format!(
356 "invalid range: lower {lo} is greater than upper {hi}"
357 ));
358 }
359 }
360 check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
361 }
362 TypeSpecification::TimeRange {
363 lower,
364 upper,
365 minimum,
366 maximum,
367 ..
368 } => {
369 if let (Some(lo), Some(hi)) = (lower, upper) {
370 if !endpoint_order_ok_times(lo, hi) {
371 return Err(format!(
372 "invalid range: lower {lo} is greater than upper {hi}"
373 ));
374 }
375 }
376 check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
377 }
378 _ => Ok(()),
379 }
380}
381
382fn check_temporal_width_pair_consistency(
383 minimum: &Option<(RationalInteger, String)>,
384 maximum: &Option<(RationalInteger, String)>,
385 unit_index: &crate::planning::unit_index::UnitIndex,
386 allow_calendar: bool,
387) -> Result<(), String> {
388 use std::cmp::Ordering;
389 let resolve = |bound: &(RationalInteger, String),
390 command: &str|
391 -> Result<(RationalInteger, Arc<LemmaType>), String> {
392 let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
393 format!(
394 "{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
395 bound.1
396 )
397 })?;
398 if allow_calendar {
399 if !owner.is_duration_like() && !owner.is_calendar_like() {
400 return Err(format!(
401 "{command} width unit '{bare}' must be a duration or calendar unit",
402 ));
403 }
404 } else if !owner.is_duration_like() {
405 return Err(format!(
406 "{command} width unit '{bare}' must be a duration unit",
407 ));
408 }
409 let TypeSpecification::Measure { units, .. } = &owner.specifications else {
410 return Err(format!(
411 "{command} width unit '{bare}' must resolve to a measure type",
412 ));
413 };
414 let canonical = measure_declared_bound_to_canonical(
415 &bound.0,
416 &bare,
417 units,
418 owner.name().as_str(),
419 command,
420 )?;
421 Ok((canonical, Arc::clone(&owner)))
422 };
423
424 match (minimum, maximum) {
425 (None, None) => Ok(()),
426 (Some(min_w), None) => {
427 resolve(min_w, "minimum")?;
428 Ok(())
429 }
430 (None, Some(max_w)) => {
431 resolve(max_w, "maximum")?;
432 Ok(())
433 }
434 (Some(min_w), Some(max_w)) => {
435 let (min_c, min_owner) = resolve(min_w, "minimum")?;
436 let (max_c, max_owner) = resolve(max_w, "maximum")?;
437 if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
438 return Err(
439 "invalid range: minimum and maximum width must not mix calendar and duration units"
440 .to_string(),
441 );
442 }
443 if min_c
444 .try_cmp(&max_c)
445 .map_err(|failure| format!("range width compare failed: {failure}"))?
446 == Ordering::Greater
447 {
448 return Err(format!(
449 "invalid range: minimum width {} {} is greater than maximum width {} {}",
450 min_w.0.display_str(),
451 min_w.1,
452 max_w.0.display_str(),
453 max_w.1
454 ));
455 }
456 Ok(())
457 }
458 }
459}
460
461fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
462 owner
463 .measure_unit_names()
464 .is_some_and(|names| names.contains(&unit_name))
465}
466
467pub fn signature_factor(
477 signature: &[(String, i32)],
478 expression_units: &crate::planning::unit_index::UnitIndex,
479 owner: Option<&LemmaType>,
480) -> Result<
481 crate::computation::rational::RationalInteger,
482 crate::computation::rational::NumericFailure,
483> {
484 use crate::computation::rational::{checked_div, checked_mul, rational_one};
485 let mut acc = rational_one();
486 for (name, exponent) in signature {
487 let factor =
488 if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
489 owner.measure_unit_factor(name).clone()
490 } else if let Some(lemma_type) = expression_units.unique_owner(name) {
491 lemma_type.measure_unit_factor(name).clone()
492 } else if !expression_units.owners_for(name).is_empty() {
493 panic!(
494 "BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
495 name
496 );
497 } else {
498 panic!(
499 "BUG: signature_factor called with unresolved unit name '{}'",
500 name
501 );
502 };
503 let mut term = rational_one();
504 let abs_exp = exponent.unsigned_abs();
505 for _ in 0..abs_exp {
506 term = checked_mul(&term, &factor)?;
507 }
508 if *exponent >= 0 {
509 acc = checked_mul(&acc, &term)?;
510 } else {
511 acc = checked_div(&acc, &term)?;
512 }
513 }
514 Ok(acc)
515}
516
517pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
518 use std::collections::BTreeMap;
519 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
520 for (name, exponent) in signature {
521 let entry = accumulator.entry(name.clone()).or_insert(0);
522 *entry = entry
523 .checked_add(*exponent)
524 .expect("BUG: planning bounds signature unit exponents");
525 }
526 accumulator
527 .into_iter()
528 .filter(|(_, exponent)| *exponent != 0)
529 .collect()
530}
531
532pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
534 decomposition
535 .iter()
536 .filter(|(_, exponent)| **exponent != 0)
537 .map(|(dimension, exponent)| (dimension.name().to_string(), *exponent))
538 .collect()
539}
540
541#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
543pub enum Dimension {
544 Duration,
545 Calendar,
546 Family(TypeFamily),
547}
548
549impl Dimension {
550 #[must_use]
552 pub fn name(&self) -> &str {
553 match self {
554 Dimension::Duration => "duration",
555 Dimension::Calendar => "calendar",
556 Dimension::Family(family) => &family.name,
557 }
558 }
559
560 #[must_use]
561 fn with_families_without_effective(&self) -> Self {
562 match self {
563 Dimension::Duration => Dimension::Duration,
564 Dimension::Calendar => Dimension::Calendar,
565 Dimension::Family(family) => Dimension::Family(family.without_effective()),
566 }
567 }
568}
569
570fn base_measure_vector_without_effective(domain: &BaseMeasureVector) -> BaseMeasureVector {
571 domain
572 .iter()
573 .map(|(dimension, exponent)| (dimension.with_families_without_effective(), *exponent))
574 .collect()
575}
576
577fn measure_units_without_effective(units: &MeasureUnits) -> MeasureUnits {
578 MeasureUnits(
579 units
580 .iter()
581 .map(|unit| MeasureUnit {
582 name: unit.name.clone(),
583 scale: unit.scale.clone(),
584 derived_measure_factors: unit.derived_measure_factors.clone(),
585 decomposition: base_measure_vector_without_effective(&unit.decomposition),
586 minimum: unit.minimum.clone(),
587 maximum: unit.maximum.clone(),
588 suggestion_magnitude: unit.suggestion_magnitude.clone(),
589 })
590 .collect(),
591 )
592}
593
594#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
595pub enum MeasureTrait {
596 Duration,
597 Calendar,
598}
599
600pub fn duration_decomposition() -> BaseMeasureVector {
601 [(Dimension::Duration, 1i32)].into_iter().collect()
602}
603
604pub fn calendar_decomposition() -> BaseMeasureVector {
605 [(Dimension::Calendar, 1i32)].into_iter().collect()
606}
607
608pub fn anonymous_measure_type() -> LemmaType {
612 LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
613}
614
615pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
618 signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
619}
620
621#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
625pub(crate) struct TypeOptions {
626 pub values: Vec<OptionValue>,
627 pub rule: Option<String>,
628}
629
630#[derive(Clone, Debug, PartialEq, Eq, Hash)]
632pub(crate) struct OptionValue {
633 pub value: BoundValueKind,
634}
635
636pub(crate) const SPEC_LIST_HAS_NO_OPTIONS: &str =
637 "`-> option` and `-> options` do not apply to a list of spec instances";
638
639pub(crate) fn spec_member_input_key(path: &[String], name: &str) -> String {
641 let mut key = String::new();
642 for segment in path {
643 key.push_str(segment);
644 key.push('.');
645 }
646 key.push_str(name);
647 key
648}
649
650impl TypeOptions {
651 #[must_use]
652 pub fn is_empty(&self) -> bool {
653 self.values.is_empty() && self.rule.is_none()
654 }
655}
656
657#[derive(Clone, Debug, PartialEq, Eq, Hash)]
658pub enum TypeSpecification {
659 Boolean {
660 help: String,
661 },
662 Measure {
663 minimum: Option<(RationalInteger, String)>,
664 maximum: Option<(RationalInteger, String)>,
665 units: MeasureUnits,
666 traits: Vec<MeasureTrait>,
667 decomposition: Option<BaseMeasureVector>,
672 unit: Option<String>,
674 help: String,
675 },
676 Number {
677 minimum: Option<RationalInteger>,
678 maximum: Option<RationalInteger>,
679 decimals: Option<u8>,
680 help: String,
681 },
682 NumberRange {
683 lower: Option<RationalInteger>,
684 upper: Option<RationalInteger>,
685 minimum: Option<RationalInteger>,
686 maximum: Option<RationalInteger>,
687 help: String,
688 },
689 Ratio {
690 minimum: Option<RationalInteger>,
691 maximum: Option<RationalInteger>,
692 units: RatioUnits,
693 unit: Option<String>,
695 help: String,
696 },
697 RatioRange {
698 lower: Option<RationalInteger>,
699 upper: Option<RationalInteger>,
700 minimum: Option<RationalInteger>,
701 maximum: Option<RationalInteger>,
702 units: RatioUnits,
703 help: String,
704 },
705 Text {
706 length: Option<usize>,
707 help: String,
708 },
709 Date {
710 minimum: Option<DateTimeValue>,
711 maximum: Option<DateTimeValue>,
712 help: String,
713 },
714 DateRange {
715 lower: Option<DateTimeValue>,
716 upper: Option<DateTimeValue>,
717 minimum: Option<(RationalInteger, String)>,
718 maximum: Option<(RationalInteger, String)>,
719 help: String,
720 },
721 Time {
722 minimum: Option<TimeValue>,
723 maximum: Option<TimeValue>,
724 help: String,
725 },
726 TimeRange {
727 lower: Option<TimeValue>,
728 upper: Option<TimeValue>,
729 minimum: Option<(RationalInteger, String)>,
730 maximum: Option<(RationalInteger, String)>,
731 help: String,
732 },
733 MeasureRange {
734 lower: Option<(RationalInteger, String)>,
735 upper: Option<(RationalInteger, String)>,
736 minimum: Option<(RationalInteger, String)>,
737 maximum: Option<(RationalInteger, String)>,
738 units: MeasureUnits,
739 decomposition: Option<BaseMeasureVector>,
740 help: String,
741 },
742 Veto {
743 message: Option<String>,
744 },
745 Undetermined,
749 Spec {
753 spec: SpecIdentity,
754 },
755 List {
757 element: Arc<LemmaType>,
758 key: ListKey,
759 help: String,
760 },
761}
762
763#[derive(Clone, Debug, PartialEq, Eq, Hash)]
765pub struct KeyField {
766 pub name: String,
767 pub path: DataPath,
768}
769
770#[derive(Clone, Debug, PartialEq, Eq, Hash)]
772pub enum ListKey {
773 Numbered,
774 Fields(Vec<KeyField>),
775}
776
777#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
780pub(crate) struct SpecVersionKey {
781 pub repository: Option<String>,
782 pub spec: String,
783 pub effective: crate::parsing::ast::EffectiveDate,
784}
785
786impl SpecVersionKey {
787 #[must_use]
788 pub(crate) fn of(
789 repository: &crate::parsing::ast::LemmaRepository,
790 spec: &crate::parsing::ast::LemmaSpec,
791 ) -> Self {
792 Self {
793 repository: repository.name.clone(),
794 spec: spec.name.clone(),
795 effective: spec.effective_from.clone(),
796 }
797 }
798}
799
800#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
808pub struct SpecIdentity {
809 pub repository: Option<String>,
810 pub spec: String,
811 pub effective: crate::parsing::ast::EffectiveDate,
812 pub slice: crate::parsing::ast::EffectiveDate,
813}
814
815impl SpecIdentity {
816 #[must_use]
817 pub(crate) fn of(
818 repository: &crate::parsing::ast::LemmaRepository,
819 spec: &crate::parsing::ast::LemmaSpec,
820 slice: crate::parsing::ast::EffectiveDate,
821 ) -> Self {
822 assert!(
823 slice >= spec.effective_from,
824 "BUG: slice {slice:?} of spec '{}' starts before its version",
825 spec.name
826 );
827 Self {
828 repository: repository.name.clone(),
829 spec: spec.name.clone(),
830 effective: spec.effective_from.clone(),
831 slice,
832 }
833 }
834}
835
836impl fmt::Display for SpecIdentity {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 if let Some(repository) = &self.repository {
841 write!(f, "{repository} ")?;
842 }
843 write!(f, "{}", self.spec)?;
844 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
845 write!(f, " {at}")?;
846 }
847 if self.slice != self.effective {
848 let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.slice else {
849 unreachable!("BUG: a slice after its version's start is a date");
850 };
851 write!(f, " slice {at}")?;
852 }
853 Ok(())
854 }
855}
856
857impl std::fmt::Display for TypeSpecification {
858 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
859 match self {
860 Self::Boolean { .. } => f.write_str("boolean"),
861 Self::Measure { .. } => f.write_str("measure"),
862 Self::MeasureRange { .. } => f.write_str("measure range"),
863 Self::Number { .. } => f.write_str("number"),
864 Self::NumberRange { .. } => f.write_str("number range"),
865 Self::Text { .. } => f.write_str("text"),
866 Self::Date { .. } => f.write_str("date"),
867 Self::DateRange { .. } => f.write_str("date range"),
868 Self::Time { .. } => f.write_str("time"),
869 Self::TimeRange { .. } => f.write_str("time range"),
870 Self::Ratio { .. } => f.write_str("ratio"),
871 Self::RatioRange { .. } => f.write_str("ratio range"),
872 Self::Veto { .. } => f.write_str("veto"),
873 Self::Undetermined => f.write_str("undetermined"),
874 Self::Spec { spec } => write!(f, "{spec}"),
875 Self::List {
876 element,
877 key: ListKey::Numbered,
878 ..
879 } => write!(f, "{} list", element.specifications),
880 Self::List {
881 element,
882 key: ListKey::Fields(fields),
883 ..
884 } => write!(
885 f,
886 "{} list key {}",
887 element.specifications,
888 fields
889 .iter()
890 .map(|field| field.name.as_str())
891 .collect::<Vec<_>>()
892 .join(", ")
893 ),
894 }
895 }
896}
897
898impl TypeSpecification {
899 pub fn help(&self) -> &str {
901 match self {
902 Self::Boolean { help, .. }
903 | Self::Measure { help, .. }
904 | Self::Number { help, .. }
905 | Self::NumberRange { help, .. }
906 | Self::Text { help, .. }
907 | Self::Date { help, .. }
908 | Self::DateRange { help, .. }
909 | Self::Time { help, .. }
910 | Self::TimeRange { help, .. }
911 | Self::Ratio { help, .. }
912 | Self::RatioRange { help, .. }
913 | Self::MeasureRange { help, .. } => help.as_str(),
914 Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
915 Self::List { element, help, .. } => {
916 if help.is_empty() {
917 element.specifications.help()
918 } else {
919 help.as_str()
920 }
921 }
922 }
923 }
924
925 #[must_use]
927 pub fn is_unresolved_measure_range(&self) -> bool {
928 matches!(
929 self,
930 Self::MeasureRange {
931 units,
932 decomposition: None,
933 ..
934 } if units.0.is_empty()
935 )
936 }
937
938 #[must_use]
939 fn with_families_without_effective(&self) -> Self {
940 match self {
941 Self::Measure {
942 minimum,
943 maximum,
944 units,
945 traits,
946 decomposition,
947 unit,
948 help,
949 } => Self::Measure {
950 minimum: minimum.clone(),
951 maximum: maximum.clone(),
952 units: measure_units_without_effective(units),
953 traits: traits.clone(),
954 decomposition: decomposition
955 .as_ref()
956 .map(base_measure_vector_without_effective),
957 unit: unit.clone(),
958 help: help.clone(),
959 },
960 Self::MeasureRange {
961 lower,
962 upper,
963 minimum,
964 maximum,
965 units,
966 decomposition,
967 help,
968 } => Self::MeasureRange {
969 lower: lower.clone(),
970 upper: upper.clone(),
971 minimum: minimum.clone(),
972 maximum: maximum.clone(),
973 units: measure_units_without_effective(units),
974 decomposition: decomposition
975 .as_ref()
976 .map(base_measure_vector_without_effective),
977 help: help.clone(),
978 },
979 Self::List { element, key, help } => Self::List {
980 element: Arc::new(element.with_families_without_effective()),
981 key: key.clone(),
982 help: help.clone(),
983 },
984 other => other.clone(),
985 }
986 }
987
988 #[must_use]
992 pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
993 match (self, other) {
994 (
995 Self::Measure {
996 minimum: a_min,
997 maximum: a_max,
998 units: a_units,
999 traits: a_traits,
1000 decomposition: a_decomp,
1001 help: a_help,
1002 unit: _,
1003 },
1004 Self::Measure {
1005 minimum: b_min,
1006 maximum: b_max,
1007 units: b_units,
1008 traits: b_traits,
1009 decomposition: b_decomp,
1010 help: b_help,
1011 unit: _,
1012 },
1013 ) => {
1014 a_min == b_min
1015 && a_max == b_max
1016 && a_units == b_units
1017 && a_traits == b_traits
1018 && a_decomp == b_decomp
1019 && a_help == b_help
1020 }
1021 (
1022 Self::Ratio {
1023 minimum: a_min,
1024 maximum: a_max,
1025 units: a_units,
1026 help: a_help,
1027 unit: _,
1028 },
1029 Self::Ratio {
1030 minimum: b_min,
1031 maximum: b_max,
1032 units: b_units,
1033 help: b_help,
1034 unit: _,
1035 },
1036 ) => a_min == b_min && a_max == b_max && a_units == b_units && a_help == b_help,
1037 _ => self == other,
1038 }
1039 }
1040}
1041
1042fn require_literal<'a>(
1048 args: &'a [CommandArg],
1049 cmd: &str,
1050) -> Result<&'a crate::literals::Value, String> {
1051 if args.len() > 1 {
1052 return Err(format!("{cmd} takes exactly one value"));
1053 }
1054 let arg = args
1055 .first()
1056 .ok_or_else(|| format!("{} requires an argument", cmd))?;
1057 match arg {
1058 CommandArg::Literal(v) => Ok(v),
1059 CommandArg::Label(name) => Err(format!(
1060 "{} requires a literal value, got identifier '{}'",
1061 cmd, name
1062 )),
1063 CommandArg::UnitExpr(_) => Err(format!(
1064 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
1065 cmd
1066 )),
1067 }
1068}
1069
1070fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
1071 match require_literal(args, "help")? {
1072 crate::literals::Value::Text(s) => {
1073 *help = s.clone();
1074 Ok(())
1075 }
1076 other => Err(format!(
1077 "help requires a text literal (quoted string), got {}",
1078 value_kind_name(other)
1079 )),
1080 }
1081}
1082
1083fn format_measure_units_list(units: &MeasureUnits) -> String {
1084 units
1085 .iter()
1086 .map(|u| u.name.as_str())
1087 .collect::<Vec<_>>()
1088 .join(", ")
1089}
1090
1091#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1093pub(crate) enum SuggestionExpectation {
1094 MeasureUnits,
1095 Text,
1096 Number,
1097 Boolean,
1098 Date,
1099 Time,
1100 Ratio,
1101 NumberRange,
1102 DateRange,
1103 TimeRange,
1104 MeasureRange,
1105 RatioRange,
1106}
1107
1108pub(crate) fn suggestion_value_mismatch_error(
1109 calendar_unit: &str,
1110 type_name: &str,
1111 expectation: SuggestionExpectation,
1112 measure_units: Option<&MeasureUnits>,
1113) -> String {
1114 let unit_label = calendar_unit;
1115 let first = format!("Unit '{unit_label}' is for calendar data.");
1116 match expectation {
1117 SuggestionExpectation::MeasureUnits => {
1118 let list = measure_units
1119 .map(format_measure_units_list)
1120 .unwrap_or_default();
1121 format!("{first} Valid '{type_name}' units are: {list}.")
1122 }
1123 SuggestionExpectation::Text => format!(
1124 "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
1125 ),
1126 SuggestionExpectation::Number => format!(
1127 "{first} Please provide a number, for example `-> suggest 42`."
1128 ),
1129 SuggestionExpectation::Boolean => format!(
1130 "{first} Please provide true or false, for example `-> suggest true`."
1131 ),
1132 SuggestionExpectation::Date => format!(
1133 "{first} Please provide a date, for example `-> suggest 2024-06-15`."
1134 ),
1135 SuggestionExpectation::Time => format!(
1136 "{first} Please provide a time, for example `-> suggest 09:00:00`."
1137 ),
1138 SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
1139 "{first} Please provide a ratio, for example `-> suggest 25%`."
1140 ),
1141 SuggestionExpectation::NumberRange => format!(
1142 "{first} Please provide a number range, for example `-> suggest 10...100`."
1143 ),
1144 SuggestionExpectation::DateRange => format!(
1145 "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1146 ),
1147 SuggestionExpectation::TimeRange => format!(
1148 "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
1149 ),
1150 SuggestionExpectation::MeasureRange => format!(
1151 "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1152 ),
1153 }
1154}
1155
1156fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
1157 let example = if traits.contains(&MeasureTrait::Duration) {
1158 "4 week"
1159 } else if traits.contains(&MeasureTrait::Calendar) {
1160 "3 month"
1161 } else {
1162 "30 kilogram"
1163 };
1164 format!(
1165 "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
1166 )
1167}
1168
1169#[derive(Clone, Copy, Debug)]
1172pub struct ConstraintNames<'a> {
1173 pub type_name: &'a str,
1174 pub data_name: &'a str,
1175}
1176
1177fn decimals_depend_on_the_unit_error(
1178 kind: &str,
1179 names: ConstraintNames<'_>,
1180 args: &[CommandArg],
1181 first_unit: Option<&str>,
1182) -> Result<String, String> {
1183 let ConstraintNames {
1184 type_name,
1185 data_name,
1186 } = names;
1187 let places = decimal_to_u8(require_decimal_literal(args, "decimals")?, "decimals")?;
1188 let step = match places {
1189 0 => "1".to_string(),
1190 _ => format!("0.{}1", "0".repeat(usize::from(places) - 1)),
1191 };
1192 let advice = match first_unit {
1193 Some(unit) => {
1194 format!("Round in a rule instead, for example '{data_name} -> round {step} {unit}'.")
1195 }
1196 None => format!(
1197 "Round in a rule instead, for example '{data_name} -> round' followed by a step of {step} in one of the type's units."
1198 ),
1199 };
1200 Ok(format!("'decimals' is not valid on {kind} type '{type_name}': precision depends on the unit. {advice}"))
1201}
1202
1203fn reject_calendar_for_suggestion(
1204 value: &crate::literals::Value,
1205 type_name: &str,
1206 expectation: SuggestionExpectation,
1207 measure_units: Option<&MeasureUnits>,
1208) -> Result<(), String> {
1209 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
1210 if calendar_unit_factor(unit).is_some() {
1211 return Err(suggestion_value_mismatch_error(
1212 unit,
1213 type_name,
1214 expectation,
1215 measure_units,
1216 ));
1217 }
1218 }
1219 Ok(())
1220}
1221
1222fn value_kind_name(v: &crate::literals::Value) -> &'static str {
1224 use crate::literals::Value;
1225 match v {
1226 Value::Number(_) => "number",
1227 Value::NumberWithUnit(_, _) => "number_with_unit",
1228 Value::Text(_) => "text",
1229 Value::Date(_) => "date",
1230 Value::Time(_) => "time",
1231 Value::Boolean(_) => "boolean",
1232 Value::Range(_, _) => "range",
1233 }
1234}
1235
1236fn require_suggestion_range_endpoints<'a>(
1237 args: &'a [CommandArg],
1238 type_name: &str,
1239 expectation: SuggestionExpectation,
1240 measure_units: Option<&MeasureUnits>,
1241) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
1242 match require_literal(args, "suggest")? {
1243 crate::literals::Value::NumberWithUnit(_, unit)
1244 if calendar_unit_factor(unit).is_some() =>
1245 {
1246 Err(suggestion_value_mismatch_error(
1247 unit,
1248 type_name,
1249 expectation,
1250 measure_units,
1251 ))
1252 }
1253 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
1254 _ => Err(match expectation {
1255 SuggestionExpectation::NumberRange => {
1256 "Please provide a number range, for example `-> suggest 10...100`.".to_string()
1257 }
1258 SuggestionExpectation::DateRange => {
1259 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1260 .to_string()
1261 }
1262 SuggestionExpectation::RatioRange => {
1263 "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
1264 }
1265 SuggestionExpectation::MeasureRange => format!(
1266 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1267 ),
1268 _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
1269 }),
1270 }
1271}
1272
1273fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
1274 crate::computation::rational::decimal_to_rational(decimal)
1275 .map_err(|failure| format!("literal failed rational lift: {failure}"))
1276}
1277
1278pub fn range_element_type_specification(
1280 range_spec: &TypeSpecification,
1281) -> Option<TypeSpecification> {
1282 range_spec.element_from_range()
1283}
1284
1285fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
1286 match (&left.specifications, &right.specifications) {
1287 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1288 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1289 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1290 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1291 left.same_measure_family(right)
1292 }
1293 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1294 _ => false,
1295 }
1296}
1297
1298pub fn range_type_specification_from_endpoints(
1300 left: &LemmaType,
1301 right: &LemmaType,
1302) -> Option<TypeSpecification> {
1303 if !range_endpoints_compatible(left, right) {
1304 return None;
1305 }
1306 left.specifications.range_from_element()
1307}
1308
1309pub enum UnitTokenResolver<'a> {
1312 Declared {
1314 unit_index: &'a UnitIndex,
1315 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1316 },
1317 Undeclared,
1319 Planning {
1321 unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
1322 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1323 },
1324 Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
1326 Typed {
1329 unit_index: &'a UnitIndex,
1330 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1331 lemma_type: &'a LemmaType,
1332 },
1333}
1334
1335impl UnitTokenResolver<'_> {
1336 pub fn resolve(&self, unit: &str) -> Result<String, String> {
1337 match self {
1338 UnitTokenResolver::Declared {
1339 unit_index,
1340 named_types,
1341 } => unit_index
1342 .resolve_with_named_types(unit, named_types)
1343 .map(|(bare, _)| bare),
1344 UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
1345 UnitTokenResolver::Planning {
1346 unit_index,
1347 named_types,
1348 } => unit_index
1349 .resolve_with_named_types(unit, named_types)
1350 .map(|(bare, _)| bare),
1351 UnitTokenResolver::Search(scopes) => scopes
1352 .iter()
1353 .find_map(|(unit_index, named_types)| {
1354 unit_index
1355 .resolve_with_named_types(unit, named_types)
1356 .ok()
1357 .map(|(bare, _)| bare)
1358 })
1359 .ok_or_else(|| unknown_unit_message(unit)),
1360 UnitTokenResolver::Typed {
1361 unit_index,
1362 named_types,
1363 lemma_type,
1364 } => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
1365 }
1366 }
1367}
1368
1369fn unknown_unit_message(unit: &str) -> String {
1370 format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
1371}
1372
1373fn resolve_declared_on_type(
1374 written: &str,
1375 lemma_type: &LemmaType,
1376 unit_index: &UnitIndex,
1377 named_types: &IndexMap<String, Arc<LemmaType>>,
1378) -> Result<String, String> {
1379 match unit_index.resolve_with_named_types(written, named_types) {
1380 Ok((bare, _)) => {
1381 if unit_name_declared_on_type(lemma_type, &bare)
1382 || range_element_declares_unit(lemma_type, &bare)
1383 {
1384 Ok(bare)
1385 } else {
1386 Err(format!(
1387 "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
1388 lemma_type.name()
1389 ))
1390 }
1391 }
1392 Err(index_err) => {
1393 if unit_name_declared_on_type(lemma_type, written)
1394 || range_element_declares_unit(lemma_type, written)
1395 {
1396 Ok(written.to_string())
1397 } else {
1398 Err(index_err)
1399 }
1400 }
1401 }
1402}
1403
1404fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
1405 let Some(element) = lemma_type.specifications.element_from_range() else {
1406 return false;
1407 };
1408 unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
1409}
1410
1411fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
1412 match &lemma_type.specifications {
1413 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
1414 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
1415 _ => false,
1416 }
1417}
1418
1419fn bare_declared_unit_on_element(
1421 element_spec: &TypeSpecification,
1422 token: &str,
1423) -> Result<String, String> {
1424 match element_spec {
1425 TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1426 TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1427 other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1428 }
1429}
1430
1431pub fn resolve_endpoint_unit_token(
1437 token: &str,
1438 element_spec: &TypeSpecification,
1439 resolve_unit: Option<&UnitTokenResolver<'_>>,
1440) -> Result<String, String> {
1441 match bare_declared_unit_on_element(element_spec, token) {
1442 Ok(bare) => Ok(bare),
1443 Err(declared_err) => {
1444 let Some(resolve) = resolve_unit else {
1445 return Err(declared_err);
1446 };
1447 let bare = resolve.resolve(token)?;
1448 bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1449 format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1450 })
1451 }
1452 }
1453}
1454
1455pub(crate) fn lift_range_endpoint(
1464 value: &crate::parsing::ast::Value,
1465 element_type: &LemmaType,
1466 resolve_unit: Option<&UnitTokenResolver<'_>>,
1467) -> Result<TypedLiteral, String> {
1468 use crate::parsing::ast::Value;
1469 let element_spec = &element_type.specifications;
1470 match value {
1471 Value::NumberWithUnit(magnitude, unit_token) => {
1472 let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1473 let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
1474 let lemma_type = match &kind {
1475 ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
1476 Arc::new(element_type.clone().with_unit(bare))
1477 }
1478 _ => Arc::new(element_type.clone()),
1479 };
1480 Ok(TypedLiteral {
1481 value: kind,
1482 lemma_type,
1483 })
1484 }
1485 _ => {
1486 let kind = parser_scalar_to_value_kind(value, element_spec)?;
1487 Ok(TypedLiteral {
1488 value: kind,
1489 lemma_type: Arc::new(element_type.clone()),
1490 })
1491 }
1492 }
1493}
1494
1495fn literal_value_from_parser_value(
1496 value: &crate::parsing::ast::Value,
1497) -> Result<TypedLiteral, String> {
1498 use crate::parsing::ast::Value;
1499
1500 enum Step<'a> {
1501 Visit(&'a Value),
1502 CombineRange,
1503 }
1504
1505 let mut steps = vec![Step::Visit(value)];
1506 let mut literals: Vec<TypedLiteral> = Vec::new();
1507 while let Some(step) = steps.pop() {
1508 match step {
1509 Step::Visit(Value::Range(left, right)) => {
1510 steps.push(Step::CombineRange);
1511 steps.push(Step::Visit(right));
1512 steps.push(Step::Visit(left));
1513 }
1514 Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
1515 Step::CombineRange => {
1516 let right = literals
1517 .pop()
1518 .expect("BUG: range right endpoint missing from literal stack");
1519 let left = literals
1520 .pop()
1521 .expect("BUG: range left endpoint missing from literal stack");
1522 let compatible = match (
1523 &left.lemma_type.specifications,
1524 &right.lemma_type.specifications,
1525 ) {
1526 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1527 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1528 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1529 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1530 left.lemma_type.same_measure_family(&right.lemma_type)
1531 || left
1532 .lemma_type
1533 .compatible_with_anonymous_measure(&right.lemma_type)
1534 || right
1535 .lemma_type
1536 .compatible_with_anonymous_measure(&left.lemma_type)
1537 }
1538 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1539 _ => false,
1540 };
1541 if !compatible {
1542 return Err(format!(
1543 "range endpoints must have the same supported base type, got {} and {}",
1544 left.lemma_type.name(),
1545 right.lemma_type.name()
1546 ));
1547 }
1548 literals.push(TypedLiteral::range(left, right));
1549 }
1550 }
1551 }
1552 Ok(literals.pop().expect("BUG: literal walk left no result"))
1553}
1554
1555fn scalar_literal_from_parser_value(
1556 value: &crate::parsing::ast::Value,
1557) -> Result<TypedLiteral, String> {
1558 use crate::parsing::ast::Value;
1559
1560 match value {
1561 Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1562 Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1563 Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1564 Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1565 Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1566 Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
1567 lift_parser_decimal(*n)?,
1568 )),
1569 Value::Range(_, _) => {
1570 unreachable!("BUG: range values are walked by literal_value_from_parser_value")
1571 }
1572 }
1573}
1574
1575fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1577 if !d.is_integer() {
1578 return Err(format!(
1579 "{} requires a whole number, got fractional value",
1580 ctx
1581 ));
1582 }
1583 d.numer_to_u8()
1584 .ok_or_else(|| format!("{} value out of range for u8", ctx))
1585}
1586
1587fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1589 if !d.is_integer() {
1590 return Err(format!(
1591 "{} requires a whole number, got fractional value",
1592 ctx
1593 ));
1594 }
1595 d.numer_to_usize()
1596 .ok_or_else(|| format!("{} value out of range for usize", ctx))
1597}
1598
1599fn ratio_bound_to_canonical_rational(
1605 args: &[CommandArg],
1606 cmd: &str,
1607 units: &RatioUnits,
1608) -> Result<RationalInteger, String> {
1609 use crate::computation::rational::{checked_div, decimal_to_rational};
1610 let lit = require_literal(args, cmd)?;
1611 match lit {
1612 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1613 let unit = units.get(unit_name.as_str())?;
1614 let magnitude_rational = decimal_to_rational(*magnitude)
1615 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1616 checked_div(&magnitude_rational, &unit.value)
1617 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1618 }
1619 other => Err(format!(
1620 "{cmd} requires a ratio literal with a unit, got {}",
1621 value_kind_name(other)
1622 )),
1623 }
1624}
1625
1626fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1627 use crate::computation::rational::decimal_to_rational;
1628 match require_literal(args, cmd)? {
1629 crate::literals::Value::Number(d) => decimal_to_rational(*d)
1630 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1631 other => Err(format!(
1632 "{} requires a number literal, got {}",
1633 cmd,
1634 value_kind_name(other)
1635 )),
1636 }
1637}
1638
1639enum UnitConstraintField {
1640 Minimum,
1641 Maximum,
1642 SuggestionMagnitude,
1643}
1644
1645fn apply_measure_unit_factor(
1646 units: &mut MeasureUnits,
1647 unit_name: String,
1648 value: rust_decimal::Decimal,
1649 derived_measure_factors: Vec<(String, i32)>,
1650) -> Result<(), String> {
1651 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1652 let new_factor = crate::computation::rational::decimal_to_rational(value)
1653 .map_err(|failure| failure.to_string())?;
1654 if existing.literal_factor() != Some(&new_factor)
1655 || existing.derived_measure_factors != derived_measure_factors
1656 {
1657 return Err(format!(
1658 "Unit '{unit_name}' is already defined in this type's inherited units; \
1659 cannot change factor or decomposition. Add a new unit name instead."
1660 ));
1661 }
1662 return Ok(());
1663 }
1664 units.0.push(MeasureUnit::from_decimal_factor(
1665 unit_name,
1666 value,
1667 derived_measure_factors,
1668 )?);
1669 Ok(())
1670}
1671
1672pub(crate) fn measure_declared_bound_to_canonical(
1673 magnitude: &RationalInteger,
1674 unit_name: &str,
1675 units: &MeasureUnits,
1676 type_name: &str,
1677 command: &str,
1678) -> Result<RationalInteger, String> {
1679 use crate::computation::rational::checked_mul;
1680 let unit = units.get(unit_name).map_err(|_| {
1681 format!(
1682 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1683 format_measure_units_list(units)
1684 )
1685 })?;
1686 let factor = unit.literal_factor().ok_or_else(|| {
1687 format!(
1688 "{command} cannot use unit '{unit_name}' on '{type_name}': its conversion factor is a reference and is not known at planning."
1689 )
1690 })?;
1691 checked_mul(magnitude, factor)
1692 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1693}
1694
1695fn parse_measure_declared_bound(
1696 args: &[CommandArg],
1697 cmd: &str,
1698 units: &MeasureUnits,
1699 type_name: &str,
1700) -> Result<(RationalInteger, String), String> {
1701 use crate::computation::rational::decimal_to_rational;
1702 let lit = require_literal(args, cmd)?;
1703 let (magnitude, unit_name) = match lit {
1704 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1705 other => {
1706 return Err(format!(
1707 "{cmd} requires a measure literal with a unit, got {}",
1708 value_kind_name(other)
1709 ));
1710 }
1711 };
1712 units.get(unit_name.as_str()).map_err(|_| {
1713 format!(
1714 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1715 format_measure_units_list(units)
1716 )
1717 })?;
1718 let magnitude_rational = decimal_to_rational(magnitude)
1719 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1720 Ok((magnitude_rational, unit_name))
1721}
1722
1723fn sync_measure_units_from_canonical(
1724 units: &mut MeasureUnits,
1725 canonical: &RationalInteger,
1726 field: UnitConstraintField,
1727) -> Result<(), String> {
1728 use crate::computation::rational::checked_div;
1729 for unit in &mut units.0 {
1730 let Some(factor) = unit.literal_factor() else {
1731 continue;
1732 };
1733 let magnitude = checked_div(canonical, factor).map_err(|failure| {
1734 format!(
1735 "cannot derive per-unit constraint for unit '{}': {failure}",
1736 unit.name
1737 )
1738 })?;
1739 match field {
1740 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1741 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1742 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1743 }
1744 }
1745 Ok(())
1746}
1747
1748fn sync_ratio_units_from_canonical(
1749 units: &mut RatioUnits,
1750 canonical: &RationalInteger,
1751 field: UnitConstraintField,
1752) -> Result<(), String> {
1753 use crate::computation::rational::checked_mul;
1754 for unit in &mut units.0 {
1755 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1756 format!(
1757 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1758 unit.name
1759 )
1760 })?;
1761 match field {
1762 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1763 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1764 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1765 }
1766 }
1767 Ok(())
1768}
1769
1770fn sync_measure_suggestion_units(
1771 units: &mut MeasureUnits,
1772 default: &ValueKind,
1773 type_name: &str,
1774) -> Result<(), String> {
1775 let ValueKind::Measure(magnitude) = default else {
1776 return Ok(());
1777 };
1778 let unit_name = units
1779 .iter()
1780 .find(|unit| unit.is_canonical_factor())
1781 .or_else(|| units.iter().next())
1782 .map(|unit| unit.name.as_str())
1783 .expect(
1784 "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1785 );
1786 units.get(unit_name).map_err(|_| {
1787 format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1788 })?;
1789 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1790}
1791
1792pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1793 specification: &mut TypeSpecification,
1794 declared_suggestion: Option<&ValueKind>,
1795 type_name: &str,
1796) -> Result<(), String> {
1797 let TypeSpecification::Measure {
1798 minimum,
1799 maximum,
1800 units,
1801 ..
1802 } = specification
1803 else {
1804 return Ok(());
1805 };
1806
1807 let mut synced_minimum = false;
1808 if let Some(bound) = minimum.as_ref() {
1809 let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
1810 if bound_unit.literal_factor().is_some() {
1811 let canonical = measure_declared_bound_to_canonical(
1812 &bound.0, &bound.1, units, type_name, "minimum",
1813 )?;
1814 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1815 synced_minimum = true;
1816 }
1817 }
1818 let mut synced_maximum = false;
1819 if let Some(bound) = maximum.as_ref() {
1820 let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
1821 if bound_unit.literal_factor().is_some() {
1822 let canonical = measure_declared_bound_to_canonical(
1823 &bound.0, &bound.1, units, type_name, "maximum",
1824 )?;
1825 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1826 synced_maximum = true;
1827 }
1828 }
1829 let mut synced_suggestion = false;
1830 if let Some(default) = declared_suggestion {
1831 if matches!(default, ValueKind::Measure(_)) {
1832 sync_measure_suggestion_units(units, default, type_name)?;
1833 synced_suggestion = true;
1834 }
1835 }
1836
1837 if synced_minimum {
1838 for unit in units.iter() {
1839 if unit.literal_factor().is_none() {
1840 continue;
1841 }
1842 assert!(
1843 unit.minimum.is_some(),
1844 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1845 unit.name
1846 );
1847 }
1848 }
1849 if synced_maximum {
1850 for unit in units.iter() {
1851 if unit.literal_factor().is_none() {
1852 continue;
1853 }
1854 assert!(
1855 unit.maximum.is_some(),
1856 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1857 unit.name
1858 );
1859 }
1860 }
1861 if synced_suggestion {
1862 for unit in units.iter() {
1863 if unit.literal_factor().is_none() {
1864 continue;
1865 }
1866 assert!(
1867 unit.suggestion_magnitude.is_some(),
1868 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1869 unit.name
1870 );
1871 }
1872 }
1873
1874 Ok(())
1875}
1876
1877fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1878 let ValueKind::Ratio(canonical) = default else {
1879 return Ok(());
1880 };
1881 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1882}
1883
1884pub(crate) fn apply_option_constraint(
1890 lemma_type: &mut LemmaType,
1891 command: TypeConstraintCommand,
1892 args: &[CommandArg],
1893 resolve_unit: Option<&UnitTokenResolver<'_>>,
1894 spec: &crate::parsing::ast::LemmaSpec,
1895) -> Result<(), String> {
1896 let mut hops = 0;
1897 {
1898 let mut probe = &*lemma_type;
1899 while let TypeSpecification::List { element, .. } = &probe.specifications {
1900 hops += 1;
1901 probe = element.as_ref();
1902 }
1903 }
1904 let mut current = lemma_type;
1905 for _ in 0..hops {
1906 let TypeSpecification::List { element, .. } = &mut current.specifications else {
1907 unreachable!("BUG: option forwarding counted a list");
1908 };
1909 current = Arc::make_mut(element);
1910 }
1911 apply_options_command(current, command, args, resolve_unit, spec)
1912}
1913
1914pub(crate) fn apply_spec_member_command(
1916 lemma_type: &LemmaType,
1917 command: TypeConstraintCommand,
1918 args: &[CommandArg],
1919 spec: &crate::parsing::ast::LemmaSpec,
1920 target: &mut Option<RawSuggestion>,
1921) -> Result<(), String> {
1922 let label = match args {
1923 [CommandArg::Label(name)] => name,
1924 _ => return Err(format!("{command} on a spec takes one instance name")),
1925 };
1926 let option = spec_alias_option(
1927 lemma_type,
1928 uses_alias(spec, label)
1929 .ok_or_else(|| format!("'{label}' is not an instance of this spec"))?,
1930 label,
1931 )?;
1932 *target = Some(RawSuggestion::Value(option.value.value));
1933 Ok(())
1934}
1935
1936pub(crate) fn apply_list_with_rows(
1938 lemma_type: &mut LemmaType,
1939 names: ConstraintNames<'_>,
1940 args: &[CommandArg],
1941 declared_value: &mut Option<RawSuggestion>,
1942 resolve_unit: Option<&UnitTokenResolver<'_>>,
1943 spec: &crate::parsing::ast::LemmaSpec,
1944) -> Result<(), String> {
1945 let mut rows = match declared_value.take() {
1946 Some(RawSuggestion::List(existing)) => existing,
1947 None => Vec::new(),
1948 Some(other) => {
1949 unreachable!("BUG: list declared value must be RawSuggestion::List, got {other:?}")
1950 }
1951 };
1952 let TypeSpecification::List { element, .. } = &mut lemma_type.specifications else {
1953 unreachable!("BUG: apply_list_with_rows on a non-list type");
1954 };
1955 for arg in args {
1956 rows.push(list_with_row(
1957 Arc::make_mut(element),
1958 names,
1959 arg,
1960 resolve_unit,
1961 spec,
1962 )?);
1963 }
1964 *declared_value = Some(RawSuggestion::List(rows));
1965 Ok(())
1966}
1967
1968fn list_with_row(
1969 element: &mut LemmaType,
1970 names: ConstraintNames<'_>,
1971 arg: &CommandArg,
1972 resolve_unit: Option<&UnitTokenResolver<'_>>,
1973 spec: &crate::parsing::ast::LemmaSpec,
1974) -> Result<RawSuggestion, String> {
1975 let mut hops = 0;
1976 {
1977 let mut probe = &*element;
1978 while let TypeSpecification::List { element, .. } = &probe.specifications {
1979 hops += 1;
1980 probe = element.as_ref();
1981 }
1982 }
1983 let mut current = element;
1984 for _ in 0..hops {
1985 let TypeSpecification::List { element, .. } = &mut current.specifications else {
1986 unreachable!("BUG: list -> with counted a list");
1987 };
1988 current = Arc::make_mut(element);
1989 }
1990 let mut row_value = None;
1991 if matches!(current.specifications, TypeSpecification::Spec { .. }) {
1992 apply_spec_member_command(
1993 current,
1994 TypeConstraintCommand::With,
1995 std::slice::from_ref(arg),
1996 spec,
1997 &mut row_value,
1998 )?;
1999 } else {
2000 let mut unused_suggestion = None;
2001 current.apply_constraint(
2002 names,
2003 TypeConstraintCommand::With,
2004 std::slice::from_ref(arg),
2005 &mut unused_suggestion,
2006 &mut row_value,
2007 resolve_unit,
2008 )?;
2009 }
2010 let mut value = row_value.expect("BUG: -> with on a list row must produce a value");
2011 for _ in 0..hops {
2012 value = RawSuggestion::List(vec![value]);
2013 }
2014 Ok(value)
2015}
2016
2017fn apply_options_command(
2018 lemma_type: &mut LemmaType,
2019 command: TypeConstraintCommand,
2020 args: &[CommandArg],
2021 resolve_unit: Option<&UnitTokenResolver<'_>>,
2022 spec: &crate::parsing::ast::LemmaSpec,
2023) -> Result<(), String> {
2024 if command == TypeConstraintCommand::Option && args.len() != 1 {
2025 return Err("option takes exactly one argument".to_string());
2026 }
2027 if args.is_empty() {
2028 return Err("options takes at least one value".to_string());
2029 }
2030 let mut literals: Vec<OptionValue> = Vec::new();
2031 let mut names: Vec<String> = Vec::new();
2032 for arg in args {
2033 match arg {
2034 CommandArg::Literal(value) => {
2035 literals.push(OptionValue {
2036 value: option_bound_from_literal(value, lemma_type, resolve_unit)?,
2037 });
2038 }
2039 CommandArg::Label(name) => names.push(name.clone()),
2040 CommandArg::UnitExpr(_) => {
2041 return Err(format!(
2042 "{command} requires a value of this type, got a unit expression"
2043 ));
2044 }
2045 }
2046 }
2047 if !literals.is_empty() && !names.is_empty() {
2048 return Err(format!(
2049 "{command} mixes values and a rule. Write the values, or name one rule."
2050 ));
2051 }
2052 if matches!(lemma_type.specifications, TypeSpecification::Spec { .. }) {
2053 return apply_spec_list_options(lemma_type, command, args, spec);
2054 }
2055 if !names.is_empty() {
2056 return apply_option_names(lemma_type, &names, spec);
2057 }
2058 append_option_literals(lemma_type, literals)?;
2059 Ok(())
2060}
2061
2062fn option_bound_from_literal(
2063 value: &crate::literals::Value,
2064 lemma_type: &LemmaType,
2065 resolve_unit: Option<&UnitTokenResolver<'_>>,
2066) -> Result<BoundValueKind, String> {
2067 let kind = parser_value_to_value_kind(value, lemma_type, resolve_unit)?;
2068 let token = match value {
2069 crate::literals::Value::NumberWithUnit(_, unit) => Some(unit.as_str()),
2070 crate::literals::Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
2071 (
2072 crate::literals::Value::NumberWithUnit(_, left_unit),
2073 crate::literals::Value::NumberWithUnit(_, right_unit),
2074 ) if left_unit == right_unit => Some(left_unit.as_str()),
2075 _ => None,
2076 },
2077 _ => None,
2078 };
2079 let unit = match token {
2080 Some(token) => Some(Arc::<str>::from(resolve_endpoint_unit_token(
2081 token,
2082 &lemma_type.specifications,
2083 resolve_unit,
2084 )?)),
2085 None => None,
2086 };
2087 Ok(BoundValueKind {
2088 value: kind,
2089 unit,
2090 instance_factors: None,
2091 })
2092}
2093
2094fn apply_spec_list_options(
2095 _lemma_type: &mut LemmaType,
2096 _command: TypeConstraintCommand,
2097 _args: &[CommandArg],
2098 _spec: &crate::parsing::ast::LemmaSpec,
2099) -> Result<(), String> {
2100 Err(SPEC_LIST_HAS_NO_OPTIONS.to_string())
2101}
2102
2103enum OptionName {
2104 Text(OptionValue),
2105 Instance(OptionValue),
2106 Rule,
2107}
2108
2109fn apply_option_names(
2110 lemma_type: &mut LemmaType,
2111 names: &[String],
2112 spec: &crate::parsing::ast::LemmaSpec,
2113) -> Result<(), String> {
2114 let mut meanings = Vec::with_capacity(names.len());
2115 for name in names {
2116 meanings.push(option_name_meaning(lemma_type, spec, name)?);
2117 }
2118 let rule_count = meanings
2119 .iter()
2120 .filter(|meaning| matches!(meaning, OptionName::Rule))
2121 .count();
2122 if rule_count > 0 {
2123 if names.len() != 1 || rule_count != 1 {
2124 return Err(
2125 "options names one rule, or lists values. A rule is not mixed with values."
2126 .to_string(),
2127 );
2128 }
2129 if let Some(existing) = &lemma_type.options.rule {
2130 return Err(format!(
2131 "`-> options` already names rule '{existing}', not '{}'",
2132 names[0]
2133 ));
2134 }
2135 lemma_type.options.rule = Some(names[0].clone());
2136 return Ok(());
2137 }
2138 let mut literals = Vec::with_capacity(meanings.len());
2139 for meaning in meanings {
2140 match meaning {
2141 OptionName::Text(value) | OptionName::Instance(value) => literals.push(value),
2142 OptionName::Rule => unreachable!("BUG: rule meanings are returned above"),
2143 }
2144 }
2145 reject_duplicate_options(lemma_type, &literals)?;
2146 append_option_literals(lemma_type, literals)?;
2147 Ok(())
2148}
2149
2150fn option_name_meaning(
2152 lemma_type: &LemmaType,
2153 spec: &crate::parsing::ast::LemmaSpec,
2154 name: &str,
2155) -> Result<OptionName, String> {
2156 let mut found: Vec<OptionName> = Vec::new();
2157 if matches!(lemma_type.specifications, TypeSpecification::Text { .. }) {
2158 found.push(OptionName::Text(OptionValue {
2159 value: BoundValueKind::unbound(ValueKind::Text(Arc::from(name))),
2160 }));
2161 }
2162 if let Some(data) = uses_alias(spec, name) {
2163 if let Ok(instance) = spec_alias_option(lemma_type, data, name) {
2164 found.push(OptionName::Instance(instance));
2165 }
2166 }
2167 if spec.rules.iter().any(|rule| rule.name == name) {
2168 found.push(OptionName::Rule);
2169 }
2170 match found.len() {
2171 1 => Ok(found.pop().expect("BUG: one option meaning")),
2172 0 => Err(format!("'{name}' is not a value of this type or a rule")),
2173 _ => Err(format!(
2174 "'{name}' is more than one of a text option, an instance, and a rule"
2175 )),
2176 }
2177}
2178
2179fn uses_alias<'a>(
2180 spec: &'a crate::parsing::ast::LemmaSpec,
2181 name: &str,
2182) -> Option<&'a crate::parsing::ast::LemmaData> {
2183 spec.data.iter().find(|data| {
2184 data.reference.name == name
2185 && matches!(data.value, crate::parsing::ast::DataValue::Import { .. })
2186 })
2187}
2188
2189fn spec_alias_option(
2190 lemma_type: &LemmaType,
2191 data: &crate::parsing::ast::LemmaData,
2192 name: &str,
2193) -> Result<OptionValue, String> {
2194 let TypeSpecification::Spec { spec } = &lemma_type.specifications else {
2195 return Err(format!("'{name}' is not a value of this type or a rule"));
2196 };
2197 let crate::parsing::ast::DataValue::Import { spec_ref, .. } = &data.value else {
2198 unreachable!("BUG: uses_alias returns an import");
2199 };
2200 if spec_ref.name != spec.spec {
2201 return Err(format!(
2202 "'{name}' is an instance of '{}', not '{}'",
2203 spec_ref.name, spec.spec
2204 ));
2205 }
2206 unreachable!(
2207 "BUG: `uses` alias `{name}` named on a spec type: a spec type is only a list element, `-> options` on it names a rule, and the parser reads `-> with {name}` on a list as an instance-list row"
2208 )
2209}
2210
2211fn append_option_literals(
2212 lemma_type: &mut LemmaType,
2213 incoming: Vec<OptionValue>,
2214) -> Result<(), String> {
2215 for option in incoming {
2216 if lemma_type
2217 .options
2218 .values
2219 .iter()
2220 .any(|existing| existing.value == option.value)
2221 {
2222 return Err(format!(
2223 "duplicate option '{}'",
2224 lemma_type.display_bound(&option.value)
2225 ));
2226 }
2227 lemma_type.options.values.push(option);
2228 }
2229 Ok(())
2230}
2231
2232fn static_option_match(options: &TypeOptions, value: &ValueKind) -> bool {
2233 options
2234 .values
2235 .iter()
2236 .any(|option| option_values_match(&option.value.value, value))
2237}
2238
2239fn options_have_unscaled_measure(options: &TypeOptions) -> bool {
2240 options
2241 .values
2242 .iter()
2243 .any(|option| option.value.value.has_unscaled_measure())
2244}
2245
2246pub(crate) fn settle_value_kind_with_scales(
2249 value: &ValueKind,
2250 scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
2251) -> Result<ValueKind, String> {
2252 use crate::computation::rational::checked_mul;
2253
2254 enum Task {
2255 Visit(ValueKind),
2256 AssembleRange {
2257 left_type: Arc<LemmaType>,
2258 right_type: Arc<LemmaType>,
2259 },
2260 AssembleList {
2261 row_types: Vec<Arc<LemmaType>>,
2262 },
2263 }
2264
2265 let mut tasks = vec![Task::Visit(value.clone())];
2266 let mut done: Vec<ValueKind> = Vec::new();
2267 while let Some(task) = tasks.pop() {
2268 match task {
2269 Task::Visit(ValueKind::UnscaledMeasure { magnitude, unit }) => {
2270 let factor = scales.get(unit.as_ref()).ok_or_else(|| {
2271 format!("unit '{unit}' scale is not settled for option membership")
2272 })?;
2273 let canonical =
2274 checked_mul(&magnitude, factor).map_err(|failure| failure.to_string())?;
2275 done.push(ValueKind::Measure(canonical));
2276 }
2277 Task::Visit(ValueKind::Range(left, right)) => {
2278 tasks.push(Task::AssembleRange {
2279 left_type: Arc::clone(&left.lemma_type),
2280 right_type: Arc::clone(&right.lemma_type),
2281 });
2282 let right = *right;
2283 let left = *left;
2284 tasks.push(Task::Visit(right.value));
2285 tasks.push(Task::Visit(left.value));
2286 }
2287 Task::Visit(ValueKind::List(rows)) => {
2288 let owned = rows.to_vec();
2289 let row_types: Vec<Arc<LemmaType>> = owned
2290 .iter()
2291 .map(|row| Arc::clone(&row.lemma_type))
2292 .collect();
2293 tasks.push(Task::AssembleList { row_types });
2294 for row in owned.into_iter().rev() {
2295 tasks.push(Task::Visit(row.value));
2296 }
2297 }
2298 Task::Visit(other) => done.push(other),
2299 Task::AssembleRange {
2300 left_type,
2301 right_type,
2302 } => {
2303 let right_value = done
2304 .pop()
2305 .expect("BUG: settled range missing right endpoint");
2306 let left_value = done
2307 .pop()
2308 .expect("BUG: settled range missing left endpoint");
2309 done.push(ValueKind::Range(
2310 Box::new(TypedLiteral {
2311 value: left_value,
2312 lemma_type: left_type,
2313 }),
2314 Box::new(TypedLiteral {
2315 value: right_value,
2316 lemma_type: right_type,
2317 }),
2318 ));
2319 }
2320 Task::AssembleList { row_types } => {
2321 let mut settled = Vec::with_capacity(row_types.len());
2322 for lemma_type in row_types.into_iter().rev() {
2323 let value = done.pop().expect("BUG: settled list missing a row");
2324 settled.push(TypedLiteral { value, lemma_type });
2325 }
2326 settled.reverse();
2327 done.push(ValueKind::List(settled.into()));
2328 }
2329 }
2330 }
2331 Ok(done
2332 .pop()
2333 .expect("BUG: settle_value_kind_with_scales leaves one result"))
2334}
2335
2336fn static_option_match_with_scales(
2337 options: &TypeOptions,
2338 value: &ValueKind,
2339 scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
2340) -> Result<bool, String> {
2341 let settled_value = settle_value_kind_with_scales(value, scales)?;
2342 for option in &options.values {
2343 let settled_option = settle_value_kind_with_scales(&option.value.value, scales)?;
2344 if option_values_match(&settled_option, &settled_value) {
2345 return Ok(true);
2346 }
2347 }
2348 Ok(false)
2349}
2350
2351pub(crate) fn disallowed_message(
2352 lemma_type: &LemmaType,
2353 options: &TypeOptions,
2354 value: &ValueKind,
2355 rule_rows: Option<&ListValue>,
2356) -> String {
2357 let mut shown = options
2358 .values
2359 .iter()
2360 .map(|option| lemma_type.display_bound(&option.value))
2361 .collect::<Vec<_>>()
2362 .join(", ");
2363 if let Some(rows) = rule_rows {
2364 let rule_shown = rows
2365 .iter()
2366 .map(|row| row.value.to_string())
2367 .collect::<Vec<_>>()
2368 .join(", ");
2369 if !shown.is_empty() && !rule_shown.is_empty() {
2370 shown.push_str(", ");
2371 }
2372 shown.push_str(&rule_shown);
2373 }
2374 format!("'{value}' is not one of the allowed options: {shown}.")
2375}
2376
2377fn option_values_match(option: &ValueKind, value: &ValueKind) -> bool {
2380 let mut values: Vec<(ValueKind, ValueKind)> = vec![(option.clone(), value.clone())];
2381 while let Some((left, right)) = values.pop() {
2382 match (left, right) {
2383 (ValueKind::Spec(_), _) | (_, ValueKind::Spec(_)) => {
2384 unreachable!(
2385 "BUG: option matching saw a spec value; {}",
2386 SPEC_LIST_HAS_NO_OPTIONS
2387 );
2388 }
2389 (ValueKind::List(left), ValueKind::List(right)) => {
2390 if left.len() != right.len() {
2391 return false;
2392 }
2393 for (a, b) in left.iter().zip(right.iter()) {
2394 if a.lemma_type != b.lemma_type {
2395 return false;
2396 }
2397 values.push((a.into_owned().value, b.into_owned().value));
2398 }
2399 }
2400 (left, right) => {
2401 if left != right {
2402 return false;
2403 }
2404 }
2405 }
2406 }
2407 true
2408}
2409
2410pub(crate) fn option_detail_line(lemma_type: &LemmaType) -> Option<String> {
2411 let options = &lemma_type.options;
2412 let mut parts = Vec::new();
2413 if !options.values.is_empty() {
2414 parts.push(
2415 options
2416 .values
2417 .iter()
2418 .map(|option| lemma_type.display_bound(&option.value))
2419 .collect::<Vec<_>>()
2420 .join(", "),
2421 );
2422 }
2423 if let Some(name) = &options.rule {
2424 parts.push(name.clone());
2425 }
2426 if parts.is_empty() {
2427 None
2428 } else {
2429 Some(format!("options: {}", parts.join(", ")))
2430 }
2431}
2432
2433fn reject_duplicate_options(lemma_type: &LemmaType, values: &[OptionValue]) -> Result<(), String> {
2434 for (index, option) in values.iter().enumerate() {
2435 if values[..index]
2436 .iter()
2437 .any(|earlier| earlier.value == option.value)
2438 {
2439 return Err(format!(
2440 "duplicate option '{}'",
2441 lemma_type.display_bound(&option.value)
2442 ));
2443 }
2444 }
2445 Ok(())
2446}
2447
2448fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
2449 match arg {
2450 CommandArg::Label(name) => Ok(name.clone()),
2451 CommandArg::Literal(other) => Err(format!(
2452 "{} requires an identifier, got {}",
2453 cmd,
2454 value_kind_name(other)
2455 )),
2456 CommandArg::UnitExpr(_) => Err(format!(
2457 "{} requires an identifier, got a unit expression",
2458 cmd
2459 )),
2460 }
2461}
2462
2463fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
2464 match measure_trait {
2465 MeasureTrait::Duration => "duration",
2466 MeasureTrait::Calendar => "calendar",
2467 }
2468}
2469
2470fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
2471 if args.len() != 1 {
2472 return Err("trait requires exactly one identifier argument".to_string());
2473 }
2474 match label_name(&args[0], "trait")?
2475 .trim()
2476 .to_lowercase()
2477 .as_str()
2478 {
2479 "duration" => Ok(MeasureTrait::Duration),
2480 "calendar" => Ok(MeasureTrait::Calendar),
2481 other => Err(format!("Unknown measure trait '{}'", other)),
2482 }
2483}
2484
2485fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2486 let month_unit = units
2487 .iter()
2488 .find(|unit| unit.name == "month")
2489 .ok_or_else(|| {
2490 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
2491 .to_string()
2492 })?;
2493 if !month_unit.is_canonical_factor() {
2494 return Err("trait calendar requires unit month 1".to_string());
2495 }
2496 Ok(())
2497}
2498
2499fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2500 let second_unit = units
2501 .iter()
2502 .find(|unit| unit.name == "second")
2503 .ok_or_else(|| {
2504 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
2505 .to_string()
2506 })?;
2507 if !second_unit.is_canonical_factor() {
2508 return Err("trait duration requires unit second 1".to_string());
2509 }
2510 Ok(())
2511}
2512
2513fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
2515 match require_literal(args, cmd)? {
2516 crate::literals::Value::Date(dt) => Ok(dt.clone()),
2517 other => Err(format!(
2518 "{} requires a date literal (e.g. 2024-01-01), got {}",
2519 cmd,
2520 value_kind_name(other)
2521 )),
2522 }
2523}
2524
2525fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
2527 match require_literal(args, cmd)? {
2528 crate::literals::Value::Time(t) => Ok(t.clone()),
2529 other => Err(format!(
2530 "{} requires a time literal (e.g. 12:30:00), got {}",
2531 cmd,
2532 value_kind_name(other)
2533 )),
2534 }
2535}
2536
2537#[must_use]
2539pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
2540 use PrimitiveKind::*;
2541 match kind {
2542 Boolean => "Whether this holds (true or false).",
2543 Number => "A dimensionless number.",
2544 NumberRange => "The lower and upper bound of the number range.",
2545 Text => "A text value.",
2546 Measure => "A numeric amount in one of this type's units.",
2547 MeasureRange => "The lower and upper bound of the measure range.",
2548 Ratio => "A ratio in one of this type's units (e.g. percent).",
2549 RatioRange => "The lower and upper bound of the ratio range.",
2550 Date => "A date, or a date and time with optional timezone.",
2551 DateRange => "The start date and end date of the date range.",
2552 Time => "A time of day, with optional timezone.",
2553 TimeRange => "The start time and end time of the time range.",
2554 }
2555}
2556
2557impl TypeSpecification {
2558 pub fn boolean() -> Self {
2559 TypeSpecification::Boolean {
2560 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
2561 }
2562 }
2563 pub fn measure() -> Self {
2564 TypeSpecification::Measure {
2565 minimum: None,
2566 maximum: None,
2567 units: MeasureUnits::new(),
2568 traits: Vec::new(),
2569 decomposition: None,
2570 unit: None,
2571 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
2572 }
2573 }
2574 pub fn number() -> Self {
2575 TypeSpecification::Number {
2576 minimum: None,
2577 maximum: None,
2578 decimals: None,
2579 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
2580 }
2581 }
2582 pub fn number_range() -> Self {
2583 TypeSpecification::NumberRange {
2584 lower: None,
2585 upper: None,
2586 minimum: None,
2587 maximum: None,
2588 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2589 }
2590 }
2591 pub fn ratio() -> Self {
2592 TypeSpecification::Ratio {
2593 minimum: None,
2594 maximum: None,
2595 units: RatioUnits(vec![
2596 RatioUnit {
2597 name: "percent".to_string(),
2598 value: crate::computation::rational::rational_new(100, 1),
2599 minimum: None,
2600 maximum: None,
2601 suggestion_magnitude: None,
2602 },
2603 RatioUnit {
2604 name: "permille".to_string(),
2605 value: crate::computation::rational::rational_new(1000, 1),
2606 minimum: None,
2607 maximum: None,
2608 suggestion_magnitude: None,
2609 },
2610 ]),
2611 unit: None,
2612 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
2613 }
2614 }
2615 pub fn ratio_range() -> Self {
2616 TypeSpecification::RatioRange {
2617 lower: None,
2618 upper: None,
2619 minimum: None,
2620 maximum: None,
2621 units: match TypeSpecification::ratio() {
2622 TypeSpecification::Ratio { units, .. } => units,
2623 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
2624 },
2625 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2626 }
2627 }
2628 pub fn text() -> Self {
2629 TypeSpecification::Text {
2630 length: None,
2631 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
2632 }
2633 }
2634 pub fn date() -> Self {
2635 TypeSpecification::Date {
2636 minimum: None,
2637 maximum: None,
2638 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
2639 }
2640 }
2641 pub fn date_range() -> Self {
2642 TypeSpecification::DateRange {
2643 lower: None,
2644 upper: None,
2645 minimum: None,
2646 maximum: None,
2647 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2648 }
2649 }
2650 pub fn time() -> Self {
2651 TypeSpecification::Time {
2652 minimum: None,
2653 maximum: None,
2654 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
2655 }
2656 }
2657 pub fn time_range() -> Self {
2658 TypeSpecification::TimeRange {
2659 lower: None,
2660 upper: None,
2661 minimum: None,
2662 maximum: None,
2663 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2664 }
2665 }
2666 pub fn measure_range() -> Self {
2667 TypeSpecification::MeasureRange {
2668 lower: None,
2669 upper: None,
2670 minimum: None,
2671 maximum: None,
2672 units: MeasureUnits::new(),
2673 decomposition: None,
2674 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2675 }
2676 }
2677
2678 #[must_use]
2680 pub fn element_from_range(&self) -> Option<Self> {
2681 match self {
2682 TypeSpecification::NumberRange { lower, upper, .. } => {
2683 Some(TypeSpecification::Number {
2684 minimum: lower.clone(),
2685 maximum: upper.clone(),
2686 decimals: None,
2687 help: String::new(),
2688 })
2689 }
2690 TypeSpecification::MeasureRange {
2691 lower,
2692 upper,
2693 units,
2694 decomposition,
2695 ..
2696 } => Some(TypeSpecification::Measure {
2697 minimum: lower.clone(),
2698 maximum: upper.clone(),
2699 units: units.clone(),
2700 traits: Vec::new(),
2701 decomposition: decomposition.clone(),
2702 unit: None,
2703 help: String::new(),
2704 }),
2705 TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
2706 minimum: lower.clone(),
2707 maximum: upper.clone(),
2708 help: String::new(),
2709 }),
2710 TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
2711 minimum: lower.clone(),
2712 maximum: upper.clone(),
2713 help: String::new(),
2714 }),
2715 TypeSpecification::RatioRange {
2716 lower,
2717 upper,
2718 units,
2719 ..
2720 } => Some(TypeSpecification::Ratio {
2721 minimum: lower.clone(),
2722 maximum: upper.clone(),
2723 units: units.clone(),
2724 unit: None,
2725 help: String::new(),
2726 }),
2727 _ => None,
2728 }
2729 }
2730
2731 #[must_use]
2733 pub fn range_from_element(&self) -> Option<Self> {
2734 match self {
2735 TypeSpecification::Number {
2736 minimum, maximum, ..
2737 } => Some(TypeSpecification::NumberRange {
2738 lower: minimum.clone(),
2739 upper: maximum.clone(),
2740 minimum: None,
2741 maximum: None,
2742 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2743 }),
2744 TypeSpecification::Measure {
2745 minimum,
2746 maximum,
2747 units,
2748 decomposition,
2749 ..
2750 } => Some(TypeSpecification::MeasureRange {
2751 lower: minimum.clone(),
2752 upper: maximum.clone(),
2753 minimum: None,
2754 maximum: None,
2755 units: units.clone(),
2756 decomposition: decomposition.clone(),
2757 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2758 }),
2759 TypeSpecification::Date {
2760 minimum, maximum, ..
2761 } => Some(TypeSpecification::DateRange {
2762 lower: minimum.clone(),
2763 upper: maximum.clone(),
2764 minimum: None,
2765 maximum: None,
2766 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2767 }),
2768 TypeSpecification::Time {
2769 minimum, maximum, ..
2770 } => Some(TypeSpecification::TimeRange {
2771 lower: minimum.clone(),
2772 upper: maximum.clone(),
2773 minimum: None,
2774 maximum: None,
2775 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2776 }),
2777 TypeSpecification::Ratio {
2778 minimum,
2779 maximum,
2780 units,
2781 ..
2782 } => Some(TypeSpecification::RatioRange {
2783 lower: minimum.clone(),
2784 upper: maximum.clone(),
2785 minimum: None,
2786 maximum: None,
2787 units: units.clone(),
2788 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2789 }),
2790 _ => None,
2791 }
2792 }
2793
2794 #[must_use]
2799 pub fn minimum_decimal(&self) -> Option<Decimal> {
2800 match self {
2801 TypeSpecification::Number { minimum, .. }
2802 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
2803 bound
2804 .try_to_decimal()
2805 .expect("BUG: planned minimum must convert to decimal")
2806 }),
2807 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
2808 bound
2809 .try_to_decimal()
2810 .expect("BUG: planned minimum must convert to decimal")
2811 }),
2812 _ => None,
2813 }
2814 }
2815
2816 #[must_use]
2821 pub fn maximum_decimal(&self) -> Option<Decimal> {
2822 match self {
2823 TypeSpecification::Number { maximum, .. }
2824 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
2825 bound
2826 .try_to_decimal()
2827 .expect("BUG: planned maximum must convert to decimal")
2828 }),
2829 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
2830 bound
2831 .try_to_decimal()
2832 .expect("BUG: planned maximum must convert to decimal")
2833 }),
2834 _ => None,
2835 }
2836 }
2837
2838 pub fn veto() -> Self {
2839 TypeSpecification::Veto { message: None }
2840 }
2841
2842 pub fn apply_constraint(
2854 &mut self,
2855 names: ConstraintNames<'_>,
2856 command: TypeConstraintCommand,
2857 args: &[CommandArg],
2858 declared_suggestion: &mut Option<RawSuggestion>,
2859 declared_value: &mut Option<RawSuggestion>,
2860 resolve_unit: Option<&UnitTokenResolver<'_>>,
2861 ) -> Result<(), String> {
2862 let type_name = names.type_name;
2863 if command == TypeConstraintCommand::With
2864 && args.len() > 1
2865 && !matches!(self, TypeSpecification::List { .. })
2866 {
2867 return Err(format!("`-> with` on {self} takes exactly one value"));
2868 }
2869 if command == TypeConstraintCommand::Trait
2870 && !matches!(&self, TypeSpecification::Measure { .. })
2871 {
2872 return Err("trait command is only valid on measure types".to_string());
2873 }
2874 match self {
2875 TypeSpecification::Boolean { help } => match command {
2876 TypeConstraintCommand::Help => {
2877 apply_type_help_command(help, args)?;
2878 }
2879 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2880 let (target, cmd) = match command {
2881 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2882 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2883 _ => unreachable!("BUG: only Suggest or With in this arm"),
2884 };
2885 let lit = require_literal(args, cmd)?;
2886 reject_calendar_for_suggestion(
2887 lit,
2888 type_name,
2889 SuggestionExpectation::Boolean,
2890 None,
2891 )?;
2892 match lit {
2893 crate::literals::Value::Boolean(bv) => {
2894 *target =
2895 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
2896 }
2897 _ => {
2898 return Err(
2899 "Please provide true or false, for example `-> suggest true`."
2900 .to_string(),
2901 );
2902 }
2903 }
2904 }
2905 other => {
2906 return Err(format!(
2907 "Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
2908 other
2909 ));
2910 }
2911 },
2912 TypeSpecification::Measure {
2913 minimum,
2914 maximum,
2915 units,
2916 traits,
2917 help,
2918 ..
2919 } => match command {
2920 TypeConstraintCommand::Decimals => {
2921 return Err(decimals_depend_on_the_unit_error(
2922 "measure",
2923 names,
2924 args,
2925 units.0.first().map(|unit| unit.name.as_str()),
2926 )?);
2927 }
2928 TypeConstraintCommand::Unit => match args {
2929 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Reference(reference))] =>
2930 {
2931 let unit_name = name.clone();
2932 let written = reference.to_string();
2933 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2934 let same = match &existing.scale {
2935 UnitScale::Unresolved {
2936 segments,
2937 name: target_name,
2938 ..
2939 } => {
2940 segments == &reference.segments
2941 && target_name == &reference.name
2942 }
2943 UnitScale::Literal(_) | UnitScale::Reference { .. } => false,
2944 };
2945 if !same {
2946 return Err(format!(
2947 "Unit '{unit_name}' is already defined in this type's inherited units; \
2948 cannot change factor or decomposition. Add a new unit name instead."
2949 ));
2950 }
2951 } else {
2952 units.0.push(MeasureUnit::from_reference(
2953 unit_name,
2954 written,
2955 reference.segments.clone(),
2956 reference.name.clone(),
2957 ));
2958 }
2959 }
2960 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(value))] =>
2961 {
2962 apply_measure_unit_factor(units, name.clone(), *value, Vec::new())?;
2963 }
2964 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(prefix, factors))] =>
2965 {
2966 let derived_measure_factors: Vec<(String, i32)> = factors
2967 .iter()
2968 .map(|f| (f.measure_ref.clone(), f.exp))
2969 .collect();
2970 apply_measure_unit_factor(
2971 units,
2972 name.clone(),
2973 *prefix,
2974 derived_measure_factors,
2975 )?;
2976 }
2977 _ => {
2978 return Err(
2979 "unit requires a unit name followed by a conversion factor, compound unit expression, or number reference (e.g., 'unit eur 1.00', 'unit mps meter/second', 'unit gbp rate')"
2980 .to_string(),
2981 );
2982 }
2983 },
2984 TypeConstraintCommand::Trait => {
2985 let measure_trait = parse_measure_trait(args)?;
2986 if traits.contains(&measure_trait) {
2987 return Err(format!(
2988 "Duplicate trait '{}' for measure type.",
2989 measure_trait_name(measure_trait)
2990 ));
2991 }
2992 if measure_trait == MeasureTrait::Duration {
2993 validate_duration_trait_requirements(units)?;
2994 }
2995 if measure_trait == MeasureTrait::Calendar {
2996 validate_calendar_trait_requirements(units)?;
2997 }
2998 traits.push(measure_trait);
2999 }
3000 TypeConstraintCommand::Minimum => {
3001 *minimum = Some(parse_measure_declared_bound(
3002 args, "minimum", units, type_name,
3003 )?);
3004 }
3005 TypeConstraintCommand::Maximum => {
3006 *maximum = Some(parse_measure_declared_bound(
3007 args, "maximum", units, type_name,
3008 )?);
3009 }
3010 TypeConstraintCommand::Help => {
3011 apply_type_help_command(help, args)?;
3012 }
3013 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3014 let (target, cmd) = match command {
3015 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3016 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3017 _ => unreachable!("BUG: only Suggest or With in this arm"),
3018 };
3019 let lit = require_literal(args, cmd)?;
3020 if !traits.contains(&MeasureTrait::Calendar) {
3021 reject_calendar_for_suggestion(
3022 lit,
3023 type_name,
3024 SuggestionExpectation::MeasureUnits,
3025 Some(units),
3026 )?;
3027 }
3028 match lit {
3029 crate::literals::Value::NumberWithUnit(n, unit_name) => {
3030 if units
3031 .get(unit_name)
3032 .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
3033 {
3034 *target = Some(RawSuggestion::Measure {
3035 magnitude: lift_parser_decimal(*n)?,
3036 unit_name: unit_name.clone(),
3037 });
3038 } else {
3039 let (magnitude, unit_name) =
3040 parse_measure_declared_bound(args, cmd, units, type_name)?;
3041 *target = Some(RawSuggestion::Measure {
3042 magnitude,
3043 unit_name,
3044 });
3045 }
3046 }
3047 _ => {
3048 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
3049 }
3050 }
3051 }
3052 _ => {
3053 return Err(format!(
3054 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, help, suggest, with",
3055 command
3056 ));
3057 }
3058 },
3059 TypeSpecification::Number {
3060 decimals,
3061 minimum,
3062 maximum,
3063 help,
3064 } => match command {
3065 TypeConstraintCommand::Decimals => {
3066 let d = require_decimal_literal(args, "decimals")?;
3067 *decimals = Some(decimal_to_u8(d, "decimals")?);
3068 }
3069 TypeConstraintCommand::Unit => {
3070 return Err(
3071 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
3072 );
3073 }
3074 TypeConstraintCommand::Minimum => {
3075 *minimum = Some(require_decimal_literal(args, "minimum")?);
3076 }
3077 TypeConstraintCommand::Maximum => {
3078 *maximum = Some(require_decimal_literal(args, "maximum")?);
3079 }
3080 TypeConstraintCommand::Help => {
3081 apply_type_help_command(help, args)?;
3082 }
3083 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3084 let (target, cmd) = match command {
3085 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3086 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3087 _ => unreachable!("BUG: only Suggest or With in this arm"),
3088 };
3089 let lit = require_literal(args, cmd)?;
3090 reject_calendar_for_suggestion(
3091 lit,
3092 type_name,
3093 SuggestionExpectation::Number,
3094 None,
3095 )?;
3096 match lit {
3097 crate::literals::Value::Number(d) => {
3098 *target = Some(RawSuggestion::Value(ValueKind::Number(
3099 lift_parser_decimal(*d)?,
3100 )));
3101 }
3102 _ => {
3103 return Err(
3104 "Please provide a number, for example `-> suggest 42`.".to_string()
3105 );
3106 }
3107 }
3108 }
3109 _ => {
3110 return Err(format!(
3111 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
3112 command
3113 ));
3114 }
3115 },
3116 TypeSpecification::NumberRange {
3117 lower,
3118 upper,
3119 minimum,
3120 maximum,
3121 help,
3122 } => match command {
3123 TypeConstraintCommand::Lower => {
3124 *lower = Some(require_decimal_literal(args, "lower")?);
3125 }
3126 TypeConstraintCommand::Upper => {
3127 *upper = Some(require_decimal_literal(args, "upper")?);
3128 }
3129 TypeConstraintCommand::Minimum => {
3130 let width = require_decimal_literal(args, "minimum")?;
3131 reject_negative_width_magnitude(&width, "minimum")?;
3132 *minimum = Some(width);
3133 }
3134 TypeConstraintCommand::Maximum => {
3135 let width = require_decimal_literal(args, "maximum")?;
3136 reject_negative_width_magnitude(&width, "maximum")?;
3137 *maximum = Some(width);
3138 }
3139 TypeConstraintCommand::Help => {
3140 apply_type_help_command(help, args)?;
3141 }
3142 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3143 let target = match command {
3144 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3145 TypeConstraintCommand::With => &mut *declared_value,
3146 _ => unreachable!("BUG: only Suggest or With in this arm"),
3147 };
3148 let (left, right) = require_suggestion_range_endpoints(
3149 args,
3150 type_name,
3151 SuggestionExpectation::NumberRange,
3152 None,
3153 )?;
3154 let left = literal_value_from_parser_value(left)?;
3155 let right = literal_value_from_parser_value(right)?;
3156 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
3157 return Err(
3158 "Please provide a number range, for example `-> suggest 10...100`."
3159 .to_string(),
3160 );
3161 }
3162 *target = Some(RawSuggestion::Value(ValueKind::Range(
3163 Box::new(left),
3164 Box::new(right),
3165 )));
3166 }
3167 _ => {
3168 return Err(format!(
3169 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3170 command
3171 ));
3172 }
3173 },
3174 TypeSpecification::Ratio {
3175 minimum,
3176 maximum,
3177 units,
3178 unit: _,
3179 help,
3180 } => match command {
3181 TypeConstraintCommand::Decimals => {
3182 return Err(decimals_depend_on_the_unit_error(
3183 "ratio",
3184 names,
3185 args,
3186 units.0.first().map(|unit| unit.name.as_str()),
3187 )?);
3188 }
3189 TypeConstraintCommand::Unit => {
3190 let (unit_name, value_dec) = match args {
3191 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
3192 (name.clone(), *v)
3193 }
3194 _ => {
3195 return Err(
3196 "unit requires a unit name followed by a numeric conversion factor (e.g., '-> unit percent: 100'). Compound unit expressions are not supported for ratio types."
3197 .to_string(),
3198 );
3199 }
3200 };
3201 let value = crate::computation::rational::decimal_to_rational(value_dec)
3202 .map_err(|failure| {
3203 format!(
3204 "ratio unit value is not exactly representable as a rational: {}",
3205 failure
3206 )
3207 })?;
3208 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
3209 if existing.value != value {
3210 return Err(format!(
3211 "Unit '{unit_name}' is already defined in this type's inherited units; \
3212 cannot change factor. Add a new unit name instead."
3213 ));
3214 }
3215 } else {
3216 units.0.push(RatioUnit {
3217 name: unit_name,
3218 value,
3219 minimum: None,
3220 maximum: None,
3221 suggestion_magnitude: None,
3222 });
3223 }
3224 }
3225 TypeConstraintCommand::Minimum => {
3226 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3227 sync_ratio_units_from_canonical(
3228 units,
3229 &canonical,
3230 UnitConstraintField::Minimum,
3231 )?;
3232 *minimum = Some(canonical);
3233 }
3234 TypeConstraintCommand::Maximum => {
3235 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3236 sync_ratio_units_from_canonical(
3237 units,
3238 &canonical,
3239 UnitConstraintField::Maximum,
3240 )?;
3241 *maximum = Some(canonical);
3242 }
3243 TypeConstraintCommand::Help => {
3244 apply_type_help_command(help, args)?;
3245 }
3246 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3247 let (target, cmd) = match command {
3248 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3249 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3250 _ => unreachable!("BUG: only Suggest or With in this arm"),
3251 };
3252 let lit = require_literal(args, cmd)?;
3253 reject_calendar_for_suggestion(
3254 lit,
3255 type_name,
3256 SuggestionExpectation::Ratio,
3257 None,
3258 )?;
3259 match lit {
3260 crate::literals::Value::NumberWithUnit(_, unit_name) => {
3261 let element_type = LemmaType::primitive(TypeSpecification::Ratio {
3262 minimum: minimum.clone(),
3263 maximum: maximum.clone(),
3264 units: units.clone(),
3265 unit: None,
3266 help: help.clone(),
3267 });
3268 let value = parser_value_to_value_kind(lit, &element_type, None)?;
3269 sync_ratio_suggestion_units(units, &value)?;
3270 *target = Some(RawSuggestion::UnitBound {
3271 value,
3272 unit_name: unit_name.clone(),
3273 });
3274 }
3275 other => {
3276 return Err(format!(
3277 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
3278 value_kind_name(other)
3279 ));
3280 }
3281 }
3282 }
3283 _ => {
3284 return Err(format!(
3285 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, help, suggest, with",
3286 command
3287 ));
3288 }
3289 },
3290 TypeSpecification::RatioRange {
3291 lower,
3292 upper,
3293 minimum,
3294 maximum,
3295 units,
3296 help,
3297 } => match command {
3298 TypeConstraintCommand::Unit => {
3299 return Err(format!(
3300 "'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
3301 ));
3302 }
3303 TypeConstraintCommand::Lower => {
3304 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
3305 }
3306 TypeConstraintCommand::Upper => {
3307 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
3308 }
3309 TypeConstraintCommand::Minimum => {
3310 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3311 reject_negative_width_magnitude(&width, "minimum")?;
3312 *minimum = Some(width);
3313 }
3314 TypeConstraintCommand::Maximum => {
3315 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3316 reject_negative_width_magnitude(&width, "maximum")?;
3317 *maximum = Some(width);
3318 }
3319 TypeConstraintCommand::Help => {
3320 apply_type_help_command(help, args)?;
3321 }
3322 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3323 let target = match command {
3324 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3325 TypeConstraintCommand::With => &mut *declared_value,
3326 _ => unreachable!("BUG: only Suggest or With in this arm"),
3327 };
3328 let (left, right) = require_suggestion_range_endpoints(
3329 args,
3330 type_name,
3331 SuggestionExpectation::RatioRange,
3332 None,
3333 )?;
3334 let element_type = LemmaType::primitive(
3335 TypeSpecification::RatioRange {
3336 lower: lower.clone(),
3337 upper: upper.clone(),
3338 minimum: minimum.clone(),
3339 maximum: maximum.clone(),
3340 units: units.clone(),
3341 help: help.clone(),
3342 }
3343 .element_from_range()
3344 .expect("BUG: RatioRange must define element_from_range"),
3345 );
3346 let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3347 let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3348 if !value_kind_matches_spec(&left.value, &element_type.specifications)
3349 || !value_kind_matches_spec(&right.value, &element_type.specifications)
3350 || !matches!(&left.value, ValueKind::Ratio(_))
3351 || !matches!(&right.value, ValueKind::Ratio(_))
3352 {
3353 return Err(
3354 "Please provide a ratio range, for example `-> suggest 10%...50%`."
3355 .to_string(),
3356 );
3357 }
3358 *target = Some(RawSuggestion::Value(ValueKind::Range(
3359 Box::new(left),
3360 Box::new(right),
3361 )));
3362 }
3363 _ => {
3364 return Err(format!(
3365 "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3366 command
3367 ));
3368 }
3369 },
3370 TypeSpecification::Text { length, help } => match command {
3371 TypeConstraintCommand::Length => {
3372 let d = require_decimal_literal(args, "length")?;
3373 *length = Some(decimal_to_usize(d, "length")?);
3374 }
3375 TypeConstraintCommand::Help => {
3376 apply_type_help_command(help, args)?;
3377 }
3378 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3379 let (target, cmd) = match command {
3380 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3381 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3382 _ => unreachable!("BUG: only Suggest or With in this arm"),
3383 };
3384 let lit = require_literal(args, cmd)?;
3385 reject_calendar_for_suggestion(
3386 lit,
3387 type_name,
3388 SuggestionExpectation::Text,
3389 None,
3390 )?;
3391 match lit {
3392 crate::literals::Value::Text(s) => {
3393 *target =
3394 Some(RawSuggestion::Value(ValueKind::Text(Arc::from(s.as_str()))));
3395 }
3396 _ => {
3397 return Err(
3398 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
3399 .to_string(),
3400 );
3401 }
3402 }
3403 }
3404 _ => {
3405 return Err(format!(
3406 "Invalid command '{}' for text type. Valid commands: length, help, suggest, with",
3407 command
3408 ));
3409 }
3410 },
3411 TypeSpecification::Date {
3412 minimum,
3413 maximum,
3414 help,
3415 } => match command {
3416 TypeConstraintCommand::Minimum => {
3417 let dt = require_date_literal(args, "minimum")?;
3418 *minimum = Some(dt);
3419 }
3420 TypeConstraintCommand::Maximum => {
3421 let dt = require_date_literal(args, "maximum")?;
3422 *maximum = Some(dt);
3423 }
3424 TypeConstraintCommand::Help => {
3425 apply_type_help_command(help, args)?;
3426 }
3427 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3428 let (target, cmd) = match command {
3429 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3430 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3431 _ => unreachable!("BUG: only Suggest or With in this arm"),
3432 };
3433 let lit = require_literal(args, cmd)?;
3434 reject_calendar_for_suggestion(
3435 lit,
3436 type_name,
3437 SuggestionExpectation::Date,
3438 None,
3439 )?;
3440 match lit {
3441 crate::literals::Value::Date(dt) => {
3442 *target = Some(RawSuggestion::Value(ValueKind::Date(
3443 date_time_to_semantic(dt),
3444 )));
3445 }
3446 _ => {
3447 return Err(
3448 "Please provide a date, for example `-> suggest 2024-06-15`."
3449 .to_string(),
3450 );
3451 }
3452 }
3453 }
3454 _ => {
3455 return Err(format!(
3456 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
3457 command
3458 ));
3459 }
3460 },
3461 TypeSpecification::DateRange {
3462 lower,
3463 upper,
3464 minimum,
3465 maximum,
3466 help,
3467 } => match command {
3468 TypeConstraintCommand::Lower => {
3469 *lower = Some(require_date_literal(args, "lower")?);
3470 }
3471 TypeConstraintCommand::Upper => {
3472 *upper = Some(require_date_literal(args, "upper")?);
3473 }
3474 TypeConstraintCommand::Minimum => {
3475 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3476 }
3477 TypeConstraintCommand::Maximum => {
3478 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3479 }
3480 TypeConstraintCommand::Help => {
3481 apply_type_help_command(help, args)?;
3482 }
3483 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3484 let target = match command {
3485 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3486 TypeConstraintCommand::With => &mut *declared_value,
3487 _ => unreachable!("BUG: only Suggest or With in this arm"),
3488 };
3489 let (left, right) = require_suggestion_range_endpoints(
3490 args,
3491 type_name,
3492 SuggestionExpectation::DateRange,
3493 None,
3494 )?;
3495 let left = literal_value_from_parser_value(left)?;
3496 let right = literal_value_from_parser_value(right)?;
3497 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
3498 return Err(
3499 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
3500 .to_string(),
3501 );
3502 }
3503 *target = Some(RawSuggestion::Value(ValueKind::Range(
3504 Box::new(left),
3505 Box::new(right),
3506 )));
3507 }
3508 _ => {
3509 return Err(format!(
3510 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3511 command
3512 ));
3513 }
3514 },
3515 TypeSpecification::Time {
3516 minimum,
3517 maximum,
3518 help,
3519 } => match command {
3520 TypeConstraintCommand::Minimum => {
3521 let t = require_time_literal(args, "minimum")?;
3522 *minimum = Some(t);
3523 }
3524 TypeConstraintCommand::Maximum => {
3525 let t = require_time_literal(args, "maximum")?;
3526 *maximum = Some(t);
3527 }
3528 TypeConstraintCommand::Help => {
3529 apply_type_help_command(help, args)?;
3530 }
3531 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3532 let (target, cmd) = match command {
3533 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3534 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3535 _ => unreachable!("BUG: only Suggest or With in this arm"),
3536 };
3537 let lit = require_literal(args, cmd)?;
3538 reject_calendar_for_suggestion(
3539 lit,
3540 type_name,
3541 SuggestionExpectation::Time,
3542 None,
3543 )?;
3544 match lit {
3545 crate::literals::Value::Time(t) => {
3546 *target =
3547 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
3548 }
3549 _ => {
3550 return Err(
3551 "Please provide a time, for example `-> suggest 09:00:00`."
3552 .to_string(),
3553 );
3554 }
3555 }
3556 }
3557 _ => {
3558 return Err(format!(
3559 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
3560 command
3561 ));
3562 }
3563 },
3564 TypeSpecification::TimeRange {
3565 lower,
3566 upper,
3567 minimum,
3568 maximum,
3569 help,
3570 } => match command {
3571 TypeConstraintCommand::Lower => {
3572 *lower = Some(require_time_literal(args, "lower")?);
3573 }
3574 TypeConstraintCommand::Upper => {
3575 *upper = Some(require_time_literal(args, "upper")?);
3576 }
3577 TypeConstraintCommand::Minimum => {
3578 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3579 }
3580 TypeConstraintCommand::Maximum => {
3581 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3582 }
3583 TypeConstraintCommand::Help => {
3584 apply_type_help_command(help, args)?;
3585 }
3586 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3587 let target = match command {
3588 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3589 TypeConstraintCommand::With => &mut *declared_value,
3590 _ => unreachable!("BUG: only Suggest or With in this arm"),
3591 };
3592 let (left, right) = require_suggestion_range_endpoints(
3593 args,
3594 type_name,
3595 SuggestionExpectation::TimeRange,
3596 None,
3597 )?;
3598 let left = literal_value_from_parser_value(left)?;
3599 let right = literal_value_from_parser_value(right)?;
3600 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
3601 return Err(
3602 "Please provide a time range, for example `-> suggest 09:00...17:00`."
3603 .to_string(),
3604 );
3605 }
3606 *target = Some(RawSuggestion::Value(ValueKind::Range(
3607 Box::new(left),
3608 Box::new(right),
3609 )));
3610 }
3611 _ => {
3612 return Err(format!(
3613 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3614 command
3615 ));
3616 }
3617 },
3618 TypeSpecification::MeasureRange {
3619 lower,
3620 upper,
3621 minimum,
3622 maximum,
3623 units,
3624 decomposition,
3625 help,
3626 } => match command {
3627 TypeConstraintCommand::Unit => {
3628 return Err(format!(
3629 "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
3630 ));
3631 }
3632 TypeConstraintCommand::Lower => {
3633 *lower = Some(parse_measure_declared_bound(
3634 args, "lower", units, type_name,
3635 )?);
3636 }
3637 TypeConstraintCommand::Upper => {
3638 *upper = Some(parse_measure_declared_bound(
3639 args, "upper", units, type_name,
3640 )?);
3641 }
3642 TypeConstraintCommand::Minimum => {
3643 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
3644 reject_negative_width_magnitude(&width.0, "minimum")?;
3645 *minimum = Some(width);
3646 }
3647 TypeConstraintCommand::Maximum => {
3648 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
3649 reject_negative_width_magnitude(&width.0, "maximum")?;
3650 *maximum = Some(width);
3651 }
3652 TypeConstraintCommand::Help => {
3653 apply_type_help_command(help, args)?;
3654 }
3655 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3656 let target = match command {
3657 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3658 TypeConstraintCommand::With => &mut *declared_value,
3659 _ => unreachable!("BUG: only Suggest or With in this arm"),
3660 };
3661 let (left, right) = require_suggestion_range_endpoints(
3662 args,
3663 type_name,
3664 SuggestionExpectation::MeasureRange,
3665 Some(units),
3666 )?;
3667 let element_type = LemmaType::primitive(
3668 TypeSpecification::MeasureRange {
3669 lower: lower.clone(),
3670 upper: upper.clone(),
3671 minimum: minimum.clone(),
3672 maximum: maximum.clone(),
3673 units: units.clone(),
3674 decomposition: decomposition.clone(),
3675 help: help.clone(),
3676 }
3677 .element_from_range()
3678 .expect("BUG: MeasureRange must define element_from_range"),
3679 );
3680 let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3681 let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3682 if !value_kind_matches_spec(&left.value, &element_type.specifications)
3683 || !value_kind_matches_spec(&right.value, &element_type.specifications)
3684 || !matches!(&left.value, ValueKind::Measure(_))
3685 || !matches!(&right.value, ValueKind::Measure(_))
3686 {
3687 return Err(format!(
3688 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
3689 ));
3690 }
3691 *target = Some(RawSuggestion::Value(ValueKind::Range(
3692 Box::new(left),
3693 Box::new(right),
3694 )));
3695 }
3696 _ => {
3697 return Err(format!(
3698 "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3699 command
3700 ));
3701 }
3702 },
3703 TypeSpecification::Veto { .. } => {
3704 return Err(format!(
3705 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
3706 command
3707 ));
3708 }
3709 TypeSpecification::Undetermined => {
3710 return Err(format!(
3711 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
3712 command
3713 ));
3714 }
3715 TypeSpecification::Spec { .. } => {
3716 return Err(format!(
3717 "Invalid command '{command}' for spec type. A spec instance has no data constraints"
3718 ));
3719 }
3720 TypeSpecification::List { element, help, .. } => match command {
3721 TypeConstraintCommand::Help => {
3722 if matches!(element.specifications, TypeSpecification::Spec { .. }) {
3723 apply_type_help_command(help, args)?;
3724 } else {
3725 let mut unused_suggestion = None;
3726 let mut unused_value = None;
3727 Arc::make_mut(element).specifications.apply_constraint(
3728 names,
3729 command,
3730 args,
3731 &mut unused_suggestion,
3732 &mut unused_value,
3733 resolve_unit,
3734 )?;
3735 }
3736 }
3737 TypeConstraintCommand::With => {
3738 return Err(
3739 "BUG: list -> with is applied before TypeSpecification::apply_constraint"
3740 .to_string(),
3741 );
3742 }
3743 other => {
3744 return Err(format!(
3745 "Invalid command '{other}' for list type. Valid commands: with, help"
3746 ));
3747 }
3748 },
3749 }
3750 Ok(())
3751 }
3752}
3753
3754pub fn parse_number_unit(
3757 value_str: &str,
3758 type_spec: &TypeSpecification,
3759) -> Result<crate::parsing::ast::Value, String> {
3760 use crate::literals::{NumberWithUnit, RatioLiteral};
3761 use crate::parsing::ast::Value;
3762
3763 let trimmed = value_str.trim();
3764 match type_spec {
3765 TypeSpecification::Measure { units, .. } => {
3766 if units.is_empty() {
3767 unreachable!(
3768 "BUG: Measure type has no units; should have been validated during planning"
3769 );
3770 }
3771 match trimmed.parse::<NumberWithUnit>() {
3772 Ok(n) => {
3773 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3774 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3775 }
3776 Err(e) => {
3777 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3778 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3779 let example_unit = units
3780 .iter()
3781 .next()
3782 .expect("BUG: units non-empty after guard")
3783 .name
3784 .as_str();
3785 Err(format!(
3786 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3787 trimmed,
3788 example_unit,
3789 valid.join(", ")
3790 ))
3791 } else {
3792 Err(e)
3793 }
3794 }
3795 }
3796 }
3797 TypeSpecification::Ratio { units, .. } => {
3798 if units.is_empty() {
3799 unreachable!(
3800 "BUG: Ratio type has no units; should have been validated during planning"
3801 );
3802 }
3803 match trimmed.parse::<RatioLiteral>()? {
3804 RatioLiteral::Bare(_) => {
3805 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3806 }
3807 RatioLiteral::Percent(n) => {
3808 let unit = units.get("percent").map_err(|e| e.to_string())?;
3809 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3810 }
3811 RatioLiteral::Permille(n) => {
3812 let unit = units.get("permille").map_err(|e| e.to_string())?;
3813 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3814 }
3815 RatioLiteral::Named { value, unit } => {
3816 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3817 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3818 }
3819 }
3820 }
3821 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3822 }
3823}
3824
3825pub fn parse_value_from_string(
3828 value_str: &str,
3829 type_spec: &TypeSpecification,
3830 source: &Source,
3831) -> Result<crate::parsing::ast::Value, Error> {
3832 match type_spec {
3833 TypeSpecification::NumberRange { .. }
3834 | TypeSpecification::MeasureRange { .. }
3835 | TypeSpecification::DateRange { .. }
3836 | TypeSpecification::TimeRange { .. }
3837 | TypeSpecification::RatioRange { .. } => {
3838 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3839 unreachable!(
3840 "BUG: range_element_type_specification missing arm for known range type"
3841 )
3842 });
3843 parse_range_value_from_string(value_str, &element_spec, source)
3844 }
3845 _ => parse_scalar_value_from_string(value_str, type_spec, source),
3846 }
3847}
3848
3849fn parse_range_value_from_string(
3850 value_str: &str,
3851 element_spec: &TypeSpecification,
3852 source: &Source,
3853) -> Result<crate::parsing::ast::Value, Error> {
3854 use crate::parsing::ast::Value;
3855
3856 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3857
3858 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3859 to_err("Range value must use '...' between the two endpoints".to_string())
3860 })?;
3861 if left_str.trim().is_empty() || right_str.trim().is_empty() {
3862 return Err(to_err(
3863 "Range value must contain a non-empty left and right endpoint".to_string(),
3864 ));
3865 }
3866 let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3867 let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3868 Ok(Value::Range(Box::new(left), Box::new(right)))
3869}
3870
3871fn parse_scalar_value_from_string(
3872 value_str: &str,
3873 type_spec: &TypeSpecification,
3874 source: &Source,
3875) -> Result<crate::parsing::ast::Value, Error> {
3876 use crate::parsing::ast::Value;
3877
3878 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3879
3880 match type_spec {
3881 TypeSpecification::Text { .. } => value_str
3882 .parse::<crate::literals::TextLiteral>()
3883 .map(|t| Value::Text(t.0))
3884 .map_err(to_err),
3885 TypeSpecification::Number { .. } => value_str
3886 .parse::<crate::literals::NumberLiteral>()
3887 .map(|n| Value::Number(n.0))
3888 .map_err(to_err),
3889 TypeSpecification::Measure { .. } => {
3890 parse_number_unit(value_str, type_spec).map_err(to_err)
3891 }
3892 TypeSpecification::Boolean { .. } => value_str
3893 .parse::<BooleanValue>()
3894 .map(Value::Boolean)
3895 .map_err(to_err),
3896 TypeSpecification::Date { .. } => {
3897 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3898 Ok(Value::Date(date))
3899 }
3900 TypeSpecification::Time { .. } => {
3901 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3902 Ok(Value::Time(time))
3903 }
3904 TypeSpecification::Ratio { .. } => {
3905 parse_number_unit(value_str, type_spec).map_err(to_err)
3906 }
3907 TypeSpecification::NumberRange { .. }
3908 | TypeSpecification::MeasureRange { .. }
3909 | TypeSpecification::DateRange { .. }
3910 | TypeSpecification::TimeRange { .. }
3911 | TypeSpecification::RatioRange { .. } => {
3912 unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3913 }
3914 TypeSpecification::Veto { .. } => Err(to_err(
3915 "Veto type cannot be parsed from string".to_string(),
3916 )),
3917 TypeSpecification::Undetermined => unreachable!(
3918 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3919 ),
3920 TypeSpecification::Spec { .. } => Err(to_err(
3921 "spec values are not evaluation-data scalars".to_string(),
3922 )),
3923 TypeSpecification::List { .. } => Err(to_err(
3924 "list values are not evaluation-data scalars".to_string(),
3925 )),
3926 }
3927}
3928
3929#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3934pub enum SemanticCalendarUnit {
3935 Month,
3936 Year,
3937}
3938
3939impl fmt::Display for SemanticCalendarUnit {
3940 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3941 let s = match self {
3942 SemanticCalendarUnit::Month => "month",
3943 SemanticCalendarUnit::Year => "year",
3944 };
3945 write!(f, "{}", s)
3946 }
3947}
3948
3949pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3950 match unit_name {
3951 "month" => SemanticCalendarUnit::Month,
3952 "year" => SemanticCalendarUnit::Year,
3953 other => unreachable!(
3954 "BUG: calendar measure signature unit must be month or year, got '{other}'"
3955 ),
3956 }
3957}
3958
3959pub fn semantic_calendar_unit_from_measure_signature(
3960 signature: &[(String, i32)],
3961) -> SemanticCalendarUnit {
3962 let unit_name = signature
3963 .first()
3964 .map(|(name, _)| name.as_str())
3965 .expect("BUG: calendar measure must carry a unit signature");
3966 semantic_calendar_unit_from_unit_name(unit_name)
3967}
3968
3969pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3970 if !lemma_type.is_calendar_like() {
3971 unreachable!(
3972 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3973 lemma_type.name()
3974 );
3975 }
3976 let signature = lemma_type.measure_runtime_signature();
3977 if signature.is_empty() {
3978 return SemanticCalendarUnit::Month;
3979 }
3980 semantic_calendar_unit_from_measure_signature(&signature)
3981}
3982
3983#[derive(Debug, Clone, PartialEq, Eq)]
3985pub enum SemanticConversionTarget {
3986 Type(ConversionTypeKind),
3987 Unit {
3989 unit_name: String,
3990 owning_type: Arc<LemmaType>,
3992 },
3993}
3994
3995impl std::hash::Hash for SemanticConversionTarget {
3996 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3997 match self {
3998 Self::Type(kind) => {
3999 0u8.hash(state);
4000 kind.hash(state);
4001 }
4002 Self::Unit {
4003 unit_name,
4004 owning_type,
4005 } => {
4006 1u8.hash(state);
4007 unit_name.hash(state);
4008 owning_type.hash(state);
4009 }
4010 }
4011 }
4012}
4013
4014impl SemanticConversionTarget {}
4015
4016impl fmt::Display for SemanticConversionTarget {
4017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4018 match self {
4019 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
4020 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
4021 }
4022 }
4023}
4024
4025#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4028pub struct SemanticTimezone {
4029 pub offset_minutes: i16,
4030}
4031
4032impl SemanticTimezone {
4033 pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
4035 TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
4036 }
4037
4038 pub fn offset_seconds(&self) -> i32 {
4039 i32::from(self.offset_minutes) * 60
4040 }
4041}
4042
4043impl From<&TimezoneValue> for SemanticTimezone {
4044 fn from(tz: &TimezoneValue) -> Self {
4045 Self {
4046 offset_minutes: tz.offset_minutes,
4047 }
4048 }
4049}
4050
4051impl From<&SemanticTimezone> for TimezoneValue {
4052 fn from(tz: &SemanticTimezone) -> Self {
4053 Self {
4054 offset_minutes: tz.offset_minutes,
4055 }
4056 }
4057}
4058
4059impl fmt::Display for SemanticTimezone {
4060 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4061 TimezoneValue::from(self).fmt(f)
4062 }
4063}
4064
4065impl Serialize for SemanticTimezone {
4066 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4067 serializer.serialize_str(&self.to_string())
4068 }
4069}
4070
4071impl<'de> Deserialize<'de> for SemanticTimezone {
4072 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4073 let s = String::deserialize(deserializer)?;
4074 Self::from_str(&s).map_err(serde::de::Error::custom)
4075 }
4076}
4077
4078impl FromStr for SemanticTimezone {
4079 type Err = String;
4080
4081 fn from_str(s: &str) -> Result<Self, Self::Err> {
4082 Ok(Self::from(&TimezoneValue::from_str(s)?))
4083 }
4084}
4085
4086#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4088pub struct SemanticTime {
4089 pub hour: u32,
4090 pub minute: u32,
4091 pub second: u32,
4092 pub microsecond: u32,
4093 pub timezone: Option<SemanticTimezone>,
4094}
4095
4096impl fmt::Display for SemanticTime {
4097 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4098 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
4099 if self.microsecond != 0 {
4100 write!(f, ".{:06}", self.microsecond)?;
4101 }
4102 if let Some(timezone) = &self.timezone {
4103 write!(f, "{}", timezone)?;
4104 }
4105 Ok(())
4106 }
4107}
4108
4109impl Serialize for SemanticTime {
4110 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4111 serializer.serialize_str(&self.to_string())
4112 }
4113}
4114
4115impl<'de> Deserialize<'de> for SemanticTime {
4116 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4117 let s = String::deserialize(deserializer)?;
4118 Self::from_str(&s).map_err(serde::de::Error::custom)
4119 }
4120}
4121
4122impl FromStr for SemanticTime {
4123 type Err = String;
4124
4125 fn from_str(s: &str) -> Result<Self, Self::Err> {
4126 Ok(time_to_semantic(&TimeValue::from_str(s)?))
4127 }
4128}
4129
4130#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4132pub struct SemanticDateTime {
4133 pub year: i32,
4134 pub month: u32,
4135 pub day: u32,
4136 pub hour: u32,
4137 pub minute: u32,
4138 pub second: u32,
4139 pub microsecond: u32,
4140 pub timezone: Option<SemanticTimezone>,
4141}
4142
4143impl fmt::Display for SemanticDateTime {
4144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4145 let has_time = self.hour != 0
4146 || self.minute != 0
4147 || self.second != 0
4148 || self.microsecond != 0
4149 || self.timezone.is_some();
4150 if !has_time {
4151 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
4152 } else {
4153 write!(
4154 f,
4155 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
4156 self.year, self.month, self.day, self.hour, self.minute, self.second
4157 )?;
4158 if self.microsecond != 0 {
4159 write!(f, ".{:06}", self.microsecond)?;
4160 }
4161 if let Some(tz) = &self.timezone {
4162 write!(f, "{}", tz)?;
4163 }
4164 Ok(())
4165 }
4166 }
4167}
4168
4169impl Serialize for SemanticDateTime {
4170 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4171 serializer.serialize_str(&self.to_string())
4172 }
4173}
4174
4175impl<'de> Deserialize<'de> for SemanticDateTime {
4176 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4177 let s = String::deserialize(deserializer)?;
4178 Self::from_str(&s).map_err(serde::de::Error::custom)
4179 }
4180}
4181
4182impl FromStr for SemanticDateTime {
4183 type Err = String;
4184
4185 fn from_str(s: &str) -> Result<Self, Self::Err> {
4186 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
4187 }
4188}
4189
4190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4194pub enum RawSuggestion {
4195 Value(ValueKind),
4196 Measure {
4198 magnitude: RationalInteger,
4199 unit_name: String,
4200 },
4201 UnitBound {
4204 value: ValueKind,
4205 unit_name: String,
4206 },
4207 List(Vec<RawSuggestion>),
4208}
4209
4210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4221pub struct BoundValueKind {
4222 pub value: ValueKind,
4223 pub unit: Option<Arc<str>>,
4224 pub instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4225}
4226
4227impl BoundValueKind {
4228 pub fn unbound(value: ValueKind) -> Self {
4229 Self {
4230 value,
4231 unit: None,
4232 instance_factors: None,
4233 }
4234 }
4235
4236 pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
4237 Self {
4238 value,
4239 unit,
4240 instance_factors: None,
4241 }
4242 }
4243
4244 pub fn with_instance_factors(
4245 value: ValueKind,
4246 unit: Option<Arc<str>>,
4247 instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4248 ) -> Self {
4249 Self {
4250 value,
4251 unit,
4252 instance_factors,
4253 }
4254 }
4255
4256 pub fn to_literal(&self) -> LiteralValue {
4257 LiteralValue {
4258 value: self.value.clone(),
4259 }
4260 }
4261
4262 #[must_use]
4265 pub fn agree_or_left_binding(
4266 left: Option<&Arc<str>>,
4267 right: Option<&Arc<str>>,
4268 ) -> Option<Arc<str>> {
4269 match (left, right) {
4270 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
4271 (Some(a), Some(_)) => Some(Arc::clone(a)),
4272 (Some(a), None) => Some(Arc::clone(a)),
4273 (None, Some(b)) => Some(Arc::clone(b)),
4274 (None, None) => None,
4275 }
4276 }
4277
4278 #[must_use]
4282 pub fn agree_instance_factors(
4283 left: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4284 right: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4285 ) -> Option<Arc<std::collections::BTreeMap<String, RationalInteger>>> {
4286 match (left, right) {
4287 (None, None) | (Some(_), None) | (None, Some(_)) => None,
4288 (Some(left), Some(right)) => {
4289 let mut agreed = std::collections::BTreeMap::new();
4290 for (unit, factor) in left.iter() {
4291 if right.get(unit) == Some(factor) {
4292 agreed.insert(unit.clone(), factor.clone());
4293 }
4294 }
4295 if agreed.is_empty() {
4296 None
4297 } else {
4298 Some(Arc::new(agreed))
4299 }
4300 }
4301 }
4302 }
4303
4304 #[must_use]
4307 pub fn binding_with_agreed_factors(
4308 unit: Option<Arc<str>>,
4309 factors: &Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4310 ) -> Option<Arc<str>> {
4311 match (unit, factors) {
4312 (Some(unit), Some(factors)) if factors.contains_key(unit.as_ref()) => Some(unit),
4313 (Some(_), Some(_)) => None,
4314 (unit, None) => unit,
4315 (None, _) => None,
4316 }
4317 }
4318}
4319
4320pub fn bound_value_kind_from_raw_suggestion(
4321 raw: RawSuggestion,
4322 specifications: &TypeSpecification,
4323 type_name: &str,
4324) -> Result<BoundValueKind, String> {
4325 enum Step<'a> {
4326 Visit(RawSuggestion, &'a TypeSpecification),
4327 CollectList {
4328 element: &'a Arc<LemmaType>,
4329 rows: usize,
4330 },
4331 }
4332
4333 let mut steps = vec![Step::Visit(raw, specifications)];
4334 let mut bounds: Vec<BoundValueKind> = Vec::new();
4335 while let Some(step) = steps.pop() {
4336 match step {
4337 Step::Visit(raw, specifications) => match raw {
4338 RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
4339 RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
4340 value,
4341 unit: Some(Arc::from(unit_name)),
4342 instance_factors: None,
4343 }),
4344 RawSuggestion::Measure {
4345 magnitude,
4346 unit_name,
4347 } => {
4348 let TypeSpecification::Measure { units, .. } = specifications else {
4349 unreachable!(
4350 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
4351 );
4352 };
4353 if units
4354 .get(&unit_name)
4355 .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
4356 {
4357 bounds.push(BoundValueKind {
4358 value: ValueKind::UnscaledMeasure {
4359 magnitude,
4360 unit: Arc::from(unit_name.as_str()),
4361 },
4362 unit: Some(Arc::from(unit_name)),
4363 instance_factors: None,
4364 });
4365 } else {
4366 let canonical = measure_declared_bound_to_canonical(
4367 &magnitude, &unit_name, units, type_name, "suggest",
4368 )?;
4369 bounds.push(BoundValueKind {
4370 value: ValueKind::Measure(canonical),
4371 unit: Some(Arc::from(unit_name)),
4372 instance_factors: None,
4373 });
4374 }
4375 }
4376 RawSuggestion::List(rows) => {
4377 let TypeSpecification::List { element, .. } = specifications else {
4378 unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
4379 };
4380 steps.push(Step::CollectList {
4381 element,
4382 rows: rows.len(),
4383 });
4384 for row in rows.into_iter().rev() {
4385 steps.push(Step::Visit(row, &element.specifications));
4386 }
4387 }
4388 },
4389 Step::CollectList { element, rows } => {
4390 let start = bounds
4391 .len()
4392 .checked_sub(rows)
4393 .expect("BUG: list rows missing from bound stack");
4394 let converted: Arc<[TypedLiteral]> = bounds
4395 .split_off(start)
4396 .iter()
4397 .map(|bound| typed_literal_from_bound(bound, element))
4398 .collect();
4399 bounds.push(BoundValueKind::unbound(ValueKind::List(converted.into())));
4400 }
4401 }
4402 }
4403 Ok(bounds.pop().expect("BUG: bound walk left no result"))
4404}
4405
4406#[must_use]
4410pub fn display_binding_unit(
4411 schema: &LemmaType,
4412 settled: Option<&BoundValueKind>,
4413 fill: Option<&BoundValueKind>,
4414 suggestion: Option<&BoundValueKind>,
4415) -> Option<String> {
4416 if schema.is_measure_range() || schema.is_ratio_range() {
4417 return None;
4418 }
4419 if let Some(unit) = schema.unit() {
4420 return Some(unit.to_string());
4421 }
4422 if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
4423 return Some(unit.to_string());
4424 }
4425 if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
4426 return Some(unit.to_string());
4427 }
4428 suggestion
4429 .and_then(|bound| bound.unit.as_ref())
4430 .map(|unit| unit.to_string())
4431}
4432
4433#[must_use]
4438pub fn lemma_type_with_display_binding(
4439 schema: &LemmaType,
4440 settled: Option<&BoundValueKind>,
4441 fill: Option<&BoundValueKind>,
4442 suggestion: Option<&BoundValueKind>,
4443) -> LemmaType {
4444 if schema.is_measure_range() || schema.is_ratio_range() {
4445 return schema.clone();
4446 }
4447 match display_binding_unit(schema, settled, fill, suggestion) {
4448 Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
4449 Some(_) | None => schema.clone(),
4450 }
4451}
4452
4453#[must_use]
4455pub fn lemma_type_arc_with_display_binding(
4456 schema: &Arc<LemmaType>,
4457 settled: Option<&BoundValueKind>,
4458 fill: Option<&BoundValueKind>,
4459 suggestion: Option<&BoundValueKind>,
4460) -> Arc<LemmaType> {
4461 if schema.is_measure_range() || schema.is_ratio_range() {
4462 return Arc::clone(schema);
4463 }
4464 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
4465 Some(unit) if schema.unit() != Some(unit.as_str()) => {
4466 Arc::new(schema.as_ref().clone().with_unit(unit))
4467 }
4468 Some(_) | None => Arc::clone(schema),
4469 }
4470}
4471
4472#[must_use]
4477pub fn typed_literal_from_bound(
4478 bound: &BoundValueKind,
4479 lemma_type: &Arc<LemmaType>,
4480) -> TypedLiteral {
4481 let lemma_type = match (&bound.unit, &lemma_type.specifications) {
4482 (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
4483 if lemma_type.unit() != Some(unit.as_ref()) =>
4484 {
4485 Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
4486 }
4487 _ => Arc::clone(lemma_type),
4488 };
4489 TypedLiteral {
4490 value: bound.value.clone(),
4491 lemma_type,
4492 }
4493}
4494
4495#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4502pub enum ValueKind {
4503 Number(RationalInteger),
4504 Measure(RationalInteger),
4510 UnscaledMeasure {
4514 magnitude: RationalInteger,
4515 unit: Arc<str>,
4516 },
4517 Text(Arc<str>),
4518 Date(SemanticDateTime),
4519 Time(SemanticTime),
4520 Boolean(bool),
4521 Ratio(RationalInteger),
4523 Range(Box<TypedLiteral>, Box<TypedLiteral>),
4525 Spec(SpecValue),
4527 List(ListValue),
4529}
4530
4531#[derive(Debug, Clone)]
4534pub struct ListValue {
4535 items: ListItems,
4536 index: Option<Arc<crate::planning::instance_list::KeyIndex>>,
4537}
4538
4539pub(crate) struct StoredRows<'l> {
4541 pub rows: &'l [Arc<RowInstance>],
4542 pub owner: Option<&'l InstanceKey>,
4543}
4544
4545#[derive(Debug, Clone)]
4546enum ListItems {
4547 Values(Arc<[TypedLiteral]>),
4548 Rows {
4551 store: Arc<crate::planning::instance_list::RowStore>,
4552 owner: Option<InstanceKey>,
4553 row_type: Arc<LemmaType>,
4554 },
4555}
4556
4557impl ListValue {
4558 fn held(items: Arc<[TypedLiteral]>) -> Self {
4560 Self {
4561 items: ListItems::Values(items),
4562 index: None,
4563 }
4564 }
4565
4566 pub(crate) fn indexed(
4567 items: Arc<[TypedLiteral]>,
4568 index: Arc<crate::planning::instance_list::KeyIndex>,
4569 ) -> Self {
4570 Self {
4571 items: ListItems::Values(items),
4572 index: Some(index),
4573 }
4574 }
4575
4576 pub(crate) fn stored_rows(
4578 store: &Arc<crate::planning::instance_list::RowStore>,
4579 owner: Option<InstanceKey>,
4580 row_type: Arc<LemmaType>,
4581 ) -> Self {
4582 Self {
4583 index: store.index.clone(),
4584 items: ListItems::Rows {
4585 store: Arc::clone(store),
4586 owner,
4587 row_type,
4588 },
4589 }
4590 }
4591
4592 pub fn len(&self) -> usize {
4593 match &self.items {
4594 ListItems::Values(items) => items.len(),
4595 ListItems::Rows { store, .. } => store.rows.len(),
4596 }
4597 }
4598
4599 pub fn is_empty(&self) -> bool {
4600 self.len() == 0
4601 }
4602
4603 pub fn get(&self, position: usize) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4604 match &self.items {
4605 ListItems::Values(items) => items.get(position).map(std::borrow::Cow::Borrowed),
4606 ListItems::Rows {
4607 store,
4608 owner,
4609 row_type,
4610 } => store.rows.get(position).map(|row| {
4611 std::borrow::Cow::Owned(TypedLiteral {
4612 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
4613 Arc::clone(row),
4614 owner.clone(),
4615 ))),
4616 lemma_type: Arc::clone(row_type),
4617 })
4618 }),
4619 }
4620 }
4621
4622 pub fn first(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4623 self.get(0)
4624 }
4625
4626 pub fn last(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4627 self.len().checked_sub(1).and_then(|last| self.get(last))
4628 }
4629
4630 pub fn iter(
4631 &self,
4632 ) -> impl DoubleEndedIterator<Item = std::borrow::Cow<'_, TypedLiteral>> + ExactSizeIterator
4633 {
4634 (0..self.len()).map(|position| {
4635 self.get(position)
4636 .expect("BUG: list position below the list length")
4637 })
4638 }
4639
4640 pub(crate) fn values(&self) -> &[TypedLiteral] {
4642 match &self.items {
4643 ListItems::Values(items) => items,
4644 ListItems::Rows { .. } => panic!("BUG: held items read from a list of stored rows"),
4645 }
4646 }
4647
4648 pub(crate) fn stored(&self) -> Option<StoredRows<'_>> {
4650 match &self.items {
4651 ListItems::Values(_) => None,
4652 ListItems::Rows { store, owner, .. } => Some(StoredRows {
4653 rows: &store.rows,
4654 owner: owner.as_ref(),
4655 }),
4656 }
4657 }
4658
4659 pub fn to_vec(&self) -> Vec<TypedLiteral> {
4661 self.iter().map(std::borrow::Cow::into_owned).collect()
4662 }
4663
4664 pub(crate) fn index(&self) -> Option<&Arc<crate::planning::instance_list::KeyIndex>> {
4665 self.index.as_ref()
4666 }
4667
4668 pub(crate) fn unindexed(&self) -> Self {
4670 Self {
4671 items: self.items.clone(),
4672 index: None,
4673 }
4674 }
4675
4676 pub(crate) fn with_index(&self, index: Arc<crate::planning::instance_list::KeyIndex>) -> Self {
4678 Self {
4679 items: self.items.clone(),
4680 index: Some(index),
4681 }
4682 }
4683}
4684
4685impl From<Arc<[TypedLiteral]>> for ListValue {
4686 fn from(items: Arc<[TypedLiteral]>) -> Self {
4687 Self::held(items)
4688 }
4689}
4690
4691impl From<Vec<TypedLiteral>> for ListValue {
4692 fn from(items: Vec<TypedLiteral>) -> Self {
4693 Self::held(Arc::from(items))
4694 }
4695}
4696
4697impl FromIterator<TypedLiteral> for ListValue {
4698 fn from_iter<I: IntoIterator<Item = TypedLiteral>>(iter: I) -> Self {
4699 Self::held(iter.into_iter().collect())
4700 }
4701}
4702
4703impl PartialEq for ListValue {
4704 fn eq(&self, other: &Self) -> bool {
4705 self.len() == other.len() && self.iter().eq(other.iter())
4706 }
4707}
4708
4709impl Eq for ListValue {}
4710
4711impl Hash for ListValue {
4712 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4713 self.len().hash(state);
4714 for item in self.iter() {
4715 item.hash(state);
4716 }
4717 }
4718}
4719
4720impl ValueKind {
4721 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
4723 match self {
4724 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
4725 n.try_to_decimal().map_err(|failure| failure.to_string())
4726 }
4727 other => Err(format!("expected numeric value kind, got {other}")),
4728 }
4729 }
4730
4731 pub fn structural_byte_size(&self) -> usize {
4736 fn rational_byte_estimate(r: &RationalInteger) -> usize {
4737 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
4738 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
4739 numer_bytes.max(1) + denom_bytes.max(1)
4740 }
4741 match self {
4742 ValueKind::Number(r) => rational_byte_estimate(r),
4743 ValueKind::Measure(r) => rational_byte_estimate(r),
4744 ValueKind::UnscaledMeasure { magnitude, unit } => {
4745 rational_byte_estimate(magnitude) + unit.len()
4746 }
4747 ValueKind::Ratio(r) => rational_byte_estimate(r),
4748 ValueKind::Text(s) => s.len(),
4749 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
4753 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
4754 }
4755 ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
4756 ValueKind::Spec(SpecValue::Row(InstanceKey { row, .. })) => row.structural_byte_size(),
4757 ValueKind::List(rows) => rows
4758 .iter()
4759 .map(|row| row.value.structural_byte_size())
4760 .sum(),
4761 }
4762 }
4763
4764 #[must_use]
4766 pub fn has_unscaled_measure(&self) -> bool {
4767 match self {
4768 ValueKind::UnscaledMeasure { .. } => true,
4769 ValueKind::Range(left, right) => match (&left.value, &right.value) {
4770 (ValueKind::UnscaledMeasure { .. }, _) | (_, ValueKind::UnscaledMeasure { .. }) => {
4771 true
4772 }
4773 (ValueKind::Range(_, _) | ValueKind::List(_), _)
4774 | (_, ValueKind::Range(_, _) | ValueKind::List(_)) => unscaled_in_value_tree(self),
4775 _ => false,
4776 },
4777 ValueKind::List(_) => unscaled_in_value_tree(self),
4778 _ => false,
4779 }
4780 }
4781}
4782
4783fn unscaled_in_value_tree(root: &ValueKind) -> bool {
4784 let mut pending = vec![root];
4785 while let Some(value) = pending.pop() {
4786 match value {
4787 ValueKind::UnscaledMeasure { .. } => return true,
4788 ValueKind::Range(left, right) => {
4789 pending.push(&right.value);
4790 pending.push(&left.value);
4791 }
4792 ValueKind::List(rows) => {
4793 if rows.stored().is_none() {
4794 for item in rows.values().iter() {
4795 pending.push(&item.value);
4796 }
4797 }
4798 }
4799 _ => {}
4800 }
4801 }
4802 false
4803}
4804
4805fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
4806 rational.display_str()
4807}
4808
4809fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
4810 use crate::parsing::ast::Value;
4811 match rational.try_to_decimal() {
4812 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
4813 Err(_) => format!("{} {}", rational.display_str(), unit),
4814 }
4815}
4816
4817impl fmt::Display for ValueKind {
4818 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4819 match self {
4820 ValueKind::Number(rational) => {
4821 write!(f, "{}", format_rational_magnitude_for_display(rational))
4822 }
4823 ValueKind::Measure(rational) => {
4824 write!(f, "{}", format_rational_magnitude_for_display(rational))
4825 }
4826 ValueKind::UnscaledMeasure { magnitude, unit } => {
4827 write!(
4828 f,
4829 "{} {unit}",
4830 format_rational_magnitude_for_display(magnitude)
4831 )
4832 }
4833 ValueKind::Text(s) => {
4834 write!(f, "{}", crate::parsing::ast::Value::Text(s.to_string()))
4835 }
4836 ValueKind::Ratio(rational) => {
4837 write!(f, "{}", format_rational_magnitude_for_display(rational))
4838 }
4839 ValueKind::Date(dt) => write!(f, "{}", dt),
4840 ValueKind::Time(t) => write!(
4841 f,
4842 "{}",
4843 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
4844 hour: t.hour as u8,
4845 minute: t.minute as u8,
4846 second: t.second as u8,
4847 microsecond: t.microsecond,
4848 timezone: t
4849 .timezone
4850 .as_ref()
4851 .map(crate::parsing::ast::TimezoneValue::from),
4852 })
4853 ),
4854 ValueKind::Boolean(b) => write!(f, "{}", b),
4855 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4856 ValueKind::Spec(value) => write!(f, "{value}"),
4857 ValueKind::List(rows) => {
4858 write!(f, "list[")?;
4859 for (i, row) in rows.iter().enumerate() {
4860 if i > 0 {
4861 write!(f, ", ")?;
4862 }
4863 write!(f, "{row}")?;
4864 }
4865 write!(f, "]")
4866 }
4867 }
4868 }
4869}
4870
4871#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4883pub struct PathSegment {
4884 pub uses: Arc<str>,
4886 pub row: Option<usize>,
4888 pub repository: Option<Arc<str>>,
4891 pub spec: Arc<str>,
4893}
4894
4895impl PathSegment {
4896 pub(crate) fn import(alias: &str, identity: &SpecIdentity) -> Self {
4898 Self {
4899 uses: Arc::from(alias),
4900 row: None,
4901 repository: identity.repository.as_deref().map(Arc::from),
4902 spec: Arc::from(identity.spec.as_str()),
4903 }
4904 }
4905
4906 pub(crate) fn list_row_shared(
4910 list: &Arc<str>,
4911 number: usize,
4912 spec: &Arc<str>,
4913 repository: Option<&Arc<str>>,
4914 ) -> Self {
4915 Self {
4916 uses: Arc::clone(list),
4917 row: Some(number),
4918 repository: repository.map(Arc::clone),
4919 spec: Arc::clone(spec),
4920 }
4921 }
4922
4923 pub fn input_key(&self) -> String {
4925 match self.row {
4926 Some(number) => format!("{}.{number}", self.uses),
4927 None => self.uses.to_string(),
4928 }
4929 }
4930}
4931
4932#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4935pub struct SpecInstance {
4936 pub prefix: Vec<PathSegment>,
4937}
4938
4939impl SpecInstance {
4940 pub fn structural_byte_size(&self) -> usize {
4941 self.prefix
4942 .iter()
4943 .map(|segment| {
4944 segment.uses.len()
4945 + segment.spec.len()
4946 + segment.repository.as_deref().map_or(0, str::len)
4947 + segment.row.map_or(0, |_| std::mem::size_of::<usize>())
4948 })
4949 .sum()
4950 }
4951}
4952
4953impl fmt::Display for SpecInstance {
4954 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4955 for (index, segment) in self.prefix.iter().enumerate() {
4956 if index > 0 {
4957 write!(f, ".")?;
4958 }
4959 write!(f, "{}", segment.input_key())?;
4960 }
4961 Ok(())
4962 }
4963}
4964
4965#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4975pub enum SpecValue {
4976 Fixed(SpecInstance),
4977 Row(InstanceKey),
4978}
4979
4980impl fmt::Display for SpecValue {
4981 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4982 match self {
4983 SpecValue::Fixed(instance) => write!(f, "{instance}"),
4984 SpecValue::Row(key) => write!(f, "{key}"),
4985 }
4986 }
4987}
4988
4989#[derive(Debug, Clone)]
4993pub struct InstanceKey {
4994 pub row: Arc<RowInstance>,
4995 pub owner: Option<Box<InstanceKey>>,
4996}
4997
4998impl InstanceKey {
4999 pub fn new(row: Arc<RowInstance>, owner: Option<InstanceKey>) -> Self {
5000 Self {
5001 row,
5002 owner: owner.map(Box::new),
5003 }
5004 }
5005
5006 pub fn owner(&self) -> Option<&InstanceKey> {
5007 self.owner.as_deref()
5008 }
5009
5010 pub(crate) fn scope(&self, hops: u32) -> Option<&InstanceKey> {
5013 let mut scope = self.owner();
5014 for _ in 0..hops {
5015 scope = scope
5016 .expect("BUG: a binding hops above the root scope")
5017 .owner();
5018 }
5019 scope
5020 }
5021
5022 pub(crate) fn under(&self, owner: &InstanceKey) -> InstanceKey {
5025 let mut rows: Vec<&Arc<RowInstance>> = Vec::new();
5026 let mut key = Some(self);
5027 while let Some(current) = key {
5028 rows.push(¤t.row);
5029 key = current.owner();
5030 }
5031 let mut rebased = owner.clone();
5032 for row in rows.into_iter().rev() {
5033 rebased = InstanceKey::new(Arc::clone(row), Some(rebased));
5034 }
5035 rebased
5036 }
5037
5038 fn naming_origins(&self) -> Vec<&RowOrigin> {
5040 let mut origins = Vec::new();
5041 let mut key = Some(self);
5042 while let Some(current) = key {
5043 origins.push(¤t.row.origin);
5044 if matches!(current.row.origin, RowOrigin::Numbered { .. }) {
5045 break;
5046 }
5047 key = current.owner();
5048 }
5049 origins
5050 }
5051
5052 pub fn path(&self) -> Option<String> {
5056 let origins = self.naming_origins();
5057 if origins.iter().any(|origin| origin.is_inline()) {
5058 return None;
5059 }
5060 Some(
5061 origins
5062 .iter()
5063 .rev()
5064 .map(ToString::to_string)
5065 .collect::<Vec<_>>()
5066 .join("."),
5067 )
5068 }
5069
5070 pub(crate) fn label_under(&self, row: &InstanceKey) -> Option<String> {
5073 let mut below: Vec<&RowOrigin> = Vec::new();
5074 let mut key = self;
5075 while key != row {
5076 below.push(&key.row.origin);
5077 key = key.owner()?;
5078 }
5079 Some(
5080 below
5081 .iter()
5082 .rev()
5083 .map(ToString::to_string)
5084 .collect::<Vec<_>>()
5085 .join("."),
5086 )
5087 }
5088
5089 pub(crate) fn table_row(&self) -> (&InstanceKey, Option<String>) {
5093 let mut key = self;
5094 loop {
5095 match &key.row.origin {
5096 RowOrigin::Numbered { .. } => return (key, self.path()),
5097 RowOrigin::Inline { .. } if key == self => return (key, None),
5098 RowOrigin::Inline { .. } => return (key, self.label_under(key)),
5099 RowOrigin::Import { .. } | RowOrigin::Written { .. } => {
5100 key = key
5101 .owner()
5102 .expect("BUG: an instance off the rows table published as a table row");
5103 }
5104 }
5105 }
5106 }
5107
5108 pub(crate) fn field_key(&self, key: &str) -> Option<String> {
5111 self.request_path().map(|path| format!("{path}.{key}"))
5112 }
5113
5114 fn request_path(&self) -> Option<String> {
5117 let origins = self.naming_origins();
5118 if origins.iter().any(|origin| origin.is_inline()) {
5119 return None;
5120 }
5121 Some(
5122 origins
5123 .iter()
5124 .rev()
5125 .map(|origin| match origin {
5126 RowOrigin::Numbered {
5127 under: Some(under), ..
5128 } => format!("{under}.{origin}"),
5129 origin => origin.to_string(),
5130 })
5131 .collect::<Vec<_>>()
5132 .join("."),
5133 )
5134 }
5135
5136 pub(crate) fn segments(&self) -> Option<Vec<PathSegment>> {
5139 let mut parts: Vec<Vec<PathSegment>> = Vec::new();
5140 let mut key = Some(self);
5141 while let Some(current) = key {
5142 match ¤t.row.origin {
5143 RowOrigin::Import { alias } => {
5144 parts.push(vec![PathSegment::import(alias, ¤t.row.identity)])
5145 }
5146 RowOrigin::Written { instance, .. } => parts.push(instance.prefix.clone()),
5147 RowOrigin::Numbered { .. } | RowOrigin::Inline { .. } => return None,
5148 }
5149 key = current.owner();
5150 }
5151 Some(parts.into_iter().rev().flatten().collect())
5152 }
5153
5154 pub(crate) fn import_hops(&self) -> Vec<PathSegment> {
5157 let mut hops = Vec::new();
5158 let mut key = Some(self);
5159 while let Some(current) = key {
5160 let RowOrigin::Import { alias } = ¤t.row.origin else {
5161 break;
5162 };
5163 hops.push(PathSegment::import(alias, ¤t.row.identity));
5164 key = current.owner();
5165 }
5166 hops.reverse();
5167 hops
5168 }
5169
5170 pub(crate) fn is_planned(&self) -> bool {
5173 let mut key = Some(self);
5174 while let Some(current) = key {
5175 if !matches!(
5176 current.row.origin,
5177 RowOrigin::Import { .. } | RowOrigin::Written { .. }
5178 ) {
5179 return false;
5180 }
5181 key = current.owner();
5182 }
5183 true
5184 }
5185}
5186
5187impl fmt::Display for InstanceKey {
5188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5189 for (index, origin) in self.naming_origins().iter().rev().enumerate() {
5190 if index > 0 {
5191 write!(f, ".")?;
5192 }
5193 write!(f, "{origin}")?;
5194 }
5195 Ok(())
5196 }
5197}
5198
5199impl PartialEq for InstanceKey {
5200 fn eq(&self, other: &Self) -> bool {
5201 Arc::ptr_eq(&self.row, &other.row) && self.owner == other.owner
5202 }
5203}
5204
5205impl Eq for InstanceKey {}
5206
5207impl std::hash::Hash for InstanceKey {
5208 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
5209 (Arc::as_ptr(&self.row) as usize).hash(state);
5210 self.owner.hash(state);
5211 }
5212}
5213
5214#[derive(Debug, Clone)]
5221pub enum RowBinding {
5222 Bound(OperationResult),
5223 Deferred {
5227 target: ReferenceTarget,
5228 hops: u32,
5229 },
5230 Rows {
5234 rows: Vec<Arc<RowInstance>>,
5235 row_type: Arc<LemmaType>,
5236 hops: u32,
5237 },
5238 Instance(Arc<RowInstance>),
5241}
5242
5243impl RowBinding {
5244 pub(crate) fn is_copy_of(&self, other: &RowBinding) -> bool {
5246 match (self, other) {
5247 (RowBinding::Bound(a), RowBinding::Bound(b)) => a == b,
5248 (
5249 RowBinding::Deferred { target, hops },
5250 RowBinding::Deferred {
5251 target: other_target,
5252 hops: other_hops,
5253 },
5254 ) => target == other_target && hops == other_hops,
5255 (
5256 RowBinding::Rows { rows, hops, .. },
5257 RowBinding::Rows {
5258 rows: other_rows,
5259 hops: other_hops,
5260 ..
5261 },
5262 ) => {
5263 hops == other_hops
5264 && rows.len() == other_rows.len()
5265 && rows
5266 .iter()
5267 .zip(other_rows)
5268 .all(|(row, other)| Arc::ptr_eq(row, other))
5269 }
5270 (RowBinding::Instance(row), RowBinding::Instance(other)) => Arc::ptr_eq(row, other),
5271 _ => false,
5272 }
5273 }
5274
5275 pub(crate) fn rows_value(
5277 rows: &[Arc<RowInstance>],
5278 row_type: &Arc<LemmaType>,
5279 owner: Option<&InstanceKey>,
5280 ) -> OperationResult {
5281 OperationResult::from_literal(LiteralValue {
5282 value: ValueKind::List(
5283 rows.iter()
5284 .map(|row| TypedLiteral {
5285 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5286 Arc::clone(row),
5287 owner.cloned(),
5288 ))),
5289 lemma_type: Arc::clone(row_type),
5290 })
5291 .collect(),
5292 ),
5293 })
5294 }
5295}
5296
5297#[derive(Debug, Clone)]
5302pub struct RowInstance {
5303 pub identity: Arc<SpecIdentity>,
5304 pub origin: RowOrigin,
5305 pub bindings: Box<[Option<RowBinding>]>,
5306}
5307
5308impl RowInstance {
5309 pub fn get_binding(
5310 &self,
5311 template: &crate::planning::execution_plan::ExecutionPlan,
5312 path: &DataPath,
5313 ) -> Option<&RowBinding> {
5314 let index = template.data.get_index_of(path)?;
5315 self.bindings.get(index)?.as_ref()
5316 }
5317
5318 pub fn structural_byte_size(&self) -> usize {
5320 let mut size = 0;
5321 let mut pending: Vec<&RowInstance> = vec![self];
5322 while let Some(row) = pending.pop() {
5323 for binding in row.bindings.iter().flatten() {
5324 match binding {
5325 RowBinding::Bound(OperationResult::Value(bound)) => {
5326 size += bound.value.structural_byte_size();
5327 }
5328 RowBinding::Rows { rows, .. } => pending.extend(rows.iter().map(Arc::as_ref)),
5329 RowBinding::Instance(row) => pending.push(row),
5330 RowBinding::Bound(OperationResult::Veto(_)) | RowBinding::Deferred { .. } => {}
5331 }
5332 }
5333 }
5334 size
5335 }
5336}
5337
5338#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5340pub enum RowOrigin {
5341 Numbered {
5345 list: String,
5346 number: usize,
5347 under: Option<Arc<str>>,
5348 },
5349 Written {
5352 instance: SpecInstance,
5353 source: Source,
5354 },
5355 Import { alias: Arc<str> },
5357 Inline { node: NormalFormId, position: usize },
5363}
5364
5365#[must_use]
5367pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
5368 format!("{list_key}.{number}")
5369}
5370
5371#[must_use]
5373pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
5374 format!("{}.{field_key}", numbered_row_origin(list_key, number))
5375}
5376
5377impl RowOrigin {
5378 pub(crate) fn list_position(&self) -> (usize, bool) {
5380 match self {
5381 RowOrigin::Numbered { number, .. } => (*number, false),
5382 RowOrigin::Written { instance, .. } => (
5383 instance
5384 .prefix
5385 .last()
5386 .and_then(|last| last.row)
5387 .expect("BUG: written row origin without a row hop"),
5388 false,
5389 ),
5390 RowOrigin::Inline { position, .. } => (*position, true),
5391 RowOrigin::Import { alias } => {
5392 unreachable!("BUG: the row of `uses {alias}` is not a list row")
5393 }
5394 }
5395 }
5396
5397 pub(crate) fn is_inline(&self) -> bool {
5398 matches!(self, RowOrigin::Inline { .. })
5399 }
5400}
5401
5402impl fmt::Display for RowOrigin {
5403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5404 match self {
5405 RowOrigin::Numbered { list, number, .. } => {
5406 write!(f, "{}", numbered_row_origin(list, *number))
5407 }
5408 RowOrigin::Written { instance, .. } => write!(f, "{instance}"),
5409 RowOrigin::Import { alias } => write!(f, "{alias}"),
5410 RowOrigin::Inline { position, .. } => write!(f, "appended row {position}"),
5411 }
5412 }
5413}
5414
5415#[must_use]
5417pub fn row_literal(
5418 identity: Arc<SpecIdentity>,
5419 origin: RowOrigin,
5420 bindings: Vec<Option<RowBinding>>,
5421 lemma_type: Arc<LemmaType>,
5422) -> TypedLiteral {
5423 TypedLiteral {
5424 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5425 Arc::new(RowInstance {
5426 identity,
5427 origin,
5428 bindings: bindings.into(),
5429 }),
5430 None,
5431 ))),
5432 lemma_type,
5433 }
5434}
5435
5436#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5438pub enum RowEnd {
5439 Data(DataPath),
5440 Rule(RulePath),
5441}
5442
5443impl fmt::Display for RowEnd {
5444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5445 match self {
5446 RowEnd::Data(path) => write!(f, "{}", path.input_key()),
5447 RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
5448 }
5449 }
5450}
5451
5452#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5456pub struct DataPath {
5457 pub segments: Vec<PathSegment>,
5459 pub data: String,
5461}
5462
5463impl DataPath {
5464 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
5466 Self { segments, data }
5467 }
5468
5469 pub fn local(data: String) -> Self {
5471 Self {
5472 segments: vec![],
5473 data,
5474 }
5475 }
5476
5477 pub fn input_key(&self) -> String {
5480 let mut s = String::new();
5481 for segment in &self.segments {
5482 s.push_str(&segment.input_key());
5483 s.push('.');
5484 }
5485 s.push_str(&self.data);
5486 s
5487 }
5488}
5489
5490#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5494pub struct RulePath {
5495 pub segments: Vec<PathSegment>,
5497 pub rule: String,
5499}
5500
5501impl RulePath {
5502 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
5504 Self { segments, rule }
5505 }
5506
5507 pub fn input_key(&self) -> String {
5510 if self.segments.is_empty() {
5511 return self.rule.clone();
5512 }
5513 let mut s = String::new();
5514 for segment in &self.segments {
5515 s.push_str(&segment.input_key());
5516 s.push('.');
5517 }
5518 s.push_str(&self.rule);
5519 s
5520 }
5521}
5522
5523pub type Expression = ExprTree<ExpressionKind>;
5536
5537#[derive(Debug, Clone, PartialEq)]
5539pub enum ExpressionKind {
5540 Literal(Box<TypedLiteral>),
5542 DataPath(DataPath),
5544 RulePath(RulePath),
5546 Logical {
5547 connective: LogicalConnective,
5548 first: ExprId,
5549 steps: Vec<ChainStep<()>>,
5550 },
5551 Arithmetic {
5553 first: ExprId,
5554 steps: Vec<ChainStep<ArithmeticComputation>>,
5555 },
5556 Comparison(ExprId, ComparisonComputation, ExprId),
5557 UnitConversion(ExprId, SemanticConversionTarget),
5558 LogicalNegation(ExprId, NegationType),
5559 MathematicalComputation(MathematicalComputation, ExprId),
5560 StepRounding {
5562 operand: ExprId,
5563 mode: RoundingMode,
5564 step: Option<ExprId>,
5565 },
5566 Veto(VetoExpression),
5567 Now,
5569 DateRelative(DateRelativeKind, ExprId),
5571 DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
5573 RangeLiteral(ExprId, ExprId),
5574 PastFutureRange(DateRelativeKind, ExprId),
5575 RangeContainment(ExprId, ExprId),
5576 RangeBound(RangeBound, ExprId),
5578 ResultIsVeto(ExprId),
5580 SpecMember {
5586 base: ExprId,
5587 path: Vec<String>,
5588 name: String,
5589 },
5590 RowField {
5595 level: u32,
5596 identity: SpecIdentity,
5597 end: RowEnd,
5598 qualifier: Option<Vec<String>>,
5601 },
5602 RowValue {
5604 level: u32,
5605 list: ExprId,
5606 },
5607 Piecewise(Vec<(ExprId, ExprId)>),
5610 ListCommand {
5613 list: ExprId,
5614 op: ListOp,
5615 row_level: Option<u32>,
5616 },
5617}
5618
5619#[derive(Debug, Clone, PartialEq)]
5621pub enum ListOp {
5622 Find {
5623 keys: Vec<ExprId>,
5624 },
5625 First,
5626 Last,
5627 Count,
5628 Reduce {
5629 kind: ListReduce,
5630 argument: Option<ExprId>,
5631 },
5632 Predicate {
5633 kind: ListPredKind,
5634 form: crate::parsing::ast::ListPredicateForm,
5635 expr: ExprId,
5636 },
5637 Sort {
5638 descending: bool,
5639 argument: Option<ExprId>,
5640 },
5641 Top {
5642 n: ExprId,
5643 },
5644 With {
5645 rows: Vec<ExprId>,
5646 },
5647 WithRow {
5649 identity: SpecIdentity,
5650 fields: Vec<(DataPath, ExprId)>,
5651 },
5652 Key {
5654 fields: Vec<DataPath>,
5655 },
5656}
5657
5658#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5659pub enum ListReduce {
5660 Sum,
5661 Avg,
5662 Product,
5663 Min,
5664 Max,
5665}
5666
5667impl ListReduce {
5668 pub fn keyword(self) -> &'static str {
5669 match self {
5670 ListReduce::Sum => "sum",
5671 ListReduce::Avg => "avg",
5672 ListReduce::Product => "product",
5673 ListReduce::Min => "min",
5674 ListReduce::Max => "max",
5675 }
5676 }
5677}
5678
5679#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5680pub enum ListPredKind {
5681 Any,
5682 All,
5683 Where,
5684}
5685
5686impl ListPredKind {
5687 pub fn keyword(self) -> &'static str {
5688 match self {
5689 ListPredKind::Any => "any",
5690 ListPredKind::All => "all",
5691 ListPredKind::Where => "where",
5692 }
5693 }
5694}
5695
5696impl ExprKind for ExpressionKind {
5697 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5698 match self {
5699 ExpressionKind::Literal(_)
5700 | ExpressionKind::DataPath(_)
5701 | ExpressionKind::RulePath(_)
5702 | ExpressionKind::Veto(_)
5703 | ExpressionKind::Now
5704 | ExpressionKind::RowField { .. }
5705 | ExpressionKind::RowValue { .. } => {}
5706 ExpressionKind::Logical { first, steps, .. } => {
5707 f(*first);
5708 steps.iter().for_each(|step| f(step.operand));
5709 }
5710 ExpressionKind::Arithmetic { first, steps } => {
5711 f(*first);
5712 steps.iter().for_each(|step| f(step.operand));
5713 }
5714 ExpressionKind::Comparison(left, _, right)
5715 | ExpressionKind::RangeLiteral(left, right)
5716 | ExpressionKind::RangeContainment(left, right) => {
5717 f(*left);
5718 f(*right);
5719 }
5720 ExpressionKind::StepRounding { operand, step, .. } => {
5721 f(*operand);
5722 if let Some(step) = step {
5723 f(*step);
5724 }
5725 }
5726 ExpressionKind::UnitConversion(inner, _)
5727 | ExpressionKind::LogicalNegation(inner, _)
5728 | ExpressionKind::MathematicalComputation(_, inner)
5729 | ExpressionKind::DateRelative(_, inner)
5730 | ExpressionKind::DateCalendar(_, _, inner)
5731 | ExpressionKind::PastFutureRange(_, inner)
5732 | ExpressionKind::RangeBound(_, inner)
5733 | ExpressionKind::ResultIsVeto(inner)
5734 | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
5735 ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
5736 f(*condition);
5737 f(*result);
5738 }),
5739 ExpressionKind::ListCommand { list, op, .. } => {
5740 f(*list);
5741 op.for_each_child(f);
5742 }
5743 }
5744 }
5745
5746 fn children_mut(&mut self) -> Vec<&mut ExprId> {
5747 match self {
5748 ExpressionKind::Literal(_)
5749 | ExpressionKind::DataPath(_)
5750 | ExpressionKind::RulePath(_)
5751 | ExpressionKind::Veto(_)
5752 | ExpressionKind::Now
5753 | ExpressionKind::RowField { .. }
5754 | ExpressionKind::RowValue { .. } => Vec::new(),
5755 ExpressionKind::Logical { first, steps, .. } => std::iter::once(first)
5756 .chain(steps.iter_mut().map(|step| &mut step.operand))
5757 .collect(),
5758 ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
5759 .chain(steps.iter_mut().map(|step| &mut step.operand))
5760 .collect(),
5761 ExpressionKind::Comparison(left, _, right)
5762 | ExpressionKind::RangeLiteral(left, right)
5763 | ExpressionKind::RangeContainment(left, right) => vec![left, right],
5764 ExpressionKind::StepRounding { operand, step, .. } => {
5765 let mut children = vec![operand];
5766 if let Some(step) = step {
5767 children.push(step);
5768 }
5769 children
5770 }
5771 ExpressionKind::UnitConversion(inner, _)
5772 | ExpressionKind::LogicalNegation(inner, _)
5773 | ExpressionKind::MathematicalComputation(_, inner)
5774 | ExpressionKind::DateRelative(_, inner)
5775 | ExpressionKind::DateCalendar(_, _, inner)
5776 | ExpressionKind::PastFutureRange(_, inner)
5777 | ExpressionKind::RangeBound(_, inner)
5778 | ExpressionKind::ResultIsVeto(inner) => vec![inner],
5779 ExpressionKind::SpecMember { base, .. } => vec![base],
5780 ExpressionKind::Piecewise(arms) => arms
5781 .iter_mut()
5782 .flat_map(|(condition, result)| [condition, result])
5783 .collect(),
5784 ExpressionKind::ListCommand { list, op, .. } => {
5785 let mut children = vec![list];
5786 children.extend(op.children_mut());
5787 children
5788 }
5789 }
5790 }
5791}
5792
5793impl ListOp {
5794 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5795 match self {
5796 ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => {}
5797 ListOp::Find { keys } => keys.iter().copied().for_each(f),
5798 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5799 argument.iter().copied().for_each(f)
5800 }
5801 ListOp::Predicate { expr, .. } => f(*expr),
5802 ListOp::Top { n } => f(*n),
5803 ListOp::With { rows } => rows.iter().copied().for_each(f),
5804 ListOp::WithRow { fields, .. } => fields.iter().for_each(|(_, value)| f(*value)),
5805 }
5806 }
5807
5808 fn children_mut(&mut self) -> Vec<&mut ExprId> {
5809 match self {
5810 ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => Vec::new(),
5811 ListOp::Find { keys } => keys.iter_mut().collect(),
5812 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5813 argument.iter_mut().collect()
5814 }
5815 ListOp::Predicate { expr, .. } => vec![expr],
5816 ListOp::Top { n } => vec![n],
5817 ListOp::With { rows } => rows.iter_mut().collect(),
5818 ListOp::WithRow { fields, .. } => fields.iter_mut().map(|(_, value)| value).collect(),
5819 }
5820 }
5821}
5822
5823#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5829pub enum TypeDefiningSpec {
5830 Local,
5832 Import,
5834}
5835
5836#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
5843pub struct TypeFamily {
5844 pub repository: Option<String>,
5845 pub spec: String,
5846 pub effective: crate::parsing::ast::EffectiveDate,
5847 pub name: String,
5848}
5849
5850impl TypeFamily {
5851 #[must_use]
5852 pub(crate) fn declared_in(spec: &SpecVersionKey, name: &str) -> Self {
5853 Self {
5854 repository: spec.repository.clone(),
5855 spec: spec.spec.clone(),
5856 effective: spec.effective.clone(),
5857 name: name.to_string(),
5858 }
5859 }
5860
5861 #[must_use]
5863 pub(crate) fn is_declared_in(&self, spec: &SpecVersionKey) -> bool {
5864 self.repository == spec.repository
5865 && self.spec == spec.spec
5866 && self.effective == spec.effective
5867 }
5868
5869 #[must_use]
5871 pub fn without_effective(&self) -> Self {
5872 Self {
5873 repository: self.repository.clone(),
5874 spec: self.spec.clone(),
5875 effective: crate::parsing::ast::EffectiveDate::Origin,
5876 name: self.name.clone(),
5877 }
5878 }
5879}
5880
5881impl fmt::Display for TypeFamily {
5883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5884 if let Some(repository) = &self.repository {
5885 write!(f, "{repository} ")?;
5886 }
5887 write!(f, "{}", self.spec)?;
5888 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
5889 write!(f, " {at}")?;
5890 }
5891 write!(f, ".{}", self.name)
5892 }
5893}
5894
5895#[derive(Clone, Debug)]
5897pub enum TypeExtends {
5898 Primitive,
5900 Custom {
5903 parent: String,
5904 family: TypeFamily,
5905 defining_spec: TypeDefiningSpec,
5906 },
5907}
5908
5909impl PartialEq for TypeExtends {
5910 fn eq(&self, other: &Self) -> bool {
5911 match (self, other) {
5912 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
5913 (
5914 TypeExtends::Custom {
5915 parent: lp,
5916 family: lf,
5917 defining_spec: ld,
5918 },
5919 TypeExtends::Custom {
5920 parent: rp,
5921 family: rf,
5922 defining_spec: rd,
5923 },
5924 ) => lp == rp && lf == rf && ld == rd,
5925 _ => false,
5926 }
5927 }
5928}
5929
5930impl Eq for TypeExtends {}
5931
5932impl std::hash::Hash for TypeExtends {
5933 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
5934 match self {
5935 TypeExtends::Primitive => {
5936 0u8.hash(state);
5937 }
5938 TypeExtends::Custom {
5939 parent,
5940 family,
5941 defining_spec,
5942 } => {
5943 1u8.hash(state);
5944 parent.hash(state);
5945 family.hash(state);
5946 defining_spec.hash(state);
5947 }
5948 }
5949 }
5950}
5951
5952impl TypeExtends {
5953 #[must_use]
5955 pub fn custom_local(parent: String, family: TypeFamily) -> Self {
5956 TypeExtends::Custom {
5957 parent,
5958 family,
5959 defining_spec: TypeDefiningSpec::Local,
5960 }
5961 }
5962
5963 #[must_use]
5965 pub fn parent_name(&self) -> Option<&str> {
5966 match self {
5967 TypeExtends::Primitive => None,
5968 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
5969 }
5970 }
5971
5972 #[must_use]
5973 fn with_families_without_effective(&self) -> Self {
5974 match self {
5975 TypeExtends::Primitive => TypeExtends::Primitive,
5976 TypeExtends::Custom {
5977 parent,
5978 family,
5979 defining_spec,
5980 } => TypeExtends::Custom {
5981 parent: parent.clone(),
5982 family: family.without_effective(),
5983 defining_spec: defining_spec.clone(),
5984 },
5985 }
5986 }
5987}
5988
5989#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5994pub struct LemmaType {
5995 pub name: Option<String>,
5997 pub specifications: TypeSpecification,
5999 pub extends: TypeExtends,
6001 pub(crate) options: TypeOptions,
6002}
6003
6004impl LemmaType {
6005 pub fn map_measure<F>(self, f: F) -> Self
6009 where
6010 F: FnOnce(
6011 MeasureUnits,
6012 Option<BaseMeasureVector>,
6013 ) -> (MeasureUnits, Option<BaseMeasureVector>),
6014 {
6015 let LemmaType {
6016 name,
6017 specifications,
6018 extends,
6019 options,
6020 } = self;
6021 let specifications = match specifications {
6022 TypeSpecification::Measure {
6023 minimum,
6024 maximum,
6025 units,
6026 traits,
6027 decomposition,
6028 unit,
6029 help,
6030 } => {
6031 let (units, decomposition) = f(units, decomposition);
6032 TypeSpecification::Measure {
6033 minimum,
6034 maximum,
6035 units,
6036 traits,
6037 decomposition,
6038 unit,
6039 help,
6040 }
6041 }
6042 other => other,
6043 };
6044 LemmaType {
6045 name,
6046 specifications,
6047 extends,
6048 options,
6049 }
6050 }
6051
6052 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
6054 Self {
6055 name: Some(name),
6056 specifications,
6057 extends,
6058 options: TypeOptions::default(),
6059 }
6060 }
6061
6062 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
6064 Self {
6065 name: None,
6066 specifications,
6067 extends,
6068 options: TypeOptions::default(),
6069 }
6070 }
6071
6072 pub fn primitive(specifications: TypeSpecification) -> Self {
6074 Self {
6075 name: None,
6076 specifications,
6077 extends: TypeExtends::Primitive,
6078 options: TypeOptions::default(),
6079 }
6080 }
6081
6082 pub fn apply_constraint(
6086 &mut self,
6087 names: ConstraintNames<'_>,
6088 command: TypeConstraintCommand,
6089 args: &[CommandArg],
6090 declared_suggestion: &mut Option<RawSuggestion>,
6091 declared_value: &mut Option<RawSuggestion>,
6092 resolve_unit: Option<&UnitTokenResolver<'_>>,
6093 ) -> Result<(), String> {
6094 self.specifications.apply_constraint(
6095 names,
6096 command,
6097 args,
6098 declared_suggestion,
6099 declared_value,
6100 resolve_unit,
6101 )
6102 }
6103
6104 pub(crate) fn reject_disallowed(
6109 &self,
6110 value: &ValueKind,
6111 runtime_scales: Option<
6112 &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
6113 >,
6114 ) -> Result<(), String> {
6115 let options = self.options_for_check();
6116 if options.rule.is_some() || options.values.is_empty() {
6117 return Ok(());
6118 }
6119 if options_have_unscaled_measure(&options) {
6120 let Some(scales) = runtime_scales else {
6121 return Ok(());
6122 };
6123 return if static_option_match_with_scales(&options, value, scales)? {
6124 Ok(())
6125 } else {
6126 Err(disallowed_message(self, &options, value, None))
6127 };
6128 }
6129 if static_option_match(&options, value) {
6130 return Ok(());
6131 }
6132 Err(disallowed_message(self, &options, value, None))
6133 }
6134
6135 #[must_use]
6137 pub fn options_rule(&self) -> Option<&str> {
6138 if let Some(name) = &self.options.rule {
6139 return Some(name.as_str());
6140 }
6141 if let TypeSpecification::List { element, .. } = &self.specifications {
6142 if let Some(name) = &element.options.rule {
6143 return Some(name.as_str());
6144 }
6145 }
6146 None
6147 }
6148
6149 #[must_use]
6151 pub(crate) fn has_unscaled_options(&self) -> bool {
6152 options_have_unscaled_measure(&self.options_for_check())
6153 }
6154
6155 #[must_use]
6158 pub(crate) fn needs_runtime_option_check(&self) -> bool {
6159 self.options_rule().is_some() || self.has_unscaled_options()
6160 }
6161
6162 #[must_use]
6164 pub fn static_options(&self) -> Vec<String> {
6165 let options = self.options_for_check();
6166 options
6167 .values
6168 .iter()
6169 .map(|option| self.display_bound(&option.value))
6170 .collect()
6171 }
6172
6173 #[must_use]
6176 pub fn inline_option_values(&self) -> Vec<BoundValueKind> {
6177 self.options_for_check()
6178 .values
6179 .iter()
6180 .map(|option| option.value.clone())
6181 .collect()
6182 }
6183
6184 pub(crate) fn display_bound(&self, bound: &BoundValueKind) -> String {
6185 typed_literal_from_bound(bound, &Arc::new(self.clone())).display_value()
6186 }
6187
6188 pub(crate) fn options_for_check(&self) -> std::borrow::Cow<'_, TypeOptions> {
6190 match &self.specifications {
6191 TypeSpecification::List { element, .. }
6192 if !self.options.is_empty() && !element.options.is_empty() =>
6193 {
6194 let mut options = self.options.clone();
6195 options
6196 .values
6197 .extend(element.options.values.iter().cloned());
6198 if options.rule.is_none() {
6199 options.rule.clone_from(&element.options.rule);
6200 }
6201 std::borrow::Cow::Owned(options)
6202 }
6203 TypeSpecification::List { element, .. } if !element.options.is_empty() => {
6204 std::borrow::Cow::Borrowed(&element.options)
6205 }
6206 _ => std::borrow::Cow::Borrowed(&self.options),
6207 }
6208 }
6209
6210 pub(crate) fn explain_disallowed(
6216 &self,
6217 value: &ValueKind,
6218 rule_rows: Option<&ListValue>,
6219 runtime_scales: Option<
6220 &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
6221 >,
6222 ) -> Option<String> {
6223 let options = self.options_for_check();
6224 let needs_scales = options_have_unscaled_measure(&options)
6225 || rule_rows
6226 .is_some_and(|rows| rows.iter().any(|row| row.value.has_unscaled_measure()))
6227 || value.has_unscaled_measure();
6228 if needs_scales && runtime_scales.is_none() {
6229 return None;
6230 }
6231 let allowed = |candidate: &ValueKind| -> Result<bool, String> {
6232 if let Some(scales) = runtime_scales {
6233 if options_have_unscaled_measure(&options) {
6234 if static_option_match_with_scales(&options, candidate, scales)? {
6235 return Ok(true);
6236 }
6237 } else if static_option_match(&options, candidate) {
6238 return Ok(true);
6239 }
6240 if let Some(rows) = rule_rows {
6241 let settled_candidate = settle_value_kind_with_scales(candidate, scales)?;
6242 for option in rows.iter() {
6243 let settled_option = settle_value_kind_with_scales(&option.value, scales)?;
6244 if option_values_match(&settled_option, &settled_candidate) {
6245 return Ok(true);
6246 }
6247 }
6248 return Ok(false);
6249 }
6250 return Ok(options.is_empty());
6251 }
6252 if static_option_match(&options, candidate) {
6253 return Ok(true);
6254 }
6255 if let Some(rows) = rule_rows {
6256 return Ok(rows
6257 .iter()
6258 .any(|option| option_values_match(&option.value, candidate)));
6259 }
6260 Ok(options.is_empty())
6261 };
6262 match (&self.specifications, value) {
6263 (TypeSpecification::List { .. }, ValueKind::List(rows)) => {
6264 for row in rows.iter() {
6265 match allowed(&row.value) {
6266 Ok(true) => {}
6267 Ok(false) => {
6268 return Some(disallowed_message(self, &options, &row.value, rule_rows));
6269 }
6270 Err(message) => return Some(message),
6271 }
6272 }
6273 None
6274 }
6275 _ => match allowed(value) {
6276 Ok(true) => None,
6277 Ok(false) => Some(disallowed_message(self, &options, value, rule_rows)),
6278 Err(message) => Some(message),
6279 },
6280 }
6281 }
6282
6283 #[must_use]
6287 pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
6288 let unit_name = unit_name.into();
6289 match &mut self.specifications {
6290 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6291 *unit = Some(unit_name);
6292 }
6293 other => panic!(
6294 "BUG: with_unit called on non-measure/non-ratio type {}",
6295 other
6296 ),
6297 }
6298 self
6299 }
6300
6301 #[must_use]
6306 pub(crate) fn reanchor_through_import_path(
6307 lemma_type: Arc<LemmaType>,
6308 base_alias: &str,
6309 base_identity: &SpecIdentity,
6310 member_path: &[String],
6311 mut nested_import: impl FnMut(&SpecIdentity, &str) -> Option<SpecIdentity>,
6312 ) -> Arc<LemmaType> {
6313 let mut hops = vec![PathSegment::import(base_alias, base_identity)];
6314 let mut identity = base_identity.clone();
6315 for segment in member_path {
6316 let Some(next) = nested_import(&identity, segment) else {
6317 break;
6318 };
6319 hops.push(PathSegment::import(segment, &next));
6320 identity = next;
6321 }
6322 let mut out = lemma_type.as_ref().clone();
6323 for hop in hops.into_iter().rev() {
6324 out = out.with_scale_import_hop(&hop);
6325 }
6326 Arc::new(out)
6327 }
6328
6329 #[must_use]
6334 pub(crate) fn with_scale_import_hop(&self, segment: &PathSegment) -> Self {
6335 fn prefix_units(units: &mut MeasureUnits, segment: &PathSegment) {
6336 for unit in units.0.iter_mut() {
6337 if let crate::literals::UnitScale::Reference { factors, .. } = &mut unit.scale {
6338 for (target, _) in factors.iter_mut() {
6339 match target {
6340 ReferenceTarget::Data(path) => path.segments.insert(0, segment.clone()),
6341 ReferenceTarget::Rule(rule) => rule.segments.insert(0, segment.clone()),
6342 }
6343 }
6344 }
6345 }
6346 }
6347 fn prefix_specifications(specifications: &mut TypeSpecification, segment: &PathSegment) {
6348 match specifications {
6349 TypeSpecification::Measure { units, .. }
6350 | TypeSpecification::MeasureRange { units, .. } => {
6351 prefix_units(units, segment);
6352 }
6353 TypeSpecification::List { element, .. } => {
6354 let prefixed = element.with_scale_import_hop(segment);
6355 *element = Arc::new(prefixed);
6356 }
6357 _ => {}
6358 }
6359 }
6360 let mut out = self.clone();
6361 prefix_specifications(&mut out.specifications, segment);
6362 out
6363 }
6364
6365 #[must_use]
6369 pub(crate) fn unit_scale_factors(&self, unit_name: &str) -> &[(ReferenceTarget, i32)] {
6370 self.measure_units_table()
6371 .and_then(|units| units.get(unit_name).ok())
6372 .map(MeasureUnit::resolved_scale_factors)
6373 .unwrap_or(&[])
6374 }
6375
6376 #[must_use]
6379 pub(crate) fn dynamic_scale_targets(&self) -> Vec<ReferenceTarget> {
6380 let Some(units) = self.measure_units_table() else {
6381 return Vec::new();
6382 };
6383 units
6384 .iter()
6385 .flat_map(|unit| {
6386 unit.resolved_scale_factors()
6387 .iter()
6388 .map(|(target, _)| target.clone())
6389 })
6390 .collect()
6391 }
6392
6393 #[must_use]
6395 pub fn unit(&self) -> Option<&str> {
6396 match &self.specifications {
6397 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6398 unit.as_deref()
6399 }
6400 _ => None,
6401 }
6402 }
6403
6404 pub fn name(&self) -> String {
6406 self.name
6407 .clone()
6408 .unwrap_or_else(|| self.specifications.to_string())
6409 }
6410
6411 pub fn is_boolean(&self) -> bool {
6413 matches!(&self.specifications, TypeSpecification::Boolean { .. })
6414 }
6415
6416 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
6417 matches!(
6418 (kind, &self.specifications),
6419 (PrimitiveKind::Number, TypeSpecification::Number { .. })
6420 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
6421 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
6422 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
6423 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
6424 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
6425 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
6426 )
6427 }
6428
6429 pub fn is_measure(&self) -> bool {
6431 matches!(&self.specifications, TypeSpecification::Measure { .. })
6432 }
6433
6434 pub fn is_measure_range(&self) -> bool {
6435 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
6436 }
6437
6438 pub fn is_number(&self) -> bool {
6440 matches!(&self.specifications, TypeSpecification::Number { .. })
6441 }
6442
6443 pub fn is_number_range(&self) -> bool {
6444 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
6445 }
6446
6447 pub fn is_numeric(&self) -> bool {
6449 matches!(
6450 &self.specifications,
6451 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
6452 )
6453 }
6454
6455 pub fn is_text(&self) -> bool {
6457 matches!(&self.specifications, TypeSpecification::Text { .. })
6458 }
6459
6460 pub fn is_date(&self) -> bool {
6462 matches!(&self.specifications, TypeSpecification::Date { .. })
6463 }
6464
6465 pub fn is_date_range(&self) -> bool {
6466 matches!(&self.specifications, TypeSpecification::DateRange { .. })
6467 }
6468
6469 pub fn is_time_range(&self) -> bool {
6470 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
6471 }
6472
6473 pub fn is_time(&self) -> bool {
6475 matches!(&self.specifications, TypeSpecification::Time { .. })
6476 }
6477
6478 pub fn has_trait_duration(&self) -> bool {
6479 match &self.specifications {
6480 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
6481 _ => false,
6482 }
6483 }
6484
6485 pub fn is_duration_like_measure(&self) -> bool {
6486 if !self.is_measure() {
6487 return false;
6488 }
6489 if self.has_trait_duration() {
6490 return true;
6491 }
6492 self.is_anonymous_measure()
6493 && self
6494 .measure_type_decomposition()
6495 .is_some_and(|d| *d == duration_decomposition())
6496 }
6497
6498 pub fn is_duration_like(&self) -> bool {
6499 self.is_duration_like_measure()
6500 }
6501
6502 pub fn has_trait_calendar(&self) -> bool {
6503 match &self.specifications {
6504 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
6505 _ => false,
6506 }
6507 }
6508
6509 pub fn is_calendar_like_measure(&self) -> bool {
6510 if !self.is_measure() {
6511 return false;
6512 }
6513 if self.has_trait_calendar() {
6514 return true;
6515 }
6516 self.is_anonymous_measure()
6517 && self
6518 .measure_type_decomposition()
6519 .is_some_and(|d| *d == calendar_decomposition())
6520 }
6521
6522 pub fn is_calendar_like(&self) -> bool {
6523 self.is_calendar_like_measure()
6524 }
6525
6526 pub fn is_ratio(&self) -> bool {
6528 matches!(&self.specifications, TypeSpecification::Ratio { .. })
6529 }
6530
6531 pub fn is_ratio_range(&self) -> bool {
6532 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
6533 }
6534
6535 pub fn is_calendar_measure_range(&self) -> bool {
6536 matches!(
6537 &self.specifications,
6538 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
6539 if *decomposition == calendar_decomposition()
6540 )
6541 }
6542
6543 pub fn is_calendar_like_range(&self) -> bool {
6544 self.is_calendar_measure_range()
6545 }
6546
6547 pub fn is_range(&self) -> bool {
6548 matches!(
6549 &self.specifications,
6550 TypeSpecification::DateRange { .. }
6551 | TypeSpecification::TimeRange { .. }
6552 | TypeSpecification::NumberRange { .. }
6553 | TypeSpecification::MeasureRange { .. }
6554 | TypeSpecification::RatioRange { .. }
6555 )
6556 }
6557
6558 pub fn is_spec(&self) -> bool {
6560 matches!(&self.specifications, TypeSpecification::Spec { .. })
6561 }
6562
6563 pub fn is_list(&self) -> bool {
6564 matches!(&self.specifications, TypeSpecification::List { .. })
6565 }
6566
6567 pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
6568 match &self.specifications {
6569 TypeSpecification::List { element, .. } => Some(&element.specifications),
6570 _ => None,
6571 }
6572 }
6573
6574 pub fn list_of(element: Arc<LemmaType>) -> Self {
6575 Self {
6576 name: Some(format!("{} list", element.name())),
6577 specifications: TypeSpecification::List {
6578 element,
6579 key: ListKey::Numbered,
6580 help: String::new(),
6581 },
6582 extends: TypeExtends::Primitive,
6583 options: TypeOptions::default(),
6584 }
6585 }
6586
6587 pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
6589 let TypeSpecification::List { element, .. } = &self.specifications else {
6590 panic!(
6591 "BUG: list_element_lemma_type on non-list type {}",
6592 self.name()
6593 );
6594 };
6595 element
6596 }
6597
6598 #[must_use]
6600 pub fn spec_identity(&self) -> Option<&SpecIdentity> {
6601 match &self.specifications {
6602 TypeSpecification::Spec { spec } => Some(spec),
6603 _ => None,
6604 }
6605 }
6606
6607 pub fn vetoed(&self) -> bool {
6609 matches!(&self.specifications, TypeSpecification::Veto { .. })
6610 }
6611
6612 pub fn is_undetermined(&self) -> bool {
6614 matches!(&self.specifications, TypeSpecification::Undetermined)
6615 }
6616
6617 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
6619 use TypeSpecification::*;
6620 matches!(
6621 (&self.specifications, &other.specifications),
6622 (Boolean { .. }, Boolean { .. })
6623 | (Number { .. }, Number { .. })
6624 | (NumberRange { .. }, NumberRange { .. })
6625 | (Measure { .. }, Measure { .. })
6626 | (MeasureRange { .. }, MeasureRange { .. })
6627 | (Text { .. }, Text { .. })
6628 | (Date { .. }, Date { .. })
6629 | (DateRange { .. }, DateRange { .. })
6630 | (Time { .. }, Time { .. })
6631 | (TimeRange { .. }, TimeRange { .. })
6632 | (Ratio { .. }, Ratio { .. })
6633 | (RatioRange { .. }, RatioRange { .. })
6634 | (Veto { .. }, Veto { .. })
6635 | (Undetermined, Undetermined)
6636 | (Spec { .. }, Spec { .. })
6637 | (List { .. }, List { .. })
6638 )
6639 }
6640
6641 #[must_use]
6643 pub fn family(&self) -> Option<&TypeFamily> {
6644 match &self.extends {
6645 TypeExtends::Custom { family, .. } => Some(family),
6646 TypeExtends::Primitive => None,
6647 }
6648 }
6649
6650 #[must_use]
6654 pub fn range_element_type(&self) -> Arc<LemmaType> {
6655 let element_spec = self
6656 .specifications
6657 .element_from_range()
6658 .expect("BUG: range_element_type requires a range specification");
6659 let name = match &self.extends {
6660 TypeExtends::Custom { family, .. } => Some(family.name.clone()),
6661 TypeExtends::Primitive => self.name.clone(),
6662 };
6663 Arc::new(LemmaType {
6664 name,
6665 specifications: element_spec,
6666 extends: self.extends.clone(),
6667 options: TypeOptions::default(),
6668 })
6669 }
6670
6671 #[must_use]
6673 pub fn measure_family(&self) -> Option<&TypeFamily> {
6674 (self.is_measure() || self.is_measure_range())
6675 .then(|| self.family())
6676 .flatten()
6677 }
6678
6679 #[must_use]
6681 pub(crate) fn held_value_type(&self) -> &LemmaType {
6682 let mut current = self;
6683 while current.is_list() {
6684 current = current.list_element_lemma_type().as_ref();
6685 }
6686 current
6687 }
6688
6689 #[must_use]
6691 pub(crate) fn held_measure_family(&self) -> Option<&TypeFamily> {
6692 self.held_value_type().measure_family()
6693 }
6694
6695 #[must_use]
6697 pub fn ratio_family(&self) -> Option<&TypeFamily> {
6698 (self.is_ratio() || self.is_ratio_range())
6699 .then(|| self.family())
6700 .flatten()
6701 }
6702
6703 #[must_use]
6705 pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
6706 self.measure_family().or_else(|| self.ratio_family())
6707 }
6708
6709 #[must_use]
6711 pub fn measure_family_name(&self) -> Option<&str> {
6712 self.measure_family().map(|family| family.name.as_str())
6713 }
6714
6715 #[must_use]
6717 pub fn ratio_family_name(&self) -> Option<&str> {
6718 self.ratio_family().map(|family| family.name.as_str())
6719 }
6720
6721 #[must_use]
6725 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
6726 if !self.has_same_base_type(other) {
6727 return false;
6728 }
6729 if self.is_measure() {
6730 return match (self.measure_family(), other.measure_family()) {
6731 (Some(_), Some(_)) => self.same_measure_family(other),
6732 (None, None) => {
6733 self.measure_type_decomposition() == other.measure_type_decomposition()
6734 }
6735 _ => false,
6736 };
6737 }
6738 if let (
6739 TypeSpecification::Spec { spec: self_spec },
6740 TypeSpecification::Spec { spec: other_spec },
6741 ) = (&self.specifications, &other.specifications)
6742 {
6743 return self_spec == other_spec;
6744 }
6745 if let (
6746 TypeSpecification::List {
6747 element: self_element,
6748 key: self_key,
6749 ..
6750 },
6751 TypeSpecification::List {
6752 element: other_element,
6753 key: other_key,
6754 ..
6755 },
6756 ) = (&self.specifications, &other.specifications)
6757 {
6758 return self_key == other_key && self_element.same_value_type(other_element);
6759 }
6760 if self.is_measure_range() {
6761 match (self.family(), other.family()) {
6762 (Some(self_family), Some(other_family)) => self_family == other_family,
6763 (None, None) => {
6764 let self_element = self
6765 .specifications
6766 .element_from_range()
6767 .expect("BUG: MeasureRange always defines element_from_range");
6768 let other_element = other
6769 .specifications
6770 .element_from_range()
6771 .expect("BUG: MeasureRange always defines element_from_range");
6772 match (&self_element, &other_element) {
6773 (
6774 TypeSpecification::Measure {
6775 decomposition: self_decomp,
6776 ..
6777 },
6778 TypeSpecification::Measure {
6779 decomposition: other_decomp,
6780 ..
6781 },
6782 ) => self_decomp == other_decomp,
6783 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
6784 }
6785 }
6786 _ => false,
6787 }
6788 } else {
6789 true
6790 }
6791 }
6792
6793 #[must_use]
6797 pub(crate) fn without_written_unit(&self) -> Self {
6798 let mut cleared = self.clone();
6799 match &mut cleared.specifications {
6800 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6801 *unit = None;
6802 }
6803 _ => {}
6804 }
6805 cleared
6806 }
6807
6808 #[must_use]
6811 pub(crate) fn without_slot_bounds(&self) -> Self {
6812 let mut cleared = self.without_written_unit();
6813 match &mut cleared.specifications {
6814 TypeSpecification::Measure {
6815 minimum,
6816 maximum,
6817 help,
6818 ..
6819 } => {
6820 *minimum = None;
6821 *maximum = None;
6822 *help = String::new();
6823 }
6824 TypeSpecification::Ratio {
6825 minimum,
6826 maximum,
6827 help,
6828 ..
6829 } => {
6830 *minimum = None;
6831 *maximum = None;
6832 *help = String::new();
6833 }
6834 TypeSpecification::Number {
6835 minimum,
6836 maximum,
6837 help,
6838 ..
6839 } => {
6840 *minimum = None;
6841 *maximum = None;
6842 *help = String::new();
6843 }
6844 TypeSpecification::Date {
6845 minimum,
6846 maximum,
6847 help,
6848 ..
6849 } => {
6850 *minimum = None;
6851 *maximum = None;
6852 *help = String::new();
6853 }
6854 TypeSpecification::Time {
6855 minimum,
6856 maximum,
6857 help,
6858 ..
6859 } => {
6860 *minimum = None;
6861 *maximum = None;
6862 *help = String::new();
6863 }
6864 TypeSpecification::MeasureRange {
6865 minimum,
6866 maximum,
6867 help,
6868 ..
6869 } => {
6870 *minimum = None;
6871 *maximum = None;
6872 *help = String::new();
6873 }
6874 TypeSpecification::DateRange {
6875 minimum,
6876 maximum,
6877 help,
6878 ..
6879 } => {
6880 *minimum = None;
6881 *maximum = None;
6882 *help = String::new();
6883 }
6884 TypeSpecification::TimeRange {
6885 minimum,
6886 maximum,
6887 help,
6888 ..
6889 } => {
6890 *minimum = None;
6891 *maximum = None;
6892 *help = String::new();
6893 }
6894 TypeSpecification::NumberRange {
6895 minimum,
6896 maximum,
6897 help,
6898 ..
6899 } => {
6900 *minimum = None;
6901 *maximum = None;
6902 *help = String::new();
6903 }
6904 TypeSpecification::RatioRange {
6905 minimum,
6906 maximum,
6907 help,
6908 ..
6909 } => {
6910 *minimum = None;
6911 *maximum = None;
6912 *help = String::new();
6913 }
6914 TypeSpecification::Boolean { help, .. } | TypeSpecification::Text { help, .. } => {
6915 *help = String::new();
6916 }
6917 TypeSpecification::Veto { .. }
6918 | TypeSpecification::Undetermined
6919 | TypeSpecification::Spec { .. }
6920 | TypeSpecification::List { .. } => {}
6921 }
6922 cleared
6923 }
6924
6925 #[must_use]
6927 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
6928 if !self.is_measure() || !other.is_measure() {
6929 return false;
6930 }
6931 match (self.measure_family(), other.measure_family()) {
6932 (Some(self_family), Some(other_family)) => self_family == other_family,
6933 _ => false,
6934 }
6935 }
6936
6937 #[must_use]
6941 pub fn same_interface(&self, other: &Self) -> bool {
6942 self.with_families_without_effective() == other.with_families_without_effective()
6943 }
6944
6945 #[must_use]
6948 pub fn same_interface_ignoring_options(&self, other: &Self) -> bool {
6949 let mut left = self.with_families_without_effective();
6950 let mut right = other.with_families_without_effective();
6951 left.options = TypeOptions::default();
6952 right.options = TypeOptions::default();
6953 left == right
6954 }
6955
6956 #[must_use]
6958 pub fn with_families_without_effective(&self) -> Self {
6959 Self {
6960 name: self.name.clone(),
6961 specifications: self.specifications.with_families_without_effective(),
6962 extends: self.extends.with_families_without_effective(),
6963 options: self.options.clone(),
6964 }
6965 }
6966
6967 #[must_use]
6968 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
6969 if !self.is_measure() || !other.is_measure() {
6970 return false;
6971 }
6972 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
6973 return false;
6974 }
6975 match (
6976 self.measure_type_decomposition(),
6977 other.measure_type_decomposition(),
6978 ) {
6979 (Some(a), Some(b)) => a == b,
6980 _ => false,
6981 }
6982 }
6983
6984 pub fn veto_type() -> Self {
6986 Self::primitive(TypeSpecification::veto())
6987 }
6988
6989 pub fn undetermined_type() -> Self {
6992 Self::primitive(TypeSpecification::Undetermined)
6993 }
6994
6995 pub fn decimal_places(&self) -> Option<u8> {
6998 match &self.specifications {
6999 TypeSpecification::Number { decimals, .. } => *decimals,
7000 _ => None,
7001 }
7002 }
7003
7004 pub fn try_rational_as_decimal(
7009 &self,
7010 magnitude: &crate::computation::rational::RationalInteger,
7011 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7012 let decimal = magnitude.try_to_decimal()?;
7013 Ok(format_decimal_for_api(decimal))
7014 }
7015
7016 pub fn try_measure_canonical_as_decimal_in_unit(
7018 &self,
7019 canonical_magnitude: &crate::computation::rational::RationalInteger,
7020 unit_name: &str,
7021 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7022 use crate::computation::rational::checked_div;
7023 let unit_factor = self.measure_unit_factor(unit_name);
7024 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
7025 self.try_rational_as_decimal(&magnitude_in_unit)
7026 }
7027
7028 pub fn try_ratio_canonical_as_decimal_in_unit(
7030 &self,
7031 canonical_magnitude: &crate::computation::rational::RationalInteger,
7032 unit_name: &str,
7033 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7034 use crate::computation::rational::checked_mul;
7035 let units = match &self.specifications {
7036 TypeSpecification::Ratio { units, .. } => units,
7037 _ => unreachable!(
7038 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
7039 self.name()
7040 ),
7041 };
7042 let ratio_unit = units
7043 .iter()
7044 .find(|unit| unit.name == unit_name)
7045 .unwrap_or_else(|| {
7046 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
7047 unreachable!(
7048 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
7049 unit_name,
7050 self.name(),
7051 valid.join(", ")
7052 )
7053 });
7054 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
7055 self.try_rational_as_decimal(&magnitude_in_unit)
7056 }
7057
7058 pub fn example_value(&self) -> &'static str {
7060 match &self.specifications {
7061 TypeSpecification::Text { .. } => "\"hello world\"",
7062 TypeSpecification::Measure { .. } => "12.50 eur",
7063 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
7064 TypeSpecification::Number { .. } => "3.14",
7065 TypeSpecification::NumberRange { .. } => "0...100",
7066 TypeSpecification::Boolean { .. } => "true",
7067 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
7068 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
7069 TypeSpecification::TimeRange { .. } => "09:00...17:00",
7070 TypeSpecification::Veto { .. } => "veto",
7071 TypeSpecification::Time { .. } => "14:30:00",
7072 TypeSpecification::Ratio { .. } => "50%",
7073 TypeSpecification::RatioRange { .. } => "10%...50%",
7074 TypeSpecification::Undetermined => unreachable!(
7075 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
7076 ),
7077 TypeSpecification::Spec { .. } => unreachable!(
7078 "BUG: example_value called on a spec type; spec instances are not prompted data"
7079 ),
7080 TypeSpecification::List { element, .. } => element.example_value(),
7081 }
7082 }
7083
7084 #[must_use]
7088 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
7092 match &self.specifications {
7093 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
7094 _ => unreachable!(
7095 "BUG: measure_type_decomposition called on non-measure type {}",
7096 self.name()
7097 ),
7098 }
7099 }
7100
7101 #[must_use]
7104 pub(crate) fn is_simple_base_measure(&self) -> bool {
7105 if !self.is_measure() {
7106 return false;
7107 }
7108 match self.measure_type_decomposition() {
7109 Some(decomposition) if decomposition.len() == 1 => {
7110 decomposition.values().next() == Some(&1)
7111 }
7112 _ => false,
7113 }
7114 }
7115
7116 #[must_use]
7118 pub fn measure_units_table(&self) -> Option<&MeasureUnits> {
7119 match &self.specifications {
7120 TypeSpecification::Measure { units, .. }
7121 | TypeSpecification::MeasureRange { units, .. } => Some(units),
7122 _ => None,
7123 }
7124 }
7125
7126 #[must_use]
7129 pub fn has_dynamic_units(&self) -> bool {
7130 if self
7131 .measure_units_table()
7132 .is_some_and(|units| units.iter().any(MeasureUnit::is_dynamic))
7133 {
7134 return true;
7135 }
7136 match &self.specifications {
7137 TypeSpecification::List { element, .. } => element.has_dynamic_units(),
7138 _ => false,
7139 }
7140 }
7141
7142 #[must_use]
7144 pub fn unit_is_dynamic(&self, unit_name: &str) -> bool {
7145 self.measure_units_table()
7146 .and_then(|units| units.get(unit_name).ok())
7147 .is_some_and(MeasureUnit::is_dynamic)
7148 }
7149
7150 #[must_use]
7152 pub(crate) fn measure_display_unit_name(
7153 &self,
7154 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7155 ) -> Option<&str> {
7156 match &self.specifications {
7157 TypeSpecification::Measure { units, .. } if !units.is_empty() => Some(
7158 measure_display_unit(units, self, runtime_scales)
7159 .name
7160 .as_str(),
7161 ),
7162 _ => None,
7163 }
7164 }
7165
7166 #[must_use]
7169 pub fn measure_fallback_unit(&self) -> Option<&MeasureUnit> {
7170 let units = self.measure_units_table()?;
7171 units
7172 .iter()
7173 .find(|unit| unit.is_canonical_factor())
7174 .or_else(|| units.iter().find(|unit| unit.literal_factor().is_some()))
7175 .or_else(|| units.iter().next())
7176 }
7177
7178 #[must_use]
7187 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
7188 if let Some(binding) = self.unit() {
7189 return vec![(binding.to_string(), 1)];
7190 }
7191 match &self.specifications {
7192 TypeSpecification::Measure {
7193 units,
7194 decomposition,
7195 ..
7196 } => {
7197 if let Some(unit) = self.measure_fallback_unit() {
7198 return vec![(unit.name.clone(), 1)];
7199 }
7200 if units.is_empty() {
7201 return base_measure_vector_as_signature(
7202 decomposition
7203 .as_ref()
7204 .expect("BUG: measure type without units must carry a decomposition"),
7205 );
7206 }
7207 unreachable!("BUG: non-empty measure unit table has no fallback unit");
7208 }
7209 TypeSpecification::MeasureRange {
7210 units,
7211 decomposition,
7212 ..
7213 } => {
7214 if let Some(unit) = self.measure_fallback_unit() {
7215 return vec![(unit.name.clone(), 1)];
7216 }
7217 if units.is_empty() {
7218 return base_measure_vector_as_signature(
7219 decomposition
7220 .as_ref()
7221 .expect("BUG: measure type without units must carry a decomposition"),
7222 );
7223 }
7224 unreachable!("BUG: non-empty measure-range unit table has no fallback unit");
7225 }
7226 _ => unreachable!(
7227 "BUG: measure_runtime_signature called on non-measure type {}",
7228 self.name()
7229 ),
7230 }
7231 }
7232
7233 #[must_use]
7235 pub fn ratio_primary_unit(&self) -> Option<&str> {
7236 match &self.specifications {
7237 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7238 units.iter().next().map(|unit| unit.name.as_str())
7239 }
7240 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7241 units.iter().next().map(|unit| unit.name.as_str())
7242 }
7243 _ => None,
7244 }
7245 }
7246
7247 pub fn is_anonymous_measure(&self) -> bool {
7250 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
7251 }
7252
7253 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
7258 Self {
7259 name: None,
7260 specifications: TypeSpecification::Measure {
7261 minimum: None,
7262 maximum: None,
7263 units: crate::literals::MeasureUnits::new(),
7264 traits: Vec::new(),
7265 decomposition: Some(decomposition),
7266 unit: None,
7267 help: String::new(),
7268 },
7269 extends: TypeExtends::Primitive,
7270 options: TypeOptions::default(),
7271 }
7272 }
7273
7274 #[must_use]
7276 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
7277 match &self.specifications {
7278 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7279 Some(units.iter().map(|unit| unit.name.as_str()).collect())
7280 }
7281 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7282 Some(units.iter().map(|unit| unit.name.as_str()).collect())
7283 }
7284 _ => None,
7285 }
7286 }
7287
7288 #[must_use]
7290 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
7291 match &self.specifications {
7292 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
7293 Some(units.iter().map(|unit| unit.name.as_str()).collect())
7294 }
7295 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
7296 Some(units.iter().map(|unit| unit.name.as_str()).collect())
7297 }
7298 _ => None,
7299 }
7300 }
7301
7302 #[must_use]
7304 pub fn label_for_data_input(&self, input_key: &str) -> String {
7305 let type_label = if let Some(parent) = self.extends.parent_name() {
7306 parent.to_string()
7307 } else {
7308 let type_name = self.name();
7309 if type_name == input_key {
7310 self.specifications.to_string()
7311 } else {
7312 type_name
7313 }
7314 };
7315 if type_label == input_key {
7316 input_key.to_string()
7317 } else {
7318 format!("{input_key} [{type_label}]")
7319 }
7320 }
7321
7322 #[must_use]
7324 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
7325 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
7326 }
7327
7328 #[must_use]
7330 pub fn empty_runtime_input_vetoes(&self) -> bool {
7331 !matches!(self.specifications, TypeSpecification::Text { .. })
7332 }
7333
7334 pub fn measure_unit_factor(
7336 &self,
7337 unit_name: &str,
7338 ) -> &crate::computation::rational::RationalInteger {
7339 let units = match &self.specifications {
7340 TypeSpecification::Measure { units, .. } => units,
7341 TypeSpecification::MeasureRange { units, .. } => units,
7342 _ => unreachable!(
7343 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
7344 self.name()
7345 ),
7346 };
7347 match units.get(unit_name) {
7348 Ok(unit) => unit.require_literal_factor(),
7349 Err(_) => {
7350 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
7351 unreachable!(
7352 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
7353 unit_name,
7354 self.name(),
7355 valid.join(", ")
7356 );
7357 }
7358 }
7359 }
7360
7361 pub fn ratio_unit_factor(
7362 &self,
7363 unit_name: &str,
7364 ) -> &crate::computation::rational::RationalInteger {
7365 let units = match &self.specifications {
7366 TypeSpecification::Ratio { units, .. } => units,
7367 _ => unreachable!(
7368 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
7369 self.name()
7370 ),
7371 };
7372 match units.get(unit_name) {
7373 Ok(RatioUnit { value, .. }) => value,
7374 Err(_) => {
7375 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
7376 unreachable!(
7377 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
7378 unit_name,
7379 self.name(),
7380 valid.join(", ")
7381 );
7382 }
7383 }
7384 }
7385
7386 pub(crate) fn measure_literal_unit_map(
7388 &self,
7389 literal: &LiteralValue,
7390 unit_names: &[&str],
7391 factor_source: UnitFactorSource<'_>,
7392 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7393 use crate::computation::rational::checked_div;
7394
7395 let ValueKind::Measure(magnitude) = &literal.value else {
7396 panic!("BUG: measure_literal_unit_map called with non-measure value");
7397 };
7398 let mut map = BTreeMap::new();
7399 for &unit_name in unit_names {
7400 let Some(unit_factor) = factor_source.try_measure_unit_factor(unit_name) else {
7401 continue;
7402 };
7403 let magnitude_in_unit = checked_div(magnitude, &unit_factor)
7404 .map_err(LiteralUnitMapFailure::UnitConversion)?;
7405 let decimal = self
7406 .try_rational_as_decimal(&magnitude_in_unit)
7407 .map_err(LiteralUnitMapFailure::Commit)?;
7408 map.insert(unit_name.to_string(), decimal);
7409 }
7410 Ok(map)
7411 }
7412
7413 pub(crate) fn ratio_literal_unit_map(
7415 &self,
7416 literal: &LiteralValue,
7417 unit_names: &[&str],
7418 factor_source: UnitFactorSource<'_>,
7419 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7420 use crate::computation::rational::checked_mul;
7421
7422 let ratio_api_type = match &self.specifications {
7423 TypeSpecification::Ratio { .. } => self,
7424 TypeSpecification::RatioRange { .. } => {
7425 return ratio_element_type_for_api(self).ratio_literal_unit_map(
7426 literal,
7427 unit_names,
7428 factor_source,
7429 );
7430 }
7431 _ => {
7432 panic!(
7433 "BUG: ratio_literal_unit_map called with non-ratio type {}",
7434 self.name()
7435 );
7436 }
7437 };
7438 let ValueKind::Ratio(canonical) = &literal.value else {
7439 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
7440 };
7441 if unit_names.is_empty() {
7442 panic!(
7443 "BUG: ratio literal type '{}' must have at least one unit name",
7444 self.name()
7445 );
7446 }
7447 let mut map = BTreeMap::new();
7448 for &unit_name in unit_names {
7449 let unit_factor = factor_source.ratio_unit_factor(unit_name);
7450 let magnitude_in_unit = checked_mul(canonical, unit_factor)
7451 .map_err(LiteralUnitMapFailure::UnitConversion)?;
7452 let decimal = ratio_api_type
7453 .try_rational_as_decimal(&magnitude_in_unit)
7454 .map_err(LiteralUnitMapFailure::Commit)?;
7455 map.insert(unit_name.to_string(), decimal);
7456 }
7457 Ok(map)
7458 }
7459}
7460
7461pub(crate) enum UnitFactorSource<'a> {
7463 DeclaredOn(&'a LemmaType),
7464 Merged {
7465 measure: Option<&'a MeasureUnits>,
7466 ratio: Option<&'a RatioUnits>,
7467 },
7468 Runtime {
7471 scales:
7472 &'a std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
7473 fallback: &'a UnitFactorSource<'a>,
7474 },
7475}
7476
7477impl UnitFactorSource<'_> {
7478 fn try_measure_unit_factor(
7481 &self,
7482 unit_name: &str,
7483 ) -> Option<crate::computation::rational::RationalInteger> {
7484 match self {
7485 UnitFactorSource::DeclaredOn(lemma_type) => {
7486 let units = lemma_type.measure_units_table().unwrap_or_else(|| {
7487 panic!(
7488 "BUG: measure_unit_factor on non-measure type {}",
7489 lemma_type.name()
7490 )
7491 });
7492 let unit = units.get(unit_name).unwrap_or_else(|_| {
7493 panic!(
7494 "BUG: unknown unit '{unit_name}' for measure type {}",
7495 lemma_type.name()
7496 )
7497 });
7498 unit.literal_factor().cloned()
7499 }
7500 UnitFactorSource::Merged { measure, .. } => {
7501 let units = (*measure)?;
7504 let unit = units.get(unit_name).ok()?;
7505 unit.literal_factor().cloned()
7506 }
7507 UnitFactorSource::Runtime { scales, fallback } => scales
7508 .get(unit_name)
7509 .cloned()
7510 .or_else(|| fallback.try_measure_unit_factor(unit_name)),
7511 }
7512 }
7513
7514 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
7515 match self {
7516 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
7517 UnitFactorSource::Merged { ratio, .. } => {
7518 let units = ratio.unwrap_or_else(|| {
7519 panic!(
7520 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
7521 )
7522 });
7523 &units
7524 .get(unit_name)
7525 .unwrap_or_else(|_| {
7526 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
7527 })
7528 .value
7529 }
7530 UnitFactorSource::Runtime { fallback, .. } => fallback.ratio_unit_factor(unit_name),
7531 }
7532 }
7533}
7534
7535pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
7537 match &lemma_type.specifications {
7538 TypeSpecification::Ratio { .. } => lemma_type.clone(),
7539 TypeSpecification::RatioRange { .. } => {
7540 let element = range_element_type_specification(&lemma_type.specifications)
7541 .expect("BUG: ratio range type must have ratio element specification");
7542 let TypeSpecification::Ratio { units, .. } = element else {
7543 panic!("BUG: ratio range element spec must be Ratio");
7544 };
7545 LemmaType::primitive(TypeSpecification::Ratio {
7546 minimum: None,
7547 maximum: None,
7548 units,
7549 unit: None,
7550 help: String::new(),
7551 })
7552 }
7553 _ => panic!(
7554 "BUG: ratio_element_type_for_api called with non-ratio type {}",
7555 lemma_type.name()
7556 ),
7557 }
7558}
7559
7560#[derive(Debug, Clone, PartialEq, Eq)]
7562pub(crate) enum LiteralUnitMapFailure {
7563 Commit(crate::computation::rational::NumericFailure),
7564 UnitConversion(crate::computation::rational::NumericFailure),
7565}
7566
7567#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7570pub struct LiteralValue {
7571 pub value: ValueKind,
7572}
7573
7574impl LiteralValue {
7575 #[inline]
7576 pub fn new(value: ValueKind) -> Self {
7577 Self { value }
7578 }
7579
7580 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
7581 match signature {
7582 [(unit_name, 1)] => Some(unit_name.as_str()),
7583 _ => None,
7584 }
7585 }
7586}
7587
7588#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7594pub struct TypedLiteral {
7595 pub value: ValueKind,
7596 pub lemma_type: Arc<LemmaType>,
7597}
7598
7599impl TypedLiteral {
7600 #[inline]
7601 pub fn to_literal(&self) -> LiteralValue {
7602 LiteralValue {
7603 value: self.value.clone(),
7604 }
7605 }
7606
7607 #[inline]
7608 pub fn into_literal(self) -> LiteralValue {
7609 LiteralValue { value: self.value }
7610 }
7611}
7612
7613impl From<TypedLiteral> for LiteralValue {
7614 fn from(typed: TypedLiteral) -> Self {
7615 typed.into_literal()
7616 }
7617}
7618
7619impl From<&TypedLiteral> for LiteralValue {
7620 fn from(typed: &TypedLiteral) -> Self {
7621 typed.to_literal()
7622 }
7623}
7624
7625impl LiteralValue {
7626 pub fn text(s: impl Into<Arc<str>>) -> Self {
7627 Self {
7628 value: ValueKind::Text(s.into()),
7629 }
7630 }
7631
7632 pub fn number(n: RationalInteger) -> Self {
7633 Self {
7634 value: ValueKind::Number(n),
7635 }
7636 }
7637
7638 pub fn number_from_decimal(decimal: Decimal) -> Self {
7639 Self::number(
7640 crate::literals::rational_from_parsed_decimal(decimal)
7641 .expect("BUG: literal number from decimal must lift at boundary"),
7642 )
7643 }
7644
7645 pub fn measure(n: RationalInteger) -> Self {
7646 Self {
7647 value: ValueKind::Measure(n),
7648 }
7649 }
7650
7651 pub fn from_bool(b: bool) -> Self {
7652 Self {
7653 value: ValueKind::Boolean(b),
7654 }
7655 }
7656
7657 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7658 Self::date(date_time_to_semantic(dt))
7659 }
7660
7661 pub fn date(dt: SemanticDateTime) -> Self {
7662 Self {
7663 value: ValueKind::Date(dt),
7664 }
7665 }
7666
7667 pub fn time(t: SemanticTime) -> Self {
7668 Self {
7669 value: ValueKind::Time(t),
7670 }
7671 }
7672
7673 pub fn ratio(r: RationalInteger) -> Self {
7674 Self {
7675 value: ValueKind::Ratio(r),
7676 }
7677 }
7678
7679 pub fn ratio_from_decimal(r: Decimal) -> Self {
7680 Self::ratio(
7681 crate::literals::rational_from_parsed_decimal(r)
7682 .expect("BUG: ratio literal from decimal must lift at boundary"),
7683 )
7684 }
7685
7686 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
7687 Self {
7688 value: ValueKind::Range(Box::new(left), Box::new(right)),
7689 }
7690 }
7691
7692 pub fn display_value(&self) -> String {
7694 match &self.value {
7695 ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
7696 format!("{}", self.value)
7697 }
7698 ValueKind::Range(left, right) => {
7699 format!("{}...{}", left.display_value(), right.display_value())
7700 }
7701 ValueKind::List(rows) if rows.is_empty() => String::new(),
7702 ValueKind::List(rows) => rows
7703 .iter()
7704 .map(|row| row.display_value())
7705 .collect::<Vec<_>>()
7706 .join(", "),
7707 _ => format!("{}", self.value),
7708 }
7709 }
7710
7711 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
7713 self.display_value_with_scales(lemma_type, None)
7714 }
7715
7716 pub fn display_value_with_scales(
7718 &self,
7719 lemma_type: &LemmaType,
7720 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7721 ) -> String {
7722 match &self.value {
7723 ValueKind::Measure(n) => {
7724 let signature = lemma_type.measure_runtime_signature();
7725 format_measure_canonical_for_display(n, lemma_type, &signature, runtime_scales)
7726 }
7727 ValueKind::UnscaledMeasure { magnitude, unit } => {
7728 format!(
7729 "{} {unit}",
7730 format_rational_magnitude_for_display(magnitude)
7731 )
7732 }
7733 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
7734 ValueKind::Range(left, right) => {
7735 format!(
7736 "{}...{}",
7737 left.to_literal()
7738 .display_value_with_scales(&left.lemma_type, runtime_scales),
7739 right
7740 .to_literal()
7741 .display_value_with_scales(&right.lemma_type, runtime_scales)
7742 )
7743 }
7744 ValueKind::List(rows) if rows.is_empty() => lemma_type.specifications.to_string(),
7745 ValueKind::List(rows) => rows
7746 .iter()
7747 .map(|row| {
7748 row.to_literal()
7749 .display_value_with_scales(&row.lemma_type, runtime_scales)
7750 })
7751 .collect::<Vec<_>>()
7752 .join(", "),
7753 _ => format!("{}", self.value),
7754 }
7755 }
7756
7757 pub fn byte_size(&self) -> usize {
7759 self.value.structural_byte_size()
7760 }
7761
7762 #[must_use]
7764 pub fn magnitude_suggestion_for_decimal_prompt(
7765 &self,
7766 lemma_type: &LemmaType,
7767 ) -> Option<rust_decimal::Decimal> {
7768 match &self.value {
7769 ValueKind::Number(n) => Some(
7770 lemma_type
7771 .try_rational_as_decimal(n)
7772 .expect("BUG: stored number literal must convert to decimal for prompt"),
7773 ),
7774 ValueKind::Measure(n) => {
7775 let signature = lemma_type.measure_runtime_signature();
7776 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
7777 "BUG: measure prompt requires exactly one signature unit with exponent 1",
7778 );
7779 Some(
7780 lemma_type
7781 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
7782 .expect("BUG: stored measure literal must convert to decimal for prompt"),
7783 )
7784 }
7785 ValueKind::Ratio(n) => {
7786 if let Some(unit_name) = lemma_type.unit() {
7787 Some(
7788 lemma_type
7789 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
7790 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
7791 )
7792 } else {
7793 Some(lemma_type.try_rational_as_decimal(n).expect(
7794 "BUG: stored bare ratio literal must convert to decimal for prompt",
7795 ))
7796 }
7797 }
7798 _ => None,
7799 }
7800 }
7801
7802 #[must_use]
7804 pub fn measure_units(
7805 &self,
7806 lemma_type: &LemmaType,
7807 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7808 if !matches!(self.value, ValueKind::Measure(_)) {
7809 return None;
7810 }
7811 lemma_type.measure_unit_names()?;
7812 let declared: Vec<&str> = lemma_type
7813 .measure_unit_names()
7814 .expect("BUG: measure_unit_names checked above");
7815 lemma_type
7816 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7817 .ok()
7818 }
7819
7820 #[must_use]
7822 pub fn ratio_units(
7823 &self,
7824 lemma_type: &LemmaType,
7825 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7826 if !matches!(self.value, ValueKind::Ratio(_)) {
7827 return None;
7828 }
7829 let has_declared_units = match &lemma_type.specifications {
7830 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
7831 TypeSpecification::RatioRange { .. } => true,
7832 _ => return None,
7833 };
7834 if !has_declared_units {
7835 return None;
7836 }
7837 let declared: Vec<&str> = lemma_type
7838 .ratio_unit_names()
7839 .expect("BUG: ratio units checked above");
7840 lemma_type
7841 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7842 .ok()
7843 }
7844
7845 #[must_use]
7847 pub fn magnitude_in_unit(
7848 &self,
7849 lemma_type: &LemmaType,
7850 unit: &str,
7851 ) -> Option<rust_decimal::Decimal> {
7852 self.measure_units(lemma_type)
7853 .and_then(|map| map.get(unit).copied())
7854 .or_else(|| {
7855 self.ratio_units(lemma_type)
7856 .and_then(|map| map.get(unit).copied())
7857 })
7858 }
7859
7860 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
7862 let ValueKind::Measure(magnitude) = &self.value else {
7863 unreachable!(
7864 "BUG: duration_canonical_seconds called with {:?}",
7865 self.value
7866 );
7867 };
7868 if !lemma_type.is_duration_like_measure() {
7869 unreachable!(
7870 "BUG: duration_canonical_seconds called with type {}",
7871 lemma_type.name()
7872 );
7873 }
7874 let factor = lemma_type.measure_unit_factor("second");
7875 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
7876 }
7877
7878 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
7880 let ValueKind::Measure(magnitude) = &self.value else {
7881 unreachable!(
7882 "BUG: calendar_canonical_months called with {:?}",
7883 self.value
7884 );
7885 };
7886 if !lemma_type.is_calendar_like() {
7887 unreachable!(
7888 "BUG: calendar_canonical_months called with type {}",
7889 lemma_type.name()
7890 );
7891 }
7892 let factor = lemma_type.measure_unit_factor("month");
7893 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
7894 }
7895}
7896
7897impl TypedLiteral {
7898 pub fn text(s: impl Into<Arc<str>>) -> Self {
7899 Self {
7900 value: ValueKind::Text(s.into()),
7901 lemma_type: primitive_text_arc().clone(),
7902 }
7903 }
7904
7905 pub fn text_with_type(s: impl Into<Arc<str>>, lemma_type: Arc<LemmaType>) -> Self {
7906 Self {
7907 value: ValueKind::Text(s.into()),
7908 lemma_type,
7909 }
7910 }
7911
7912 pub fn number(n: RationalInteger) -> Self {
7913 Self {
7914 value: ValueKind::Number(n),
7915 lemma_type: primitive_number_arc().clone(),
7916 }
7917 }
7918
7919 pub fn number_from_decimal(decimal: Decimal) -> Self {
7920 Self::number(
7921 crate::literals::rational_from_parsed_decimal(decimal)
7922 .expect("BUG: literal number from decimal must lift at boundary"),
7923 )
7924 }
7925
7926 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7927 Self {
7928 value: ValueKind::Number(n),
7929 lemma_type,
7930 }
7931 }
7932
7933 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
7934 Self::number_with_type(
7935 crate::literals::rational_from_parsed_decimal(decimal)
7936 .expect("BUG: literal number from decimal must lift at boundary"),
7937 lemma_type,
7938 )
7939 }
7940
7941 pub fn measure_with_bound_unit(
7943 n: RationalInteger,
7944 unit_name: impl Into<String>,
7945 lemma_type: Arc<LemmaType>,
7946 ) -> Self {
7947 Self::measure_with_type(
7948 n,
7949 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
7950 )
7951 }
7952
7953 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7954 Self {
7955 value: ValueKind::Measure(n),
7956 lemma_type,
7957 }
7958 }
7959
7960 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7961 Self {
7962 value: ValueKind::Measure(n),
7963 lemma_type,
7964 }
7965 }
7966
7967 pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
7969 Self {
7970 value: ValueKind::Measure(value),
7971 lemma_type: Arc::new(anonymous_measure_type()),
7972 }
7973 }
7974
7975 pub fn from_bool(b: bool) -> Self {
7976 Self {
7977 value: ValueKind::Boolean(b),
7978 lemma_type: primitive_boolean_arc().clone(),
7979 }
7980 }
7981
7982 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7983 Self::date(date_time_to_semantic(dt))
7984 }
7985
7986 pub fn date(dt: SemanticDateTime) -> Self {
7987 Self {
7988 value: ValueKind::Date(dt),
7989 lemma_type: primitive_date_arc().clone(),
7990 }
7991 }
7992
7993 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
7994 Self {
7995 value: ValueKind::Date(dt),
7996 lemma_type,
7997 }
7998 }
7999
8000 pub fn time(t: SemanticTime) -> Self {
8001 Self {
8002 value: ValueKind::Time(t),
8003 lemma_type: primitive_time_arc().clone(),
8004 }
8005 }
8006
8007 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
8008 Self {
8009 value: ValueKind::Time(t),
8010 lemma_type,
8011 }
8012 }
8013
8014 pub fn calendar(
8015 value: RationalInteger,
8016 unit: SemanticCalendarUnit,
8017 lemma_type: Arc<LemmaType>,
8018 ) -> Self {
8019 let unit_name = unit.to_string();
8020 debug_assert_eq!(
8021 semantic_calendar_unit_from_unit_name(&unit_name),
8022 unit,
8023 "BUG: calendar unit name must round-trip"
8024 );
8025 Self::measure_with_bound_unit(value, unit_name, lemma_type)
8026 }
8027
8028 pub fn calendar_from_decimal(
8029 value: Decimal,
8030 unit: SemanticCalendarUnit,
8031 lemma_type: Arc<LemmaType>,
8032 ) -> Self {
8033 Self::calendar(
8034 crate::literals::rational_from_parsed_decimal(value)
8035 .expect("BUG: calendar literal from decimal must lift at boundary"),
8036 unit,
8037 lemma_type,
8038 )
8039 }
8040
8041 pub fn calendar_with_type(
8042 value: RationalInteger,
8043 unit: SemanticCalendarUnit,
8044 lemma_type: Arc<LemmaType>,
8045 ) -> Self {
8046 Self::calendar(value, unit, lemma_type)
8047 }
8048
8049 pub fn ratio(r: RationalInteger) -> Self {
8050 Self {
8051 value: ValueKind::Ratio(r),
8052 lemma_type: primitive_ratio_arc().clone(),
8053 }
8054 }
8055
8056 pub fn ratio_from_decimal(r: Decimal) -> Self {
8057 Self::ratio(
8058 crate::literals::rational_from_parsed_decimal(r)
8059 .expect("BUG: ratio literal from decimal must lift at boundary"),
8060 )
8061 }
8062
8063 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
8064 Self {
8065 value: ValueKind::Ratio(r),
8066 lemma_type,
8067 }
8068 }
8069
8070 pub fn ratio_with_bound_unit(
8071 r: RationalInteger,
8072 unit_name: impl Into<String>,
8073 lemma_type: Arc<LemmaType>,
8074 ) -> Self {
8075 Self::ratio_with_type(
8076 r,
8077 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
8078 )
8079 }
8080
8081 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
8082 let specifications =
8083 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
8084 .unwrap_or_else(|| {
8085 unreachable!(
8086 "BUG: attempted to construct a range literal from incompatible endpoint types"
8087 )
8088 });
8089
8090 Self {
8091 value: ValueKind::Range(Box::new(left), Box::new(right)),
8092 lemma_type: Arc::new(LemmaType::primitive(specifications)),
8093 }
8094 }
8095
8096 pub fn display_value(&self) -> String {
8097 self.to_literal().display_value_with_type(&self.lemma_type)
8098 }
8099
8100 pub fn get_type(&self) -> &LemmaType {
8101 &self.lemma_type
8102 }
8103
8104 pub fn byte_size(&self) -> usize {
8105 self.value.structural_byte_size()
8106 }
8107}
8108
8109impl fmt::Display for TypedLiteral {
8110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8111 write!(f, "{}", self.display_value())
8112 }
8113}
8114
8115#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8118pub enum ReferenceTarget {
8119 Data(DataPath),
8120 Rule(RulePath),
8121}
8122
8123impl fmt::Display for ReferenceTarget {
8124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8125 match self {
8126 ReferenceTarget::Data(path) => write!(f, "{path}"),
8127 ReferenceTarget::Rule(path) => write!(f, "{path}"),
8128 }
8129 }
8130}
8131
8132#[derive(Clone, Debug, PartialEq, Eq)]
8136pub(crate) enum ReferenceEnd {
8137 Promptable(DataPath),
8138 Rule(RulePath),
8139}
8140
8141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8148pub enum DataOrigin {
8149 Declared,
8150 Bound,
8151}
8152
8153#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8155pub enum ImportEffectivePin {
8156 Data(DataPath),
8157 Rule(RulePath),
8158}
8159
8160#[derive(Clone, Debug)]
8162pub enum DataDefinition {
8163 Value {
8165 value: LiteralValue,
8166 resolved_type: Arc<LemmaType>,
8167 origin: DataOrigin,
8168 source: Source,
8169 },
8170 TypeDeclaration {
8176 resolved_type: Arc<LemmaType>,
8177 declared_suggestion: Option<BoundValueKind>,
8178 declared_value: Option<BoundValueKind>,
8179 source: Source,
8180 },
8181 Import {
8190 row: Arc<RowInstance>,
8191 pin: Option<ImportEffectivePin>,
8193 candidates: std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>>,
8195 source: Source,
8196 },
8197 InstanceList {
8202 resolved_type: Arc<LemmaType>,
8203 origin: DataOrigin,
8204 source: Source,
8205 },
8206 Reference {
8231 target: ReferenceTarget,
8232 resolved_type: Arc<LemmaType>,
8233 local_constraints: Option<Vec<Constraint>>,
8234 local_suggestion: Option<BoundValueKind>,
8235 local_value: Option<BoundValueKind>,
8236 origin: DataOrigin,
8237 source: Source,
8238 },
8239}
8240
8241impl DataDefinition {
8242 pub fn is_bound(&self) -> bool {
8245 match self {
8246 DataDefinition::Value { origin, .. }
8247 | DataDefinition::InstanceList { origin, .. }
8248 | DataDefinition::Reference { origin, .. } => *origin == DataOrigin::Bound,
8249 DataDefinition::TypeDeclaration { .. } | DataDefinition::Import { .. } => false,
8250 }
8251 }
8252
8253 pub fn lemma_type(&self) -> Option<&LemmaType> {
8255 self.resolved_type_arc().map(|arc| arc.as_ref())
8256 }
8257
8258 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
8260 match self {
8261 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
8262 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
8263 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
8264 DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
8265 DataDefinition::Import { .. } => None,
8266 }
8267 }
8268
8269 #[inline]
8271 pub fn schema_type(&self) -> Option<&LemmaType> {
8272 self.lemma_type()
8273 }
8274
8275 pub fn value(&self) -> Option<LiteralValue> {
8279 match self {
8280 DataDefinition::Value { value, .. } => Some(value.clone()),
8281 DataDefinition::TypeDeclaration {
8282 declared_value: Some(dv),
8283 ..
8284 } => Some(dv.to_literal()),
8285 DataDefinition::Reference {
8286 local_value: Some(dv),
8287 resolved_type: _,
8288 ..
8289 } => Some(dv.to_literal()),
8290 DataDefinition::TypeDeclaration { .. }
8291 | DataDefinition::Import { .. }
8292 | DataDefinition::InstanceList { .. }
8293 | DataDefinition::Reference { .. } => None,
8294 }
8295 }
8296
8297 pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
8299 match self {
8300 DataDefinition::TypeDeclaration {
8301 declared_value: Some(dv),
8302 ..
8303 }
8304 | DataDefinition::Reference {
8305 local_value: Some(dv),
8306 ..
8307 } => Some(dv),
8308 DataDefinition::Value { .. }
8309 | DataDefinition::TypeDeclaration { .. }
8310 | DataDefinition::Reference { .. }
8311 | DataDefinition::InstanceList { .. }
8312 | DataDefinition::Import { .. } => None,
8313 }
8314 }
8315
8316 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
8320 match self {
8321 DataDefinition::TypeDeclaration {
8322 declared_suggestion: Some(dv),
8323 ..
8324 }
8325 | DataDefinition::Reference {
8326 local_suggestion: Some(dv),
8327 ..
8328 } => Some(dv),
8329 DataDefinition::Value { .. }
8330 | DataDefinition::TypeDeclaration { .. }
8331 | DataDefinition::Reference { .. }
8332 | DataDefinition::InstanceList { .. }
8333 | DataDefinition::Import { .. } => None,
8334 }
8335 }
8336
8337 pub fn source(&self) -> &Source {
8339 match self {
8340 DataDefinition::Value { source, .. } => source,
8341 DataDefinition::TypeDeclaration { source, .. } => source,
8342 DataDefinition::Import { source, .. } => source,
8343 DataDefinition::InstanceList { source, .. } => source,
8344 DataDefinition::Reference { source, .. } => source,
8345 }
8346 }
8347
8348 pub fn import_sole_row(&self) -> &Arc<RowInstance> {
8350 let DataDefinition::Import { pin, row, .. } = self else {
8351 panic!("BUG: import_sole_row on non-import data");
8352 };
8353 assert!(
8354 pin.is_none(),
8355 "BUG: dynamic import needs pin selection before reading a row"
8356 );
8357 row
8358 }
8359
8360 pub fn import_candidates(&self) -> &std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>> {
8362 let DataDefinition::Import {
8363 pin, candidates, ..
8364 } = self
8365 else {
8366 panic!("BUG: import_candidates on non-import data");
8367 };
8368 assert!(pin.is_some(), "BUG: static import has no candidate map");
8369 candidates
8370 }
8371
8372 pub fn import_pin(&self) -> Option<&ImportEffectivePin> {
8374 match self {
8375 DataDefinition::Import { pin, .. } => pin.as_ref(),
8376 _ => None,
8377 }
8378 }
8379
8380 pub fn import_row_for(&self, identity: &SpecIdentity) -> &Arc<RowInstance> {
8382 let DataDefinition::Import {
8383 pin,
8384 row,
8385 candidates,
8386 ..
8387 } = self
8388 else {
8389 panic!("BUG: import_row_for on non-import data");
8390 };
8391 if pin.is_none() {
8392 return row;
8393 }
8394 candidates
8395 .get(identity)
8396 .unwrap_or_else(|| panic!("BUG: dynamic import has no row for identity '{identity}'"))
8397 }
8398}
8399
8400pub fn number_with_unit_to_value_kind(
8402 magnitude: rust_decimal::Decimal,
8403 unit_name: &str,
8404 lemma_type: &LemmaType,
8405) -> Result<ValueKind, String> {
8406 match &lemma_type.specifications {
8407 TypeSpecification::Ratio { units, .. } => {
8408 use crate::computation::rational::{checked_div, decimal_to_rational};
8409 let unit = units.get(unit_name)?;
8410 let magnitude_rational = decimal_to_rational(magnitude)
8411 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8412 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8413 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8414 Ok(ValueKind::Ratio(canonical_rational))
8415 }
8416 TypeSpecification::Measure { units, .. } => {
8417 use crate::computation::rational::checked_mul;
8418 let rational = lift_parser_decimal(magnitude)?;
8419 let unit = units.get(unit_name)?;
8420 match unit.literal_factor() {
8421 Some(factor) => {
8422 let canonical = checked_mul(&rational, factor).map_err(|failure| {
8423 format!("measure canonicalization overflow: {failure}")
8424 })?;
8425 Ok(ValueKind::Measure(canonical))
8426 }
8427 None => Ok(ValueKind::UnscaledMeasure {
8428 magnitude: rational,
8429 unit: std::sync::Arc::from(unit_name),
8430 }),
8431 }
8432 }
8433 _ => Err(format!(
8434 "Unit '{}' is defined on type '{}' which is not measure or ratio",
8435 unit_name,
8436 lemma_type.name()
8437 )),
8438 }
8439}
8440
8441pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
8444 matches!(
8445 (type_spec, value),
8446 (TypeSpecification::Number { .. }, ValueKind::Number(_))
8447 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
8448 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
8449 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
8450 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
8451 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
8452 | (
8453 TypeSpecification::Measure { .. },
8454 ValueKind::UnscaledMeasure { .. }
8455 )
8456 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
8457 | (
8458 TypeSpecification::NumberRange { .. },
8459 ValueKind::Range(_, _)
8460 )
8461 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
8462 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
8463 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
8464 | (
8465 TypeSpecification::MeasureRange { .. },
8466 ValueKind::Range(_, _)
8467 )
8468 | (TypeSpecification::Veto { .. }, _)
8469 | (TypeSpecification::Undetermined, _)
8470 | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
8471 | (TypeSpecification::List { .. }, ValueKind::List(_))
8472 )
8473}
8474
8475fn parser_value_type_mismatch(
8476 value: &crate::literals::Value,
8477 type_spec: &TypeSpecification,
8478) -> String {
8479 use crate::parsing::ast::AsLemmaSource;
8480 let value_str = format!("{}", AsLemmaSource(value));
8481 match type_spec {
8482 TypeSpecification::Measure { units, .. } => {
8483 let unit_hint = units
8484 .iter()
8485 .find(|u| u.is_canonical_factor())
8486 .map(|u| u.name.as_str())
8487 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
8488 .unwrap_or("unit");
8489 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8490 }
8491 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
8492 let unit_hint = units
8493 .iter()
8494 .next()
8495 .map(|u| u.name.as_str())
8496 .unwrap_or("unit");
8497 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8498 }
8499 _ => format!("cannot use {value_str} as {type_spec}"),
8500 }
8501}
8502
8503pub fn refresh_measure_literal_canonical_magnitude(
8508 lit: &mut LiteralValue,
8509 previous_type: &LemmaType,
8510 resolved_type: &LemmaType,
8511) -> Result<(), crate::computation::rational::NumericFailure> {
8512 use crate::computation::rational::{checked_div, checked_mul};
8513 let ValueKind::Measure(magnitude) = &mut lit.value else {
8514 if matches!(lit.value, ValueKind::UnscaledMeasure { .. }) {
8515 return Ok(());
8516 }
8517 return Ok(());
8518 };
8519 let signature = previous_type.measure_runtime_signature();
8523 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
8524 return Ok(());
8525 };
8526 if signature.len() != 1 {
8527 return Ok(());
8528 }
8529 let stored_factor = previous_type.measure_unit_factor(unit_name);
8530 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
8531 if stored_factor == resolved_factor {
8532 return Ok(());
8533 }
8534 let scaled = checked_mul(magnitude, resolved_factor)?;
8535 *magnitude = checked_div(&scaled, stored_factor)?;
8536 Ok(())
8537}
8538
8539fn number_with_unit_value_kind(
8540 magnitude: rust_decimal::Decimal,
8541 unit_name: &str,
8542 type_spec: &TypeSpecification,
8543) -> Result<ValueKind, String> {
8544 use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
8545 match type_spec {
8546 TypeSpecification::Ratio { units, .. } => {
8547 let unit = units.get(unit_name)?;
8548 let magnitude_rational = decimal_to_rational(magnitude)
8549 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8550 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8551 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8552 Ok(ValueKind::Ratio(canonical_rational))
8553 }
8554 TypeSpecification::Measure { units, .. } => {
8555 let rational = lift_parser_decimal(magnitude)?;
8556 let unit = units.get(unit_name)?;
8557 match unit.literal_factor() {
8558 Some(factor) => {
8559 let canonical = checked_mul(&rational, factor).map_err(|failure| {
8560 format!("measure canonicalization overflow: {failure}")
8561 })?;
8562 Ok(ValueKind::Measure(canonical))
8563 }
8564 None => Ok(ValueKind::UnscaledMeasure {
8565 magnitude: rational,
8566 unit: Arc::from(unit_name),
8567 }),
8568 }
8569 }
8570 _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
8571 }
8572}
8573
8574fn parser_scalar_to_value_kind(
8579 value: &crate::literals::Value,
8580 type_spec: &TypeSpecification,
8581) -> Result<ValueKind, String> {
8582 use crate::literals::Value;
8583 match (value, type_spec) {
8584 (Value::NumberWithUnit(magnitude, unit_name), _) => {
8585 number_with_unit_value_kind(*magnitude, unit_name, type_spec)
8586 }
8587 (Value::Number(n), TypeSpecification::Number { .. }) => {
8588 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
8589 }
8590 (Value::Text(s), TypeSpecification::Text { .. }) => {
8591 Ok(ValueKind::Text(Arc::from(s.as_str())))
8592 }
8593 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
8594 (Value::Date(dt), TypeSpecification::Date { .. }) => {
8595 Ok(ValueKind::Date(date_time_to_semantic(dt)))
8596 }
8597 (Value::Time(t), TypeSpecification::Time { .. }) => {
8598 Ok(ValueKind::Time(time_to_semantic(t)))
8599 }
8600 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
8601 }
8602}
8603
8604pub fn parser_value_to_value_kind(
8610 value: &crate::literals::Value,
8611 lemma_type: &LemmaType,
8612 resolve_unit: Option<&UnitTokenResolver<'_>>,
8613) -> Result<ValueKind, String> {
8614 use crate::literals::Value;
8615 match (value, &lemma_type.specifications) {
8616 (
8617 Value::Range(left, right),
8618 TypeSpecification::NumberRange { .. }
8619 | TypeSpecification::DateRange { .. }
8620 | TypeSpecification::TimeRange { .. }
8621 | TypeSpecification::RatioRange { .. }
8622 | TypeSpecification::MeasureRange { .. },
8623 ) => {
8624 let endpoint = match &lemma_type.specifications {
8625 TypeSpecification::MeasureRange { .. } | TypeSpecification::RatioRange { .. } => {
8626 lemma_type.range_element_type()
8627 }
8628 other => Arc::new(LemmaType::primitive(
8629 range_element_type_specification(other).expect(
8630 "BUG: range_element_type_specification missing arm for known range type",
8631 ),
8632 )),
8633 };
8634 let left_lit = lift_range_endpoint(left, endpoint.as_ref(), resolve_unit)?;
8635 let right_lit = lift_range_endpoint(right, endpoint.as_ref(), resolve_unit)?;
8636 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
8637 }
8638 (value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
8639 }
8640}
8641
8642pub(crate) fn typed_literal_from_parsed(
8644 parsed: &crate::literals::Value,
8645 lemma_type: &Arc<LemmaType>,
8646 unit_index: &UnitIndex,
8647 named_types: &IndexMap<String, Arc<LemmaType>>,
8648) -> Result<TypedLiteral, String> {
8649 let resolve = UnitTokenResolver::Typed {
8650 unit_index,
8651 named_types,
8652 lemma_type: lemma_type.as_ref(),
8653 };
8654 let kind = parser_value_to_value_kind(parsed, lemma_type.as_ref(), Some(&resolve))?;
8655 let binding = match binding_unit_from_parser_value(parsed) {
8656 Some(written) => Some(resolve.resolve(&written)?),
8657 None => None,
8658 };
8659 Ok(typed_literal_with_binding_unit(kind, binding, lemma_type))
8660}
8661
8662pub(crate) fn typed_literal_with_binding_unit(
8663 kind: ValueKind,
8664 binding_unit: Option<String>,
8665 lemma_type: &Arc<LemmaType>,
8666) -> TypedLiteral {
8667 let typed_type = match binding_unit {
8668 Some(unit)
8669 if matches!(
8670 &lemma_type.specifications,
8671 TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
8672 ) =>
8673 {
8674 Arc::new(lemma_type.as_ref().clone().with_unit(unit))
8675 }
8676 Some(_) | None => Arc::clone(lemma_type),
8677 };
8678 TypedLiteral {
8679 value: kind,
8680 lemma_type: typed_type,
8681 }
8682}
8683
8684fn binding_unit_from_parser_value(value: &crate::literals::Value) -> Option<String> {
8685 use crate::literals::Value;
8686 match value {
8687 Value::NumberWithUnit(_, unit) => Some(unit.clone()),
8688 Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
8689 (Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
8690 if left_unit == right_unit =>
8691 {
8692 Some(left_unit.clone())
8693 }
8694 _ => None,
8695 },
8696 _ => None,
8697 }
8698}
8699
8700pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
8704 use crate::parsing::ast::Value;
8705 Ok(match value {
8706 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
8707 Value::Text(s) => ValueKind::Text(Arc::from(s.as_str())),
8708 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
8709 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
8710 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
8711 Value::NumberWithUnit(_, _) => {
8712 return Err(
8713 "number_with_unit literal requires type context (measure or ratio)".to_string(),
8714 );
8715 }
8716 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
8717 })
8718}
8719
8720pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
8722 SemanticDateTime {
8723 year: dt.year,
8724 month: dt.month,
8725 day: dt.day,
8726 hour: dt.hour,
8727 minute: dt.minute,
8728 second: dt.second,
8729 microsecond: dt.microsecond,
8730 timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
8731 }
8732}
8733
8734pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
8736 SemanticTime {
8737 hour: date.hour,
8738 minute: date.minute,
8739 second: date.second,
8740 microsecond: date.microsecond,
8741 timezone: date.timezone.clone(),
8742 }
8743}
8744
8745pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
8747 SemanticTime {
8748 hour: t.hour.into(),
8749 minute: t.minute.into(),
8750 second: t.second.into(),
8751 microsecond: t.microsecond,
8752 timezone: t.timezone.as_ref().map(SemanticTimezone::from),
8753 }
8754}
8755
8756pub(crate) fn compare_semantic_dates(
8760 left: &SemanticDateTime,
8761 right: &SemanticDateTime,
8762) -> std::cmp::Ordering {
8763 left.year
8764 .cmp(&right.year)
8765 .then_with(|| left.month.cmp(&right.month))
8766 .then_with(|| left.day.cmp(&right.day))
8767 .then_with(|| left.hour.cmp(&right.hour))
8768 .then_with(|| left.minute.cmp(&right.minute))
8769 .then_with(|| left.second.cmp(&right.second))
8770 .then_with(|| left.microsecond.cmp(&right.microsecond))
8771}
8772
8773pub(crate) fn compare_semantic_times(
8776 left: &SemanticTime,
8777 right: &SemanticTime,
8778) -> std::cmp::Ordering {
8779 left.hour
8780 .cmp(&right.hour)
8781 .then_with(|| left.minute.cmp(&right.minute))
8782 .then_with(|| left.second.cmp(&right.second))
8783 .then_with(|| left.microsecond.cmp(&right.microsecond))
8784}
8785
8786pub fn conversion_target_to_semantic(
8788 ct: &ConversionTarget,
8789 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
8790 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
8791) -> Result<SemanticConversionTarget, String> {
8792 match ct {
8793 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
8794 ConversionTarget::Unit { unit_name } => {
8795 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
8796 let (bare, owning_type) = match resolved_types {
8797 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
8798 None => index.resolve(unit_name)?,
8799 };
8800 Ok(SemanticConversionTarget::Unit {
8801 unit_name: bare,
8802 owning_type,
8803 })
8804 }
8805 }
8806}
8807
8808static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
8814static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
8815static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
8816static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8817static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8818static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
8819static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
8820static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8821static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8822static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8823static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8824static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8825
8826#[must_use]
8827pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
8828 lemma_type_for_primitive(PrimitiveKind::Boolean)
8829}
8830
8831#[must_use]
8832pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
8833 lemma_type_for_primitive(PrimitiveKind::Number)
8834}
8835
8836#[must_use]
8837pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
8838 lemma_type_for_primitive(PrimitiveKind::Text)
8839}
8840
8841#[must_use]
8842pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
8843 lemma_type_for_primitive(PrimitiveKind::Date)
8844}
8845
8846#[must_use]
8847pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
8848 lemma_type_for_primitive(PrimitiveKind::DateRange)
8849}
8850
8851#[must_use]
8852pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
8853 lemma_type_for_primitive(PrimitiveKind::Time)
8854}
8855
8856#[must_use]
8857pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
8858 lemma_type_for_primitive(PrimitiveKind::Ratio)
8859}
8860
8861#[must_use]
8863pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
8864 let cell = match kind {
8865 PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
8866 PrimitiveKind::Number => &PRIMITIVE_NUMBER,
8867 PrimitiveKind::Text => &PRIMITIVE_TEXT,
8868 PrimitiveKind::Date => &PRIMITIVE_DATE,
8869 PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
8870 PrimitiveKind::Time => &PRIMITIVE_TIME,
8871 PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
8872 PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
8873 PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
8874 PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
8875 PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
8876 PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
8877 };
8878 cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
8879}
8880
8881#[must_use]
8883pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
8884 match kind {
8885 PrimitiveKind::Boolean => TypeSpecification::boolean(),
8886 PrimitiveKind::Measure => TypeSpecification::measure(),
8887 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
8888 PrimitiveKind::Number => TypeSpecification::number(),
8889 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
8890 PrimitiveKind::Ratio => TypeSpecification::ratio(),
8891 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
8892 PrimitiveKind::Text => TypeSpecification::text(),
8893 PrimitiveKind::Date => TypeSpecification::date(),
8894 PrimitiveKind::DateRange => TypeSpecification::date_range(),
8895 PrimitiveKind::Time => TypeSpecification::time(),
8896 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
8897 }
8898}
8899
8900impl fmt::Display for PathSegment {
8905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8906 let hop = self.input_key();
8907 match &self.repository {
8908 Some(repository) => write!(f, "{hop} → {} {}", repository, self.spec),
8909 None => write!(f, "{hop} → {}", self.spec),
8910 }
8911 }
8912}
8913
8914impl fmt::Display for DataPath {
8915 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8916 write!(f, "{}", self.input_key())
8917 }
8918}
8919
8920impl fmt::Display for RulePath {
8921 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8922 write!(f, "{}", self.input_key())
8923 }
8924}
8925
8926impl fmt::Display for LemmaType {
8927 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8928 write!(f, "{}", self.name())
8929 }
8930}
8931
8932pub(crate) fn format_decimal_for_api(decimal: rust_decimal::Decimal) -> rust_decimal::Decimal {
8933 let normalized = decimal.normalize();
8934 if normalized.fract().is_zero() {
8935 normalized.trunc()
8936 } else {
8937 normalized
8938 }
8939}
8940
8941fn format_decimal_for_human_display(decimal: rust_decimal::Decimal) -> String {
8942 decimal.normalize().to_string()
8943}
8944
8945fn format_rational_for_human_display(
8946 magnitude: &crate::computation::rational::RationalInteger,
8947) -> String {
8948 match magnitude.try_to_decimal() {
8949 Ok(decimal) => format_decimal_for_human_display(decimal),
8950 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
8951 Err(_) => magnitude.display_str(),
8952 }
8953}
8954
8955pub(crate) fn measure_family_has_expressible_unit(
8958 lemma_type: &LemmaType,
8959 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8960) -> bool {
8961 let Some(units) = lemma_type.measure_units_table() else {
8962 return true;
8963 };
8964 if units.is_empty() {
8965 return true;
8966 }
8967 units.iter().any(|unit| {
8968 unit.literal_factor().is_some()
8969 || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8970 })
8971}
8972
8973fn measure_display_unit<'a>(
8974 units: &'a crate::literals::MeasureUnits,
8975 lemma_type: &LemmaType,
8976 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8977) -> &'a crate::literals::MeasureUnit {
8978 let factor_known = |unit: &crate::literals::MeasureUnit| {
8979 unit.literal_factor().is_some()
8980 || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8981 };
8982 if let Some(binding) = lemma_type.unit() {
8983 let unit = units
8984 .iter()
8985 .find(|unit| unit.name == binding)
8986 .unwrap_or_else(|| {
8987 panic!(
8988 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
8989 lemma_type.name()
8990 )
8991 });
8992 if factor_known(unit) {
8993 return unit;
8994 }
8995 }
8996 units
8997 .iter()
8998 .find(|unit| unit.is_canonical_factor() && factor_known(unit))
8999 .or_else(|| {
9000 units
9001 .iter()
9002 .find(|unit| unit.literal_factor().is_some() && factor_known(unit))
9003 })
9004 .or_else(|| units.iter().find(|unit| factor_known(unit)))
9005 .unwrap_or_else(|| {
9006 panic!(
9007 "BUG: measure display of '{}' reached with no factor-known unit; callers must veto first",
9008 lemma_type.name()
9009 )
9010 })
9011}
9012
9013fn format_measure_canonical_for_display(
9014 canonical: &crate::computation::rational::RationalInteger,
9015 lemma_type: &LemmaType,
9016 signature: &[(String, i32)],
9017 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
9018) -> String {
9019 use crate::computation::rational::checked_div;
9020
9021 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
9022 if !units.is_empty() {
9023 let unit = measure_display_unit(units, lemma_type, runtime_scales);
9024 let factor = unit
9025 .literal_factor()
9026 .cloned()
9027 .or_else(|| {
9028 runtime_scales.and_then(|scales| scales.get(&unit.name).cloned())
9029 })
9030 .unwrap_or_else(|| {
9031 panic!(
9032 "BUG: display of unit '{}' on {} requires a literal factor or a settled runtime scale",
9033 unit.name,
9034 lemma_type.name()
9035 )
9036 });
9037 let in_unit = checked_div(canonical, &factor)
9038 .expect("BUG: de-canonicalization for measure display must not fail");
9039 return format_number_with_unit_for_display(&in_unit, &unit.name);
9040 }
9041 }
9042
9043 let unit_label = match signature {
9044 [] => String::new(),
9045 [(name, 1)] => name.clone(),
9046 _ => format_signature_operator_style(signature),
9047 };
9048 let formatted = format_rational_for_human_display(canonical);
9049 if unit_label.is_empty() {
9050 formatted
9051 } else {
9052 format!("{formatted} {unit_label}")
9053 }
9054}
9055
9056fn format_ratio_canonical_for_display(
9057 canonical: &crate::computation::rational::RationalInteger,
9058 lemma_type: &LemmaType,
9059) -> String {
9060 use crate::computation::rational::checked_mul;
9061
9062 let units = match &lemma_type.specifications {
9063 TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
9064 units
9065 }
9066 _ => unreachable!(
9067 "BUG: ratio display with non-ratio type {}",
9068 lemma_type.name()
9069 ),
9070 };
9071 let display_unit = lemma_type
9072 .unit()
9073 .or_else(|| lemma_type.ratio_primary_unit());
9074 let Some(unit_name) = display_unit else {
9075 return format_rational_magnitude_for_display(canonical);
9076 };
9077 let unit = units.get(unit_name).unwrap_or_else(|_| {
9078 panic!(
9079 "BUG: ratio display unit '{unit_name}' is not declared on type {}",
9080 lemma_type.name()
9081 )
9082 });
9083 let in_unit = checked_mul(canonical, &unit.value)
9084 .expect("BUG: de-canonicalization for ratio display must not fail");
9085 format_number_with_unit_for_display(&in_unit, unit_name)
9086}
9087
9088impl fmt::Display for LiteralValue {
9089 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9090 match &self.value {
9094 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
9095 write!(f, "{}", self.value)
9096 }
9097 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
9098 _ => write!(f, "{}", self.value),
9099 }
9100 }
9101}
9102
9103#[cfg(test)]
9108pub(crate) mod tests {
9109 use super::*;
9110 use crate::computation::rational::decimal_to_rational;
9111 use crate::literals::DateGranularity;
9112 use crate::literals::Value;
9113 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
9114 use rust_decimal::Decimal;
9115 use std::str::FromStr;
9116
9117 fn meter_axis(exponent: i32) -> BaseMeasureVector {
9118 [(Dimension::Family(test_type_family("meter")), exponent)]
9119 .into_iter()
9120 .collect()
9121 }
9122
9123 #[test]
9124 fn scale_decomposition_exponent_one_is_identity() {
9125 let base = meter_axis(1);
9126 assert_eq!(scale_decomposition(&base, 1), Some(base));
9127 }
9128
9129 #[test]
9130 fn scale_decomposition_squares_meter() {
9131 assert_eq!(scale_decomposition(&meter_axis(1), 2), Some(meter_axis(2)));
9132 }
9133
9134 #[test]
9135 fn scale_decomposition_zero_is_empty() {
9136 assert_eq!(
9137 scale_decomposition(&meter_axis(1), 0),
9138 Some(BaseMeasureVector::new())
9139 );
9140 }
9141
9142 #[test]
9143 fn scale_decomposition_negative_inverts_meter() {
9144 assert_eq!(
9145 scale_decomposition(&meter_axis(1), -1),
9146 Some(meter_axis(-1))
9147 );
9148 }
9149
9150 #[test]
9151 fn scale_decomposition_at_exponent_cap() {
9152 assert_eq!(
9153 scale_decomposition(&meter_axis(32), 2),
9154 Some(meter_axis(64))
9155 );
9156 }
9157
9158 #[test]
9159 fn scale_decomposition_past_exponent_cap_is_none() {
9160 assert_eq!(scale_decomposition(&meter_axis(1), 65), None);
9161 assert_eq!(scale_decomposition(&meter_axis(32), 3), None);
9162 }
9163
9164 #[must_use]
9165 pub(crate) fn test_type_family(name: &str) -> TypeFamily {
9166 TypeFamily {
9167 repository: None,
9168 spec: "test".to_string(),
9169 effective: crate::parsing::ast::EffectiveDate::Origin,
9170 name: name.to_string(),
9171 }
9172 }
9173
9174 #[must_use]
9175 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
9176 crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
9177 }
9178
9179 #[must_use]
9180 pub(crate) fn primitive_measure() -> &'static LemmaType {
9181 primitive_measure_arc().as_ref()
9182 }
9183
9184 #[test]
9185 fn default_primitive_help_is_goal_oriented() {
9186 let kinds = [
9187 PrimitiveKind::Boolean,
9188 PrimitiveKind::Measure,
9189 PrimitiveKind::MeasureRange,
9190 PrimitiveKind::Number,
9191 PrimitiveKind::NumberRange,
9192 PrimitiveKind::Ratio,
9193 PrimitiveKind::RatioRange,
9194 PrimitiveKind::Text,
9195 PrimitiveKind::Date,
9196 PrimitiveKind::DateRange,
9197 PrimitiveKind::Time,
9198 PrimitiveKind::TimeRange,
9199 ];
9200 for kind in kinds {
9201 let spec = type_spec_for_primitive(kind);
9202 let help = match &spec {
9203 TypeSpecification::Boolean { help, .. }
9204 | TypeSpecification::Number { help, .. }
9205 | TypeSpecification::NumberRange { help, .. }
9206 | TypeSpecification::Text { help, .. }
9207 | TypeSpecification::Measure { help, .. }
9208 | TypeSpecification::MeasureRange { help, .. }
9209 | TypeSpecification::Ratio { help, .. }
9210 | TypeSpecification::RatioRange { help, .. }
9211 | TypeSpecification::Date { help, .. }
9212 | TypeSpecification::DateRange { help, .. }
9213 | TypeSpecification::TimeRange { help, .. }
9214 | TypeSpecification::Time { help, .. } => help,
9215 TypeSpecification::Veto { .. }
9216 | TypeSpecification::Undetermined
9217 | TypeSpecification::Spec { .. }
9218 | TypeSpecification::List { .. } => {
9219 unreachable!(
9220 "BUG: primitive kind {:?} mapped to non-primitive spec",
9221 kind
9222 )
9223 }
9224 };
9225 assert!(!help.is_empty(), "help for {:?}", kind);
9226 assert!(
9227 !help.to_ascii_lowercase().contains("format:"),
9228 "help for {:?} must not describe syntax: {:?}",
9229 kind,
9230 help
9231 );
9232 assert_eq!(help, default_help_for_primitive(kind));
9233 }
9234 }
9235
9236 #[test]
9237 fn data_path_and_rule_path_display_are_input_key() {
9238 let implicit = PathSegment {
9239 uses: Arc::from("bag"),
9240 row: None,
9241 repository: None,
9242 spec: Arc::from("bag"),
9243 };
9244 let explicit = PathSegment {
9245 uses: Arc::from("bag"),
9246 row: None,
9247 repository: None,
9248 spec: Arc::from("item_bag"),
9249 };
9250 let named_repo = PathSegment {
9251 uses: Arc::from("calc"),
9252 row: None,
9253 repository: Some(Arc::from("alpha")),
9254 spec: Arc::from("pricing"),
9255 };
9256 let row = PathSegment {
9257 uses: Arc::from("staff"),
9258 row: Some(2),
9259 repository: None,
9260 spec: Arc::from("employee"),
9261 };
9262
9263 assert_eq!(
9264 DataPath::new(vec![row.clone()], "salary".to_string()).to_string(),
9265 "staff.2.salary"
9266 );
9267 assert_eq!(
9268 RulePath::new(vec![row.clone()], "double".to_string()).to_string(),
9269 "staff.2.double"
9270 );
9271 assert_eq!(row.to_string(), "staff.2 → employee");
9272 assert_eq!(
9273 SpecInstance {
9274 prefix: vec![implicit.clone(), row]
9275 }
9276 .to_string(),
9277 "bag.staff.2"
9278 );
9279 assert_eq!(
9280 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
9281 "bag.weight"
9282 );
9283 assert_eq!(
9284 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
9285 "bag.weight"
9286 );
9287 assert_eq!(
9288 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
9289 "calc.price"
9290 );
9291 assert_eq!(
9292 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
9293 "bag.cost"
9294 );
9295 assert_eq!(
9296 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
9297 "bag.cost"
9298 );
9299 assert_eq!(
9300 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
9301 "calc.price"
9302 );
9303
9304 assert_eq!(
9305 PathSegment {
9306 uses: Arc::from("bag"),
9307 row: None,
9308 repository: None,
9309 spec: Arc::from("item_bag"),
9310 }
9311 .to_string(),
9312 "bag → item_bag"
9313 );
9314 assert_eq!(
9315 PathSegment {
9316 uses: Arc::from("calc"),
9317 row: None,
9318 repository: Some(Arc::from("alpha")),
9319 spec: Arc::from("pricing"),
9320 }
9321 .to_string(),
9322 "calc → alpha pricing"
9323 );
9324 }
9325
9326 #[test]
9327 fn test_negated_comparison() {
9328 assert_eq!(
9329 negated_comparison(ComparisonComputation::LessThan),
9330 ComparisonComputation::GreaterThanOrEqual
9331 );
9332 assert_eq!(
9333 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
9334 ComparisonComputation::LessThan
9335 );
9336 assert_eq!(
9337 negated_comparison(ComparisonComputation::Is),
9338 ComparisonComputation::IsNot
9339 );
9340 assert_eq!(
9341 negated_comparison(ComparisonComputation::IsNot),
9342 ComparisonComputation::Is
9343 );
9344 }
9345
9346 #[test]
9347 fn value_to_semantic_number_is_decimal() {
9348 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
9349 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
9350 }
9351
9352 #[test]
9353 fn value_kind_measure_serializes_magnitude_only() {
9354 let kind =
9355 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
9356 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
9357 assert_eq!(json["measure"]["value"], "99.5");
9358 assert!(json["measure"].get("signature").is_none());
9359 }
9360
9361 #[test]
9362 fn value_kind_measure_roundtrips() {
9363 let original =
9364 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
9365 let api = crate::api::ValueKind::from(&original);
9366 let json = serde_json::to_string(&api).unwrap();
9367 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9368 assert_eq!(api, parsed);
9369 }
9370
9371 #[test]
9372 fn value_kind_measure_empty_roundtrips() {
9373 let original =
9374 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
9375 let api = crate::api::ValueKind::from(&original);
9376 let json = serde_json::to_string(&api).unwrap();
9377 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9378 assert_eq!(api, parsed);
9379 }
9380
9381 #[test]
9382 fn literal_value_number_serde_not_rational_array() {
9383 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
9384 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
9385 let number = json
9386 .get("value")
9387 .and_then(|v| v.get("number"))
9388 .expect("number field");
9389 assert!(number.is_string());
9390 assert_eq!(number.as_str(), Some("20"));
9391 assert!(
9392 !number.is_array(),
9393 "stored number must not serialize as [n,d]"
9394 );
9395 }
9396
9397 #[test]
9398 fn test_literal_value_to_primitive_type() {
9399 let one = rational_new(1, 1);
9400
9401 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
9402 assert_eq!(
9403 TypedLiteral::number(one.clone()).lemma_type.name(),
9404 "number"
9405 );
9406 assert_eq!(
9407 TypedLiteral::from_bool(bool::from(BooleanValue::True))
9408 .lemma_type
9409 .name(),
9410 "boolean"
9411 );
9412
9413 let dt = DateTimeValue {
9414 year: 2024,
9415 month: 1,
9416 day: 1,
9417 hour: 0,
9418 minute: 0,
9419 second: 0,
9420 microsecond: 0,
9421 timezone: None,
9422
9423 granularity: DateGranularity::Full,
9424 };
9425 assert_eq!(
9426 TypedLiteral::date(date_time_to_semantic(&dt))
9427 .lemma_type
9428 .name(),
9429 "date"
9430 );
9431 assert_eq!(
9432 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
9433 .lemma_type
9434 .name(),
9435 "ratio"
9436 );
9437 let dur_type = LemmaType::new(
9438 "duration".to_string(),
9439 TypeSpecification::Measure {
9440 minimum: None,
9441 maximum: None,
9442 units: MeasureUnits::from(vec![MeasureUnit {
9443 name: "second".to_string(),
9444 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9445 derived_measure_factors: Vec::new(),
9446 decomposition: BaseMeasureVector::new(),
9447 minimum: None,
9448 maximum: None,
9449 suggestion_magnitude: None,
9450 }]),
9451 traits: vec![MeasureTrait::Duration],
9452 decomposition: None,
9453 unit: None,
9454 help: String::new(),
9455 },
9456 TypeExtends::Primitive,
9457 );
9458 assert_eq!(
9459 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
9460 .lemma_type
9461 .name(),
9462 "duration"
9463 );
9464 }
9465
9466 #[test]
9467 fn test_type_display() {
9468 let specs = TypeSpecification::text();
9469 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
9470 assert_eq!(format!("{}", lemma_type), "name");
9471 }
9472
9473 #[test]
9474 fn test_type_serialization() {
9475 let specs = TypeSpecification::number();
9476 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
9477 let api = crate::api::LemmaType::from(&lemma_type);
9478 let serialized = serde_json::to_string(&api).unwrap();
9479 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
9480 assert_eq!(api, deserialized);
9481 }
9482
9483 #[test]
9484 fn empty_list_display_value_with_type_is_type_spec() {
9485 let element = Arc::new(LemmaType::new(
9486 "item".to_string(),
9487 TypeSpecification::Spec {
9488 spec: SpecIdentity {
9489 repository: None,
9490 spec: "item".to_string(),
9491 effective: crate::parsing::ast::EffectiveDate::Origin,
9492 slice: crate::parsing::ast::EffectiveDate::Origin,
9493 },
9494 },
9495 TypeExtends::Primitive,
9496 ));
9497 let list_ty = LemmaType::list_of(element);
9498 let empty = LiteralValue::new(ValueKind::List(Vec::new().into()));
9499 assert_eq!(empty.display_value_with_type(&list_ty), "item list");
9500 assert_eq!(empty.display_value(), "");
9501 }
9502
9503 #[test]
9504 fn test_literal_value_display_value() {
9505 let ten = rational_new(10, 1);
9506
9507 assert_eq!(
9508 LiteralValue::text("hello".to_string()).display_value(),
9509 "hello"
9510 );
9511 assert_eq!(LiteralValue::number(ten).display_value(), "10");
9512 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
9513 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
9514
9515 let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
9517 let ten_percent_ratio = LiteralValue::ratio(
9518 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
9519 .expect("ratio decimal"),
9520 );
9521 assert_eq!(
9522 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
9523 "10%"
9524 );
9525
9526 let time = TimeValue {
9527 hour: 14,
9528 minute: 30,
9529 second: 0,
9530 microsecond: 0,
9531 timezone: None,
9532 };
9533 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
9534 assert!(time_display.contains("14"));
9535 assert!(time_display.contains("30"));
9536 }
9537
9538 #[test]
9539 fn test_measure_display_trims_trailing_zeros() {
9540 let money_type = LemmaType {
9541 name: Some("money".to_string()),
9542 specifications: TypeSpecification::Measure {
9543 minimum: None,
9544 maximum: None,
9545 units: MeasureUnits::from(vec![MeasureUnit {
9546 name: "eur".to_string(),
9547 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9548 derived_measure_factors: Vec::new(),
9549 decomposition: BaseMeasureVector::new(),
9550 minimum: None,
9551 maximum: None,
9552 suggestion_magnitude: None,
9553 }]),
9554 traits: Vec::new(),
9555 decomposition: None,
9556 unit: None,
9557 help: String::new(),
9558 },
9559 extends: TypeExtends::Primitive,
9560 options: TypeOptions::default(),
9561 };
9562 let money_type = Arc::new(money_type);
9563 let val =
9564 LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
9565 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.8 eur");
9566 let more_precision = LiteralValue::measure(
9567 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
9568 );
9569 assert_eq!(
9570 more_precision.display_value_with_type(money_type.as_ref()),
9571 "1.8 eur"
9572 );
9573 let count_type = Arc::new(LemmaType {
9574 name: Some("count".to_string()),
9575 specifications: TypeSpecification::Measure {
9576 minimum: None,
9577 maximum: None,
9578 units: MeasureUnits::from(vec![MeasureUnit {
9579 name: "items".to_string(),
9580 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9581 derived_measure_factors: Vec::new(),
9582 decomposition: BaseMeasureVector::new(),
9583 minimum: None,
9584 maximum: None,
9585 suggestion_magnitude: None,
9586 }]),
9587 traits: Vec::new(),
9588 decomposition: None,
9589 unit: None,
9590 help: String::new(),
9591 },
9592 extends: TypeExtends::Primitive,
9593 options: TypeOptions::default(),
9594 });
9595 let val_any = LiteralValue::measure(
9596 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
9597 );
9598 assert_eq!(
9599 val_any.display_value_with_type(count_type.as_ref()),
9600 "42.5 items"
9601 );
9602 }
9603
9604 #[test]
9605 fn test_literal_value_time_type() {
9606 let time = TimeValue {
9607 hour: 14,
9608 minute: 30,
9609 second: 0,
9610 microsecond: 0,
9611 timezone: None,
9612 };
9613 let lit = TypedLiteral::time(time_to_semantic(&time));
9614 assert_eq!(lit.lemma_type.name(), "time");
9615 }
9616
9617 #[test]
9618 fn test_measure_family_name_none_without_declaring_spec() {
9619 let money_primitive = LemmaType::new(
9620 "money".to_string(),
9621 TypeSpecification::measure(),
9622 TypeExtends::Primitive,
9623 );
9624 assert_eq!(money_primitive.measure_family_name(), None);
9625 }
9626
9627 #[test]
9628 fn test_measure_family_name_custom() {
9629 let measure_spec = TypeSpecification::measure();
9630 let money_custom = LemmaType::new(
9631 "money".to_string(),
9632 measure_spec,
9633 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9634 );
9635 assert_eq!(money_custom.measure_family_name(), Some("money"));
9636 }
9637
9638 #[test]
9639 fn test_same_named_roots_in_different_specs_are_different_families() {
9640 let declared_in = |spec: &str| TypeFamily {
9641 repository: None,
9642 spec: spec.to_string(),
9643 effective: crate::parsing::ast::EffectiveDate::Origin,
9644 name: "money".to_string(),
9645 };
9646 let pricing_money = LemmaType::new(
9647 "money".to_string(),
9648 TypeSpecification::measure(),
9649 TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
9650 );
9651 let shop_money = LemmaType::new(
9652 "money".to_string(),
9653 TypeSpecification::measure(),
9654 TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
9655 );
9656 assert!(!pricing_money.same_measure_family(&shop_money));
9657 assert!(!shop_money.same_measure_family(&pricing_money));
9658 }
9659
9660 #[test]
9661 fn test_same_measure_family_parent_and_child() {
9662 let measure_spec = TypeSpecification::measure();
9663 let type_x = LemmaType::new(
9664 "x".to_string(),
9665 measure_spec.clone(),
9666 TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
9667 );
9668 let type_x2 = LemmaType::new(
9669 "x2".to_string(),
9670 measure_spec,
9671 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9672 );
9673 assert_eq!(type_x.measure_family_name(), Some("x"));
9674 assert_eq!(type_x2.measure_family_name(), Some("x"));
9675 assert!(type_x.same_measure_family(&type_x2));
9676 assert!(type_x2.same_measure_family(&type_x));
9677 }
9678
9679 #[test]
9680 fn test_same_measure_family_siblings() {
9681 let measure_spec = TypeSpecification::measure();
9682 let type_x2_a = LemmaType::new(
9683 "x2a".to_string(),
9684 measure_spec.clone(),
9685 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9686 );
9687 let type_x2_b = LemmaType::new(
9688 "x2b".to_string(),
9689 measure_spec,
9690 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9691 );
9692 assert!(type_x2_a.same_measure_family(&type_x2_b));
9693 }
9694
9695 #[test]
9696 fn test_same_measure_family_different_families() {
9697 let measure_spec = TypeSpecification::measure();
9698 let money = LemmaType::new(
9699 "money".to_string(),
9700 measure_spec.clone(),
9701 TypeExtends::Primitive,
9702 );
9703 let temperature = LemmaType::new(
9704 "temperature".to_string(),
9705 measure_spec,
9706 TypeExtends::Primitive,
9707 );
9708 assert!(!money.same_measure_family(&temperature));
9709 assert!(!temperature.same_measure_family(&money));
9710 }
9711
9712 #[test]
9713 fn test_same_measure_family_measure_vs_non_measure() {
9714 let measure_spec = TypeSpecification::measure();
9715 let number_spec = TypeSpecification::number();
9716 let measure_type =
9717 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
9718 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9719 assert!(!measure_type.same_measure_family(&number_type));
9720 assert!(!number_type.same_measure_family(&measure_type));
9721 }
9722
9723 #[test]
9724 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
9725 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
9726 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
9727
9728 assert!(!left.same_measure_family(&right));
9729 assert!(left.compatible_with_anonymous_measure(&right));
9730 }
9731
9732 #[test]
9733 fn same_value_type_distinct_families_same_decomposition_are_not_same() {
9734 let decomp: BaseMeasureVector = [
9735 (Dimension::Family(test_type_family("length")), 1i32),
9736 (Dimension::Family(test_type_family("elapsed")), -1i32),
9737 ]
9738 .into_iter()
9739 .collect();
9740 let speed_spec = |name: &str, family: &str| {
9741 let mut spec = TypeSpecification::measure();
9742 if let TypeSpecification::Measure {
9743 decomposition,
9744 help,
9745 ..
9746 } = &mut spec
9747 {
9748 *decomposition = Some(decomp.clone());
9749 *help = String::new();
9750 }
9751 LemmaType::new(
9752 name.to_string(),
9753 spec,
9754 TypeExtends::custom_local("measure".to_string(), test_type_family(family)),
9755 )
9756 };
9757 let speed_a = speed_spec("speed_a", "speed_a");
9758 let speed_b = speed_spec("speed_b", "speed_b");
9759 assert_eq!(
9760 speed_a.measure_type_decomposition(),
9761 speed_b.measure_type_decomposition()
9762 );
9763 assert!(!speed_a.same_measure_family(&speed_b));
9764 assert!(
9765 !speed_a.same_value_type(&speed_b),
9766 "same decomposition alone must not make distinct measure families same_value_type"
9767 );
9768 }
9769
9770 #[test]
9771 fn test_measure_family_name_non_measure_returns_none() {
9772 let number_spec = TypeSpecification::number();
9773 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9774 assert_eq!(number_type.measure_family_name(), None);
9775 }
9776
9777 #[test]
9778 fn test_lemma_type_inequality_local_vs_import_same_shape() {
9779 let measure_spec = TypeSpecification::measure();
9780 let local = LemmaType::new(
9781 "t".to_string(),
9782 measure_spec.clone(),
9783 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9784 );
9785 let imported = LemmaType::new(
9786 "t".to_string(),
9787 measure_spec,
9788 TypeExtends::Custom {
9789 parent: "money".to_string(),
9790 family: test_type_family("money"),
9791 defining_spec: TypeDefiningSpec::Import,
9792 },
9793 );
9794 assert_ne!(local, imported);
9795 }
9796
9797 #[test]
9798 fn test_lemma_type_equality_import_unit_variant() {
9799 let measure_spec = TypeSpecification::measure();
9800 let left = LemmaType::new(
9801 "t".to_string(),
9802 measure_spec.clone(),
9803 TypeExtends::Custom {
9804 parent: "money".to_string(),
9805 family: test_type_family("money"),
9806 defining_spec: TypeDefiningSpec::Import,
9807 },
9808 );
9809 let right = LemmaType::new(
9810 "t".to_string(),
9811 measure_spec,
9812 TypeExtends::Custom {
9813 parent: "money".to_string(),
9814 family: test_type_family("money"),
9815 defining_spec: TypeDefiningSpec::Import,
9816 },
9817 );
9818 assert_eq!(left, right);
9819 }
9820
9821 fn month_suggestion_arg() -> CommandArg {
9822 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9823 Decimal::ONE,
9824 "month".to_string(),
9825 ))
9826 }
9827
9828 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
9829 [
9830 CommandArg::Label(name.to_string()),
9831 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
9832 ]
9833 }
9834
9835 #[test]
9836 fn default_calendar_on_text_reports_hint() {
9837 let mut specs = TypeSpecification::text();
9838 let mut default = None;
9839 let err = specs
9840 .apply_constraint(
9841 ConstraintNames {
9842 type_name: "notes",
9843 data_name: "notes",
9844 },
9845 TypeConstraintCommand::Suggest,
9846 &[month_suggestion_arg()],
9847 &mut default,
9848 &mut None,
9849 None,
9850 )
9851 .unwrap_err();
9852 assert!(err.contains("Unit 'month' is for calendar data"));
9853 assert!(err.contains("double quotes"));
9854 }
9855
9856 #[test]
9857 fn default_calendar_on_duration_reports_valid_units() {
9858 let mut specs = TypeSpecification::measure();
9859 specs
9860 .apply_constraint(
9861 ConstraintNames {
9862 type_name: "duration",
9863 data_name: "duration",
9864 },
9865 TypeConstraintCommand::Unit,
9866 &unit_factor_arg("second", 1),
9867 &mut None,
9868 &mut None,
9869 None,
9870 )
9871 .unwrap();
9872 specs
9873 .apply_constraint(
9874 ConstraintNames {
9875 type_name: "duration",
9876 data_name: "duration",
9877 },
9878 TypeConstraintCommand::Unit,
9879 &unit_factor_arg("week", 604_800),
9880 &mut None,
9881 &mut None,
9882 None,
9883 )
9884 .unwrap();
9885 specs
9886 .apply_constraint(
9887 ConstraintNames {
9888 type_name: "duration",
9889 data_name: "duration",
9890 },
9891 TypeConstraintCommand::Trait,
9892 &[CommandArg::Label("duration".to_string())],
9893 &mut None,
9894 &mut None,
9895 None,
9896 )
9897 .unwrap();
9898 let mut default = None;
9899 let err = specs
9900 .apply_constraint(
9901 ConstraintNames {
9902 type_name: "duration",
9903 data_name: "duration",
9904 },
9905 TypeConstraintCommand::Suggest,
9906 &[month_suggestion_arg()],
9907 &mut default,
9908 &mut None,
9909 None,
9910 )
9911 .unwrap_err();
9912 assert!(err.contains("Unit 'month' is for calendar data"));
9913 assert!(err.contains("Valid 'duration' units are"));
9914 assert!(err.contains("week"));
9915 }
9916
9917 #[test]
9918 fn default_valid_duration_weeks_accepted() {
9919 let mut specs = TypeSpecification::measure();
9920 specs
9921 .apply_constraint(
9922 ConstraintNames {
9923 type_name: "duration",
9924 data_name: "duration",
9925 },
9926 TypeConstraintCommand::Unit,
9927 &unit_factor_arg("second", 1),
9928 &mut None,
9929 &mut None,
9930 None,
9931 )
9932 .unwrap();
9933 specs
9934 .apply_constraint(
9935 ConstraintNames {
9936 type_name: "duration",
9937 data_name: "duration",
9938 },
9939 TypeConstraintCommand::Unit,
9940 &unit_factor_arg("week", 604_800),
9941 &mut None,
9942 &mut None,
9943 None,
9944 )
9945 .unwrap();
9946 specs
9947 .apply_constraint(
9948 ConstraintNames {
9949 type_name: "duration",
9950 data_name: "duration",
9951 },
9952 TypeConstraintCommand::Trait,
9953 &[CommandArg::Label("duration".to_string())],
9954 &mut None,
9955 &mut None,
9956 None,
9957 )
9958 .unwrap();
9959 let mut default = None;
9960 specs
9961 .apply_constraint(
9962 ConstraintNames {
9963 type_name: "duration",
9964 data_name: "duration",
9965 },
9966 TypeConstraintCommand::Suggest,
9967 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9968 Decimal::from(4),
9969 "week".to_string(),
9970 ))],
9971 &mut default,
9972 &mut None,
9973 None,
9974 )
9975 .unwrap();
9976 assert!(matches!(
9977 default,
9978 Some(RawSuggestion::Measure {
9979 unit_name,
9980 ..
9981 }) if unit_name == "week"
9982 ));
9983 }
9984
9985 #[test]
9986 fn default_unknown_unit_on_duration_lists_valid_units() {
9987 let mut specs = TypeSpecification::measure();
9988 specs
9989 .apply_constraint(
9990 ConstraintNames {
9991 type_name: "duration",
9992 data_name: "duration",
9993 },
9994 TypeConstraintCommand::Unit,
9995 &unit_factor_arg("second", 1),
9996 &mut None,
9997 &mut None,
9998 None,
9999 )
10000 .unwrap();
10001 specs
10002 .apply_constraint(
10003 ConstraintNames {
10004 type_name: "duration",
10005 data_name: "duration",
10006 },
10007 TypeConstraintCommand::Trait,
10008 &[CommandArg::Label("duration".to_string())],
10009 &mut None,
10010 &mut None,
10011 None,
10012 )
10013 .unwrap();
10014 let mut default = None;
10015 let err = specs
10016 .apply_constraint(
10017 ConstraintNames {
10018 type_name: "duration",
10019 data_name: "duration",
10020 },
10021 TypeConstraintCommand::Suggest,
10022 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
10023 Decimal::ONE,
10024 "fortnight".to_string(),
10025 ))],
10026 &mut default,
10027 &mut None,
10028 None,
10029 )
10030 .unwrap_err();
10031 assert!(err.contains("fortnight"));
10032 assert!(err.contains("not defined on 'duration'"));
10033 assert!(err.contains("Valid units are"));
10034 }
10035
10036 fn money_measure_type() -> LemmaType {
10037 LemmaType::new(
10038 "Money".to_string(),
10039 TypeSpecification::Measure {
10040 minimum: None,
10041 maximum: None,
10042 units: MeasureUnits::from(vec![
10043 MeasureUnit {
10044 name: "eur".to_string(),
10045 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10046 derived_measure_factors: Vec::new(),
10047 decomposition: BaseMeasureVector::new(),
10048 minimum: None,
10049 maximum: None,
10050 suggestion_magnitude: None,
10051 },
10052 MeasureUnit {
10053 name: "usd".to_string(),
10054 scale: UnitScale::Literal(
10055 crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
10056 .expect("factor"),
10057 ),
10058 derived_measure_factors: Vec::new(),
10059 decomposition: BaseMeasureVector::new(),
10060 minimum: None,
10061 maximum: None,
10062 suggestion_magnitude: None,
10063 },
10064 ]),
10065 traits: Vec::new(),
10066 decomposition: None,
10067 unit: None,
10068 help: String::new(),
10069 },
10070 TypeExtends::Primitive,
10071 )
10072 }
10073
10074 #[test]
10075 fn measure_unit_names_for_named_measure() {
10076 let money = money_measure_type();
10077 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
10078 }
10079
10080 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
10085 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
10086 }
10087
10088 #[test]
10089 fn combine_signatures_multiply_adds_exponents() {
10090 let left = sig(&[("eur", 1)]);
10091 let right = sig(&[("hour", -1)]);
10092 let result = combine_signatures(&left, &right, true);
10093 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10094 }
10095
10096 #[test]
10097 fn combine_signatures_divide_subtracts_exponents() {
10098 let left = sig(&[("eur", 1)]);
10099 let right = sig(&[("hour", 1)]);
10100 let result = combine_signatures(&left, &right, false);
10101 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10102 }
10103
10104 #[test]
10105 fn combine_signatures_cancels_to_empty() {
10106 let left = sig(&[("ce", 1), ("minute", -1)]);
10107 let right = sig(&[("minute", 1)]);
10108 let result = combine_signatures(&left, &right, true);
10109 assert_eq!(result, sig(&[("ce", 1)]));
10111 }
10112
10113 #[test]
10114 fn combine_signatures_output_is_canonical_form() {
10115 let left = sig(&[("eur", 1), ("hour", 1)]);
10116 let right = sig(&[("minute", 1)]);
10117 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
10120 assert_eq!(result, expected);
10121 }
10122
10123 #[test]
10124 fn canonicalize_signature_drops_zero_exponents() {
10125 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
10126 let result = canonicalize_signature(&sig_with_zero);
10127 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10128 }
10129
10130 #[test]
10131 fn canonicalize_signature_sorts_by_name() {
10132 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
10133 let result = canonicalize_signature(&unsorted);
10134 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10135 }
10136
10137 #[test]
10143 fn format_signature_operator_style_numerator_only() {
10144 let signature = sig(&[("eur", 1)]);
10145 let result = format_signature_operator_style(&signature);
10146 assert_eq!(result, "eur");
10147 }
10148
10149 #[test]
10150 fn format_signature_operator_style_with_denominator() {
10151 let signature = sig(&[("eur", 1), ("hour", -1)]);
10152 let result = format_signature_operator_style(&signature);
10153 assert_eq!(result, "eur/hour");
10154 }
10155
10156 #[test]
10157 fn format_signature_operator_style_denominator_only() {
10158 let signature = sig(&[("meter", -1)]);
10159 let result = format_signature_operator_style(&signature);
10160 assert_eq!(result, "1/meter");
10161 }
10162
10163 #[test]
10164 fn format_signature_operator_style_with_exponents() {
10165 let signature = sig(&[("meter", 2), ("second", -2)]);
10166 let result = format_signature_operator_style(&signature);
10167 assert_eq!(result, "meter^2/second^2");
10168 }
10169
10170 #[test]
10175 fn calendar_unit_factor_table_completeness() {
10176 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
10179 let name = unit.to_string();
10180 assert!(
10181 calendar_unit_factor(&name).is_some(),
10182 "calendar_unit_factor('{}') must return Some",
10183 name
10184 );
10185 }
10186 }
10187
10188 #[test]
10189 fn semantic_calendar_unit_display_returns_singular() {
10190 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
10193 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
10194 }
10195
10196 #[test]
10201 fn signature_factor_with_calendar_units() {
10202 let calendar = test_calendar_type_for_signature_factor();
10203 let unit_index = crate::planning::unit_index::UnitIndex::new();
10204 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
10207 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
10208 .expect("must not overflow");
10209 let expected = rational_new(1, 12);
10210 assert_eq!(factor, expected, "month/year factor must be 1/12");
10211 }
10212
10213 fn test_calendar_type_for_signature_factor() -> LemmaType {
10214 use crate::computation::rational::{decimal_to_rational, rational_one};
10215 use crate::literals::{MeasureUnit, MeasureUnits};
10216 use rust_decimal::Decimal;
10217 LemmaType::new(
10218 "calendar".to_string(),
10219 TypeSpecification::Measure {
10220 minimum: None,
10221 maximum: None,
10222 units: MeasureUnits::from(vec![
10223 MeasureUnit {
10224 name: "month".to_string(),
10225 scale: UnitScale::Literal(rational_one()),
10226 minimum: None,
10227 maximum: None,
10228 suggestion_magnitude: None,
10229 decomposition: calendar_decomposition(),
10230 derived_measure_factors: Vec::new(),
10231 },
10232 MeasureUnit {
10233 name: "year".to_string(),
10234 scale: UnitScale::Literal(
10235 decimal_to_rational(Decimal::from(12)).expect("year factor"),
10236 ),
10237 minimum: None,
10238 maximum: None,
10239 suggestion_magnitude: None,
10240 decomposition: calendar_decomposition(),
10241 derived_measure_factors: Vec::new(),
10242 },
10243 ]),
10244 traits: vec![MeasureTrait::Calendar],
10245 decomposition: Some(calendar_decomposition()),
10246 unit: None,
10247 help: String::new(),
10248 },
10249 TypeExtends::Primitive,
10250 )
10251 }
10252
10253 #[test]
10254 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
10255 fn signature_factor_panics_on_unresolved_name() {
10256 let unit_index = crate::planning::unit_index::UnitIndex::new();
10257 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
10258 signature_factor(&bad_sig, &unit_index, None)
10259 .expect("BUG: signature_factor must panic before returning a failure");
10260 }
10261
10262 #[test]
10263 fn signature_factor_uses_owner_when_expression_index_empty() {
10264 let money = test_money_type_for_signature_factor();
10265 let expression_units = crate::planning::unit_index::UnitIndex::new();
10266 let sig_usd = sig(&[("usd", 1)]);
10267 let factor =
10268 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
10269 assert_eq!(factor, rational_new(91, 100));
10270 }
10271
10272 fn test_money_type_for_signature_factor() -> LemmaType {
10273 use crate::computation::rational::decimal_to_rational;
10274 use crate::literals::{MeasureUnit, MeasureUnits};
10275 use rust_decimal::Decimal;
10276 LemmaType::new(
10277 "money".to_string(),
10278 TypeSpecification::Measure {
10279 minimum: None,
10280 maximum: None,
10281 units: MeasureUnits::from(vec![
10282 MeasureUnit {
10283 name: "eur".to_string(),
10284 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10285 minimum: None,
10286 maximum: None,
10287 suggestion_magnitude: None,
10288 decomposition: BaseMeasureVector::new(),
10289 derived_measure_factors: Vec::new(),
10290 },
10291 MeasureUnit {
10292 name: "usd".to_string(),
10293 scale: UnitScale::Literal(
10294 decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
10295 ),
10296 minimum: None,
10297 maximum: None,
10298 suggestion_magnitude: None,
10299 decomposition: BaseMeasureVector::new(),
10300 derived_measure_factors: Vec::new(),
10301 },
10302 ]),
10303 traits: Vec::new(),
10304 decomposition: None,
10305 unit: None,
10306 help: String::new(),
10307 },
10308 TypeExtends::Primitive,
10309 )
10310 }
10311
10312 fn measure_type_with_kilogram() -> TypeSpecification {
10313 use crate::computation::rational::rational_one;
10314 use crate::literals::{MeasureUnit, MeasureUnits};
10315 let mut units = MeasureUnits::new();
10316 units.push(MeasureUnit {
10317 name: "kilogram".to_string(),
10318 scale: UnitScale::Literal(rational_one()),
10319 minimum: None,
10320 maximum: None,
10321 suggestion_magnitude: None,
10322 decomposition: BaseMeasureVector::new(),
10323 derived_measure_factors: Vec::new(),
10324 });
10325 TypeSpecification::Measure {
10326 minimum: None,
10327 maximum: None,
10328 units,
10329 traits: Vec::new(),
10330 decomposition: None,
10331 unit: None,
10332 help: String::new(),
10333 }
10334 }
10335
10336 #[test]
10337 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
10338 let ten = Value::Number(Decimal::from(10));
10339 let measure = LemmaType::primitive(measure_type_with_kilogram());
10340 let err = parser_value_to_value_kind(&ten, &measure, None)
10341 .expect_err("bare number must not bind to measure");
10342 assert!(
10343 err.contains("kilogram"),
10344 "error must hint expected unit, got: {err}"
10345 );
10346 }
10347
10348 #[test]
10349 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
10350 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
10351 let measure = LemmaType::primitive(measure_type_with_kilogram());
10352 let kind = parser_value_to_value_kind(&ten_kg, &measure, None)
10353 .expect("10 kilogram must bind to measure");
10354 assert!(matches!(kind, ValueKind::Measure(_)));
10355 }
10356
10357 #[test]
10358 fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
10359 let ten = Value::Number(Decimal::from(10));
10360 let ratio = LemmaType::primitive(TypeSpecification::ratio());
10361 let err = parser_value_to_value_kind(&ten, &ratio, None)
10362 .expect_err("bare number must not bind to ratio");
10363 assert!(
10364 err.contains("percent"),
10365 "error must hint expected unit, got: {err}"
10366 );
10367 }
10368
10369 #[test]
10370 fn value_kind_matches_spec_rejects_number_for_measure() {
10371 let n = ValueKind::Number(rational_new(10, 1));
10372 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
10373 }
10374
10375 #[test]
10376 fn value_kind_matches_spec_rejects_number_for_ratio() {
10377 let n = ValueKind::Number(rational_new(1, 2));
10378 assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
10379 }
10380
10381 #[test]
10382 fn apply_constraint_rejects_inherited_unit_factor_change() {
10383 let mut specs = TypeSpecification::measure();
10384 specs
10385 .apply_constraint(
10386 ConstraintNames {
10387 type_name: "money",
10388 data_name: "money",
10389 },
10390 TypeConstraintCommand::Unit,
10391 &unit_factor_arg("eur", 1),
10392 &mut None,
10393 &mut None,
10394 None,
10395 )
10396 .expect("seed eur");
10397 let err = specs
10398 .apply_constraint(
10399 ConstraintNames {
10400 type_name: "money",
10401 data_name: "money",
10402 },
10403 TypeConstraintCommand::Unit,
10404 &[
10405 CommandArg::Label("eur".to_string()),
10406 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
10407 ],
10408 &mut None,
10409 &mut None,
10410 None,
10411 )
10412 .expect_err("must not change inherited unit factor");
10413 assert!(err.contains("eur"), "error must name unit, got: {err}");
10414 assert!(
10415 err.contains("inherited") || err.contains("cannot change"),
10416 "error must reject factor change, got: {err}"
10417 );
10418 }
10419
10420 #[test]
10421 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
10422 let mut specs = TypeSpecification::measure();
10423 specs
10424 .apply_constraint(
10425 ConstraintNames {
10426 type_name: "money",
10427 data_name: "money",
10428 },
10429 TypeConstraintCommand::Unit,
10430 &unit_factor_arg("eur", 1),
10431 &mut None,
10432 &mut None,
10433 None,
10434 )
10435 .expect("seed eur");
10436 specs
10437 .apply_constraint(
10438 ConstraintNames {
10439 type_name: "money",
10440 data_name: "money",
10441 },
10442 TypeConstraintCommand::Unit,
10443 &unit_factor_arg("usd", 1),
10444 &mut None,
10445 &mut None,
10446 None,
10447 )
10448 .expect("add usd");
10449 match &specs {
10450 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
10451 other => panic!("expected Measure, got {other:?}"),
10452 }
10453 }
10454
10455 #[test]
10456 fn apply_constraint_idempotent_inherited_unit_redeclare() {
10457 let mut specs = TypeSpecification::measure();
10458 specs
10459 .apply_constraint(
10460 ConstraintNames {
10461 type_name: "money",
10462 data_name: "money",
10463 },
10464 TypeConstraintCommand::Unit,
10465 &unit_factor_arg("eur", 1),
10466 &mut None,
10467 &mut None,
10468 None,
10469 )
10470 .expect("seed eur");
10471 specs
10472 .apply_constraint(
10473 ConstraintNames {
10474 type_name: "money",
10475 data_name: "money",
10476 },
10477 TypeConstraintCommand::Unit,
10478 &unit_factor_arg("eur", 1),
10479 &mut None,
10480 &mut None,
10481 None,
10482 )
10483 .expect("idempotent eur");
10484 match &specs {
10485 TypeSpecification::Measure { units, .. } => {
10486 assert_eq!(units.len(), 1);
10487 assert_eq!(
10488 *units
10489 .iter()
10490 .find(|u| u.name == "eur")
10491 .expect("eur")
10492 .require_literal_factor(),
10493 crate::computation::rational::rational_one()
10494 );
10495 }
10496 other => panic!("expected Measure, got {other:?}"),
10497 }
10498 }
10499
10500 #[test]
10501 fn element_from_range_returns_element_for_every_range_primitive() {
10502 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
10503 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
10504 (PrimitiveKind::NumberRange, |element| {
10505 matches!(element, TypeSpecification::Number { .. })
10506 }),
10507 (PrimitiveKind::MeasureRange, |element| {
10508 matches!(element, TypeSpecification::Measure { .. })
10509 }),
10510 (PrimitiveKind::RatioRange, |element| {
10511 matches!(element, TypeSpecification::Ratio { .. })
10512 }),
10513 (PrimitiveKind::DateRange, |element| {
10514 matches!(element, TypeSpecification::Date { .. })
10515 }),
10516 (PrimitiveKind::TimeRange, |element| {
10517 matches!(element, TypeSpecification::Time { .. })
10518 }),
10519 ];
10520 for (kind, matches_element) in cases {
10521 let range_spec = type_spec_for_primitive(kind);
10522 let element = range_spec
10523 .element_from_range()
10524 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
10525 assert!(
10526 matches_element(&element),
10527 "{kind:?} element must match documented mapping, got {element:?}"
10528 );
10529 }
10530 }
10531
10532 #[test]
10533 fn element_from_range_returns_none_for_non_range_primitives() {
10534 let non_range = [
10535 type_spec_for_primitive(PrimitiveKind::Boolean),
10536 type_spec_for_primitive(PrimitiveKind::Measure),
10537 TypeSpecification::Undetermined,
10538 TypeSpecification::veto(),
10539 ];
10540 for spec in non_range {
10541 assert!(
10542 spec.element_from_range().is_none(),
10543 "{spec:?} must not define element_from_range"
10544 );
10545 }
10546 }
10547
10548 fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
10549 LemmaType::primitive(TypeSpecification::Spec {
10550 spec: SpecIdentity {
10551 repository: None,
10552 spec: name.to_string(),
10553 effective: effective.clone(),
10554 slice: effective,
10555 },
10556 })
10557 }
10558
10559 #[test]
10560 fn same_value_type_spec_identity() {
10561 use crate::parsing::ast::EffectiveDate;
10562 let origin = EffectiveDate::Origin;
10563 let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
10564 year: 2026,
10565 month: 1,
10566 day: 1,
10567 hour: 0,
10568 minute: 0,
10569 second: 0,
10570 microsecond: 0,
10571 timezone: None,
10572 granularity: crate::literals::DateGranularity::Full,
10573 });
10574 let tax = spec_type("tax_bracket", origin.clone());
10575 let tax_again = spec_type("tax_bracket", origin.clone());
10576 let other = spec_type("menu_item", origin);
10577 let tax_dated = spec_type("tax_bracket", dated.clone());
10578 assert!(tax.same_value_type(&tax_again));
10579 assert!(!tax.same_value_type(&other));
10580 assert!(!tax.same_value_type(&tax_dated));
10581 let mut tax_later_slice = tax.clone();
10582 match &mut tax_later_slice.specifications {
10583 TypeSpecification::Spec { spec } => spec.slice = dated,
10584 other => panic!("expected Spec, got {other:?}"),
10585 }
10586 assert!(!tax.same_value_type(&tax_later_slice));
10587 let mut other_repo = tax.clone();
10588 match &mut other_repo.specifications {
10589 TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
10590 other => panic!("expected Spec, got {other:?}"),
10591 }
10592 assert!(!tax.same_value_type(&other_repo));
10593 }
10594
10595 #[test]
10596 fn spec_identity_displays_repository_space_spec_space_effective() {
10597 use crate::parsing::ast::EffectiveDate;
10598 let workspace = SpecIdentity {
10599 repository: None,
10600 spec: "tax_bracket".to_string(),
10601 effective: EffectiveDate::Origin,
10602 slice: EffectiveDate::Origin,
10603 };
10604 assert_eq!(workspace.to_string(), "tax_bracket");
10605 let start =
10606 EffectiveDate::DateTimeValue(DateTimeValue::from_str("2026-01-01").expect("date"));
10607 let dated = SpecIdentity {
10608 repository: Some("@jack/finance".to_string()),
10609 spec: "tax_bracket".to_string(),
10610 effective: start.clone(),
10611 slice: start,
10612 };
10613 assert_eq!(
10614 dated.to_string(),
10615 format!(
10616 "@jack/finance tax_bracket {}",
10617 DateTimeValue::from_str("2026-01-01").expect("date")
10618 )
10619 );
10620 let later_slice = SpecIdentity {
10621 slice: EffectiveDate::DateTimeValue(
10622 DateTimeValue::from_str("2026-07-01").expect("date"),
10623 ),
10624 ..workspace
10625 };
10626 assert_eq!(
10627 later_slice.to_string(),
10628 format!(
10629 "tax_bracket slice {}",
10630 DateTimeValue::from_str("2026-07-01").expect("date")
10631 )
10632 );
10633 }
10634
10635 #[test]
10636 fn type_family_displays_declaring_spec_then_type() {
10637 let workspace = TypeFamily {
10638 repository: None,
10639 spec: "pricing".to_string(),
10640 effective: crate::parsing::ast::EffectiveDate::Origin,
10641 name: "money".to_string(),
10642 };
10643 assert_eq!(workspace.to_string(), "pricing.money");
10644 let stdlib = TypeFamily {
10645 repository: Some("lemma".to_string()),
10646 spec: "units".to_string(),
10647 effective: crate::parsing::ast::EffectiveDate::Origin,
10648 name: "mass".to_string(),
10649 };
10650 assert_eq!(stdlib.to_string(), "lemma units.mass");
10651 let dated = TypeFamily {
10652 repository: None,
10653 spec: "pricing".to_string(),
10654 effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
10655 DateTimeValue::from_str("2024-01-01").expect("date"),
10656 ),
10657 name: "money".to_string(),
10658 };
10659 assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
10660 }
10661
10662 #[test]
10663 fn piecewise_type_keeps_spec_identity() {
10664 use crate::parsing::ast::EffectiveDate;
10665 use crate::planning::typing::{piecewise_type, MeasureScope};
10666 use crate::planning::unit_index::UnitIndex;
10667 let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
10668 let unit_index = UnitIndex::new();
10669 let signature_index = crate::computation::arithmetic::SignatureIndex::new();
10670 let named_types = IndexMap::new();
10671 let scope = MeasureScope {
10672 unit_index: &unit_index,
10673 signature_index: &signature_index,
10674 named_types: &named_types,
10675 };
10676 let picked = piecewise_type([Arc::new(LemmaType::veto_type()), Arc::clone(&tax)], &scope);
10677 assert!(picked.same_value_type(&tax));
10678 }
10679
10680 #[test]
10681 #[should_panic(expected = "BUG: literal factor required")]
10682 fn measure_unit_factor_panics_on_reference_scale() {
10683 let units = crate::literals::MeasureUnits::from(vec![
10684 crate::literals::MeasureUnit::from_reference(
10685 "gbp".to_string(),
10686 "rate".to_string(),
10687 Vec::new(),
10688 "rate".to_string(),
10689 ),
10690 ]);
10691 let lemma_type = LemmaType::new(
10692 "money".to_string(),
10693 TypeSpecification::Measure {
10694 units,
10695 traits: vec![],
10696 decomposition: None,
10697 minimum: None,
10698 maximum: None,
10699 unit: None,
10700 help: String::new(),
10701 },
10702 TypeExtends::Primitive,
10703 );
10704 let _ = lemma_type.measure_unit_factor("gbp");
10705 }
10706}