1use crate::parsing::arena::{ExprId, ExprKind, ExprTree};
9pub use crate::parsing::ast::{
10 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
11 RangeBound, 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::parsing::ast::Constraint;
46use crate::parsing::ast::{
47 BooleanValue, CalendarPeriodUnit, ChainStep, CommandArg, ConversionTarget, DateCalendarKind,
48 DateRelativeKind, DateTimeValue, PrimitiveKind, TimeValue, TimezoneValue,
49 TypeConstraintCommand,
50};
51use crate::planning::unit_index::UnitIndex;
52use crate::Error;
53use indexmap::IndexMap;
54use rust_decimal::Decimal;
55use serde::{Deserialize, Deserializer, Serialize, Serializer};
56use std::collections::BTreeMap;
57use std::fmt;
58use std::hash::Hash;
59use std::str::FromStr;
60use std::sync::{Arc, OnceLock};
61
62pub use crate::literals::{BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits};
69
70pub fn combine_decompositions(
74 left: &BaseMeasureVector,
75 right: &BaseMeasureVector,
76 is_multiply: bool,
77) -> Option<BaseMeasureVector> {
78 let mut result = left.clone();
79 for (dim, &exp) in right {
80 let delta = if is_multiply { exp } else { exp.checked_neg()? };
81 let entry = result.entry(dim.clone()).or_insert(0);
82 *entry = entry.checked_add(delta)?;
83 if entry.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
84 return None;
85 }
86 if *entry == 0 {
87 result.remove(dim);
88 }
89 }
90 Some(result)
91}
92
93#[cfg(test)]
100pub fn combine_signatures(
101 left: &[(String, i32)],
102 right: &[(String, i32)],
103 is_multiply: bool,
104) -> Vec<(String, i32)> {
105 use std::collections::BTreeMap;
106 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
107 for (name, exponent) in left {
108 *accumulator.entry(name.clone()).or_insert(0) += exponent;
109 }
110 for (name, exponent) in right {
111 let delta = if is_multiply { *exponent } else { -*exponent };
112 *accumulator.entry(name.clone()).or_insert(0) += delta;
113 }
114 accumulator
115 .into_iter()
116 .filter(|(_, exponent)| *exponent != 0)
117 .collect()
118}
119
120pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
133 let canonical = canonicalize_signature(signature);
134 let mut numerator: Vec<(String, i32)> = Vec::new();
135 let mut denominator: Vec<(String, i32)> = Vec::new();
136 for (name, exponent) in canonical {
137 if exponent > 0 {
138 numerator.push((name, exponent));
139 } else if exponent < 0 {
140 denominator.push((name, -exponent));
141 }
142 }
143 let render = |terms: &[(String, i32)]| -> String {
144 terms
145 .iter()
146 .map(|(name, exp)| {
147 if *exp == 1 {
148 name.clone()
149 } else {
150 format!("{name}^{exp}")
151 }
152 })
153 .collect::<Vec<_>>()
154 .join("*")
155 };
156 match (numerator.is_empty(), denominator.is_empty()) {
157 (true, true) => String::new(),
158 (false, true) => render(&numerator),
159 (true, false) => format!("1/{}", render(&denominator)),
160 (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
161 }
162}
163
164pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
171 use crate::computation::rational::rational_one;
172 match name {
173 "month" => Some(rational_one()),
174 "year" => Some(rational_new(12, 1)),
175 _ => None,
176 }
177}
178
179fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
180 use crate::computation::rational::rational_zero;
181 use std::cmp::Ordering;
182 match magnitude.try_cmp(&rational_zero()) {
183 Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
184 Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
185 Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
186 }
187}
188
189fn parse_unresolved_width_bound(
192 args: &[CommandArg],
193 cmd: &str,
194) -> Result<(RationalInteger, String), String> {
195 use crate::computation::rational::decimal_to_rational;
196 let lit = require_literal(args, cmd)?;
197 let (magnitude, unit_name) = match lit {
198 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
199 other => {
200 return Err(format!(
201 "{cmd} requires a measure literal with a unit, got {}",
202 value_kind_name(other)
203 ));
204 }
205 };
206 let magnitude_rational = decimal_to_rational(magnitude)
207 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
208 reject_negative_width_magnitude(&magnitude_rational, cmd)?;
209 Ok((magnitude_rational, unit_name))
210}
211
212pub(crate) fn check_range_bound_consistency(
217 spec: &TypeSpecification,
218 unit_index: &crate::planning::unit_index::UnitIndex,
219) -> Result<(), String> {
220 use std::cmp::Ordering;
221
222 fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
223 compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
224 != Ordering::Greater
225 }
226 fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
227 compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
228 }
229
230 match spec {
231 TypeSpecification::NumberRange {
232 lower,
233 upper,
234 minimum,
235 maximum,
236 ..
237 }
238 | TypeSpecification::RatioRange {
239 lower,
240 upper,
241 minimum,
242 maximum,
243 ..
244 } => {
245 if let (Some(lo), Some(hi)) = (lower, upper) {
246 if lo
247 .try_cmp(hi)
248 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
249 == Ordering::Greater
250 {
251 return Err(format!(
252 "invalid range: lower {} is greater than upper {}",
253 lo.display_str(),
254 hi.display_str()
255 ));
256 }
257 }
258 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
259 if min_w
260 .try_cmp(max_w)
261 .map_err(|failure| format!("range width compare failed: {failure}"))?
262 == Ordering::Greater
263 {
264 return Err(format!(
265 "invalid range: minimum width {} is greater than maximum width {}",
266 min_w.display_str(),
267 max_w.display_str()
268 ));
269 }
270 }
271 Ok(())
272 }
273 TypeSpecification::MeasureRange {
274 lower,
275 upper,
276 minimum,
277 maximum,
278 units,
279 ..
280 } => {
281 if let (Some(lo), Some(hi)) = (lower, upper) {
282 let lo_c =
283 measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
284 let hi_c =
285 measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
286 if lo_c
287 .try_cmp(&hi_c)
288 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
289 == Ordering::Greater
290 {
291 return Err(format!(
292 "invalid range: lower {} {} is greater than upper {} {}",
293 lo.0.display_str(),
294 lo.1,
295 hi.0.display_str(),
296 hi.1
297 ));
298 }
299 }
300 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
301 let min_c = measure_declared_bound_to_canonical(
302 &min_w.0, &min_w.1, units, "range", "minimum",
303 )?;
304 let max_c = measure_declared_bound_to_canonical(
305 &max_w.0, &max_w.1, units, "range", "maximum",
306 )?;
307 if min_c
308 .try_cmp(&max_c)
309 .map_err(|failure| format!("range width compare failed: {failure}"))?
310 == Ordering::Greater
311 {
312 return Err(format!(
313 "invalid range: minimum width {} {} is greater than maximum width {} {}",
314 min_w.0.display_str(),
315 min_w.1,
316 max_w.0.display_str(),
317 max_w.1
318 ));
319 }
320 }
321 Ok(())
322 }
323 TypeSpecification::DateRange {
324 lower,
325 upper,
326 minimum,
327 maximum,
328 ..
329 } => {
330 if let (Some(lo), Some(hi)) = (lower, upper) {
331 if !endpoint_order_ok_dates(lo, hi) {
332 return Err(format!(
333 "invalid range: lower {lo} is greater than upper {hi}"
334 ));
335 }
336 }
337 check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
338 }
339 TypeSpecification::TimeRange {
340 lower,
341 upper,
342 minimum,
343 maximum,
344 ..
345 } => {
346 if let (Some(lo), Some(hi)) = (lower, upper) {
347 if !endpoint_order_ok_times(lo, hi) {
348 return Err(format!(
349 "invalid range: lower {lo} is greater than upper {hi}"
350 ));
351 }
352 }
353 check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
354 }
355 _ => Ok(()),
356 }
357}
358
359fn check_temporal_width_pair_consistency(
360 minimum: &Option<(RationalInteger, String)>,
361 maximum: &Option<(RationalInteger, String)>,
362 unit_index: &crate::planning::unit_index::UnitIndex,
363 allow_calendar: bool,
364) -> Result<(), String> {
365 use std::cmp::Ordering;
366 let resolve = |bound: &(RationalInteger, String),
367 command: &str|
368 -> Result<(RationalInteger, Arc<LemmaType>), String> {
369 let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
370 format!(
371 "{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
372 bound.1
373 )
374 })?;
375 if allow_calendar {
376 if !owner.is_duration_like() && !owner.is_calendar_like() {
377 return Err(format!(
378 "{command} width unit '{bare}' must be a duration or calendar unit",
379 ));
380 }
381 } else if !owner.is_duration_like() {
382 return Err(format!(
383 "{command} width unit '{bare}' must be a duration unit",
384 ));
385 }
386 let TypeSpecification::Measure { units, .. } = &owner.specifications else {
387 return Err(format!(
388 "{command} width unit '{bare}' must resolve to a measure type",
389 ));
390 };
391 let canonical = measure_declared_bound_to_canonical(
392 &bound.0,
393 &bare,
394 units,
395 owner.name().as_str(),
396 command,
397 )?;
398 Ok((canonical, Arc::clone(&owner)))
399 };
400
401 match (minimum, maximum) {
402 (None, None) => Ok(()),
403 (Some(min_w), None) => {
404 resolve(min_w, "minimum")?;
405 Ok(())
406 }
407 (None, Some(max_w)) => {
408 resolve(max_w, "maximum")?;
409 Ok(())
410 }
411 (Some(min_w), Some(max_w)) => {
412 let (min_c, min_owner) = resolve(min_w, "minimum")?;
413 let (max_c, max_owner) = resolve(max_w, "maximum")?;
414 if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
415 return Err(
416 "invalid range: minimum and maximum width must not mix calendar and duration units"
417 .to_string(),
418 );
419 }
420 if min_c
421 .try_cmp(&max_c)
422 .map_err(|failure| format!("range width compare failed: {failure}"))?
423 == Ordering::Greater
424 {
425 return Err(format!(
426 "invalid range: minimum width {} {} is greater than maximum width {} {}",
427 min_w.0.display_str(),
428 min_w.1,
429 max_w.0.display_str(),
430 max_w.1
431 ));
432 }
433 Ok(())
434 }
435 }
436}
437
438fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
439 owner
440 .measure_unit_names()
441 .is_some_and(|names| names.contains(&unit_name))
442}
443
444pub fn signature_factor(
454 signature: &[(String, i32)],
455 expression_units: &crate::planning::unit_index::UnitIndex,
456 owner: Option<&LemmaType>,
457) -> Result<
458 crate::computation::rational::RationalInteger,
459 crate::computation::rational::NumericFailure,
460> {
461 use crate::computation::rational::{checked_div, checked_mul, rational_one};
462 let mut acc = rational_one();
463 for (name, exponent) in signature {
464 let factor =
465 if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
466 owner.measure_unit_factor(name).clone()
467 } else if let Some(lemma_type) = expression_units.unique_owner(name) {
468 lemma_type.measure_unit_factor(name).clone()
469 } else if !expression_units.owners_for(name).is_empty() {
470 panic!(
471 "BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
472 name
473 );
474 } else {
475 panic!(
476 "BUG: signature_factor called with unresolved unit name '{}'",
477 name
478 );
479 };
480 let mut term = rational_one();
481 let abs_exp = exponent.unsigned_abs();
482 for _ in 0..abs_exp {
483 term = checked_mul(&term, &factor)?;
484 }
485 if *exponent >= 0 {
486 acc = checked_mul(&acc, &term)?;
487 } else {
488 acc = checked_div(&acc, &term)?;
489 }
490 }
491 Ok(acc)
492}
493
494pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
495 use std::collections::BTreeMap;
496 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
497 for (name, exponent) in signature {
498 let entry = accumulator.entry(name.clone()).or_insert(0);
499 *entry = entry
500 .checked_add(*exponent)
501 .expect("BUG: planning bounds signature unit exponents");
502 }
503 accumulator
504 .into_iter()
505 .filter(|(_, exponent)| *exponent != 0)
506 .collect()
507}
508
509pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
511 decomposition
512 .iter()
513 .filter(|(_, exponent)| **exponent != 0)
514 .map(|(dimension, exponent)| (dimension.name().to_string(), *exponent))
515 .collect()
516}
517
518#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
520pub enum Dimension {
521 Duration,
522 Calendar,
523 Family(TypeFamily),
524}
525
526impl Dimension {
527 #[must_use]
529 pub fn name(&self) -> &str {
530 match self {
531 Dimension::Duration => "duration",
532 Dimension::Calendar => "calendar",
533 Dimension::Family(family) => &family.name,
534 }
535 }
536
537 #[must_use]
538 fn with_families_without_effective(&self) -> Self {
539 match self {
540 Dimension::Duration => Dimension::Duration,
541 Dimension::Calendar => Dimension::Calendar,
542 Dimension::Family(family) => Dimension::Family(family.without_effective()),
543 }
544 }
545}
546
547fn base_measure_vector_without_effective(domain: &BaseMeasureVector) -> BaseMeasureVector {
548 domain
549 .iter()
550 .map(|(dimension, exponent)| (dimension.with_families_without_effective(), *exponent))
551 .collect()
552}
553
554fn measure_units_without_effective(units: &MeasureUnits) -> MeasureUnits {
555 MeasureUnits(
556 units
557 .iter()
558 .map(|unit| MeasureUnit {
559 name: unit.name.clone(),
560 factor: unit.factor.clone(),
561 derived_measure_factors: unit.derived_measure_factors.clone(),
562 decomposition: base_measure_vector_without_effective(&unit.decomposition),
563 minimum: unit.minimum.clone(),
564 maximum: unit.maximum.clone(),
565 suggestion_magnitude: unit.suggestion_magnitude.clone(),
566 })
567 .collect(),
568 )
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
572#[serde(rename_all = "snake_case")]
573pub enum MeasureTrait {
574 Duration,
575 Calendar,
576}
577
578pub fn duration_decomposition() -> BaseMeasureVector {
579 [(Dimension::Duration, 1i32)].into_iter().collect()
580}
581
582pub fn calendar_decomposition() -> BaseMeasureVector {
583 [(Dimension::Calendar, 1i32)].into_iter().collect()
584}
585
586pub fn anonymous_measure_type() -> LemmaType {
590 LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
591}
592
593pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
596 signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
597}
598
599#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
600pub enum TypeSpecification {
601 Boolean {
602 help: String,
603 },
604 Measure {
605 minimum: Option<(RationalInteger, String)>,
606 maximum: Option<(RationalInteger, String)>,
607 decimals: Option<u8>,
608 units: MeasureUnits,
609 traits: Vec<MeasureTrait>,
610 decomposition: Option<BaseMeasureVector>,
615 unit: Option<String>,
617 help: String,
618 },
619 Number {
620 minimum: Option<RationalInteger>,
621 maximum: Option<RationalInteger>,
622 decimals: Option<u8>,
623 help: String,
624 },
625 NumberRange {
626 lower: Option<RationalInteger>,
627 upper: Option<RationalInteger>,
628 minimum: Option<RationalInteger>,
629 maximum: Option<RationalInteger>,
630 help: String,
631 },
632 Ratio {
633 minimum: Option<RationalInteger>,
634 maximum: Option<RationalInteger>,
635 decimals: Option<u8>,
636 units: RatioUnits,
637 unit: Option<String>,
639 help: String,
640 },
641 RatioRange {
642 lower: Option<RationalInteger>,
643 upper: Option<RationalInteger>,
644 minimum: Option<RationalInteger>,
645 maximum: Option<RationalInteger>,
646 units: RatioUnits,
647 help: String,
648 },
649 Text {
650 length: Option<usize>,
651 options: Vec<String>,
652 help: String,
653 },
654 Date {
655 minimum: Option<DateTimeValue>,
656 maximum: Option<DateTimeValue>,
657 help: String,
658 },
659 DateRange {
660 lower: Option<DateTimeValue>,
661 upper: Option<DateTimeValue>,
662 minimum: Option<(RationalInteger, String)>,
663 maximum: Option<(RationalInteger, String)>,
664 help: String,
665 },
666 Time {
667 minimum: Option<TimeValue>,
668 maximum: Option<TimeValue>,
669 help: String,
670 },
671 TimeRange {
672 lower: Option<TimeValue>,
673 upper: Option<TimeValue>,
674 minimum: Option<(RationalInteger, String)>,
675 maximum: Option<(RationalInteger, String)>,
676 help: String,
677 },
678 MeasureRange {
679 lower: Option<(RationalInteger, String)>,
680 upper: Option<(RationalInteger, String)>,
681 minimum: Option<(RationalInteger, String)>,
682 maximum: Option<(RationalInteger, String)>,
683 units: MeasureUnits,
684 decomposition: Option<BaseMeasureVector>,
685 help: String,
686 },
687 Veto {
688 message: Option<String>,
689 },
690 Undetermined,
694 Spec {
698 spec: SpecIdentity,
699 },
700 List {
702 #[serde(with = "crate::snapshot::shared::arc")]
703 element: Arc<LemmaType>,
704 key: ListKey,
705 },
706}
707
708#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
710pub enum ListKey {
711 Numbered,
712 Fields(Vec<String>),
713}
714
715#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
719pub struct SpecIdentity {
720 pub repository: Option<String>,
721 pub spec: String,
722 pub effective: crate::parsing::ast::EffectiveDate,
723}
724
725impl SpecIdentity {
726 #[must_use]
727 pub fn of(
728 repository: &crate::parsing::ast::LemmaRepository,
729 spec: &crate::parsing::ast::LemmaSpec,
730 ) -> Self {
731 Self {
732 repository: repository.name.clone(),
733 spec: spec.name.clone(),
734 effective: spec.effective_from.clone(),
735 }
736 }
737}
738
739impl fmt::Display for SpecIdentity {
740 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
741 if let Some(repository) = &self.repository {
742 write!(f, "{repository} ")?;
743 }
744 write!(f, "{}", self.spec)?;
745 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
746 write!(f, " {at}")?;
747 }
748 Ok(())
749 }
750}
751
752impl std::fmt::Display for TypeSpecification {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 match self {
755 Self::Boolean { .. } => f.write_str("boolean"),
756 Self::Measure { .. } => f.write_str("measure"),
757 Self::MeasureRange { .. } => f.write_str("measure range"),
758 Self::Number { .. } => f.write_str("number"),
759 Self::NumberRange { .. } => f.write_str("number range"),
760 Self::Text { .. } => f.write_str("text"),
761 Self::Date { .. } => f.write_str("date"),
762 Self::DateRange { .. } => f.write_str("date range"),
763 Self::Time { .. } => f.write_str("time"),
764 Self::TimeRange { .. } => f.write_str("time range"),
765 Self::Ratio { .. } => f.write_str("ratio"),
766 Self::RatioRange { .. } => f.write_str("ratio range"),
767 Self::Veto { .. } => f.write_str("veto"),
768 Self::Undetermined => f.write_str("undetermined"),
769 Self::Spec { spec } => write!(f, "{spec}"),
770 Self::List {
771 element,
772 key: ListKey::Numbered,
773 } => write!(f, "{} list", element.specifications),
774 Self::List {
775 element,
776 key: ListKey::Fields(fields),
777 } => write!(
778 f,
779 "{} list key {}",
780 element.specifications,
781 fields.join(", ")
782 ),
783 }
784 }
785}
786
787impl TypeSpecification {
788 pub fn help(&self) -> &str {
790 match self {
791 Self::Boolean { help, .. }
792 | Self::Measure { help, .. }
793 | Self::Number { help, .. }
794 | Self::NumberRange { help, .. }
795 | Self::Text { help, .. }
796 | Self::Date { help, .. }
797 | Self::DateRange { help, .. }
798 | Self::Time { help, .. }
799 | Self::TimeRange { help, .. }
800 | Self::Ratio { help, .. }
801 | Self::RatioRange { help, .. }
802 | Self::MeasureRange { help, .. } => help.as_str(),
803 Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
804 Self::List { element, .. } => element.specifications.help(),
805 }
806 }
807
808 #[must_use]
809 fn with_families_without_effective(&self) -> Self {
810 match self {
811 Self::Measure {
812 minimum,
813 maximum,
814 decimals,
815 units,
816 traits,
817 decomposition,
818 unit,
819 help,
820 } => Self::Measure {
821 minimum: minimum.clone(),
822 maximum: maximum.clone(),
823 decimals: *decimals,
824 units: measure_units_without_effective(units),
825 traits: traits.clone(),
826 decomposition: decomposition
827 .as_ref()
828 .map(base_measure_vector_without_effective),
829 unit: unit.clone(),
830 help: help.clone(),
831 },
832 Self::MeasureRange {
833 lower,
834 upper,
835 minimum,
836 maximum,
837 units,
838 decomposition,
839 help,
840 } => Self::MeasureRange {
841 lower: lower.clone(),
842 upper: upper.clone(),
843 minimum: minimum.clone(),
844 maximum: maximum.clone(),
845 units: measure_units_without_effective(units),
846 decomposition: decomposition
847 .as_ref()
848 .map(base_measure_vector_without_effective),
849 help: help.clone(),
850 },
851 Self::List { element, key } => Self::List {
852 element: Arc::new(element.with_families_without_effective()),
853 key: key.clone(),
854 },
855 other => other.clone(),
856 }
857 }
858
859 #[must_use]
863 pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
864 match (self, other) {
865 (
866 Self::Measure {
867 minimum: a_min,
868 maximum: a_max,
869 decimals: a_dec,
870 units: a_units,
871 traits: a_traits,
872 decomposition: a_decomp,
873 help: a_help,
874 unit: _,
875 },
876 Self::Measure {
877 minimum: b_min,
878 maximum: b_max,
879 decimals: b_dec,
880 units: b_units,
881 traits: b_traits,
882 decomposition: b_decomp,
883 help: b_help,
884 unit: _,
885 },
886 ) => {
887 a_min == b_min
888 && a_max == b_max
889 && a_dec == b_dec
890 && a_units == b_units
891 && a_traits == b_traits
892 && a_decomp == b_decomp
893 && a_help == b_help
894 }
895 (
896 Self::Ratio {
897 minimum: a_min,
898 maximum: a_max,
899 decimals: a_dec,
900 units: a_units,
901 help: a_help,
902 unit: _,
903 },
904 Self::Ratio {
905 minimum: b_min,
906 maximum: b_max,
907 decimals: b_dec,
908 units: b_units,
909 help: b_help,
910 unit: _,
911 },
912 ) => {
913 a_min == b_min
914 && a_max == b_max
915 && a_dec == b_dec
916 && a_units == b_units
917 && a_help == b_help
918 }
919 _ => self == other,
920 }
921 }
922}
923
924fn require_literal<'a>(
930 args: &'a [CommandArg],
931 cmd: &str,
932) -> Result<&'a crate::literals::Value, String> {
933 if args.len() > 1 {
934 return Err(format!("{cmd} takes exactly one value"));
935 }
936 let arg = args
937 .first()
938 .ok_or_else(|| format!("{} requires an argument", cmd))?;
939 match arg {
940 CommandArg::Literal(v) => Ok(v),
941 CommandArg::Label(name) => Err(format!(
942 "{} requires a literal value, got identifier '{}'",
943 cmd, name
944 )),
945 CommandArg::UnitExpr(_) => Err(format!(
946 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
947 cmd
948 )),
949 }
950}
951
952fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
953 match require_literal(args, "help")? {
954 crate::literals::Value::Text(s) => {
955 *help = s.clone();
956 Ok(())
957 }
958 other => Err(format!(
959 "help requires a text literal (quoted string), got {}",
960 value_kind_name(other)
961 )),
962 }
963}
964
965fn format_measure_units_list(units: &MeasureUnits) -> String {
966 units
967 .iter()
968 .map(|u| u.name.as_str())
969 .collect::<Vec<_>>()
970 .join(", ")
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
975pub(crate) enum SuggestionExpectation {
976 MeasureUnits,
977 Text,
978 Number,
979 Boolean,
980 Date,
981 Time,
982 Ratio,
983 NumberRange,
984 DateRange,
985 TimeRange,
986 MeasureRange,
987 RatioRange,
988}
989
990pub(crate) fn suggestion_value_mismatch_error(
991 calendar_unit: &str,
992 type_name: &str,
993 expectation: SuggestionExpectation,
994 measure_units: Option<&MeasureUnits>,
995) -> String {
996 let unit_label = calendar_unit;
997 let first = format!("Unit '{unit_label}' is for calendar data.");
998 match expectation {
999 SuggestionExpectation::MeasureUnits => {
1000 let list = measure_units
1001 .map(format_measure_units_list)
1002 .unwrap_or_default();
1003 format!("{first} Valid '{type_name}' units are: {list}.")
1004 }
1005 SuggestionExpectation::Text => format!(
1006 "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
1007 ),
1008 SuggestionExpectation::Number => format!(
1009 "{first} Please provide a number, for example `-> suggest 42`."
1010 ),
1011 SuggestionExpectation::Boolean => format!(
1012 "{first} Please provide true or false, for example `-> suggest true`."
1013 ),
1014 SuggestionExpectation::Date => format!(
1015 "{first} Please provide a date, for example `-> suggest 2024-06-15`."
1016 ),
1017 SuggestionExpectation::Time => format!(
1018 "{first} Please provide a time, for example `-> suggest 09:00:00`."
1019 ),
1020 SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
1021 "{first} Please provide a ratio, for example `-> suggest 25%`."
1022 ),
1023 SuggestionExpectation::NumberRange => format!(
1024 "{first} Please provide a number range, for example `-> suggest 10...100`."
1025 ),
1026 SuggestionExpectation::DateRange => format!(
1027 "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1028 ),
1029 SuggestionExpectation::TimeRange => format!(
1030 "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
1031 ),
1032 SuggestionExpectation::MeasureRange => format!(
1033 "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1034 ),
1035 }
1036}
1037
1038fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
1039 let example = if traits.contains(&MeasureTrait::Duration) {
1040 "4 week"
1041 } else if traits.contains(&MeasureTrait::Calendar) {
1042 "3 month"
1043 } else {
1044 "30 kilogram"
1045 };
1046 format!(
1047 "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
1048 )
1049}
1050
1051fn reject_calendar_for_suggestion(
1052 value: &crate::literals::Value,
1053 type_name: &str,
1054 expectation: SuggestionExpectation,
1055 measure_units: Option<&MeasureUnits>,
1056) -> Result<(), String> {
1057 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
1058 if calendar_unit_factor(unit).is_some() {
1059 return Err(suggestion_value_mismatch_error(
1060 unit,
1061 type_name,
1062 expectation,
1063 measure_units,
1064 ));
1065 }
1066 }
1067 Ok(())
1068}
1069
1070fn value_kind_name(v: &crate::literals::Value) -> &'static str {
1072 use crate::literals::Value;
1073 match v {
1074 Value::Number(_) => "number",
1075 Value::NumberWithUnit(_, _) => "number_with_unit",
1076 Value::Text(_) => "text",
1077 Value::Date(_) => "date",
1078 Value::Time(_) => "time",
1079 Value::Boolean(_) => "boolean",
1080 Value::Range(_, _) => "range",
1081 }
1082}
1083
1084fn require_suggestion_range_endpoints<'a>(
1085 args: &'a [CommandArg],
1086 type_name: &str,
1087 expectation: SuggestionExpectation,
1088 measure_units: Option<&MeasureUnits>,
1089) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
1090 match require_literal(args, "suggest")? {
1091 crate::literals::Value::NumberWithUnit(_, unit)
1092 if calendar_unit_factor(unit).is_some() =>
1093 {
1094 Err(suggestion_value_mismatch_error(
1095 unit,
1096 type_name,
1097 expectation,
1098 measure_units,
1099 ))
1100 }
1101 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
1102 _ => Err(match expectation {
1103 SuggestionExpectation::NumberRange => {
1104 "Please provide a number range, for example `-> suggest 10...100`.".to_string()
1105 }
1106 SuggestionExpectation::DateRange => {
1107 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1108 .to_string()
1109 }
1110 SuggestionExpectation::RatioRange => {
1111 "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
1112 }
1113 SuggestionExpectation::MeasureRange => format!(
1114 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1115 ),
1116 _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
1117 }),
1118 }
1119}
1120
1121fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
1122 crate::computation::rational::decimal_to_rational(decimal)
1123 .map_err(|failure| format!("literal failed rational lift: {failure}"))
1124}
1125
1126pub fn range_element_type_specification(
1128 range_spec: &TypeSpecification,
1129) -> Option<TypeSpecification> {
1130 range_spec.element_from_range()
1131}
1132
1133fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
1134 match (&left.specifications, &right.specifications) {
1135 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1136 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1137 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1138 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1139 left.same_measure_family(right)
1140 || left.compatible_with_anonymous_measure(right)
1141 || right.compatible_with_anonymous_measure(left)
1142 }
1143 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1144 _ => false,
1145 }
1146}
1147
1148pub fn range_type_specification_from_endpoints(
1150 left: &LemmaType,
1151 right: &LemmaType,
1152) -> Option<TypeSpecification> {
1153 if !range_endpoints_compatible(left, right) {
1154 return None;
1155 }
1156 left.specifications.range_from_element()
1157}
1158
1159pub enum UnitTokenResolver<'a> {
1162 Declared {
1164 unit_index: &'a UnitIndex,
1165 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1166 },
1167 Undeclared,
1169 Planning {
1171 unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
1172 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1173 },
1174 Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
1176 Typed {
1179 unit_index: &'a UnitIndex,
1180 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1181 lemma_type: &'a LemmaType,
1182 },
1183}
1184
1185impl UnitTokenResolver<'_> {
1186 pub fn resolve(&self, unit: &str) -> Result<String, String> {
1187 match self {
1188 UnitTokenResolver::Declared {
1189 unit_index,
1190 named_types,
1191 } => unit_index
1192 .resolve_with_named_types(unit, named_types)
1193 .map(|(bare, _)| bare),
1194 UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
1195 UnitTokenResolver::Planning {
1196 unit_index,
1197 named_types,
1198 } => unit_index
1199 .resolve_with_named_types(unit, named_types)
1200 .map(|(bare, _)| bare),
1201 UnitTokenResolver::Search(scopes) => scopes
1202 .iter()
1203 .find_map(|(unit_index, named_types)| {
1204 unit_index
1205 .resolve_with_named_types(unit, named_types)
1206 .ok()
1207 .map(|(bare, _)| bare)
1208 })
1209 .ok_or_else(|| unknown_unit_message(unit)),
1210 UnitTokenResolver::Typed {
1211 unit_index,
1212 named_types,
1213 lemma_type,
1214 } => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
1215 }
1216 }
1217}
1218
1219fn unknown_unit_message(unit: &str) -> String {
1220 format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
1221}
1222
1223fn resolve_declared_on_type(
1224 written: &str,
1225 lemma_type: &LemmaType,
1226 unit_index: &UnitIndex,
1227 named_types: &IndexMap<String, Arc<LemmaType>>,
1228) -> Result<String, String> {
1229 match unit_index.resolve_with_named_types(written, named_types) {
1230 Ok((bare, _)) => {
1231 if unit_name_declared_on_type(lemma_type, &bare)
1232 || range_element_declares_unit(lemma_type, &bare)
1233 {
1234 Ok(bare)
1235 } else {
1236 Err(format!(
1237 "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
1238 lemma_type.name()
1239 ))
1240 }
1241 }
1242 Err(index_err) => {
1243 if unit_name_declared_on_type(lemma_type, written)
1244 || range_element_declares_unit(lemma_type, written)
1245 {
1246 Ok(written.to_string())
1247 } else {
1248 Err(index_err)
1249 }
1250 }
1251 }
1252}
1253
1254fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
1255 let Some(element) = lemma_type.specifications.element_from_range() else {
1256 return false;
1257 };
1258 unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
1259}
1260
1261fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
1262 match &lemma_type.specifications {
1263 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
1264 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
1265 _ => false,
1266 }
1267}
1268
1269fn bare_declared_unit_on_element(
1271 element_spec: &TypeSpecification,
1272 token: &str,
1273) -> Result<String, String> {
1274 match element_spec {
1275 TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1276 TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1277 other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1278 }
1279}
1280
1281pub fn resolve_endpoint_unit_token(
1287 token: &str,
1288 element_spec: &TypeSpecification,
1289 resolve_unit: Option<&UnitTokenResolver<'_>>,
1290) -> Result<String, String> {
1291 match bare_declared_unit_on_element(element_spec, token) {
1292 Ok(bare) => Ok(bare),
1293 Err(declared_err) => {
1294 let Some(resolve) = resolve_unit else {
1295 return Err(declared_err);
1296 };
1297 let bare = resolve.resolve(token)?;
1298 bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1299 format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1300 })
1301 }
1302 }
1303}
1304
1305pub(crate) fn lift_range_endpoint(
1311 value: &crate::parsing::ast::Value,
1312 element_spec: &TypeSpecification,
1313 resolve_unit: Option<&UnitTokenResolver<'_>>,
1314) -> Result<TypedLiteral, String> {
1315 use crate::parsing::ast::Value;
1316 match value {
1317 Value::NumberWithUnit(magnitude, unit_token) => {
1318 let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1319 let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
1320 let lemma_type = match &kind {
1321 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
1322 Arc::new(LemmaType::primitive(element_spec.clone()).with_unit(bare))
1323 }
1324 _ => Arc::new(LemmaType::primitive(element_spec.clone())),
1325 };
1326 Ok(TypedLiteral {
1327 value: kind,
1328 lemma_type,
1329 })
1330 }
1331 _ => {
1332 let kind = parser_scalar_to_value_kind(value, element_spec)?;
1333 Ok(TypedLiteral {
1334 value: kind,
1335 lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
1336 })
1337 }
1338 }
1339}
1340
1341fn literal_value_from_parser_value(
1342 value: &crate::parsing::ast::Value,
1343) -> Result<TypedLiteral, String> {
1344 use crate::parsing::ast::Value;
1345
1346 enum Step<'a> {
1347 Visit(&'a Value),
1348 CombineRange,
1349 }
1350
1351 let mut steps = vec![Step::Visit(value)];
1352 let mut literals: Vec<TypedLiteral> = Vec::new();
1353 while let Some(step) = steps.pop() {
1354 match step {
1355 Step::Visit(Value::Range(left, right)) => {
1356 steps.push(Step::CombineRange);
1357 steps.push(Step::Visit(right));
1358 steps.push(Step::Visit(left));
1359 }
1360 Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
1361 Step::CombineRange => {
1362 let right = literals
1363 .pop()
1364 .expect("BUG: range right endpoint missing from literal stack");
1365 let left = literals
1366 .pop()
1367 .expect("BUG: range left endpoint missing from literal stack");
1368 let compatible = match (
1369 &left.lemma_type.specifications,
1370 &right.lemma_type.specifications,
1371 ) {
1372 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1373 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1374 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1375 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1376 left.lemma_type.same_measure_family(&right.lemma_type)
1377 || left
1378 .lemma_type
1379 .compatible_with_anonymous_measure(&right.lemma_type)
1380 || right
1381 .lemma_type
1382 .compatible_with_anonymous_measure(&left.lemma_type)
1383 }
1384 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1385 _ => false,
1386 };
1387 if !compatible {
1388 return Err(format!(
1389 "range endpoints must have the same supported base type, got {} and {}",
1390 left.lemma_type.name(),
1391 right.lemma_type.name()
1392 ));
1393 }
1394 literals.push(TypedLiteral::range(left, right));
1395 }
1396 }
1397 }
1398 Ok(literals.pop().expect("BUG: literal walk left no result"))
1399}
1400
1401fn scalar_literal_from_parser_value(
1402 value: &crate::parsing::ast::Value,
1403) -> Result<TypedLiteral, String> {
1404 use crate::parsing::ast::Value;
1405
1406 match value {
1407 Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1408 Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1409 Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1410 Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1411 Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1412 Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
1413 lift_parser_decimal(*n)?,
1414 )),
1415 Value::Range(_, _) => {
1416 unreachable!("BUG: range values are walked by literal_value_from_parser_value")
1417 }
1418 }
1419}
1420
1421fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1423 if !d.is_integer() {
1424 return Err(format!(
1425 "{} requires a whole number, got fractional value",
1426 ctx
1427 ));
1428 }
1429 d.numer_to_u8()
1430 .ok_or_else(|| format!("{} value out of range for u8", ctx))
1431}
1432
1433fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1435 if !d.is_integer() {
1436 return Err(format!(
1437 "{} requires a whole number, got fractional value",
1438 ctx
1439 ));
1440 }
1441 d.numer_to_usize()
1442 .ok_or_else(|| format!("{} value out of range for usize", ctx))
1443}
1444
1445fn ratio_bound_to_canonical_rational(
1451 args: &[CommandArg],
1452 cmd: &str,
1453 units: &RatioUnits,
1454) -> Result<RationalInteger, String> {
1455 use crate::computation::rational::{checked_div, decimal_to_rational};
1456 let lit = require_literal(args, cmd)?;
1457 match lit {
1458 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1459 let unit = units.get(unit_name.as_str())?;
1460 let magnitude_rational = decimal_to_rational(*magnitude)
1461 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1462 checked_div(&magnitude_rational, &unit.value)
1463 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1464 }
1465 other => Err(format!(
1466 "{cmd} requires a ratio literal with a unit, got {}",
1467 value_kind_name(other)
1468 )),
1469 }
1470}
1471
1472fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1473 use crate::computation::rational::decimal_to_rational;
1474 match require_literal(args, cmd)? {
1475 crate::literals::Value::Number(d) => decimal_to_rational(*d)
1476 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1477 other => Err(format!(
1478 "{} requires a number literal, got {}",
1479 cmd,
1480 value_kind_name(other)
1481 )),
1482 }
1483}
1484
1485enum UnitConstraintField {
1486 Minimum,
1487 Maximum,
1488 SuggestionMagnitude,
1489}
1490
1491pub(crate) fn measure_declared_bound_to_canonical(
1492 magnitude: &RationalInteger,
1493 unit_name: &str,
1494 units: &MeasureUnits,
1495 type_name: &str,
1496 command: &str,
1497) -> Result<RationalInteger, String> {
1498 use crate::computation::rational::checked_mul;
1499 let unit = units.get(unit_name).map_err(|_| {
1500 format!(
1501 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1502 format_measure_units_list(units)
1503 )
1504 })?;
1505 checked_mul(magnitude, &unit.factor)
1506 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1507}
1508
1509fn parse_measure_declared_bound(
1510 args: &[CommandArg],
1511 cmd: &str,
1512 units: &MeasureUnits,
1513 type_name: &str,
1514) -> Result<(RationalInteger, String), String> {
1515 use crate::computation::rational::decimal_to_rational;
1516 let lit = require_literal(args, cmd)?;
1517 let (magnitude, unit_name) = match lit {
1518 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1519 other => {
1520 return Err(format!(
1521 "{cmd} requires a measure literal with a unit, got {}",
1522 value_kind_name(other)
1523 ));
1524 }
1525 };
1526 units.get(unit_name.as_str()).map_err(|_| {
1527 format!(
1528 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1529 format_measure_units_list(units)
1530 )
1531 })?;
1532 let magnitude_rational = decimal_to_rational(magnitude)
1533 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1534 Ok((magnitude_rational, unit_name))
1535}
1536
1537fn sync_measure_units_from_canonical(
1538 units: &mut MeasureUnits,
1539 canonical: &RationalInteger,
1540 field: UnitConstraintField,
1541) -> Result<(), String> {
1542 use crate::computation::rational::checked_div;
1543 for unit in &mut units.0 {
1544 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
1545 format!(
1546 "cannot derive per-unit constraint for unit '{}': {failure}",
1547 unit.name
1548 )
1549 })?;
1550 match field {
1551 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1552 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1553 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1554 }
1555 }
1556 Ok(())
1557}
1558
1559fn sync_ratio_units_from_canonical(
1560 units: &mut RatioUnits,
1561 canonical: &RationalInteger,
1562 field: UnitConstraintField,
1563) -> Result<(), String> {
1564 use crate::computation::rational::checked_mul;
1565 for unit in &mut units.0 {
1566 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1567 format!(
1568 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1569 unit.name
1570 )
1571 })?;
1572 match field {
1573 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1574 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1575 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1576 }
1577 }
1578 Ok(())
1579}
1580
1581fn sync_measure_suggestion_units(
1582 units: &mut MeasureUnits,
1583 default: &ValueKind,
1584 type_name: &str,
1585) -> Result<(), String> {
1586 let ValueKind::Measure(magnitude) = default else {
1587 return Ok(());
1588 };
1589 let unit_name = units
1590 .iter()
1591 .find(|unit| unit.is_canonical_factor())
1592 .or_else(|| units.iter().next())
1593 .map(|unit| unit.name.as_str())
1594 .expect(
1595 "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1596 );
1597 units.get(unit_name).map_err(|_| {
1598 format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1599 })?;
1600 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1601}
1602
1603pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1604 specification: &mut TypeSpecification,
1605 declared_suggestion: Option<&ValueKind>,
1606 type_name: &str,
1607) -> Result<(), String> {
1608 let TypeSpecification::Measure {
1609 minimum,
1610 maximum,
1611 units,
1612 ..
1613 } = specification
1614 else {
1615 return Ok(());
1616 };
1617
1618 if let Some(bound) = minimum.as_ref() {
1619 let canonical =
1620 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "minimum")?;
1621 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1622 }
1623 if let Some(bound) = maximum.as_ref() {
1624 let canonical =
1625 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "maximum")?;
1626 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1627 }
1628 if let Some(default) = declared_suggestion {
1629 sync_measure_suggestion_units(units, default, type_name)?;
1630 }
1631
1632 if minimum.is_some() {
1633 for unit in units.iter() {
1634 assert!(
1635 unit.minimum.is_some(),
1636 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1637 unit.name
1638 );
1639 }
1640 }
1641 if maximum.is_some() {
1642 for unit in units.iter() {
1643 assert!(
1644 unit.maximum.is_some(),
1645 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1646 unit.name
1647 );
1648 }
1649 }
1650 if declared_suggestion.is_some() {
1651 for unit in units.iter() {
1652 assert!(
1653 unit.suggestion_magnitude.is_some(),
1654 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1655 unit.name
1656 );
1657 }
1658 }
1659
1660 Ok(())
1661}
1662
1663fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1664 let ValueKind::Ratio(canonical) = default else {
1665 return Ok(());
1666 };
1667 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1668}
1669
1670fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1676 match arg {
1677 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1678 CommandArg::Label(name) => Ok(name.clone()),
1679 CommandArg::Literal(other) => Err(format!(
1680 "{} requires a text literal or identifier, got {}",
1681 cmd,
1682 value_kind_name(other)
1683 )),
1684 CommandArg::UnitExpr(_) => Err(format!(
1685 "{} requires a text literal or identifier, got a unit expression",
1686 cmd
1687 )),
1688 }
1689}
1690
1691fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1692 match arg {
1693 CommandArg::Label(name) => Ok(name.clone()),
1694 CommandArg::Literal(other) => Err(format!(
1695 "{} requires an identifier, got {}",
1696 cmd,
1697 value_kind_name(other)
1698 )),
1699 CommandArg::UnitExpr(_) => Err(format!(
1700 "{} requires an identifier, got a unit expression",
1701 cmd
1702 )),
1703 }
1704}
1705
1706fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
1707 match measure_trait {
1708 MeasureTrait::Duration => "duration",
1709 MeasureTrait::Calendar => "calendar",
1710 }
1711}
1712
1713fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
1714 if args.len() != 1 {
1715 return Err("trait requires exactly one identifier argument".to_string());
1716 }
1717 match label_name(&args[0], "trait")?
1718 .trim()
1719 .to_lowercase()
1720 .as_str()
1721 {
1722 "duration" => Ok(MeasureTrait::Duration),
1723 "calendar" => Ok(MeasureTrait::Calendar),
1724 other => Err(format!("Unknown measure trait '{}'", other)),
1725 }
1726}
1727
1728fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1729 let month_unit = units
1730 .iter()
1731 .find(|unit| unit.name == "month")
1732 .ok_or_else(|| {
1733 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1734 .to_string()
1735 })?;
1736 if !month_unit.is_canonical_factor() {
1737 return Err("trait calendar requires unit month 1".to_string());
1738 }
1739 Ok(())
1740}
1741
1742fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1743 let second_unit = units
1744 .iter()
1745 .find(|unit| unit.name == "second")
1746 .ok_or_else(|| {
1747 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1748 .to_string()
1749 })?;
1750 if !second_unit.is_canonical_factor() {
1751 return Err("trait duration requires unit second 1".to_string());
1752 }
1753 Ok(())
1754}
1755
1756fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1758 match require_literal(args, cmd)? {
1759 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1760 other => Err(format!(
1761 "{} requires a date literal (e.g. 2024-01-01), got {}",
1762 cmd,
1763 value_kind_name(other)
1764 )),
1765 }
1766}
1767
1768fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1770 match require_literal(args, cmd)? {
1771 crate::literals::Value::Time(t) => Ok(t.clone()),
1772 other => Err(format!(
1773 "{} requires a time literal (e.g. 12:30:00), got {}",
1774 cmd,
1775 value_kind_name(other)
1776 )),
1777 }
1778}
1779
1780#[must_use]
1782pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1783 use PrimitiveKind::*;
1784 match kind {
1785 Boolean => "Whether this holds (true or false).",
1786 Number => "A dimensionless number.",
1787 NumberRange => "The lower and upper bound of the number range.",
1788 Text => "A text value.",
1789 Measure => "A numeric amount in one of this type's units.",
1790 MeasureRange => "The lower and upper bound of the measure range.",
1791 Ratio => "A ratio in one of this type's units (e.g. percent).",
1792 RatioRange => "The lower and upper bound of the ratio range.",
1793 Date => "A date, or a date and time with optional timezone.",
1794 DateRange => "The start date and end date of the date range.",
1795 Time => "A time of day, with optional timezone.",
1796 TimeRange => "The start time and end time of the time range.",
1797 }
1798}
1799
1800impl TypeSpecification {
1801 pub fn boolean() -> Self {
1802 TypeSpecification::Boolean {
1803 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1804 }
1805 }
1806 pub fn measure() -> Self {
1807 TypeSpecification::Measure {
1808 minimum: None,
1809 maximum: None,
1810 decimals: None,
1811 units: MeasureUnits::new(),
1812 traits: Vec::new(),
1813 decomposition: None,
1814 unit: None,
1815 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
1816 }
1817 }
1818 pub fn number() -> Self {
1819 TypeSpecification::Number {
1820 minimum: None,
1821 maximum: None,
1822 decimals: None,
1823 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1824 }
1825 }
1826 pub fn number_range() -> Self {
1827 TypeSpecification::NumberRange {
1828 lower: None,
1829 upper: None,
1830 minimum: None,
1831 maximum: None,
1832 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1833 }
1834 }
1835 pub fn ratio() -> Self {
1836 TypeSpecification::Ratio {
1837 minimum: None,
1838 maximum: None,
1839 decimals: None,
1840 units: RatioUnits(vec![
1841 RatioUnit {
1842 name: "percent".to_string(),
1843 value: crate::computation::rational::rational_new(100, 1),
1844 minimum: None,
1845 maximum: None,
1846 suggestion_magnitude: None,
1847 },
1848 RatioUnit {
1849 name: "permille".to_string(),
1850 value: crate::computation::rational::rational_new(1000, 1),
1851 minimum: None,
1852 maximum: None,
1853 suggestion_magnitude: None,
1854 },
1855 ]),
1856 unit: None,
1857 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1858 }
1859 }
1860 pub fn ratio_range() -> Self {
1861 TypeSpecification::RatioRange {
1862 lower: None,
1863 upper: None,
1864 minimum: None,
1865 maximum: None,
1866 units: match TypeSpecification::ratio() {
1867 TypeSpecification::Ratio { units, .. } => units,
1868 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1869 },
1870 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1871 }
1872 }
1873 pub fn text() -> Self {
1874 TypeSpecification::Text {
1875 length: None,
1876 options: vec![],
1877 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1878 }
1879 }
1880 pub fn date() -> Self {
1881 TypeSpecification::Date {
1882 minimum: None,
1883 maximum: None,
1884 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1885 }
1886 }
1887 pub fn date_range() -> Self {
1888 TypeSpecification::DateRange {
1889 lower: None,
1890 upper: None,
1891 minimum: None,
1892 maximum: None,
1893 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1894 }
1895 }
1896 pub fn time() -> Self {
1897 TypeSpecification::Time {
1898 minimum: None,
1899 maximum: None,
1900 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1901 }
1902 }
1903 pub fn time_range() -> Self {
1904 TypeSpecification::TimeRange {
1905 lower: None,
1906 upper: None,
1907 minimum: None,
1908 maximum: None,
1909 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1910 }
1911 }
1912 pub fn measure_range() -> Self {
1913 TypeSpecification::MeasureRange {
1914 lower: None,
1915 upper: None,
1916 minimum: None,
1917 maximum: None,
1918 units: MeasureUnits::new(),
1919 decomposition: None,
1920 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1921 }
1922 }
1923
1924 #[must_use]
1926 pub fn element_from_range(&self) -> Option<Self> {
1927 match self {
1928 TypeSpecification::NumberRange { lower, upper, .. } => {
1929 Some(TypeSpecification::Number {
1930 minimum: lower.clone(),
1931 maximum: upper.clone(),
1932 decimals: None,
1933 help: String::new(),
1934 })
1935 }
1936 TypeSpecification::MeasureRange {
1937 lower,
1938 upper,
1939 units,
1940 decomposition,
1941 ..
1942 } => Some(TypeSpecification::Measure {
1943 minimum: lower.clone(),
1944 maximum: upper.clone(),
1945 decimals: None,
1946 units: units.clone(),
1947 traits: Vec::new(),
1948 decomposition: decomposition.clone(),
1949 unit: None,
1950 help: String::new(),
1951 }),
1952 TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
1953 minimum: lower.clone(),
1954 maximum: upper.clone(),
1955 help: String::new(),
1956 }),
1957 TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
1958 minimum: lower.clone(),
1959 maximum: upper.clone(),
1960 help: String::new(),
1961 }),
1962 TypeSpecification::RatioRange {
1963 lower,
1964 upper,
1965 units,
1966 ..
1967 } => Some(TypeSpecification::Ratio {
1968 minimum: lower.clone(),
1969 maximum: upper.clone(),
1970 decimals: None,
1971 units: units.clone(),
1972 unit: None,
1973 help: String::new(),
1974 }),
1975 _ => None,
1976 }
1977 }
1978
1979 #[must_use]
1981 pub fn range_from_element(&self) -> Option<Self> {
1982 match self {
1983 TypeSpecification::Number {
1984 minimum, maximum, ..
1985 } => Some(TypeSpecification::NumberRange {
1986 lower: minimum.clone(),
1987 upper: maximum.clone(),
1988 minimum: None,
1989 maximum: None,
1990 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1991 }),
1992 TypeSpecification::Measure {
1993 minimum,
1994 maximum,
1995 units,
1996 decomposition,
1997 ..
1998 } => Some(TypeSpecification::MeasureRange {
1999 lower: minimum.clone(),
2000 upper: maximum.clone(),
2001 minimum: None,
2002 maximum: None,
2003 units: units.clone(),
2004 decomposition: decomposition.clone(),
2005 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2006 }),
2007 TypeSpecification::Date {
2008 minimum, maximum, ..
2009 } => Some(TypeSpecification::DateRange {
2010 lower: minimum.clone(),
2011 upper: maximum.clone(),
2012 minimum: None,
2013 maximum: None,
2014 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2015 }),
2016 TypeSpecification::Time {
2017 minimum, maximum, ..
2018 } => Some(TypeSpecification::TimeRange {
2019 lower: minimum.clone(),
2020 upper: maximum.clone(),
2021 minimum: None,
2022 maximum: None,
2023 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2024 }),
2025 TypeSpecification::Ratio {
2026 minimum,
2027 maximum,
2028 units,
2029 ..
2030 } => Some(TypeSpecification::RatioRange {
2031 lower: minimum.clone(),
2032 upper: maximum.clone(),
2033 minimum: None,
2034 maximum: None,
2035 units: units.clone(),
2036 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2037 }),
2038 _ => None,
2039 }
2040 }
2041
2042 #[must_use]
2044 pub fn minimum_decimal(&self) -> Option<Decimal> {
2045 match self {
2046 TypeSpecification::Number { minimum, .. }
2047 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
2048 bound
2049 .try_to_decimal()
2050 .expect("BUG: planned minimum must convert to decimal")
2051 }),
2052 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
2053 bound
2054 .try_to_decimal()
2055 .expect("BUG: planned minimum must convert to decimal")
2056 }),
2057 _ => None,
2058 }
2059 }
2060
2061 #[must_use]
2063 pub fn maximum_decimal(&self) -> Option<Decimal> {
2064 match self {
2065 TypeSpecification::Number { maximum, .. }
2066 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
2067 bound
2068 .try_to_decimal()
2069 .expect("BUG: planned maximum must convert to decimal")
2070 }),
2071 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
2072 bound
2073 .try_to_decimal()
2074 .expect("BUG: planned maximum must convert to decimal")
2075 }),
2076 _ => None,
2077 }
2078 }
2079
2080 pub fn veto() -> Self {
2081 TypeSpecification::Veto { message: None }
2082 }
2083
2084 pub fn apply_constraint(
2096 &mut self,
2097 type_name: &str,
2098 command: TypeConstraintCommand,
2099 args: &[CommandArg],
2100 declared_suggestion: &mut Option<RawSuggestion>,
2101 declared_value: &mut Option<RawSuggestion>,
2102 resolve_unit: Option<&UnitTokenResolver<'_>>,
2103 ) -> Result<(), String> {
2104 if command == TypeConstraintCommand::With
2105 && args.len() > 1
2106 && !matches!(self, TypeSpecification::List { .. })
2107 {
2108 return Err(format!("`-> with` on {self} takes exactly one value"));
2109 }
2110 if command == TypeConstraintCommand::Trait
2111 && !matches!(&self, TypeSpecification::Measure { .. })
2112 {
2113 return Err("trait command is only valid on measure types".to_string());
2114 }
2115 match self {
2116 TypeSpecification::Boolean { help } => match command {
2117 TypeConstraintCommand::Help => {
2118 apply_type_help_command(help, args)?;
2119 }
2120 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2121 let (target, cmd) = match command {
2122 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2123 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2124 _ => unreachable!("BUG: only Suggest or With in this arm"),
2125 };
2126 let lit = require_literal(args, cmd)?;
2127 reject_calendar_for_suggestion(
2128 lit,
2129 type_name,
2130 SuggestionExpectation::Boolean,
2131 None,
2132 )?;
2133 match lit {
2134 crate::literals::Value::Boolean(bv) => {
2135 *target =
2136 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
2137 }
2138 _ => {
2139 return Err(
2140 "Please provide true or false, for example `-> suggest true`."
2141 .to_string(),
2142 );
2143 }
2144 }
2145 }
2146 other => {
2147 return Err(format!(
2148 "Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
2149 other
2150 ));
2151 }
2152 },
2153 TypeSpecification::Measure {
2154 decimals,
2155 minimum,
2156 maximum,
2157 units,
2158 traits,
2159 help,
2160 ..
2161 } => match command {
2162 TypeConstraintCommand::Decimals => {
2163 let d = require_decimal_literal(args, "decimals")?;
2164 *decimals = Some(decimal_to_u8(d, "decimals")?);
2165 }
2166 TypeConstraintCommand::Unit => {
2167 let (unit_name, value, derived_measure_factors) = match args {
2168 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2169 (name.clone(), *v, Vec::new())
2170 }
2171 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2172 prefix,
2173 factors,
2174 ))] => {
2175 let raw: Vec<(String, i32)> = factors
2176 .iter()
2177 .map(|f| (f.measure_ref.clone(), f.exp))
2178 .collect();
2179 (name.clone(), *prefix, raw)
2180 }
2181 _ => {
2182 return Err(
2183 "unit requires a unit name followed by a conversion factor or compound unit expression (e.g., 'unit eur 1.00' or 'unit mps meter/second')"
2184 .to_string(),
2185 );
2186 }
2187 };
2188 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2189 let new_factor = crate::computation::rational::decimal_to_rational(value)
2190 .map_err(|failure| failure.to_string())?;
2191 if existing.factor != new_factor
2192 || existing.derived_measure_factors != derived_measure_factors
2193 {
2194 return Err(format!(
2195 "Unit '{unit_name}' is already defined in this type's inherited units; \
2196 cannot change factor or decomposition. Add a new unit name instead."
2197 ));
2198 }
2199 } else {
2200 units.0.push(MeasureUnit::from_decimal_factor(
2201 unit_name,
2202 value,
2203 derived_measure_factors,
2204 )?);
2205 }
2206 }
2207 TypeConstraintCommand::Trait => {
2208 let measure_trait = parse_measure_trait(args)?;
2209 if traits.contains(&measure_trait) {
2210 return Err(format!(
2211 "Duplicate trait '{}' for measure type.",
2212 measure_trait_name(measure_trait)
2213 ));
2214 }
2215 if measure_trait == MeasureTrait::Duration {
2216 validate_duration_trait_requirements(units)?;
2217 }
2218 if measure_trait == MeasureTrait::Calendar {
2219 validate_calendar_trait_requirements(units)?;
2220 }
2221 traits.push(measure_trait);
2222 }
2223 TypeConstraintCommand::Minimum => {
2224 *minimum = Some(parse_measure_declared_bound(
2225 args, "minimum", units, type_name,
2226 )?);
2227 }
2228 TypeConstraintCommand::Maximum => {
2229 *maximum = Some(parse_measure_declared_bound(
2230 args, "maximum", units, type_name,
2231 )?);
2232 }
2233 TypeConstraintCommand::Help => {
2234 apply_type_help_command(help, args)?;
2235 }
2236 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2237 let (target, cmd) = match command {
2238 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2239 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2240 _ => unreachable!("BUG: only Suggest or With in this arm"),
2241 };
2242 let lit = require_literal(args, cmd)?;
2243 if !traits.contains(&MeasureTrait::Calendar) {
2244 reject_calendar_for_suggestion(
2245 lit,
2246 type_name,
2247 SuggestionExpectation::MeasureUnits,
2248 Some(units),
2249 )?;
2250 }
2251 match lit {
2252 crate::literals::Value::NumberWithUnit(_, _) => {
2253 let (magnitude, unit_name) =
2254 parse_measure_declared_bound(args, cmd, units, type_name)?;
2255 *target = Some(RawSuggestion::Measure {
2256 magnitude,
2257 unit_name,
2258 });
2259 }
2260 _ => {
2261 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
2262 }
2263 }
2264 }
2265 _ => {
2266 return Err(format!(
2267 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest, with",
2268 command
2269 ));
2270 }
2271 },
2272 TypeSpecification::Number {
2273 decimals,
2274 minimum,
2275 maximum,
2276 help,
2277 } => match command {
2278 TypeConstraintCommand::Decimals => {
2279 let d = require_decimal_literal(args, "decimals")?;
2280 *decimals = Some(decimal_to_u8(d, "decimals")?);
2281 }
2282 TypeConstraintCommand::Unit => {
2283 return Err(
2284 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
2285 );
2286 }
2287 TypeConstraintCommand::Minimum => {
2288 *minimum = Some(require_decimal_literal(args, "minimum")?);
2289 }
2290 TypeConstraintCommand::Maximum => {
2291 *maximum = Some(require_decimal_literal(args, "maximum")?);
2292 }
2293 TypeConstraintCommand::Help => {
2294 apply_type_help_command(help, args)?;
2295 }
2296 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2297 let (target, cmd) = match command {
2298 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2299 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2300 _ => unreachable!("BUG: only Suggest or With in this arm"),
2301 };
2302 let lit = require_literal(args, cmd)?;
2303 reject_calendar_for_suggestion(
2304 lit,
2305 type_name,
2306 SuggestionExpectation::Number,
2307 None,
2308 )?;
2309 match lit {
2310 crate::literals::Value::Number(d) => {
2311 *target = Some(RawSuggestion::Value(ValueKind::Number(
2312 lift_parser_decimal(*d)?,
2313 )));
2314 }
2315 _ => {
2316 return Err(
2317 "Please provide a number, for example `-> suggest 42`.".to_string()
2318 );
2319 }
2320 }
2321 }
2322 _ => {
2323 return Err(format!(
2324 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
2325 command
2326 ));
2327 }
2328 },
2329 TypeSpecification::NumberRange {
2330 lower,
2331 upper,
2332 minimum,
2333 maximum,
2334 help,
2335 } => match command {
2336 TypeConstraintCommand::Lower => {
2337 *lower = Some(require_decimal_literal(args, "lower")?);
2338 }
2339 TypeConstraintCommand::Upper => {
2340 *upper = Some(require_decimal_literal(args, "upper")?);
2341 }
2342 TypeConstraintCommand::Minimum => {
2343 let width = require_decimal_literal(args, "minimum")?;
2344 reject_negative_width_magnitude(&width, "minimum")?;
2345 *minimum = Some(width);
2346 }
2347 TypeConstraintCommand::Maximum => {
2348 let width = require_decimal_literal(args, "maximum")?;
2349 reject_negative_width_magnitude(&width, "maximum")?;
2350 *maximum = Some(width);
2351 }
2352 TypeConstraintCommand::Help => {
2353 apply_type_help_command(help, args)?;
2354 }
2355 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2356 let target = match command {
2357 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2358 TypeConstraintCommand::With => &mut *declared_value,
2359 _ => unreachable!("BUG: only Suggest or With in this arm"),
2360 };
2361 let (left, right) = require_suggestion_range_endpoints(
2362 args,
2363 type_name,
2364 SuggestionExpectation::NumberRange,
2365 None,
2366 )?;
2367 let left = literal_value_from_parser_value(left)?;
2368 let right = literal_value_from_parser_value(right)?;
2369 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
2370 return Err(
2371 "Please provide a number range, for example `-> suggest 10...100`."
2372 .to_string(),
2373 );
2374 }
2375 *target = Some(RawSuggestion::Value(ValueKind::Range(
2376 Box::new(left),
2377 Box::new(right),
2378 )));
2379 }
2380 _ => {
2381 return Err(format!(
2382 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2383 command
2384 ));
2385 }
2386 },
2387 TypeSpecification::Ratio {
2388 decimals,
2389 minimum,
2390 maximum,
2391 units,
2392 unit: _,
2393 help,
2394 } => match command {
2395 TypeConstraintCommand::Decimals => {
2396 let d = require_decimal_literal(args, "decimals")?;
2397 *decimals = Some(decimal_to_u8(d, "decimals")?);
2398 }
2399 TypeConstraintCommand::Unit => {
2400 let (unit_name, value_dec) = match args {
2401 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2402 (name.clone(), *v)
2403 }
2404 _ => {
2405 return Err(
2406 "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."
2407 .to_string(),
2408 );
2409 }
2410 };
2411 let value = crate::computation::rational::decimal_to_rational(value_dec)
2412 .map_err(|failure| {
2413 format!(
2414 "ratio unit value is not exactly representable as a rational: {}",
2415 failure
2416 )
2417 })?;
2418 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2419 if existing.value != value {
2420 return Err(format!(
2421 "Unit '{unit_name}' is already defined in this type's inherited units; \
2422 cannot change factor. Add a new unit name instead."
2423 ));
2424 }
2425 } else {
2426 units.0.push(RatioUnit {
2427 name: unit_name,
2428 value,
2429 minimum: None,
2430 maximum: None,
2431 suggestion_magnitude: None,
2432 });
2433 }
2434 }
2435 TypeConstraintCommand::Minimum => {
2436 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2437 sync_ratio_units_from_canonical(
2438 units,
2439 &canonical,
2440 UnitConstraintField::Minimum,
2441 )?;
2442 *minimum = Some(canonical);
2443 }
2444 TypeConstraintCommand::Maximum => {
2445 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2446 sync_ratio_units_from_canonical(
2447 units,
2448 &canonical,
2449 UnitConstraintField::Maximum,
2450 )?;
2451 *maximum = Some(canonical);
2452 }
2453 TypeConstraintCommand::Help => {
2454 apply_type_help_command(help, args)?;
2455 }
2456 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2457 let (target, cmd) = match command {
2458 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2459 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2460 _ => unreachable!("BUG: only Suggest or With in this arm"),
2461 };
2462 let lit = require_literal(args, cmd)?;
2463 reject_calendar_for_suggestion(
2464 lit,
2465 type_name,
2466 SuggestionExpectation::Ratio,
2467 None,
2468 )?;
2469 match lit {
2470 crate::literals::Value::NumberWithUnit(_, unit_name) => {
2471 let element_spec = TypeSpecification::Ratio {
2472 decimals: *decimals,
2473 minimum: minimum.clone(),
2474 maximum: maximum.clone(),
2475 units: units.clone(),
2476 unit: None,
2477 help: help.clone(),
2478 };
2479 let value = parser_value_to_value_kind(lit, &element_spec, None)?;
2480 sync_ratio_suggestion_units(units, &value)?;
2481 *target = Some(RawSuggestion::UnitBound {
2482 value,
2483 unit_name: unit_name.clone(),
2484 });
2485 }
2486 other => {
2487 return Err(format!(
2488 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2489 value_kind_name(other)
2490 ));
2491 }
2492 }
2493 }
2494 _ => {
2495 return Err(format!(
2496 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest, with",
2497 command
2498 ));
2499 }
2500 },
2501 TypeSpecification::RatioRange {
2502 lower,
2503 upper,
2504 minimum,
2505 maximum,
2506 units,
2507 help,
2508 } => match command {
2509 TypeConstraintCommand::Unit => {
2510 return Err(format!(
2511 "'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
2512 ));
2513 }
2514 TypeConstraintCommand::Lower => {
2515 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2516 }
2517 TypeConstraintCommand::Upper => {
2518 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2519 }
2520 TypeConstraintCommand::Minimum => {
2521 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2522 reject_negative_width_magnitude(&width, "minimum")?;
2523 *minimum = Some(width);
2524 }
2525 TypeConstraintCommand::Maximum => {
2526 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2527 reject_negative_width_magnitude(&width, "maximum")?;
2528 *maximum = Some(width);
2529 }
2530 TypeConstraintCommand::Help => {
2531 apply_type_help_command(help, args)?;
2532 }
2533 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2534 let target = match command {
2535 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2536 TypeConstraintCommand::With => &mut *declared_value,
2537 _ => unreachable!("BUG: only Suggest or With in this arm"),
2538 };
2539 let (left, right) = require_suggestion_range_endpoints(
2540 args,
2541 type_name,
2542 SuggestionExpectation::RatioRange,
2543 None,
2544 )?;
2545 let element_spec = TypeSpecification::RatioRange {
2546 lower: lower.clone(),
2547 upper: upper.clone(),
2548 minimum: minimum.clone(),
2549 maximum: maximum.clone(),
2550 units: units.clone(),
2551 help: help.clone(),
2552 }
2553 .element_from_range()
2554 .expect("BUG: RatioRange must define element_from_range");
2555 let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2556 let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2557 if !value_kind_matches_spec(&left.value, &element_spec)
2558 || !value_kind_matches_spec(&right.value, &element_spec)
2559 || !matches!(&left.value, ValueKind::Ratio(_))
2560 || !matches!(&right.value, ValueKind::Ratio(_))
2561 {
2562 return Err(
2563 "Please provide a ratio range, for example `-> suggest 10%...50%`."
2564 .to_string(),
2565 );
2566 }
2567 *target = Some(RawSuggestion::Value(ValueKind::Range(
2568 Box::new(left),
2569 Box::new(right),
2570 )));
2571 }
2572 _ => {
2573 return Err(format!(
2574 "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2575 command
2576 ));
2577 }
2578 },
2579 TypeSpecification::Text {
2580 length,
2581 options,
2582 help,
2583 } => match command {
2584 TypeConstraintCommand::Option => {
2585 if args.len() != 1 {
2586 return Err("option takes exactly one argument".to_string());
2587 }
2588 options.push(option_name(&args[0], "option")?);
2589 }
2590 TypeConstraintCommand::Options => {
2591 let mut collected = Vec::with_capacity(args.len());
2592 for arg in args {
2593 collected.push(option_name(arg, "options")?);
2594 }
2595 *options = collected;
2596 }
2597 TypeConstraintCommand::Length => {
2598 let d = require_decimal_literal(args, "length")?;
2599 *length = Some(decimal_to_usize(d, "length")?);
2600 }
2601 TypeConstraintCommand::Help => {
2602 apply_type_help_command(help, args)?;
2603 }
2604 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2605 let (target, cmd) = match command {
2606 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2607 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2608 _ => unreachable!("BUG: only Suggest or With in this arm"),
2609 };
2610 let lit = require_literal(args, cmd)?;
2611 reject_calendar_for_suggestion(
2612 lit,
2613 type_name,
2614 SuggestionExpectation::Text,
2615 None,
2616 )?;
2617 match lit {
2618 crate::literals::Value::Text(s) => {
2619 *target = Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2620 }
2621 _ => {
2622 return Err(
2623 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2624 .to_string(),
2625 );
2626 }
2627 }
2628 }
2629 _ => {
2630 return Err(format!(
2631 "Invalid command '{}' for text type. Valid commands: options, length, help, suggest, with",
2632 command
2633 ));
2634 }
2635 },
2636 TypeSpecification::Date {
2637 minimum,
2638 maximum,
2639 help,
2640 } => match command {
2641 TypeConstraintCommand::Minimum => {
2642 let dt = require_date_literal(args, "minimum")?;
2643 *minimum = Some(dt);
2644 }
2645 TypeConstraintCommand::Maximum => {
2646 let dt = require_date_literal(args, "maximum")?;
2647 *maximum = Some(dt);
2648 }
2649 TypeConstraintCommand::Help => {
2650 apply_type_help_command(help, args)?;
2651 }
2652 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2653 let (target, cmd) = match command {
2654 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2655 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2656 _ => unreachable!("BUG: only Suggest or With in this arm"),
2657 };
2658 let lit = require_literal(args, cmd)?;
2659 reject_calendar_for_suggestion(
2660 lit,
2661 type_name,
2662 SuggestionExpectation::Date,
2663 None,
2664 )?;
2665 match lit {
2666 crate::literals::Value::Date(dt) => {
2667 *target = Some(RawSuggestion::Value(ValueKind::Date(
2668 date_time_to_semantic(dt),
2669 )));
2670 }
2671 _ => {
2672 return Err(
2673 "Please provide a date, for example `-> suggest 2024-06-15`."
2674 .to_string(),
2675 );
2676 }
2677 }
2678 }
2679 _ => {
2680 return Err(format!(
2681 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
2682 command
2683 ));
2684 }
2685 },
2686 TypeSpecification::DateRange {
2687 lower,
2688 upper,
2689 minimum,
2690 maximum,
2691 help,
2692 } => match command {
2693 TypeConstraintCommand::Lower => {
2694 *lower = Some(require_date_literal(args, "lower")?);
2695 }
2696 TypeConstraintCommand::Upper => {
2697 *upper = Some(require_date_literal(args, "upper")?);
2698 }
2699 TypeConstraintCommand::Minimum => {
2700 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2701 }
2702 TypeConstraintCommand::Maximum => {
2703 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2704 }
2705 TypeConstraintCommand::Help => {
2706 apply_type_help_command(help, args)?;
2707 }
2708 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2709 let target = match command {
2710 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2711 TypeConstraintCommand::With => &mut *declared_value,
2712 _ => unreachable!("BUG: only Suggest or With in this arm"),
2713 };
2714 let (left, right) = require_suggestion_range_endpoints(
2715 args,
2716 type_name,
2717 SuggestionExpectation::DateRange,
2718 None,
2719 )?;
2720 let left = literal_value_from_parser_value(left)?;
2721 let right = literal_value_from_parser_value(right)?;
2722 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2723 return Err(
2724 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2725 .to_string(),
2726 );
2727 }
2728 *target = Some(RawSuggestion::Value(ValueKind::Range(
2729 Box::new(left),
2730 Box::new(right),
2731 )));
2732 }
2733 _ => {
2734 return Err(format!(
2735 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2736 command
2737 ));
2738 }
2739 },
2740 TypeSpecification::Time {
2741 minimum,
2742 maximum,
2743 help,
2744 } => match command {
2745 TypeConstraintCommand::Minimum => {
2746 let t = require_time_literal(args, "minimum")?;
2747 *minimum = Some(t);
2748 }
2749 TypeConstraintCommand::Maximum => {
2750 let t = require_time_literal(args, "maximum")?;
2751 *maximum = Some(t);
2752 }
2753 TypeConstraintCommand::Help => {
2754 apply_type_help_command(help, args)?;
2755 }
2756 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2757 let (target, cmd) = match command {
2758 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2759 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2760 _ => unreachable!("BUG: only Suggest or With in this arm"),
2761 };
2762 let lit = require_literal(args, cmd)?;
2763 reject_calendar_for_suggestion(
2764 lit,
2765 type_name,
2766 SuggestionExpectation::Time,
2767 None,
2768 )?;
2769 match lit {
2770 crate::literals::Value::Time(t) => {
2771 *target =
2772 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2773 }
2774 _ => {
2775 return Err(
2776 "Please provide a time, for example `-> suggest 09:00:00`."
2777 .to_string(),
2778 );
2779 }
2780 }
2781 }
2782 _ => {
2783 return Err(format!(
2784 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
2785 command
2786 ));
2787 }
2788 },
2789 TypeSpecification::TimeRange {
2790 lower,
2791 upper,
2792 minimum,
2793 maximum,
2794 help,
2795 } => match command {
2796 TypeConstraintCommand::Lower => {
2797 *lower = Some(require_time_literal(args, "lower")?);
2798 }
2799 TypeConstraintCommand::Upper => {
2800 *upper = Some(require_time_literal(args, "upper")?);
2801 }
2802 TypeConstraintCommand::Minimum => {
2803 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2804 }
2805 TypeConstraintCommand::Maximum => {
2806 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2807 }
2808 TypeConstraintCommand::Help => {
2809 apply_type_help_command(help, args)?;
2810 }
2811 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2812 let target = match command {
2813 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2814 TypeConstraintCommand::With => &mut *declared_value,
2815 _ => unreachable!("BUG: only Suggest or With in this arm"),
2816 };
2817 let (left, right) = require_suggestion_range_endpoints(
2818 args,
2819 type_name,
2820 SuggestionExpectation::TimeRange,
2821 None,
2822 )?;
2823 let left = literal_value_from_parser_value(left)?;
2824 let right = literal_value_from_parser_value(right)?;
2825 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2826 return Err(
2827 "Please provide a time range, for example `-> suggest 09:00...17:00`."
2828 .to_string(),
2829 );
2830 }
2831 *target = Some(RawSuggestion::Value(ValueKind::Range(
2832 Box::new(left),
2833 Box::new(right),
2834 )));
2835 }
2836 _ => {
2837 return Err(format!(
2838 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2839 command
2840 ));
2841 }
2842 },
2843 TypeSpecification::MeasureRange {
2844 lower,
2845 upper,
2846 minimum,
2847 maximum,
2848 units,
2849 decomposition,
2850 help,
2851 } => match command {
2852 TypeConstraintCommand::Unit => {
2853 return Err(format!(
2854 "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
2855 ));
2856 }
2857 TypeConstraintCommand::Lower => {
2858 *lower = Some(parse_measure_declared_bound(
2859 args, "lower", units, type_name,
2860 )?);
2861 }
2862 TypeConstraintCommand::Upper => {
2863 *upper = Some(parse_measure_declared_bound(
2864 args, "upper", units, type_name,
2865 )?);
2866 }
2867 TypeConstraintCommand::Minimum => {
2868 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2869 reject_negative_width_magnitude(&width.0, "minimum")?;
2870 *minimum = Some(width);
2871 }
2872 TypeConstraintCommand::Maximum => {
2873 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2874 reject_negative_width_magnitude(&width.0, "maximum")?;
2875 *maximum = Some(width);
2876 }
2877 TypeConstraintCommand::Help => {
2878 apply_type_help_command(help, args)?;
2879 }
2880 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2881 let target = match command {
2882 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2883 TypeConstraintCommand::With => &mut *declared_value,
2884 _ => unreachable!("BUG: only Suggest or With in this arm"),
2885 };
2886 let (left, right) = require_suggestion_range_endpoints(
2887 args,
2888 type_name,
2889 SuggestionExpectation::MeasureRange,
2890 Some(units),
2891 )?;
2892 let element_spec = TypeSpecification::MeasureRange {
2893 lower: lower.clone(),
2894 upper: upper.clone(),
2895 minimum: minimum.clone(),
2896 maximum: maximum.clone(),
2897 units: units.clone(),
2898 decomposition: decomposition.clone(),
2899 help: help.clone(),
2900 }
2901 .element_from_range()
2902 .expect("BUG: MeasureRange must define element_from_range");
2903 let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2904 let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2905 if !value_kind_matches_spec(&left.value, &element_spec)
2906 || !value_kind_matches_spec(&right.value, &element_spec)
2907 || !matches!(&left.value, ValueKind::Measure(_))
2908 || !matches!(&right.value, ValueKind::Measure(_))
2909 {
2910 return Err(format!(
2911 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2912 ));
2913 }
2914 *target = Some(RawSuggestion::Value(ValueKind::Range(
2915 Box::new(left),
2916 Box::new(right),
2917 )));
2918 }
2919 _ => {
2920 return Err(format!(
2921 "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2922 command
2923 ));
2924 }
2925 },
2926 TypeSpecification::Veto { .. } => {
2927 return Err(format!(
2928 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2929 command
2930 ));
2931 }
2932 TypeSpecification::Undetermined => {
2933 return Err(format!(
2934 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2935 command
2936 ));
2937 }
2938 TypeSpecification::Spec { .. } => {
2939 return Err(format!(
2940 "Invalid command '{command}' for spec type. A spec instance has no data constraints"
2941 ));
2942 }
2943 TypeSpecification::List { element, .. } => match command {
2944 TypeConstraintCommand::Help => {
2945 let mut unused_suggestion = None;
2946 let mut unused_value = None;
2947 Arc::make_mut(element).specifications.apply_constraint(
2948 type_name,
2949 command,
2950 args,
2951 &mut unused_suggestion,
2952 &mut unused_value,
2953 resolve_unit,
2954 )?;
2955 }
2956 TypeConstraintCommand::With => {
2957 let mut rows = match declared_value.take() {
2958 Some(RawSuggestion::List(existing)) => existing,
2959 None => Vec::new(),
2960 Some(other) => unreachable!(
2961 "BUG: list declared value must be RawSuggestion::List, got {other:?}"
2962 ),
2963 };
2964 for arg in args {
2965 let mut row_value = None;
2966 let mut unused_suggestion = None;
2967 Arc::make_mut(element).specifications.apply_constraint(
2968 type_name,
2969 TypeConstraintCommand::With,
2970 std::slice::from_ref(arg),
2971 &mut unused_suggestion,
2972 &mut row_value,
2973 resolve_unit,
2974 )?;
2975 rows.push(
2976 row_value.expect("BUG: -> with on a list row must produce a value"),
2977 );
2978 }
2979 *declared_value = Some(RawSuggestion::List(rows));
2980 }
2981 other => {
2982 return Err(format!(
2983 "Invalid command '{other}' for list type. Valid commands: with, help"
2984 ));
2985 }
2986 },
2987 }
2988 Ok(())
2989 }
2990}
2991
2992pub fn parse_number_unit(
2995 value_str: &str,
2996 type_spec: &TypeSpecification,
2997) -> Result<crate::parsing::ast::Value, String> {
2998 use crate::literals::{NumberWithUnit, RatioLiteral};
2999 use crate::parsing::ast::Value;
3000
3001 let trimmed = value_str.trim();
3002 match type_spec {
3003 TypeSpecification::Measure { units, .. } => {
3004 if units.is_empty() {
3005 unreachable!(
3006 "BUG: Measure type has no units; should have been validated during planning"
3007 );
3008 }
3009 match trimmed.parse::<NumberWithUnit>() {
3010 Ok(n) => {
3011 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3012 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3013 }
3014 Err(e) => {
3015 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3016 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3017 let example_unit = units
3018 .iter()
3019 .next()
3020 .expect("BUG: units non-empty after guard")
3021 .name
3022 .as_str();
3023 Err(format!(
3024 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3025 trimmed,
3026 example_unit,
3027 valid.join(", ")
3028 ))
3029 } else {
3030 Err(e)
3031 }
3032 }
3033 }
3034 }
3035 TypeSpecification::Ratio { units, .. } => {
3036 if units.is_empty() {
3037 unreachable!(
3038 "BUG: Ratio type has no units; should have been validated during planning"
3039 );
3040 }
3041 match trimmed.parse::<RatioLiteral>()? {
3042 RatioLiteral::Bare(_) => {
3043 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3044 }
3045 RatioLiteral::Percent(n) => {
3046 let unit = units.get("percent").map_err(|e| e.to_string())?;
3047 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3048 }
3049 RatioLiteral::Permille(n) => {
3050 let unit = units.get("permille").map_err(|e| e.to_string())?;
3051 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3052 }
3053 RatioLiteral::Named { value, unit } => {
3054 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3055 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3056 }
3057 }
3058 }
3059 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3060 }
3061}
3062
3063pub fn parse_value_from_string(
3066 value_str: &str,
3067 type_spec: &TypeSpecification,
3068 source: &Source,
3069) -> Result<crate::parsing::ast::Value, Error> {
3070 match type_spec {
3071 TypeSpecification::NumberRange { .. }
3072 | TypeSpecification::MeasureRange { .. }
3073 | TypeSpecification::DateRange { .. }
3074 | TypeSpecification::TimeRange { .. }
3075 | TypeSpecification::RatioRange { .. } => {
3076 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3077 unreachable!(
3078 "BUG: range_element_type_specification missing arm for known range type"
3079 )
3080 });
3081 parse_range_value_from_string(value_str, &element_spec, source)
3082 }
3083 _ => parse_scalar_value_from_string(value_str, type_spec, source),
3084 }
3085}
3086
3087fn parse_range_value_from_string(
3088 value_str: &str,
3089 element_spec: &TypeSpecification,
3090 source: &Source,
3091) -> Result<crate::parsing::ast::Value, Error> {
3092 use crate::parsing::ast::Value;
3093
3094 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3095
3096 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3097 to_err("Range value must use '...' between the two endpoints".to_string())
3098 })?;
3099 if left_str.trim().is_empty() || right_str.trim().is_empty() {
3100 return Err(to_err(
3101 "Range value must contain a non-empty left and right endpoint".to_string(),
3102 ));
3103 }
3104 let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3105 let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3106 Ok(Value::Range(Box::new(left), Box::new(right)))
3107}
3108
3109fn parse_scalar_value_from_string(
3110 value_str: &str,
3111 type_spec: &TypeSpecification,
3112 source: &Source,
3113) -> Result<crate::parsing::ast::Value, Error> {
3114 use crate::parsing::ast::Value;
3115
3116 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3117
3118 match type_spec {
3119 TypeSpecification::Text { .. } => value_str
3120 .parse::<crate::literals::TextLiteral>()
3121 .map(|t| Value::Text(t.0))
3122 .map_err(to_err),
3123 TypeSpecification::Number { .. } => value_str
3124 .parse::<crate::literals::NumberLiteral>()
3125 .map(|n| Value::Number(n.0))
3126 .map_err(to_err),
3127 TypeSpecification::Measure { .. } => {
3128 parse_number_unit(value_str, type_spec).map_err(to_err)
3129 }
3130 TypeSpecification::Boolean { .. } => value_str
3131 .parse::<BooleanValue>()
3132 .map(Value::Boolean)
3133 .map_err(to_err),
3134 TypeSpecification::Date { .. } => {
3135 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3136 Ok(Value::Date(date))
3137 }
3138 TypeSpecification::Time { .. } => {
3139 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3140 Ok(Value::Time(time))
3141 }
3142 TypeSpecification::Ratio { .. } => {
3143 parse_number_unit(value_str, type_spec).map_err(to_err)
3144 }
3145 TypeSpecification::NumberRange { .. }
3146 | TypeSpecification::MeasureRange { .. }
3147 | TypeSpecification::DateRange { .. }
3148 | TypeSpecification::TimeRange { .. }
3149 | TypeSpecification::RatioRange { .. } => {
3150 unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3151 }
3152 TypeSpecification::Veto { .. } => Err(to_err(
3153 "Veto type cannot be parsed from string".to_string(),
3154 )),
3155 TypeSpecification::Undetermined => unreachable!(
3156 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3157 ),
3158 TypeSpecification::Spec { .. } => Err(to_err(
3159 "spec values are not evaluation-data scalars".to_string(),
3160 )),
3161 TypeSpecification::List { .. } => Err(to_err(
3162 "list values are not evaluation-data scalars".to_string(),
3163 )),
3164 }
3165}
3166
3167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3172#[serde(rename_all = "snake_case")]
3173pub enum SemanticCalendarUnit {
3174 Month,
3175 Year,
3176}
3177
3178impl fmt::Display for SemanticCalendarUnit {
3179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3180 let s = match self {
3181 SemanticCalendarUnit::Month => "month",
3182 SemanticCalendarUnit::Year => "year",
3183 };
3184 write!(f, "{}", s)
3185 }
3186}
3187
3188pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3189 match unit_name {
3190 "month" => SemanticCalendarUnit::Month,
3191 "year" => SemanticCalendarUnit::Year,
3192 other => unreachable!(
3193 "BUG: calendar measure signature unit must be month or year, got '{other}'"
3194 ),
3195 }
3196}
3197
3198pub fn semantic_calendar_unit_from_measure_signature(
3199 signature: &[(String, i32)],
3200) -> SemanticCalendarUnit {
3201 let unit_name = signature
3202 .first()
3203 .map(|(name, _)| name.as_str())
3204 .expect("BUG: calendar measure must carry a unit signature");
3205 semantic_calendar_unit_from_unit_name(unit_name)
3206}
3207
3208pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3209 if !lemma_type.is_calendar_like() {
3210 unreachable!(
3211 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3212 lemma_type.name()
3213 );
3214 }
3215 let signature = lemma_type.measure_runtime_signature();
3216 if signature.is_empty() {
3217 return SemanticCalendarUnit::Month;
3218 }
3219 semantic_calendar_unit_from_measure_signature(&signature)
3220}
3221
3222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3224pub enum SemanticConversionTarget {
3225 Type(PrimitiveKind),
3226 Unit {
3228 unit_name: String,
3229 #[serde(with = "crate::snapshot::shared::arc")]
3231 owning_type: Arc<LemmaType>,
3232 },
3233}
3234
3235impl std::hash::Hash for SemanticConversionTarget {
3236 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3237 match self {
3238 Self::Type(kind) => {
3239 0u8.hash(state);
3240 kind.hash(state);
3241 }
3242 Self::Unit {
3243 unit_name,
3244 owning_type,
3245 } => {
3246 1u8.hash(state);
3247 unit_name.hash(state);
3248 owning_type.hash(state);
3249 }
3250 }
3251 }
3252}
3253
3254impl SemanticConversionTarget {}
3255
3256impl fmt::Display for SemanticConversionTarget {
3257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3258 match self {
3259 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
3260 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
3261 }
3262 }
3263}
3264
3265#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3268pub struct SemanticTimezone {
3269 pub offset_minutes: i16,
3270}
3271
3272impl SemanticTimezone {
3273 pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
3275 TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
3276 }
3277
3278 pub fn offset_seconds(&self) -> i32 {
3279 i32::from(self.offset_minutes) * 60
3280 }
3281}
3282
3283impl From<&TimezoneValue> for SemanticTimezone {
3284 fn from(tz: &TimezoneValue) -> Self {
3285 Self {
3286 offset_minutes: tz.offset_minutes,
3287 }
3288 }
3289}
3290
3291impl From<&SemanticTimezone> for TimezoneValue {
3292 fn from(tz: &SemanticTimezone) -> Self {
3293 Self {
3294 offset_minutes: tz.offset_minutes,
3295 }
3296 }
3297}
3298
3299impl fmt::Display for SemanticTimezone {
3300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3301 TimezoneValue::from(self).fmt(f)
3302 }
3303}
3304
3305impl Serialize for SemanticTimezone {
3306 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3307 serializer.serialize_str(&self.to_string())
3308 }
3309}
3310
3311impl<'de> Deserialize<'de> for SemanticTimezone {
3312 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3313 let s = String::deserialize(deserializer)?;
3314 Self::from_str(&s).map_err(serde::de::Error::custom)
3315 }
3316}
3317
3318impl FromStr for SemanticTimezone {
3319 type Err = String;
3320
3321 fn from_str(s: &str) -> Result<Self, Self::Err> {
3322 Ok(Self::from(&TimezoneValue::from_str(s)?))
3323 }
3324}
3325
3326#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3328pub struct SemanticTime {
3329 pub hour: u32,
3330 pub minute: u32,
3331 pub second: u32,
3332 pub microsecond: u32,
3333 pub timezone: Option<SemanticTimezone>,
3334}
3335
3336impl fmt::Display for SemanticTime {
3337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3338 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
3339 if self.microsecond != 0 {
3340 write!(f, ".{:06}", self.microsecond)?;
3341 }
3342 if let Some(timezone) = &self.timezone {
3343 write!(f, "{}", timezone)?;
3344 }
3345 Ok(())
3346 }
3347}
3348
3349impl Serialize for SemanticTime {
3350 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3351 serializer.serialize_str(&self.to_string())
3352 }
3353}
3354
3355impl<'de> Deserialize<'de> for SemanticTime {
3356 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3357 let s = String::deserialize(deserializer)?;
3358 Self::from_str(&s).map_err(serde::de::Error::custom)
3359 }
3360}
3361
3362impl FromStr for SemanticTime {
3363 type Err = String;
3364
3365 fn from_str(s: &str) -> Result<Self, Self::Err> {
3366 Ok(time_to_semantic(&TimeValue::from_str(s)?))
3367 }
3368}
3369
3370#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3372pub struct SemanticDateTime {
3373 pub year: i32,
3374 pub month: u32,
3375 pub day: u32,
3376 pub hour: u32,
3377 pub minute: u32,
3378 pub second: u32,
3379 pub microsecond: u32,
3380 pub timezone: Option<SemanticTimezone>,
3381}
3382
3383impl fmt::Display for SemanticDateTime {
3384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3385 let has_time = self.hour != 0
3386 || self.minute != 0
3387 || self.second != 0
3388 || self.microsecond != 0
3389 || self.timezone.is_some();
3390 if !has_time {
3391 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
3392 } else {
3393 write!(
3394 f,
3395 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
3396 self.year, self.month, self.day, self.hour, self.minute, self.second
3397 )?;
3398 if self.microsecond != 0 {
3399 write!(f, ".{:06}", self.microsecond)?;
3400 }
3401 if let Some(tz) = &self.timezone {
3402 write!(f, "{}", tz)?;
3403 }
3404 Ok(())
3405 }
3406 }
3407}
3408
3409impl Serialize for SemanticDateTime {
3410 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3411 serializer.serialize_str(&self.to_string())
3412 }
3413}
3414
3415impl<'de> Deserialize<'de> for SemanticDateTime {
3416 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3417 let s = String::deserialize(deserializer)?;
3418 Self::from_str(&s).map_err(serde::de::Error::custom)
3419 }
3420}
3421
3422impl FromStr for SemanticDateTime {
3423 type Err = String;
3424
3425 fn from_str(s: &str) -> Result<Self, Self::Err> {
3426 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
3427 }
3428}
3429
3430#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3434pub enum RawSuggestion {
3435 Value(ValueKind),
3436 Measure {
3438 magnitude: RationalInteger,
3439 unit_name: String,
3440 },
3441 UnitBound {
3444 value: ValueKind,
3445 unit_name: String,
3446 },
3447 List(Vec<RawSuggestion>),
3448}
3449
3450#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3457pub struct BoundValueKind {
3458 pub value: ValueKind,
3459 pub unit: Option<Arc<str>>,
3460}
3461
3462impl BoundValueKind {
3463 pub fn unbound(value: ValueKind) -> Self {
3464 Self { value, unit: None }
3465 }
3466
3467 pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
3468 Self { value, unit }
3469 }
3470
3471 pub fn to_literal(&self) -> LiteralValue {
3472 LiteralValue {
3473 value: self.value.clone(),
3474 }
3475 }
3476
3477 #[must_use]
3480 pub fn agree_or_left_binding(
3481 left: Option<&Arc<str>>,
3482 right: Option<&Arc<str>>,
3483 ) -> Option<Arc<str>> {
3484 match (left, right) {
3485 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
3486 (Some(a), Some(_)) => Some(Arc::clone(a)),
3487 (Some(a), None) => Some(Arc::clone(a)),
3488 (None, Some(b)) => Some(Arc::clone(b)),
3489 (None, None) => None,
3490 }
3491 }
3492}
3493
3494pub fn bound_value_kind_from_raw_suggestion(
3495 raw: RawSuggestion,
3496 specifications: &TypeSpecification,
3497 type_name: &str,
3498) -> Result<BoundValueKind, String> {
3499 enum Step<'a> {
3500 Visit(RawSuggestion, &'a TypeSpecification),
3501 CollectList {
3502 element: &'a Arc<LemmaType>,
3503 rows: usize,
3504 },
3505 }
3506
3507 let mut steps = vec![Step::Visit(raw, specifications)];
3508 let mut bounds: Vec<BoundValueKind> = Vec::new();
3509 while let Some(step) = steps.pop() {
3510 match step {
3511 Step::Visit(raw, specifications) => match raw {
3512 RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
3513 RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
3514 value,
3515 unit: Some(Arc::from(unit_name)),
3516 }),
3517 RawSuggestion::Measure {
3518 magnitude,
3519 unit_name,
3520 } => {
3521 let TypeSpecification::Measure { units, .. } = specifications else {
3522 unreachable!(
3523 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
3524 );
3525 };
3526 let canonical = measure_declared_bound_to_canonical(
3527 &magnitude, &unit_name, units, type_name, "suggest",
3528 )?;
3529 bounds.push(BoundValueKind {
3530 value: ValueKind::Measure(canonical),
3531 unit: Some(Arc::from(unit_name)),
3532 });
3533 }
3534 RawSuggestion::List(rows) => {
3535 let TypeSpecification::List { element, .. } = specifications else {
3536 unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
3537 };
3538 steps.push(Step::CollectList {
3539 element,
3540 rows: rows.len(),
3541 });
3542 for row in rows.into_iter().rev() {
3543 steps.push(Step::Visit(row, &element.specifications));
3544 }
3545 }
3546 },
3547 Step::CollectList { element, rows } => {
3548 let start = bounds
3549 .len()
3550 .checked_sub(rows)
3551 .expect("BUG: list rows missing from bound stack");
3552 let converted: Vec<TypedLiteral> = bounds
3553 .split_off(start)
3554 .iter()
3555 .map(|bound| typed_literal_from_bound(bound, element))
3556 .collect();
3557 bounds.push(BoundValueKind::unbound(ValueKind::List(converted)));
3558 }
3559 }
3560 }
3561 Ok(bounds.pop().expect("BUG: bound walk left no result"))
3562}
3563
3564#[must_use]
3568pub fn display_binding_unit(
3569 schema: &LemmaType,
3570 settled: Option<&BoundValueKind>,
3571 fill: Option<&BoundValueKind>,
3572 suggestion: Option<&BoundValueKind>,
3573) -> Option<String> {
3574 if schema.is_measure_range() || schema.is_ratio_range() {
3575 return None;
3576 }
3577 if let Some(unit) = schema.unit() {
3578 return Some(unit.to_string());
3579 }
3580 if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
3581 return Some(unit.to_string());
3582 }
3583 if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
3584 return Some(unit.to_string());
3585 }
3586 suggestion
3587 .and_then(|bound| bound.unit.as_ref())
3588 .map(|unit| unit.to_string())
3589}
3590
3591#[must_use]
3596pub fn lemma_type_with_display_binding(
3597 schema: &LemmaType,
3598 settled: Option<&BoundValueKind>,
3599 fill: Option<&BoundValueKind>,
3600 suggestion: Option<&BoundValueKind>,
3601) -> LemmaType {
3602 if schema.is_measure_range() || schema.is_ratio_range() {
3603 return schema.clone();
3604 }
3605 match display_binding_unit(schema, settled, fill, suggestion) {
3606 Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
3607 Some(_) | None => schema.clone(),
3608 }
3609}
3610
3611#[must_use]
3613pub fn lemma_type_arc_with_display_binding(
3614 schema: &Arc<LemmaType>,
3615 settled: Option<&BoundValueKind>,
3616 fill: Option<&BoundValueKind>,
3617 suggestion: Option<&BoundValueKind>,
3618) -> Arc<LemmaType> {
3619 if schema.is_measure_range() || schema.is_ratio_range() {
3620 return Arc::clone(schema);
3621 }
3622 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
3623 Some(unit) if schema.unit() != Some(unit.as_str()) => {
3624 Arc::new(schema.as_ref().clone().with_unit(unit))
3625 }
3626 Some(_) | None => Arc::clone(schema),
3627 }
3628}
3629
3630#[must_use]
3635pub fn typed_literal_from_bound(
3636 bound: &BoundValueKind,
3637 lemma_type: &Arc<LemmaType>,
3638) -> TypedLiteral {
3639 let lemma_type = match (&bound.unit, &lemma_type.specifications) {
3640 (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
3641 if lemma_type.unit() != Some(unit.as_ref()) =>
3642 {
3643 Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
3644 }
3645 _ => Arc::clone(lemma_type),
3646 };
3647 TypedLiteral {
3648 value: bound.value.clone(),
3649 lemma_type,
3650 }
3651}
3652
3653#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3660pub enum ValueKind {
3661 Number(RationalInteger),
3662 Measure(RationalInteger),
3668 Text(String),
3669 Date(SemanticDateTime),
3670 Time(SemanticTime),
3671 Boolean(bool),
3672 Ratio(RationalInteger),
3674 Range(Box<TypedLiteral>, Box<TypedLiteral>),
3676 Spec(SpecValue),
3678 List(Vec<TypedLiteral>),
3680}
3681
3682impl ValueKind {
3683 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
3685 match self {
3686 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
3687 n.try_to_decimal().map_err(|failure| failure.to_string())
3688 }
3689 other => Err(format!("expected numeric value kind, got {other}")),
3690 }
3691 }
3692
3693 pub fn structural_byte_size(&self) -> usize {
3698 fn rational_byte_estimate(r: &RationalInteger) -> usize {
3699 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
3700 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
3701 numer_bytes.max(1) + denom_bytes.max(1)
3702 }
3703 match self {
3704 ValueKind::Number(r) => rational_byte_estimate(r),
3705 ValueKind::Measure(r) => rational_byte_estimate(r),
3706 ValueKind::Ratio(r) => rational_byte_estimate(r),
3707 ValueKind::Text(s) => s.len(),
3708 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
3712 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
3713 }
3714 ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
3715 ValueKind::Spec(SpecValue::Row(row)) => row
3716 .bindings
3717 .values()
3718 .map(|binding| match binding {
3719 crate::computation::OperationResult::Value(bound) => {
3720 bound.value.structural_byte_size()
3721 }
3722 crate::computation::OperationResult::Veto(_) => 0,
3723 })
3724 .sum(),
3725 ValueKind::List(rows) => rows
3726 .iter()
3727 .map(|row| row.value.structural_byte_size())
3728 .sum(),
3729 }
3730 }
3731}
3732
3733fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
3734 rational.display_str()
3735}
3736
3737fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
3738 use crate::parsing::ast::Value;
3739 match rational.try_to_decimal() {
3740 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
3741 Err(_) => format!("{} {}", rational.display_str(), unit),
3742 }
3743}
3744
3745impl fmt::Display for ValueKind {
3746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3747 match self {
3748 ValueKind::Number(rational) => {
3749 write!(f, "{}", format_rational_magnitude_for_display(rational))
3750 }
3751 ValueKind::Measure(rational) => {
3752 write!(f, "{}", format_rational_magnitude_for_display(rational))
3753 }
3754 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3755 ValueKind::Ratio(rational) => {
3756 write!(f, "{}", format_rational_magnitude_for_display(rational))
3757 }
3758 ValueKind::Date(dt) => write!(f, "{}", dt),
3759 ValueKind::Time(t) => write!(
3760 f,
3761 "{}",
3762 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3763 hour: t.hour as u8,
3764 minute: t.minute as u8,
3765 second: t.second as u8,
3766 microsecond: t.microsecond,
3767 timezone: t
3768 .timezone
3769 .as_ref()
3770 .map(crate::parsing::ast::TimezoneValue::from),
3771 })
3772 ),
3773 ValueKind::Boolean(b) => write!(f, "{}", b),
3774 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3775 ValueKind::Spec(value) => write!(f, "{value}"),
3776 ValueKind::List(rows) => {
3777 write!(f, "list[")?;
3778 for (i, row) in rows.iter().enumerate() {
3779 if i > 0 {
3780 write!(f, ", ")?;
3781 }
3782 write!(f, "{row}")?;
3783 }
3784 write!(f, "]")
3785 }
3786 }
3787 }
3788}
3789
3790#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3801pub struct PathSegment {
3802 pub uses: String,
3804 pub repository: Option<String>,
3807 pub spec: String,
3809}
3810
3811#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3815pub struct SpecInstance {
3816 pub prefix: Vec<PathSegment>,
3817}
3818
3819impl SpecInstance {
3820 pub fn structural_byte_size(&self) -> usize {
3821 self.prefix
3822 .iter()
3823 .map(|segment| {
3824 segment.uses.len()
3825 + segment.spec.len()
3826 + segment.repository.as_ref().map_or(0, String::len)
3827 })
3828 .sum()
3829 }
3830}
3831
3832impl fmt::Display for SpecInstance {
3833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3834 for (index, segment) in self.prefix.iter().enumerate() {
3835 if index > 0 {
3836 write!(f, ".")?;
3837 }
3838 write!(f, "{}", segment.uses)?;
3839 }
3840 Ok(())
3841 }
3842}
3843
3844#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3849pub enum SpecValue {
3850 Fixed(SpecInstance),
3851 #[serde(skip)]
3852 Row(Arc<RowInstance>),
3853}
3854
3855impl fmt::Display for SpecValue {
3856 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3857 match self {
3858 SpecValue::Fixed(instance) => write!(f, "{instance}"),
3859 SpecValue::Row(row) => write!(f, "{}", row.origin),
3860 }
3861 }
3862}
3863
3864#[derive(Debug, Clone, PartialEq, Eq)]
3874pub struct RowInstance {
3875 pub identity: SpecIdentity,
3876 pub origin: RowOrigin,
3877 pub bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3879}
3880
3881impl std::hash::Hash for RowInstance {
3882 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3883 self.identity.hash(state);
3884 self.origin.hash(state);
3885 self.bindings.len().hash(state);
3886 let mut entries: Vec<_> = self.bindings.iter().collect();
3887 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
3888 for (path, result) in entries {
3889 path.hash(state);
3890 result.hash(state);
3891 }
3892 }
3893}
3894
3895#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3897pub enum RowOrigin {
3898 Numbered { list: String, number: usize },
3900 Appended { number: usize },
3902}
3903
3904#[must_use]
3906pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
3907 format!("{list_key}.{number}")
3908}
3909
3910#[must_use]
3912pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
3913 format!("{}.{field_key}", numbered_row_origin(list_key, number))
3914}
3915
3916impl fmt::Display for RowOrigin {
3917 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3918 match self {
3919 RowOrigin::Numbered { list, number } => {
3920 write!(f, "{}", numbered_row_origin(list, *number))
3921 }
3922 RowOrigin::Appended { number } => write!(f, "appended row {number}"),
3923 }
3924 }
3925}
3926
3927#[must_use]
3929pub fn row_literal(
3930 identity: SpecIdentity,
3931 origin: RowOrigin,
3932 bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3933 lemma_type: Arc<LemmaType>,
3934) -> TypedLiteral {
3935 TypedLiteral {
3936 value: ValueKind::Spec(SpecValue::Row(Arc::new(RowInstance {
3937 identity,
3938 origin,
3939 bindings,
3940 }))),
3941 lemma_type,
3942 }
3943}
3944
3945#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3947pub enum RowEnd {
3948 Data(DataPath),
3949 Rule(RulePath),
3950}
3951
3952impl fmt::Display for RowEnd {
3953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3954 match self {
3955 RowEnd::Data(path) => write!(f, "{}", path.input_key()),
3956 RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
3957 }
3958 }
3959}
3960
3961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3963pub enum SpecMemberEnd {
3964 Rule(RulePath),
3965 Data(DataPath),
3966 Instance {
3968 instance: SpecInstance,
3969 identity: SpecIdentity,
3970 },
3971}
3972
3973#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3975pub struct SpecMemberTarget {
3976 pub instance: SpecInstance,
3977 pub end: SpecMemberEnd,
3978}
3979
3980#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3984pub struct DataPath {
3985 pub segments: Vec<PathSegment>,
3987 pub data: String,
3989}
3990
3991impl DataPath {
3992 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3994 Self { segments, data }
3995 }
3996
3997 pub fn local(data: String) -> Self {
3999 Self {
4000 segments: vec![],
4001 data,
4002 }
4003 }
4004
4005 pub fn input_key_segments(&self) -> impl Iterator<Item = &str> {
4007 self.segments
4008 .iter()
4009 .map(|segment| segment.uses.as_str())
4010 .chain(std::iter::once(self.data.as_str()))
4011 }
4012
4013 pub fn input_key(&self) -> String {
4016 let mut segments = self.input_key_segments();
4017 let mut s = segments
4018 .next()
4019 .expect("BUG: DataPath always has a data name segment")
4020 .to_string();
4021 for segment in segments {
4022 s.push('.');
4023 s.push_str(segment);
4024 }
4025 s
4026 }
4027}
4028
4029#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4033pub struct RulePath {
4034 pub segments: Vec<PathSegment>,
4036 pub rule: String,
4038}
4039
4040impl RulePath {
4041 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
4043 Self { segments, rule }
4044 }
4045
4046 pub fn input_key(&self) -> String {
4049 if self.segments.is_empty() {
4050 return self.rule.clone();
4051 }
4052 let mut s = String::new();
4053 for segment in &self.segments {
4054 s.push_str(&segment.uses);
4055 s.push('.');
4056 }
4057 s.push_str(&self.rule);
4058 s
4059 }
4060}
4061
4062pub type Expression = ExprTree<ExpressionKind>;
4075
4076#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4078#[serde(rename_all = "snake_case")]
4079pub enum ExpressionKind {
4080 Literal(Box<TypedLiteral>),
4082 DataPath(DataPath),
4084 RulePath(RulePath),
4086 LogicalAnd {
4088 first: ExprId,
4089 steps: Vec<ChainStep<()>>,
4090 },
4091 Arithmetic {
4093 first: ExprId,
4094 steps: Vec<ChainStep<ArithmeticComputation>>,
4095 },
4096 Comparison(ExprId, ComparisonComputation, ExprId),
4097 UnitConversion(ExprId, SemanticConversionTarget),
4098 LogicalNegation(ExprId, NegationType),
4099 MathematicalComputation(MathematicalComputation, ExprId),
4100 Veto(VetoExpression),
4101 Now,
4103 DateRelative(DateRelativeKind, ExprId),
4105 DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
4107 RangeLiteral(ExprId, ExprId),
4108 PastFutureRange(DateRelativeKind, ExprId),
4109 RangeContainment(ExprId, ExprId),
4110 RangeBound(RangeBound, ExprId),
4112 ResultIsVeto(ExprId),
4114 SpecMember {
4121 base: ExprId,
4122 path: Vec<String>,
4123 name: String,
4124 targets: Vec<SpecMemberTarget>,
4125 },
4126 RowField {
4128 level: u32,
4129 identity: SpecIdentity,
4130 end: RowEnd,
4131 },
4132 Piecewise(Vec<(ExprId, ExprId)>),
4135 ListCommand {
4138 list: ExprId,
4139 op: ListOp,
4140 row_level: Option<u32>,
4141 },
4142}
4143
4144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4146pub enum ListOp {
4147 Find {
4148 keys: Vec<ExprId>,
4149 },
4150 First,
4151 Last,
4152 Count,
4153 Reduce {
4154 kind: ListReduce,
4155 argument: Option<ExprId>,
4156 },
4157 Predicate {
4158 kind: ListPredKind,
4159 op: crate::parsing::ast::RowCondition,
4160 right: ExprId,
4161 },
4162 InstancePredicate {
4163 kind: ListPredKind,
4164 expr: ExprId,
4165 },
4166 Sort {
4167 descending: bool,
4168 argument: Option<ExprId>,
4169 },
4170 Top {
4171 n: ExprId,
4172 },
4173 With {
4174 rows: Vec<ExprId>,
4175 },
4176 WithRow {
4178 fields: Vec<(DataPath, ExprId)>,
4179 },
4180}
4181
4182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4183pub enum ListReduce {
4184 Sum,
4185 Avg,
4186 Product,
4187 Min,
4188 Max,
4189}
4190
4191impl ListReduce {
4192 pub fn keyword(self) -> &'static str {
4193 match self {
4194 ListReduce::Sum => "sum",
4195 ListReduce::Avg => "avg",
4196 ListReduce::Product => "product",
4197 ListReduce::Min => "min",
4198 ListReduce::Max => "max",
4199 }
4200 }
4201}
4202
4203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4204pub enum ListPredKind {
4205 Any,
4206 All,
4207 Where,
4208}
4209
4210impl ListPredKind {
4211 pub fn keyword(self) -> &'static str {
4212 match self {
4213 ListPredKind::Any => "any",
4214 ListPredKind::All => "all",
4215 ListPredKind::Where => "where",
4216 }
4217 }
4218}
4219
4220impl ExprKind for ExpressionKind {
4221 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4222 match self {
4223 ExpressionKind::Literal(_)
4224 | ExpressionKind::DataPath(_)
4225 | ExpressionKind::RulePath(_)
4226 | ExpressionKind::Veto(_)
4227 | ExpressionKind::Now
4228 | ExpressionKind::RowField { .. } => {}
4229 ExpressionKind::LogicalAnd { first, steps } => {
4230 f(*first);
4231 steps.iter().for_each(|step| f(step.operand));
4232 }
4233 ExpressionKind::Arithmetic { first, steps } => {
4234 f(*first);
4235 steps.iter().for_each(|step| f(step.operand));
4236 }
4237 ExpressionKind::Comparison(left, _, right)
4238 | ExpressionKind::RangeLiteral(left, right)
4239 | ExpressionKind::RangeContainment(left, right) => {
4240 f(*left);
4241 f(*right);
4242 }
4243 ExpressionKind::UnitConversion(inner, _)
4244 | ExpressionKind::LogicalNegation(inner, _)
4245 | ExpressionKind::MathematicalComputation(_, inner)
4246 | ExpressionKind::DateRelative(_, inner)
4247 | ExpressionKind::DateCalendar(_, _, inner)
4248 | ExpressionKind::PastFutureRange(_, inner)
4249 | ExpressionKind::RangeBound(_, inner)
4250 | ExpressionKind::ResultIsVeto(inner)
4251 | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
4252 ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
4253 f(*condition);
4254 f(*result);
4255 }),
4256 ExpressionKind::ListCommand { list, op, .. } => {
4257 f(*list);
4258 op.for_each_child(f);
4259 }
4260 }
4261 }
4262
4263 fn children_mut(&mut self) -> Vec<&mut ExprId> {
4264 match self {
4265 ExpressionKind::Literal(_)
4266 | ExpressionKind::DataPath(_)
4267 | ExpressionKind::RulePath(_)
4268 | ExpressionKind::Veto(_)
4269 | ExpressionKind::Now
4270 | ExpressionKind::RowField { .. } => Vec::new(),
4271 ExpressionKind::LogicalAnd { first, steps } => std::iter::once(first)
4272 .chain(steps.iter_mut().map(|step| &mut step.operand))
4273 .collect(),
4274 ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
4275 .chain(steps.iter_mut().map(|step| &mut step.operand))
4276 .collect(),
4277 ExpressionKind::Comparison(left, _, right)
4278 | ExpressionKind::RangeLiteral(left, right)
4279 | ExpressionKind::RangeContainment(left, right) => vec![left, right],
4280 ExpressionKind::UnitConversion(inner, _)
4281 | ExpressionKind::LogicalNegation(inner, _)
4282 | ExpressionKind::MathematicalComputation(_, inner)
4283 | ExpressionKind::DateRelative(_, inner)
4284 | ExpressionKind::DateCalendar(_, _, inner)
4285 | ExpressionKind::PastFutureRange(_, inner)
4286 | ExpressionKind::RangeBound(_, inner)
4287 | ExpressionKind::ResultIsVeto(inner) => vec![inner],
4288 ExpressionKind::SpecMember { base, .. } => vec![base],
4289 ExpressionKind::Piecewise(arms) => arms
4290 .iter_mut()
4291 .flat_map(|(condition, result)| [condition, result])
4292 .collect(),
4293 ExpressionKind::ListCommand { list, op, .. } => {
4294 let mut children = vec![list];
4295 children.extend(op.children_mut());
4296 children
4297 }
4298 }
4299 }
4300}
4301
4302impl ListOp {
4303 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4304 match self {
4305 ListOp::First | ListOp::Last | ListOp::Count => {}
4306 ListOp::Find { keys } => keys.iter().copied().for_each(f),
4307 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4308 argument.iter().copied().for_each(f)
4309 }
4310 ListOp::Predicate { right, .. } => f(*right),
4311 ListOp::InstancePredicate { expr, .. } => f(*expr),
4312 ListOp::Top { n } => f(*n),
4313 ListOp::With { rows } => rows.iter().copied().for_each(f),
4314 ListOp::WithRow { fields } => fields.iter().for_each(|(_, value)| f(*value)),
4315 }
4316 }
4317
4318 fn children_mut(&mut self) -> Vec<&mut ExprId> {
4319 match self {
4320 ListOp::First | ListOp::Last | ListOp::Count => Vec::new(),
4321 ListOp::Find { keys } => keys.iter_mut().collect(),
4322 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4323 argument.iter_mut().collect()
4324 }
4325 ListOp::Predicate { right, .. } => vec![right],
4326 ListOp::InstancePredicate { expr, .. } => vec![expr],
4327 ListOp::Top { n } => vec![n],
4328 ListOp::With { rows } => rows.iter_mut().collect(),
4329 ListOp::WithRow { fields } => fields.iter_mut().map(|(_, value)| value).collect(),
4330 }
4331 }
4332}
4333
4334#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4340pub enum TypeDefiningSpec {
4341 Local,
4343 Import,
4345}
4346
4347#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4354pub struct TypeFamily {
4355 pub repository: Option<String>,
4356 pub spec: String,
4357 pub effective: crate::parsing::ast::EffectiveDate,
4358 pub name: String,
4359}
4360
4361impl TypeFamily {
4362 #[must_use]
4363 pub fn declared_in(spec: &SpecIdentity, name: &str) -> Self {
4364 Self {
4365 repository: spec.repository.clone(),
4366 spec: spec.spec.clone(),
4367 effective: spec.effective.clone(),
4368 name: name.to_string(),
4369 }
4370 }
4371
4372 #[must_use]
4374 pub fn is_declared_in(&self, spec: &SpecIdentity) -> bool {
4375 self.repository == spec.repository
4376 && self.spec == spec.spec
4377 && self.effective == spec.effective
4378 }
4379
4380 #[must_use]
4382 pub fn without_effective(&self) -> Self {
4383 Self {
4384 repository: self.repository.clone(),
4385 spec: self.spec.clone(),
4386 effective: crate::parsing::ast::EffectiveDate::Origin,
4387 name: self.name.clone(),
4388 }
4389 }
4390}
4391
4392impl fmt::Display for TypeFamily {
4394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4395 if let Some(repository) = &self.repository {
4396 write!(f, "{repository} ")?;
4397 }
4398 write!(f, "{}", self.spec)?;
4399 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
4400 write!(f, " {at}")?;
4401 }
4402 write!(f, ".{}", self.name)
4403 }
4404}
4405
4406#[derive(Clone, Debug, Serialize, Deserialize)]
4408pub enum TypeExtends {
4409 Primitive,
4411 Custom {
4414 parent: String,
4415 family: TypeFamily,
4416 defining_spec: TypeDefiningSpec,
4417 },
4418}
4419
4420impl PartialEq for TypeExtends {
4421 fn eq(&self, other: &Self) -> bool {
4422 match (self, other) {
4423 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
4424 (
4425 TypeExtends::Custom {
4426 parent: lp,
4427 family: lf,
4428 defining_spec: ld,
4429 },
4430 TypeExtends::Custom {
4431 parent: rp,
4432 family: rf,
4433 defining_spec: rd,
4434 },
4435 ) => lp == rp && lf == rf && ld == rd,
4436 _ => false,
4437 }
4438 }
4439}
4440
4441impl Eq for TypeExtends {}
4442
4443impl std::hash::Hash for TypeExtends {
4444 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4445 match self {
4446 TypeExtends::Primitive => {
4447 0u8.hash(state);
4448 }
4449 TypeExtends::Custom {
4450 parent,
4451 family,
4452 defining_spec,
4453 } => {
4454 1u8.hash(state);
4455 parent.hash(state);
4456 family.hash(state);
4457 defining_spec.hash(state);
4458 }
4459 }
4460 }
4461}
4462
4463impl TypeExtends {
4464 #[must_use]
4466 pub fn custom_local(parent: String, family: TypeFamily) -> Self {
4467 TypeExtends::Custom {
4468 parent,
4469 family,
4470 defining_spec: TypeDefiningSpec::Local,
4471 }
4472 }
4473
4474 #[must_use]
4476 pub fn parent_name(&self) -> Option<&str> {
4477 match self {
4478 TypeExtends::Primitive => None,
4479 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
4480 }
4481 }
4482
4483 #[must_use]
4484 fn with_families_without_effective(&self) -> Self {
4485 match self {
4486 TypeExtends::Primitive => TypeExtends::Primitive,
4487 TypeExtends::Custom {
4488 parent,
4489 family,
4490 defining_spec,
4491 } => TypeExtends::Custom {
4492 parent: parent.clone(),
4493 family: family.without_effective(),
4494 defining_spec: defining_spec.clone(),
4495 },
4496 }
4497 }
4498}
4499
4500#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4505pub struct LemmaType {
4506 pub name: Option<String>,
4508 pub specifications: TypeSpecification,
4510 pub extends: TypeExtends,
4512}
4513
4514impl LemmaType {
4515 pub fn map_measure<F>(self, f: F) -> Self
4519 where
4520 F: FnOnce(
4521 MeasureUnits,
4522 Option<BaseMeasureVector>,
4523 ) -> (MeasureUnits, Option<BaseMeasureVector>),
4524 {
4525 let LemmaType {
4526 name,
4527 specifications,
4528 extends,
4529 } = self;
4530 let specifications = match specifications {
4531 TypeSpecification::Measure {
4532 minimum,
4533 maximum,
4534 decimals,
4535 units,
4536 traits,
4537 decomposition,
4538 unit,
4539 help,
4540 } => {
4541 let (units, decomposition) = f(units, decomposition);
4542 TypeSpecification::Measure {
4543 minimum,
4544 maximum,
4545 decimals,
4546 units,
4547 traits,
4548 decomposition,
4549 unit,
4550 help,
4551 }
4552 }
4553 other => other,
4554 };
4555 LemmaType {
4556 name,
4557 specifications,
4558 extends,
4559 }
4560 }
4561
4562 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
4564 Self {
4565 name: Some(name),
4566 specifications,
4567 extends,
4568 }
4569 }
4570
4571 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
4573 Self {
4574 name: None,
4575 specifications,
4576 extends,
4577 }
4578 }
4579
4580 pub fn primitive(specifications: TypeSpecification) -> Self {
4582 Self {
4583 name: None,
4584 specifications,
4585 extends: TypeExtends::Primitive,
4586 }
4587 }
4588
4589 #[must_use]
4593 pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
4594 let unit_name = unit_name.into();
4595 match &mut self.specifications {
4596 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4597 *unit = Some(unit_name);
4598 }
4599 other => panic!(
4600 "BUG: with_unit called on non-measure/non-ratio type {}",
4601 other
4602 ),
4603 }
4604 self
4605 }
4606
4607 #[must_use]
4609 pub fn unit(&self) -> Option<&str> {
4610 match &self.specifications {
4611 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4612 unit.as_deref()
4613 }
4614 _ => None,
4615 }
4616 }
4617
4618 pub fn name(&self) -> String {
4620 self.name
4621 .clone()
4622 .unwrap_or_else(|| self.specifications.to_string())
4623 }
4624
4625 pub fn is_boolean(&self) -> bool {
4627 matches!(&self.specifications, TypeSpecification::Boolean { .. })
4628 }
4629
4630 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
4631 matches!(
4632 (kind, &self.specifications),
4633 (PrimitiveKind::Number, TypeSpecification::Number { .. })
4634 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
4635 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
4636 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
4637 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
4638 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
4639 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
4640 )
4641 }
4642
4643 pub fn is_measure(&self) -> bool {
4645 matches!(&self.specifications, TypeSpecification::Measure { .. })
4646 }
4647
4648 pub fn is_measure_range(&self) -> bool {
4649 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
4650 }
4651
4652 pub fn is_number(&self) -> bool {
4654 matches!(&self.specifications, TypeSpecification::Number { .. })
4655 }
4656
4657 pub fn is_number_range(&self) -> bool {
4658 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
4659 }
4660
4661 pub fn is_numeric(&self) -> bool {
4663 matches!(
4664 &self.specifications,
4665 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
4666 )
4667 }
4668
4669 pub fn is_text(&self) -> bool {
4671 matches!(&self.specifications, TypeSpecification::Text { .. })
4672 }
4673
4674 pub fn is_date(&self) -> bool {
4676 matches!(&self.specifications, TypeSpecification::Date { .. })
4677 }
4678
4679 pub fn is_date_range(&self) -> bool {
4680 matches!(&self.specifications, TypeSpecification::DateRange { .. })
4681 }
4682
4683 pub fn is_time_range(&self) -> bool {
4684 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
4685 }
4686
4687 pub fn is_time(&self) -> bool {
4689 matches!(&self.specifications, TypeSpecification::Time { .. })
4690 }
4691
4692 pub fn has_trait_duration(&self) -> bool {
4693 match &self.specifications {
4694 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
4695 _ => false,
4696 }
4697 }
4698
4699 pub fn is_duration_like_measure(&self) -> bool {
4700 if !self.is_measure() {
4701 return false;
4702 }
4703 if self.has_trait_duration() {
4704 return true;
4705 }
4706 self.is_anonymous_measure()
4707 && self
4708 .measure_type_decomposition()
4709 .is_some_and(|d| *d == duration_decomposition())
4710 }
4711
4712 pub fn is_duration_like(&self) -> bool {
4713 self.is_duration_like_measure()
4714 }
4715
4716 pub fn has_trait_calendar(&self) -> bool {
4717 match &self.specifications {
4718 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
4719 _ => false,
4720 }
4721 }
4722
4723 pub fn is_calendar_like_measure(&self) -> bool {
4724 if !self.is_measure() {
4725 return false;
4726 }
4727 if self.has_trait_calendar() {
4728 return true;
4729 }
4730 self.is_anonymous_measure()
4731 && self
4732 .measure_type_decomposition()
4733 .is_some_and(|d| *d == calendar_decomposition())
4734 }
4735
4736 pub fn is_calendar_like(&self) -> bool {
4737 self.is_calendar_like_measure()
4738 }
4739
4740 pub fn is_ratio(&self) -> bool {
4742 matches!(&self.specifications, TypeSpecification::Ratio { .. })
4743 }
4744
4745 pub fn is_ratio_range(&self) -> bool {
4746 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
4747 }
4748
4749 pub fn is_calendar_measure_range(&self) -> bool {
4750 matches!(
4751 &self.specifications,
4752 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
4753 if *decomposition == calendar_decomposition()
4754 )
4755 }
4756
4757 pub fn is_calendar_like_range(&self) -> bool {
4758 self.is_calendar_measure_range()
4759 }
4760
4761 pub fn is_range(&self) -> bool {
4762 matches!(
4763 &self.specifications,
4764 TypeSpecification::DateRange { .. }
4765 | TypeSpecification::TimeRange { .. }
4766 | TypeSpecification::NumberRange { .. }
4767 | TypeSpecification::MeasureRange { .. }
4768 | TypeSpecification::RatioRange { .. }
4769 )
4770 }
4771
4772 pub fn is_spec(&self) -> bool {
4774 matches!(&self.specifications, TypeSpecification::Spec { .. })
4775 }
4776
4777 pub fn is_list(&self) -> bool {
4778 matches!(&self.specifications, TypeSpecification::List { .. })
4779 }
4780
4781 pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
4782 match &self.specifications {
4783 TypeSpecification::List { element, .. } => Some(&element.specifications),
4784 _ => None,
4785 }
4786 }
4787
4788 pub fn list_of(element: Arc<LemmaType>) -> Self {
4789 Self {
4790 name: Some(format!("{} list", element.name())),
4791 specifications: TypeSpecification::List {
4792 element,
4793 key: ListKey::Numbered,
4794 },
4795 extends: TypeExtends::Primitive,
4796 }
4797 }
4798
4799 pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
4801 let TypeSpecification::List { element, .. } = &self.specifications else {
4802 panic!(
4803 "BUG: list_element_lemma_type on non-list type {}",
4804 self.name()
4805 );
4806 };
4807 element
4808 }
4809
4810 #[must_use]
4812 pub fn spec_identity(&self) -> Option<&SpecIdentity> {
4813 match &self.specifications {
4814 TypeSpecification::Spec { spec } => Some(spec),
4815 _ => None,
4816 }
4817 }
4818
4819 pub fn vetoed(&self) -> bool {
4821 matches!(&self.specifications, TypeSpecification::Veto { .. })
4822 }
4823
4824 pub fn is_undetermined(&self) -> bool {
4826 matches!(&self.specifications, TypeSpecification::Undetermined)
4827 }
4828
4829 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
4831 use TypeSpecification::*;
4832 matches!(
4833 (&self.specifications, &other.specifications),
4834 (Boolean { .. }, Boolean { .. })
4835 | (Number { .. }, Number { .. })
4836 | (NumberRange { .. }, NumberRange { .. })
4837 | (Measure { .. }, Measure { .. })
4838 | (MeasureRange { .. }, MeasureRange { .. })
4839 | (Text { .. }, Text { .. })
4840 | (Date { .. }, Date { .. })
4841 | (DateRange { .. }, DateRange { .. })
4842 | (Time { .. }, Time { .. })
4843 | (TimeRange { .. }, TimeRange { .. })
4844 | (Ratio { .. }, Ratio { .. })
4845 | (RatioRange { .. }, RatioRange { .. })
4846 | (Veto { .. }, Veto { .. })
4847 | (Undetermined, Undetermined)
4848 | (Spec { .. }, Spec { .. })
4849 | (List { .. }, List { .. })
4850 )
4851 }
4852
4853 #[must_use]
4855 pub fn family(&self) -> Option<&TypeFamily> {
4856 match &self.extends {
4857 TypeExtends::Custom { family, .. } => Some(family),
4858 TypeExtends::Primitive => None,
4859 }
4860 }
4861
4862 #[must_use]
4864 pub fn measure_family(&self) -> Option<&TypeFamily> {
4865 self.is_measure().then(|| self.family()).flatten()
4866 }
4867
4868 #[must_use]
4870 pub fn ratio_family(&self) -> Option<&TypeFamily> {
4871 self.is_ratio().then(|| self.family()).flatten()
4872 }
4873
4874 #[must_use]
4876 pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
4877 self.measure_family().or_else(|| self.ratio_family())
4878 }
4879
4880 #[must_use]
4882 pub fn measure_family_name(&self) -> Option<&str> {
4883 self.measure_family().map(|family| family.name.as_str())
4884 }
4885
4886 #[must_use]
4888 pub fn ratio_family_name(&self) -> Option<&str> {
4889 self.ratio_family().map(|family| family.name.as_str())
4890 }
4891
4892 #[must_use]
4896 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
4897 if !self.has_same_base_type(other) {
4898 return false;
4899 }
4900 if self.is_measure() {
4901 return self.measure_type_decomposition() == other.measure_type_decomposition();
4902 }
4903 if let (
4904 TypeSpecification::Spec { spec: self_spec },
4905 TypeSpecification::Spec { spec: other_spec },
4906 ) = (&self.specifications, &other.specifications)
4907 {
4908 return self_spec == other_spec;
4909 }
4910 if let (
4911 TypeSpecification::List {
4912 element: self_element,
4913 key: self_key,
4914 },
4915 TypeSpecification::List {
4916 element: other_element,
4917 key: other_key,
4918 },
4919 ) = (&self.specifications, &other.specifications)
4920 {
4921 return self_key == other_key && self_element.same_value_type(other_element);
4922 }
4923 if self.is_measure_range() {
4924 let self_element = self
4925 .specifications
4926 .element_from_range()
4927 .expect("BUG: MeasureRange always defines element_from_range");
4928 let other_element = other
4929 .specifications
4930 .element_from_range()
4931 .expect("BUG: MeasureRange always defines element_from_range");
4932 match (&self_element, &other_element) {
4933 (
4934 TypeSpecification::Measure {
4935 decomposition: self_decomp,
4936 ..
4937 },
4938 TypeSpecification::Measure {
4939 decomposition: other_decomp,
4940 ..
4941 },
4942 ) => self_decomp == other_decomp,
4943 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
4944 }
4945 } else {
4946 true
4947 }
4948 }
4949
4950 #[must_use]
4952 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
4953 if !self.is_measure() || !other.is_measure() {
4954 return false;
4955 }
4956 match (self.measure_family(), other.measure_family()) {
4957 (Some(self_family), Some(other_family)) => self_family == other_family,
4958 _ => false,
4959 }
4960 }
4961
4962 #[must_use]
4966 pub fn same_interface(&self, other: &Self) -> bool {
4967 self.with_families_without_effective() == other.with_families_without_effective()
4968 }
4969
4970 #[must_use]
4972 pub fn with_families_without_effective(&self) -> Self {
4973 Self {
4974 name: self.name.clone(),
4975 specifications: self.specifications.with_families_without_effective(),
4976 extends: self.extends.with_families_without_effective(),
4977 }
4978 }
4979
4980 #[must_use]
4981 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
4982 if !self.is_measure() || !other.is_measure() {
4983 return false;
4984 }
4985 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
4986 return false;
4987 }
4988 match (
4989 self.measure_type_decomposition(),
4990 other.measure_type_decomposition(),
4991 ) {
4992 (Some(a), Some(b)) => a == b,
4993 _ => false,
4994 }
4995 }
4996
4997 pub fn veto_type() -> Self {
4999 Self::primitive(TypeSpecification::veto())
5000 }
5001
5002 pub fn undetermined_type() -> Self {
5005 Self::primitive(TypeSpecification::Undetermined)
5006 }
5007
5008 pub fn decimal_places(&self) -> Option<u8> {
5011 match &self.specifications {
5012 TypeSpecification::Number { decimals, .. } => *decimals,
5013 TypeSpecification::Measure { decimals, .. } => *decimals,
5014 TypeSpecification::Ratio { decimals, .. } => *decimals,
5015 _ => None,
5016 }
5017 }
5018
5019 pub fn try_rational_as_decimal(
5024 &self,
5025 magnitude: &crate::computation::rational::RationalInteger,
5026 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5027 let decimal = magnitude.try_to_decimal()?;
5028 Ok(format_decimal_for_api(decimal, self.decimal_places()))
5029 }
5030
5031 pub fn try_measure_canonical_as_decimal_in_unit(
5033 &self,
5034 canonical_magnitude: &crate::computation::rational::RationalInteger,
5035 unit_name: &str,
5036 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5037 use crate::computation::rational::checked_div;
5038 let unit_factor = self.measure_unit_factor(unit_name);
5039 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
5040 self.try_rational_as_decimal(&magnitude_in_unit)
5041 }
5042
5043 pub fn try_ratio_canonical_as_decimal_in_unit(
5045 &self,
5046 canonical_magnitude: &crate::computation::rational::RationalInteger,
5047 unit_name: &str,
5048 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5049 use crate::computation::rational::checked_mul;
5050 let units = match &self.specifications {
5051 TypeSpecification::Ratio { units, .. } => units,
5052 _ => unreachable!(
5053 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
5054 self.name()
5055 ),
5056 };
5057 let ratio_unit = units
5058 .iter()
5059 .find(|unit| unit.name == unit_name)
5060 .unwrap_or_else(|| {
5061 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
5062 unreachable!(
5063 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
5064 unit_name,
5065 self.name(),
5066 valid.join(", ")
5067 )
5068 });
5069 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
5070 self.try_rational_as_decimal(&magnitude_in_unit)
5071 }
5072
5073 pub fn example_value(&self) -> &'static str {
5075 match &self.specifications {
5076 TypeSpecification::Text { .. } => "\"hello world\"",
5077 TypeSpecification::Measure { .. } => "12.50 eur",
5078 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
5079 TypeSpecification::Number { .. } => "3.14",
5080 TypeSpecification::NumberRange { .. } => "0...100",
5081 TypeSpecification::Boolean { .. } => "true",
5082 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
5083 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
5084 TypeSpecification::TimeRange { .. } => "09:00...17:00",
5085 TypeSpecification::Veto { .. } => "veto",
5086 TypeSpecification::Time { .. } => "14:30:00",
5087 TypeSpecification::Ratio { .. } => "50%",
5088 TypeSpecification::RatioRange { .. } => "10%...50%",
5089 TypeSpecification::Undetermined => unreachable!(
5090 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
5091 ),
5092 TypeSpecification::Spec { .. } => unreachable!(
5093 "BUG: example_value called on a spec type; spec instances are not prompted data"
5094 ),
5095 TypeSpecification::List { element, .. } => element.example_value(),
5096 }
5097 }
5098
5099 #[must_use]
5103 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
5107 match &self.specifications {
5108 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
5109 _ => unreachable!(
5110 "BUG: measure_type_decomposition called on non-measure type {}",
5111 self.name()
5112 ),
5113 }
5114 }
5115
5116 #[must_use]
5122 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
5123 if let Some(binding) = self.unit() {
5124 return vec![(binding.to_string(), 1)];
5125 }
5126 match &self.specifications {
5127 TypeSpecification::Measure {
5128 units,
5129 decomposition,
5130 ..
5131 } => {
5132 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5133 return vec![(canonical.name.clone(), 1)];
5134 }
5135 if let Some(first) = units.iter().next() {
5136 return vec![(first.name.clone(), 1)];
5137 }
5138 base_measure_vector_as_signature(
5139 decomposition
5140 .as_ref()
5141 .expect("BUG: measure type without units must carry a decomposition"),
5142 )
5143 }
5144 TypeSpecification::MeasureRange {
5145 units,
5146 decomposition,
5147 ..
5148 } => {
5149 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5150 return vec![(canonical.name.clone(), 1)];
5151 }
5152 if let Some(first) = units.iter().next() {
5153 return vec![(first.name.clone(), 1)];
5154 }
5155 base_measure_vector_as_signature(
5156 decomposition
5157 .as_ref()
5158 .expect("BUG: measure type without units must carry a decomposition"),
5159 )
5160 }
5161 _ => unreachable!(
5162 "BUG: measure_runtime_signature called on non-measure type {}",
5163 self.name()
5164 ),
5165 }
5166 }
5167
5168 #[must_use]
5170 pub fn ratio_primary_unit(&self) -> Option<&str> {
5171 match &self.specifications {
5172 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5173 units.iter().next().map(|unit| unit.name.as_str())
5174 }
5175 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5176 units.iter().next().map(|unit| unit.name.as_str())
5177 }
5178 _ => None,
5179 }
5180 }
5181
5182 pub fn is_anonymous_measure(&self) -> bool {
5185 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
5186 }
5187
5188 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
5193 Self {
5194 name: None,
5195 specifications: TypeSpecification::Measure {
5196 minimum: None,
5197 maximum: None,
5198 decimals: None,
5199 units: crate::literals::MeasureUnits::new(),
5200 traits: Vec::new(),
5201 decomposition: Some(decomposition),
5202 unit: None,
5203 help: String::new(),
5204 },
5205 extends: TypeExtends::Primitive,
5206 }
5207 }
5208
5209 #[must_use]
5211 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
5212 match &self.specifications {
5213 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5214 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5215 }
5216 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5217 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5218 }
5219 _ => None,
5220 }
5221 }
5222
5223 #[must_use]
5225 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
5226 match &self.specifications {
5227 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
5228 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5229 }
5230 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
5231 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5232 }
5233 _ => None,
5234 }
5235 }
5236
5237 #[must_use]
5239 pub fn label_for_data_input(&self, input_key: &str) -> String {
5240 let type_label = if let Some(parent) = self.extends.parent_name() {
5241 parent.to_string()
5242 } else {
5243 let type_name = self.name();
5244 if type_name == input_key {
5245 self.specifications.to_string()
5246 } else {
5247 type_name
5248 }
5249 };
5250 if type_label == input_key {
5251 input_key.to_string()
5252 } else {
5253 format!("{input_key} [{type_label}]")
5254 }
5255 }
5256
5257 #[must_use]
5259 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
5260 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
5261 }
5262
5263 #[must_use]
5265 pub fn empty_runtime_input_vetoes(&self) -> bool {
5266 !matches!(self.specifications, TypeSpecification::Text { .. })
5267 }
5268
5269 pub fn measure_unit_factor(
5271 &self,
5272 unit_name: &str,
5273 ) -> &crate::computation::rational::RationalInteger {
5274 let units = match &self.specifications {
5275 TypeSpecification::Measure { units, .. } => units,
5276 TypeSpecification::MeasureRange { units, .. } => units,
5277 _ => unreachable!(
5278 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
5279 self.name()
5280 ),
5281 };
5282 match units.get(unit_name) {
5283 Ok(MeasureUnit { factor, .. }) => factor,
5284 Err(_) => {
5285 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5286 unreachable!(
5287 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
5288 unit_name,
5289 self.name(),
5290 valid.join(", ")
5291 );
5292 }
5293 }
5294 }
5295
5296 pub fn ratio_unit_factor(
5297 &self,
5298 unit_name: &str,
5299 ) -> &crate::computation::rational::RationalInteger {
5300 let units = match &self.specifications {
5301 TypeSpecification::Ratio { units, .. } => units,
5302 _ => unreachable!(
5303 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
5304 self.name()
5305 ),
5306 };
5307 match units.get(unit_name) {
5308 Ok(RatioUnit { value, .. }) => value,
5309 Err(_) => {
5310 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
5311 unreachable!(
5312 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
5313 unit_name,
5314 self.name(),
5315 valid.join(", ")
5316 );
5317 }
5318 }
5319 }
5320
5321 pub(crate) fn measure_literal_unit_map(
5323 &self,
5324 literal: &LiteralValue,
5325 unit_names: &[&str],
5326 factor_source: UnitFactorSource<'_>,
5327 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5328 use crate::computation::rational::checked_div;
5329
5330 let ValueKind::Measure(magnitude) = &literal.value else {
5331 panic!("BUG: measure_literal_unit_map called with non-measure value");
5332 };
5333 let mut map = BTreeMap::new();
5334 for &unit_name in unit_names {
5335 let unit_factor = factor_source.measure_unit_factor(unit_name);
5336 let magnitude_in_unit = checked_div(magnitude, unit_factor)
5337 .map_err(LiteralUnitMapFailure::UnitConversion)?;
5338 let decimal = self
5339 .try_rational_as_decimal(&magnitude_in_unit)
5340 .map_err(LiteralUnitMapFailure::Commit)?;
5341 map.insert(unit_name.to_string(), decimal);
5342 }
5343 Ok(map)
5344 }
5345
5346 pub(crate) fn ratio_literal_unit_map(
5348 &self,
5349 literal: &LiteralValue,
5350 unit_names: &[&str],
5351 factor_source: UnitFactorSource<'_>,
5352 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5353 use crate::computation::rational::checked_mul;
5354
5355 let ratio_api_type = match &self.specifications {
5356 TypeSpecification::Ratio { .. } => self,
5357 TypeSpecification::RatioRange { .. } => {
5358 return ratio_element_type_for_api(self).ratio_literal_unit_map(
5359 literal,
5360 unit_names,
5361 factor_source,
5362 );
5363 }
5364 _ => {
5365 panic!(
5366 "BUG: ratio_literal_unit_map called with non-ratio type {}",
5367 self.name()
5368 );
5369 }
5370 };
5371 let ValueKind::Ratio(canonical) = &literal.value else {
5372 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
5373 };
5374 if unit_names.is_empty() {
5375 panic!(
5376 "BUG: ratio literal type '{}' must have at least one unit name",
5377 self.name()
5378 );
5379 }
5380 let mut map = BTreeMap::new();
5381 for &unit_name in unit_names {
5382 let unit_factor = factor_source.ratio_unit_factor(unit_name);
5383 let magnitude_in_unit = checked_mul(canonical, unit_factor)
5384 .map_err(LiteralUnitMapFailure::UnitConversion)?;
5385 let decimal = ratio_api_type
5386 .try_rational_as_decimal(&magnitude_in_unit)
5387 .map_err(LiteralUnitMapFailure::Commit)?;
5388 map.insert(unit_name.to_string(), decimal);
5389 }
5390 Ok(map)
5391 }
5392}
5393
5394pub(crate) enum UnitFactorSource<'a> {
5396 DeclaredOn(&'a LemmaType),
5397 Merged {
5398 measure: Option<&'a MeasureUnits>,
5399 ratio: Option<&'a RatioUnits>,
5400 },
5401}
5402
5403impl UnitFactorSource<'_> {
5404 fn measure_unit_factor(
5405 &self,
5406 unit_name: &str,
5407 ) -> &crate::computation::rational::RationalInteger {
5408 match self {
5409 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.measure_unit_factor(unit_name),
5410 UnitFactorSource::Merged { measure, .. } => {
5411 let units = measure.unwrap_or_else(|| {
5412 panic!(
5413 "BUG: family measure expansion missing merged table for unit '{unit_name}'"
5414 )
5415 });
5416 &units
5417 .get(unit_name)
5418 .unwrap_or_else(|_| {
5419 panic!("BUG: family unit '{unit_name}' missing from merged measure table")
5420 })
5421 .factor
5422 }
5423 }
5424 }
5425
5426 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
5427 match self {
5428 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
5429 UnitFactorSource::Merged { ratio, .. } => {
5430 let units = ratio.unwrap_or_else(|| {
5431 panic!(
5432 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
5433 )
5434 });
5435 &units
5436 .get(unit_name)
5437 .unwrap_or_else(|_| {
5438 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
5439 })
5440 .value
5441 }
5442 }
5443 }
5444}
5445
5446pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
5448 match &lemma_type.specifications {
5449 TypeSpecification::Ratio { .. } => lemma_type.clone(),
5450 TypeSpecification::RatioRange { .. } => {
5451 let element = range_element_type_specification(&lemma_type.specifications)
5452 .expect("BUG: ratio range type must have ratio element specification");
5453 let TypeSpecification::Ratio {
5454 units, decimals, ..
5455 } = element
5456 else {
5457 panic!("BUG: ratio range element spec must be Ratio");
5458 };
5459 LemmaType::primitive(TypeSpecification::Ratio {
5460 minimum: None,
5461 maximum: None,
5462 decimals,
5463 units,
5464 unit: None,
5465 help: String::new(),
5466 })
5467 }
5468 _ => panic!(
5469 "BUG: ratio_element_type_for_api called with non-ratio type {}",
5470 lemma_type.name()
5471 ),
5472 }
5473}
5474
5475#[derive(Debug, Clone, PartialEq, Eq)]
5477pub(crate) enum LiteralUnitMapFailure {
5478 Commit(crate::computation::rational::NumericFailure),
5479 UnitConversion(crate::computation::rational::NumericFailure),
5480}
5481
5482#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5485pub struct LiteralValue {
5486 pub value: ValueKind,
5487}
5488
5489impl LiteralValue {
5490 #[inline]
5491 pub fn new(value: ValueKind) -> Self {
5492 Self { value }
5493 }
5494
5495 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
5496 match signature {
5497 [(unit_name, 1)] => Some(unit_name.as_str()),
5498 _ => None,
5499 }
5500 }
5501}
5502
5503#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5509pub struct TypedLiteral {
5510 pub value: ValueKind,
5511 #[serde(with = "crate::snapshot::shared::arc")]
5512 pub lemma_type: Arc<LemmaType>,
5513}
5514
5515impl TypedLiteral {
5516 #[inline]
5517 pub fn to_literal(&self) -> LiteralValue {
5518 LiteralValue {
5519 value: self.value.clone(),
5520 }
5521 }
5522
5523 #[inline]
5524 pub fn into_literal(self) -> LiteralValue {
5525 LiteralValue { value: self.value }
5526 }
5527}
5528
5529impl From<TypedLiteral> for LiteralValue {
5530 fn from(typed: TypedLiteral) -> Self {
5531 typed.into_literal()
5532 }
5533}
5534
5535impl From<&TypedLiteral> for LiteralValue {
5536 fn from(typed: &TypedLiteral) -> Self {
5537 typed.to_literal()
5538 }
5539}
5540
5541impl LiteralValue {
5542 pub fn text(s: String) -> Self {
5543 Self {
5544 value: ValueKind::Text(s),
5545 }
5546 }
5547
5548 pub fn number(n: RationalInteger) -> Self {
5549 Self {
5550 value: ValueKind::Number(n),
5551 }
5552 }
5553
5554 pub fn number_from_decimal(decimal: Decimal) -> Self {
5555 Self::number(
5556 crate::literals::rational_from_parsed_decimal(decimal)
5557 .expect("BUG: literal number from decimal must lift at boundary"),
5558 )
5559 }
5560
5561 pub fn measure(n: RationalInteger) -> Self {
5562 Self {
5563 value: ValueKind::Measure(n),
5564 }
5565 }
5566
5567 pub fn from_bool(b: bool) -> Self {
5568 Self {
5569 value: ValueKind::Boolean(b),
5570 }
5571 }
5572
5573 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5574 Self::date(date_time_to_semantic(dt))
5575 }
5576
5577 pub fn date(dt: SemanticDateTime) -> Self {
5578 Self {
5579 value: ValueKind::Date(dt),
5580 }
5581 }
5582
5583 pub fn time(t: SemanticTime) -> Self {
5584 Self {
5585 value: ValueKind::Time(t),
5586 }
5587 }
5588
5589 pub fn ratio(r: RationalInteger) -> Self {
5590 Self {
5591 value: ValueKind::Ratio(r),
5592 }
5593 }
5594
5595 pub fn ratio_from_decimal(r: Decimal) -> Self {
5596 Self::ratio(
5597 crate::literals::rational_from_parsed_decimal(r)
5598 .expect("BUG: ratio literal from decimal must lift at boundary"),
5599 )
5600 }
5601
5602 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5603 Self {
5604 value: ValueKind::Range(Box::new(left), Box::new(right)),
5605 }
5606 }
5607
5608 pub fn display_value(&self) -> String {
5610 match &self.value {
5611 ValueKind::Measure(_) | ValueKind::Ratio(_) => format!("{}", self.value),
5612 ValueKind::Range(left, right) => {
5613 format!("{}...{}", left.display_value(), right.display_value())
5614 }
5615 ValueKind::List(rows) => rows
5616 .iter()
5617 .map(|row| row.display_value())
5618 .collect::<Vec<_>>()
5619 .join(", "),
5620 _ => format!("{}", self.value),
5621 }
5622 }
5623
5624 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
5626 match &self.value {
5627 ValueKind::Measure(n) => {
5628 let signature = lemma_type.measure_runtime_signature();
5629 format_measure_canonical_for_display(n, lemma_type, &signature)
5630 }
5631 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
5632 ValueKind::Range(left, right) => {
5633 format!("{}...{}", left.display_value(), right.display_value())
5634 }
5635 ValueKind::List(rows) => rows
5636 .iter()
5637 .map(|row| row.display_value())
5638 .collect::<Vec<_>>()
5639 .join(", "),
5640 _ => format!("{}", self.value),
5641 }
5642 }
5643
5644 pub fn byte_size(&self) -> usize {
5646 self.value.structural_byte_size()
5647 }
5648
5649 #[must_use]
5651 pub fn magnitude_suggestion_for_decimal_prompt(
5652 &self,
5653 lemma_type: &LemmaType,
5654 ) -> Option<rust_decimal::Decimal> {
5655 match &self.value {
5656 ValueKind::Number(n) => Some(
5657 lemma_type
5658 .try_rational_as_decimal(n)
5659 .expect("BUG: stored number literal must convert to decimal for prompt"),
5660 ),
5661 ValueKind::Measure(n) => {
5662 let signature = lemma_type.measure_runtime_signature();
5663 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
5664 "BUG: measure prompt requires exactly one signature unit with exponent 1",
5665 );
5666 Some(
5667 lemma_type
5668 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
5669 .expect("BUG: stored measure literal must convert to decimal for prompt"),
5670 )
5671 }
5672 ValueKind::Ratio(n) => {
5673 if let Some(unit_name) = lemma_type.unit() {
5674 Some(
5675 lemma_type
5676 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
5677 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
5678 )
5679 } else {
5680 Some(lemma_type.try_rational_as_decimal(n).expect(
5681 "BUG: stored bare ratio literal must convert to decimal for prompt",
5682 ))
5683 }
5684 }
5685 _ => None,
5686 }
5687 }
5688
5689 #[must_use]
5691 pub fn measure_units(
5692 &self,
5693 lemma_type: &LemmaType,
5694 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5695 if !matches!(self.value, ValueKind::Measure(_)) {
5696 return None;
5697 }
5698 lemma_type.measure_unit_names()?;
5699 let declared: Vec<&str> = lemma_type
5700 .measure_unit_names()
5701 .expect("BUG: measure_unit_names checked above");
5702 lemma_type
5703 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5704 .ok()
5705 }
5706
5707 #[must_use]
5709 pub fn ratio_units(
5710 &self,
5711 lemma_type: &LemmaType,
5712 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5713 if !matches!(self.value, ValueKind::Ratio(_)) {
5714 return None;
5715 }
5716 let has_declared_units = match &lemma_type.specifications {
5717 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
5718 TypeSpecification::RatioRange { .. } => true,
5719 _ => return None,
5720 };
5721 if !has_declared_units {
5722 return None;
5723 }
5724 let declared: Vec<&str> = lemma_type
5725 .ratio_unit_names()
5726 .expect("BUG: ratio units checked above");
5727 lemma_type
5728 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5729 .ok()
5730 }
5731
5732 #[must_use]
5734 pub fn magnitude_in_unit(
5735 &self,
5736 lemma_type: &LemmaType,
5737 unit: &str,
5738 ) -> Option<rust_decimal::Decimal> {
5739 self.measure_units(lemma_type)
5740 .and_then(|map| map.get(unit).copied())
5741 .or_else(|| {
5742 self.ratio_units(lemma_type)
5743 .and_then(|map| map.get(unit).copied())
5744 })
5745 }
5746
5747 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
5749 let ValueKind::Measure(magnitude) = &self.value else {
5750 unreachable!(
5751 "BUG: duration_canonical_seconds called with {:?}",
5752 self.value
5753 );
5754 };
5755 if !lemma_type.is_duration_like_measure() {
5756 unreachable!(
5757 "BUG: duration_canonical_seconds called with type {}",
5758 lemma_type.name()
5759 );
5760 }
5761 let factor = lemma_type.measure_unit_factor("second");
5762 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
5763 }
5764
5765 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
5767 let ValueKind::Measure(magnitude) = &self.value else {
5768 unreachable!(
5769 "BUG: calendar_canonical_months called with {:?}",
5770 self.value
5771 );
5772 };
5773 if !lemma_type.is_calendar_like() {
5774 unreachable!(
5775 "BUG: calendar_canonical_months called with type {}",
5776 lemma_type.name()
5777 );
5778 }
5779 let factor = lemma_type.measure_unit_factor("month");
5780 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
5781 }
5782}
5783
5784impl TypedLiteral {
5785 pub fn text(s: String) -> Self {
5786 Self {
5787 value: ValueKind::Text(s),
5788 lemma_type: primitive_text_arc().clone(),
5789 }
5790 }
5791
5792 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
5793 Self {
5794 value: ValueKind::Text(s),
5795 lemma_type,
5796 }
5797 }
5798
5799 pub fn number(n: RationalInteger) -> Self {
5800 Self {
5801 value: ValueKind::Number(n),
5802 lemma_type: primitive_number_arc().clone(),
5803 }
5804 }
5805
5806 pub fn number_from_decimal(decimal: Decimal) -> Self {
5807 Self::number(
5808 crate::literals::rational_from_parsed_decimal(decimal)
5809 .expect("BUG: literal number from decimal must lift at boundary"),
5810 )
5811 }
5812
5813 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5814 Self {
5815 value: ValueKind::Number(n),
5816 lemma_type,
5817 }
5818 }
5819
5820 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
5821 Self::number_with_type(
5822 crate::literals::rational_from_parsed_decimal(decimal)
5823 .expect("BUG: literal number from decimal must lift at boundary"),
5824 lemma_type,
5825 )
5826 }
5827
5828 pub fn measure_with_bound_unit(
5830 n: RationalInteger,
5831 unit_name: impl Into<String>,
5832 lemma_type: Arc<LemmaType>,
5833 ) -> Self {
5834 Self::measure_with_type(
5835 n,
5836 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5837 )
5838 }
5839
5840 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5841 Self {
5842 value: ValueKind::Measure(n),
5843 lemma_type,
5844 }
5845 }
5846
5847 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5848 Self {
5849 value: ValueKind::Measure(n),
5850 lemma_type,
5851 }
5852 }
5853
5854 pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
5856 Self {
5857 value: ValueKind::Measure(value),
5858 lemma_type: Arc::new(anonymous_measure_type()),
5859 }
5860 }
5861
5862 pub fn from_bool(b: bool) -> Self {
5863 Self {
5864 value: ValueKind::Boolean(b),
5865 lemma_type: primitive_boolean_arc().clone(),
5866 }
5867 }
5868
5869 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5870 Self::date(date_time_to_semantic(dt))
5871 }
5872
5873 pub fn date(dt: SemanticDateTime) -> Self {
5874 Self {
5875 value: ValueKind::Date(dt),
5876 lemma_type: primitive_date_arc().clone(),
5877 }
5878 }
5879
5880 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
5881 Self {
5882 value: ValueKind::Date(dt),
5883 lemma_type,
5884 }
5885 }
5886
5887 pub fn time(t: SemanticTime) -> Self {
5888 Self {
5889 value: ValueKind::Time(t),
5890 lemma_type: primitive_time_arc().clone(),
5891 }
5892 }
5893
5894 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
5895 Self {
5896 value: ValueKind::Time(t),
5897 lemma_type,
5898 }
5899 }
5900
5901 pub fn calendar(
5902 value: RationalInteger,
5903 unit: SemanticCalendarUnit,
5904 lemma_type: Arc<LemmaType>,
5905 ) -> Self {
5906 let unit_name = unit.to_string();
5907 debug_assert_eq!(
5908 semantic_calendar_unit_from_unit_name(&unit_name),
5909 unit,
5910 "BUG: calendar unit name must round-trip"
5911 );
5912 Self::measure_with_bound_unit(value, unit_name, lemma_type)
5913 }
5914
5915 pub fn calendar_from_decimal(
5916 value: Decimal,
5917 unit: SemanticCalendarUnit,
5918 lemma_type: Arc<LemmaType>,
5919 ) -> Self {
5920 Self::calendar(
5921 crate::literals::rational_from_parsed_decimal(value)
5922 .expect("BUG: calendar literal from decimal must lift at boundary"),
5923 unit,
5924 lemma_type,
5925 )
5926 }
5927
5928 pub fn calendar_with_type(
5929 value: RationalInteger,
5930 unit: SemanticCalendarUnit,
5931 lemma_type: Arc<LemmaType>,
5932 ) -> Self {
5933 Self::calendar(value, unit, lemma_type)
5934 }
5935
5936 pub fn ratio(r: RationalInteger) -> Self {
5937 Self {
5938 value: ValueKind::Ratio(r),
5939 lemma_type: primitive_ratio_arc().clone(),
5940 }
5941 }
5942
5943 pub fn ratio_from_decimal(r: Decimal) -> Self {
5944 Self::ratio(
5945 crate::literals::rational_from_parsed_decimal(r)
5946 .expect("BUG: ratio literal from decimal must lift at boundary"),
5947 )
5948 }
5949
5950 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5951 Self {
5952 value: ValueKind::Ratio(r),
5953 lemma_type,
5954 }
5955 }
5956
5957 pub fn ratio_with_bound_unit(
5958 r: RationalInteger,
5959 unit_name: impl Into<String>,
5960 lemma_type: Arc<LemmaType>,
5961 ) -> Self {
5962 Self::ratio_with_type(
5963 r,
5964 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5965 )
5966 }
5967
5968 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5969 let specifications =
5970 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
5971 .unwrap_or_else(|| {
5972 unreachable!(
5973 "BUG: attempted to construct a range literal from incompatible endpoint types"
5974 )
5975 });
5976
5977 Self {
5978 value: ValueKind::Range(Box::new(left), Box::new(right)),
5979 lemma_type: Arc::new(LemmaType::primitive(specifications)),
5980 }
5981 }
5982
5983 pub fn display_value(&self) -> String {
5984 self.to_literal().display_value_with_type(&self.lemma_type)
5985 }
5986
5987 pub fn get_type(&self) -> &LemmaType {
5988 &self.lemma_type
5989 }
5990
5991 pub fn byte_size(&self) -> usize {
5992 self.value.structural_byte_size()
5993 }
5994}
5995
5996impl fmt::Display for TypedLiteral {
5997 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5998 write!(f, "{}", self.display_value())
5999 }
6000}
6001
6002#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6005pub enum ReferenceTarget {
6006 Data(DataPath),
6007 Rule(RulePath),
6008}
6009
6010#[derive(Clone, Debug, Serialize, Deserialize)]
6016pub enum ImportOverlay {
6017 Literal(BoundValueKind),
6018 Reference {
6019 target: ReferenceTarget,
6020 constraints: Option<Vec<Constraint>>,
6021 },
6022}
6023
6024#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6028pub(crate) enum ReferenceEnd {
6029 Promptable(DataPath),
6030 Rule(RulePath),
6031 Import,
6032}
6033
6034#[derive(Clone, Debug, Serialize, Deserialize)]
6036pub enum DataDefinition {
6037 Value {
6039 value: LiteralValue,
6040 #[serde(with = "crate::snapshot::shared::arc")]
6041 resolved_type: Arc<LemmaType>,
6042 source: Source,
6043 },
6044 TypeDeclaration {
6050 #[serde(with = "crate::snapshot::shared::arc")]
6051 resolved_type: Arc<LemmaType>,
6052 declared_suggestion: Option<BoundValueKind>,
6053 declared_value: Option<BoundValueKind>,
6054 source: Source,
6055 },
6056 Import {
6063 identity: SpecIdentity,
6064 overlay: IndexMap<DataPath, ImportOverlay>,
6066 source: Source,
6067 },
6068 InstanceList {
6072 #[serde(with = "crate::snapshot::shared::arc")]
6073 resolved_type: Arc<LemmaType>,
6074 source: Source,
6075 },
6076 Reference {
6102 target: ReferenceTarget,
6103 #[serde(with = "crate::snapshot::shared::arc")]
6104 resolved_type: Arc<LemmaType>,
6105 local_constraints: Option<Vec<Constraint>>,
6106 local_suggestion: Option<BoundValueKind>,
6107 local_value: Option<BoundValueKind>,
6108 source: Source,
6109 },
6110}
6111
6112impl DataDefinition {
6113 pub fn lemma_type(&self) -> Option<&LemmaType> {
6115 self.resolved_type_arc().map(|arc| arc.as_ref())
6116 }
6117
6118 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
6120 match self {
6121 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
6122 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
6123 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
6124 DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
6125 DataDefinition::Import { .. } => None,
6126 }
6127 }
6128
6129 #[inline]
6131 pub fn schema_type(&self) -> Option<&LemmaType> {
6132 self.lemma_type()
6133 }
6134
6135 pub fn value(&self) -> Option<LiteralValue> {
6139 match self {
6140 DataDefinition::Value { value, .. } => Some(value.clone()),
6141 DataDefinition::TypeDeclaration {
6142 declared_value: Some(dv),
6143 ..
6144 } => Some(dv.to_literal()),
6145 DataDefinition::Reference {
6146 local_value: Some(dv),
6147 resolved_type: _,
6148 ..
6149 } => Some(dv.to_literal()),
6150 DataDefinition::TypeDeclaration { .. }
6151 | DataDefinition::Import { .. }
6152 | DataDefinition::InstanceList { .. }
6153 | DataDefinition::Reference { .. } => None,
6154 }
6155 }
6156
6157 pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
6159 match self {
6160 DataDefinition::TypeDeclaration {
6161 declared_value: Some(dv),
6162 ..
6163 }
6164 | DataDefinition::Reference {
6165 local_value: Some(dv),
6166 ..
6167 } => Some(dv),
6168 DataDefinition::Value { .. }
6169 | DataDefinition::TypeDeclaration { .. }
6170 | DataDefinition::Reference { .. }
6171 | DataDefinition::InstanceList { .. }
6172 | DataDefinition::Import { .. } => None,
6173 }
6174 }
6175
6176 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
6180 match self {
6181 DataDefinition::TypeDeclaration {
6182 declared_suggestion: Some(dv),
6183 ..
6184 }
6185 | DataDefinition::Reference {
6186 local_suggestion: Some(dv),
6187 ..
6188 } => Some(dv),
6189 DataDefinition::Value { .. }
6190 | DataDefinition::TypeDeclaration { .. }
6191 | DataDefinition::Reference { .. }
6192 | DataDefinition::InstanceList { .. }
6193 | DataDefinition::Import { .. } => None,
6194 }
6195 }
6196
6197 pub fn source(&self) -> &Source {
6199 match self {
6200 DataDefinition::Value { source, .. } => source,
6201 DataDefinition::TypeDeclaration { source, .. } => source,
6202 DataDefinition::Import { source, .. } => source,
6203 DataDefinition::InstanceList { source, .. } => source,
6204 DataDefinition::Reference { source, .. } => source,
6205 }
6206 }
6207}
6208
6209pub fn number_with_unit_to_value_kind(
6211 magnitude: rust_decimal::Decimal,
6212 unit_name: &str,
6213 lemma_type: &LemmaType,
6214) -> Result<ValueKind, String> {
6215 match &lemma_type.specifications {
6216 TypeSpecification::Ratio { units, .. } => {
6217 use crate::computation::rational::{checked_div, decimal_to_rational};
6218 let unit = units.get(unit_name)?;
6219 let magnitude_rational = decimal_to_rational(magnitude)
6220 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6221 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6222 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6223 Ok(ValueKind::Ratio(canonical_rational))
6224 }
6225 TypeSpecification::Measure { units, .. } => {
6226 use crate::computation::rational::checked_mul;
6227 let rational = lift_parser_decimal(magnitude)?;
6228 let unit = units.get(unit_name)?;
6229 let canonical = checked_mul(&rational, &unit.factor)
6230 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6231 Ok(ValueKind::Measure(canonical))
6232 }
6233 _ => Err(format!(
6234 "Unit '{}' is defined on type '{}' which is not measure or ratio",
6235 unit_name,
6236 lemma_type.name()
6237 )),
6238 }
6239}
6240
6241pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
6244 matches!(
6245 (type_spec, value),
6246 (TypeSpecification::Number { .. }, ValueKind::Number(_))
6247 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
6248 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
6249 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
6250 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
6251 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
6252 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
6253 | (
6254 TypeSpecification::NumberRange { .. },
6255 ValueKind::Range(_, _)
6256 )
6257 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
6258 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
6259 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
6260 | (
6261 TypeSpecification::MeasureRange { .. },
6262 ValueKind::Range(_, _)
6263 )
6264 | (TypeSpecification::Veto { .. }, _)
6265 | (TypeSpecification::Undetermined, _)
6266 | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
6267 | (TypeSpecification::List { .. }, ValueKind::List(_))
6268 )
6269}
6270
6271fn parser_value_type_mismatch(
6272 value: &crate::literals::Value,
6273 type_spec: &TypeSpecification,
6274) -> String {
6275 use crate::parsing::ast::AsLemmaSource;
6276 let value_str = format!("{}", AsLemmaSource(value));
6277 match type_spec {
6278 TypeSpecification::Measure { units, .. } => {
6279 let unit_hint = units
6280 .iter()
6281 .find(|u| u.factor == crate::computation::rational::rational_one())
6282 .map(|u| u.name.as_str())
6283 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
6284 .unwrap_or("unit");
6285 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
6286 }
6287 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
6288 let unit_hint = units
6289 .iter()
6290 .next()
6291 .map(|u| u.name.as_str())
6292 .unwrap_or("unit");
6293 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
6294 }
6295 _ => format!("cannot use {value_str} as {type_spec}"),
6296 }
6297}
6298
6299pub fn refresh_measure_literal_canonical_magnitude(
6304 lit: &mut LiteralValue,
6305 previous_type: &LemmaType,
6306 resolved_type: &LemmaType,
6307) -> Result<(), crate::computation::rational::NumericFailure> {
6308 use crate::computation::rational::{checked_div, checked_mul};
6309 let ValueKind::Measure(magnitude) = &mut lit.value else {
6310 return Ok(());
6311 };
6312 let signature = previous_type.measure_runtime_signature();
6316 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
6317 return Ok(());
6318 };
6319 if signature.len() != 1 {
6320 return Ok(());
6321 }
6322 let stored_factor = previous_type.measure_unit_factor(unit_name);
6323 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
6324 if stored_factor == resolved_factor {
6325 return Ok(());
6326 }
6327 let scaled = checked_mul(magnitude, resolved_factor)?;
6328 *magnitude = checked_div(&scaled, stored_factor)?;
6329 Ok(())
6330}
6331
6332fn number_with_unit_value_kind(
6333 magnitude: rust_decimal::Decimal,
6334 unit_name: &str,
6335 type_spec: &TypeSpecification,
6336) -> Result<ValueKind, String> {
6337 use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
6338 match type_spec {
6339 TypeSpecification::Ratio { units, .. } => {
6340 let unit = units.get(unit_name)?;
6341 let magnitude_rational = decimal_to_rational(magnitude)
6342 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6343 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6344 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6345 Ok(ValueKind::Ratio(canonical_rational))
6346 }
6347 TypeSpecification::Measure { units, .. } => {
6348 let rational = lift_parser_decimal(magnitude)?;
6349 let unit = units.get(unit_name)?;
6350 let canonical = checked_mul(&rational, &unit.factor)
6351 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6352 Ok(ValueKind::Measure(canonical))
6353 }
6354 _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
6355 }
6356}
6357
6358fn parser_scalar_to_value_kind(
6363 value: &crate::literals::Value,
6364 type_spec: &TypeSpecification,
6365) -> Result<ValueKind, String> {
6366 use crate::literals::Value;
6367 match (value, type_spec) {
6368 (Value::NumberWithUnit(magnitude, unit_name), _) => {
6369 number_with_unit_value_kind(*magnitude, unit_name, type_spec)
6370 }
6371 (Value::Number(n), TypeSpecification::Number { .. }) => {
6372 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
6373 }
6374 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
6375 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
6376 (Value::Date(dt), TypeSpecification::Date { .. }) => {
6377 Ok(ValueKind::Date(date_time_to_semantic(dt)))
6378 }
6379 (Value::Time(t), TypeSpecification::Time { .. }) => {
6380 Ok(ValueKind::Time(time_to_semantic(t)))
6381 }
6382 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
6383 }
6384}
6385
6386pub fn parser_value_to_value_kind(
6391 value: &crate::literals::Value,
6392 type_spec: &TypeSpecification,
6393 resolve_unit: Option<&UnitTokenResolver<'_>>,
6394) -> Result<ValueKind, String> {
6395 use crate::literals::Value;
6396 match (value, type_spec) {
6397 (
6398 Value::Range(left, right),
6399 range_spec @ (TypeSpecification::NumberRange { .. }
6400 | TypeSpecification::DateRange { .. }
6401 | TypeSpecification::TimeRange { .. }
6402 | TypeSpecification::RatioRange { .. }
6403 | TypeSpecification::MeasureRange { .. }),
6404 ) => {
6405 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
6406 "BUG: range_element_type_specification missing arm for range type".to_string()
6407 })?;
6408 let left_lit = lift_range_endpoint(left, &endpoint, resolve_unit)?;
6409 let right_lit = lift_range_endpoint(right, &endpoint, resolve_unit)?;
6410 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
6411 }
6412 (value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
6413 }
6414}
6415
6416pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
6420 use crate::parsing::ast::Value;
6421 Ok(match value {
6422 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
6423 Value::Text(s) => ValueKind::Text(s.clone()),
6424 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
6425 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
6426 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
6427 Value::NumberWithUnit(_, _) => {
6428 return Err(
6429 "number_with_unit literal requires type context (measure or ratio)".to_string(),
6430 );
6431 }
6432 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
6433 })
6434}
6435
6436pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
6438 SemanticDateTime {
6439 year: dt.year,
6440 month: dt.month,
6441 day: dt.day,
6442 hour: dt.hour,
6443 minute: dt.minute,
6444 second: dt.second,
6445 microsecond: dt.microsecond,
6446 timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
6447 }
6448}
6449
6450pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
6452 SemanticTime {
6453 hour: date.hour,
6454 minute: date.minute,
6455 second: date.second,
6456 microsecond: date.microsecond,
6457 timezone: date.timezone.clone(),
6458 }
6459}
6460
6461pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
6463 SemanticTime {
6464 hour: t.hour.into(),
6465 minute: t.minute.into(),
6466 second: t.second.into(),
6467 microsecond: t.microsecond,
6468 timezone: t.timezone.as_ref().map(SemanticTimezone::from),
6469 }
6470}
6471
6472pub(crate) fn compare_semantic_dates(
6476 left: &SemanticDateTime,
6477 right: &SemanticDateTime,
6478) -> std::cmp::Ordering {
6479 left.year
6480 .cmp(&right.year)
6481 .then_with(|| left.month.cmp(&right.month))
6482 .then_with(|| left.day.cmp(&right.day))
6483 .then_with(|| left.hour.cmp(&right.hour))
6484 .then_with(|| left.minute.cmp(&right.minute))
6485 .then_with(|| left.second.cmp(&right.second))
6486 .then_with(|| left.microsecond.cmp(&right.microsecond))
6487}
6488
6489pub(crate) fn compare_semantic_times(
6492 left: &SemanticTime,
6493 right: &SemanticTime,
6494) -> std::cmp::Ordering {
6495 left.hour
6496 .cmp(&right.hour)
6497 .then_with(|| left.minute.cmp(&right.minute))
6498 .then_with(|| left.second.cmp(&right.second))
6499 .then_with(|| left.microsecond.cmp(&right.microsecond))
6500}
6501
6502pub fn conversion_target_to_semantic(
6504 ct: &ConversionTarget,
6505 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
6506 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
6507) -> Result<SemanticConversionTarget, String> {
6508 match ct {
6509 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
6510 ConversionTarget::Unit { unit_name } => {
6511 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
6512 let (bare, owning_type) = match resolved_types {
6513 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
6514 None => index.resolve(unit_name)?,
6515 };
6516 Ok(SemanticConversionTarget::Unit {
6517 unit_name: bare,
6518 owning_type,
6519 })
6520 }
6521 }
6522}
6523
6524static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
6530static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
6531static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
6532static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6533static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6534static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
6535static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
6536static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6537static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6538static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6539static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6540static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6541
6542#[must_use]
6543pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
6544 lemma_type_for_primitive(PrimitiveKind::Boolean)
6545}
6546
6547#[must_use]
6548pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
6549 lemma_type_for_primitive(PrimitiveKind::Number)
6550}
6551
6552#[must_use]
6553pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
6554 lemma_type_for_primitive(PrimitiveKind::Text)
6555}
6556
6557#[must_use]
6558pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
6559 lemma_type_for_primitive(PrimitiveKind::Date)
6560}
6561
6562#[must_use]
6563pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
6564 lemma_type_for_primitive(PrimitiveKind::DateRange)
6565}
6566
6567#[must_use]
6568pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
6569 lemma_type_for_primitive(PrimitiveKind::Time)
6570}
6571
6572#[must_use]
6573pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
6574 lemma_type_for_primitive(PrimitiveKind::Ratio)
6575}
6576
6577#[must_use]
6579pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
6580 let cell = match kind {
6581 PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
6582 PrimitiveKind::Number => &PRIMITIVE_NUMBER,
6583 PrimitiveKind::Text => &PRIMITIVE_TEXT,
6584 PrimitiveKind::Date => &PRIMITIVE_DATE,
6585 PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
6586 PrimitiveKind::Time => &PRIMITIVE_TIME,
6587 PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
6588 PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
6589 PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
6590 PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
6591 PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
6592 PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
6593 };
6594 cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
6595}
6596
6597#[must_use]
6599pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
6600 match kind {
6601 PrimitiveKind::Boolean => TypeSpecification::boolean(),
6602 PrimitiveKind::Measure => TypeSpecification::measure(),
6603 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
6604 PrimitiveKind::Number => TypeSpecification::number(),
6605 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
6606 PrimitiveKind::Ratio => TypeSpecification::ratio(),
6607 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
6608 PrimitiveKind::Text => TypeSpecification::text(),
6609 PrimitiveKind::Date => TypeSpecification::date(),
6610 PrimitiveKind::DateRange => TypeSpecification::date_range(),
6611 PrimitiveKind::Time => TypeSpecification::time(),
6612 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
6613 }
6614}
6615
6616impl fmt::Display for PathSegment {
6621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6622 match &self.repository {
6623 Some(repository) => write!(f, "{} → {} {}", self.uses, repository, self.spec),
6624 None => write!(f, "{} → {}", self.uses, self.spec),
6625 }
6626 }
6627}
6628
6629impl fmt::Display for DataPath {
6630 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6631 write!(f, "{}", self.input_key())
6632 }
6633}
6634
6635impl fmt::Display for RulePath {
6636 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6637 write!(f, "{}", self.input_key())
6638 }
6639}
6640
6641impl fmt::Display for LemmaType {
6642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6643 write!(f, "{}", self.name())
6644 }
6645}
6646
6647pub(crate) fn format_decimal_for_api(
6648 decimal: rust_decimal::Decimal,
6649 decimal_places: Option<u8>,
6650) -> rust_decimal::Decimal {
6651 match decimal_places {
6652 Some(decimal_places) => {
6653 let places = u32::from(decimal_places);
6654 let mut rounded = decimal.round_dp(places);
6655 rounded.rescale(places);
6656 rounded
6657 }
6658 None => {
6659 let normalized = decimal.normalize();
6660 if normalized.fract().is_zero() {
6661 normalized.trunc()
6662 } else {
6663 normalized
6664 }
6665 }
6666 }
6667}
6668
6669fn format_decimal_for_human_display(
6670 decimal: rust_decimal::Decimal,
6671 decimal_places: Option<u8>,
6672) -> String {
6673 match decimal_places {
6674 Some(decimal_places) => {
6675 let rounded = decimal.round_dp(u32::from(decimal_places));
6676 format!("{:.prec$}", rounded, prec = decimal_places as usize)
6677 }
6678 None => decimal.normalize().to_string(),
6679 }
6680}
6681
6682fn format_rational_for_human_display(
6683 magnitude: &crate::computation::rational::RationalInteger,
6684 decimal_places: Option<u8>,
6685) -> String {
6686 match magnitude.try_to_decimal() {
6687 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
6688 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
6689 Err(_) => magnitude.display_str(),
6690 }
6691}
6692
6693fn format_measure_canonical_for_display(
6694 canonical: &crate::computation::rational::RationalInteger,
6695 lemma_type: &LemmaType,
6696 signature: &[(String, i32)],
6697) -> String {
6698 use crate::computation::rational::checked_div;
6699
6700 let decimals = lemma_type.decimal_places();
6701
6702 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
6703 if !units.is_empty() {
6704 let unit = match lemma_type.unit() {
6706 Some(binding) => units
6707 .iter()
6708 .find(|unit| unit.name == binding)
6709 .unwrap_or_else(|| {
6710 panic!(
6711 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
6712 lemma_type.name()
6713 )
6714 }),
6715 None => units
6716 .iter()
6717 .next()
6718 .expect("BUG: measure type with non-empty units must have a first unit"),
6719 };
6720 let in_unit = checked_div(canonical, &unit.factor)
6721 .expect("BUG: de-canonicalization for measure display must not fail");
6722 let formatted = format_rational_for_human_display(&in_unit, decimals);
6723 return format!("{} {}", formatted, unit.name);
6724 }
6725 }
6726
6727 let unit_label = match signature {
6728 [] => String::new(),
6729 [(name, 1)] => name.clone(),
6730 _ => format_signature_operator_style(signature),
6731 };
6732 let formatted = format_rational_for_human_display(canonical, decimals);
6733 if unit_label.is_empty() {
6734 formatted
6735 } else {
6736 format!("{formatted} {unit_label}")
6737 }
6738}
6739
6740fn format_ratio_canonical_for_display(
6741 canonical: &crate::computation::rational::RationalInteger,
6742 lemma_type: &LemmaType,
6743) -> String {
6744 use crate::computation::rational::checked_mul;
6745
6746 let units = match &lemma_type.specifications {
6747 TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
6748 units
6749 }
6750 _ => unreachable!(
6751 "BUG: ratio display with non-ratio type {}",
6752 lemma_type.name()
6753 ),
6754 };
6755 let display_unit = lemma_type
6756 .unit()
6757 .or_else(|| lemma_type.ratio_primary_unit());
6758 let Some(unit_name) = display_unit else {
6759 return format_rational_magnitude_for_display(canonical);
6760 };
6761 let unit = units.get(unit_name).unwrap_or_else(|_| {
6762 panic!(
6763 "BUG: ratio display unit '{unit_name}' is not declared on type {}",
6764 lemma_type.name()
6765 )
6766 });
6767 let in_unit = checked_mul(canonical, &unit.value)
6768 .expect("BUG: de-canonicalization for ratio display must not fail");
6769 format_number_with_unit_for_display(&in_unit, unit_name)
6770}
6771
6772impl fmt::Display for LiteralValue {
6773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6774 match &self.value {
6778 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
6779 write!(f, "{}", self.value)
6780 }
6781 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
6782 _ => write!(f, "{}", self.value),
6783 }
6784 }
6785}
6786
6787#[cfg(test)]
6792pub(crate) mod tests {
6793 use super::*;
6794 use crate::computation::rational::decimal_to_rational;
6795 use crate::literals::DateGranularity;
6796 use crate::literals::Value;
6797 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
6798 use rust_decimal::Decimal;
6799 use std::str::FromStr;
6800
6801 #[must_use]
6802 pub(crate) fn test_type_family(name: &str) -> TypeFamily {
6803 TypeFamily {
6804 repository: None,
6805 spec: "test".to_string(),
6806 effective: crate::parsing::ast::EffectiveDate::Origin,
6807 name: name.to_string(),
6808 }
6809 }
6810
6811 #[must_use]
6812 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
6813 crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
6814 }
6815
6816 #[must_use]
6817 pub(crate) fn primitive_measure() -> &'static LemmaType {
6818 primitive_measure_arc().as_ref()
6819 }
6820
6821 #[test]
6822 fn default_primitive_help_is_goal_oriented() {
6823 let kinds = [
6824 PrimitiveKind::Boolean,
6825 PrimitiveKind::Measure,
6826 PrimitiveKind::MeasureRange,
6827 PrimitiveKind::Number,
6828 PrimitiveKind::NumberRange,
6829 PrimitiveKind::Ratio,
6830 PrimitiveKind::RatioRange,
6831 PrimitiveKind::Text,
6832 PrimitiveKind::Date,
6833 PrimitiveKind::DateRange,
6834 PrimitiveKind::Time,
6835 PrimitiveKind::TimeRange,
6836 ];
6837 for kind in kinds {
6838 let spec = type_spec_for_primitive(kind);
6839 let help = match &spec {
6840 TypeSpecification::Boolean { help, .. }
6841 | TypeSpecification::Number { help, .. }
6842 | TypeSpecification::NumberRange { help, .. }
6843 | TypeSpecification::Text { help, .. }
6844 | TypeSpecification::Measure { help, .. }
6845 | TypeSpecification::MeasureRange { help, .. }
6846 | TypeSpecification::Ratio { help, .. }
6847 | TypeSpecification::RatioRange { help, .. }
6848 | TypeSpecification::Date { help, .. }
6849 | TypeSpecification::DateRange { help, .. }
6850 | TypeSpecification::TimeRange { help, .. }
6851 | TypeSpecification::Time { help, .. } => help,
6852 TypeSpecification::Veto { .. }
6853 | TypeSpecification::Undetermined
6854 | TypeSpecification::Spec { .. }
6855 | TypeSpecification::List { .. } => {
6856 unreachable!(
6857 "BUG: primitive kind {:?} mapped to non-primitive spec",
6858 kind
6859 )
6860 }
6861 };
6862 assert!(!help.is_empty(), "help for {:?}", kind);
6863 assert!(
6864 !help.to_ascii_lowercase().contains("format:"),
6865 "help for {:?} must not describe syntax: {:?}",
6866 kind,
6867 help
6868 );
6869 assert_eq!(help, default_help_for_primitive(kind));
6870 }
6871 }
6872
6873 #[test]
6874 fn data_path_and_rule_path_display_are_input_key() {
6875 let implicit = PathSegment {
6876 uses: "bag".to_string(),
6877 repository: None,
6878 spec: "bag".to_string(),
6879 };
6880 let explicit = PathSegment {
6881 uses: "bag".to_string(),
6882 repository: None,
6883 spec: "nut_bag".to_string(),
6884 };
6885 let named_repo = PathSegment {
6886 uses: "calc".to_string(),
6887 repository: Some("alpha".to_string()),
6888 spec: "pricing".to_string(),
6889 };
6890
6891 assert_eq!(
6892 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
6893 "bag.weight"
6894 );
6895 assert_eq!(
6896 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
6897 "bag.weight"
6898 );
6899 assert_eq!(
6900 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
6901 "calc.price"
6902 );
6903 assert_eq!(
6904 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
6905 "bag.cost"
6906 );
6907 assert_eq!(
6908 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
6909 "bag.cost"
6910 );
6911 assert_eq!(
6912 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
6913 "calc.price"
6914 );
6915
6916 assert_eq!(
6917 PathSegment {
6918 uses: "bag".to_string(),
6919 repository: None,
6920 spec: "nut_bag".to_string(),
6921 }
6922 .to_string(),
6923 "bag → nut_bag"
6924 );
6925 assert_eq!(
6926 PathSegment {
6927 uses: "calc".to_string(),
6928 repository: Some("alpha".to_string()),
6929 spec: "pricing".to_string(),
6930 }
6931 .to_string(),
6932 "calc → alpha pricing"
6933 );
6934 }
6935
6936 #[test]
6937 fn test_negated_comparison() {
6938 assert_eq!(
6939 negated_comparison(ComparisonComputation::LessThan),
6940 ComparisonComputation::GreaterThanOrEqual
6941 );
6942 assert_eq!(
6943 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
6944 ComparisonComputation::LessThan
6945 );
6946 assert_eq!(
6947 negated_comparison(ComparisonComputation::Is),
6948 ComparisonComputation::IsNot
6949 );
6950 assert_eq!(
6951 negated_comparison(ComparisonComputation::IsNot),
6952 ComparisonComputation::Is
6953 );
6954 }
6955
6956 #[test]
6957 fn value_to_semantic_number_is_decimal() {
6958 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
6959 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
6960 }
6961
6962 #[test]
6963 fn value_kind_measure_serializes_magnitude_only() {
6964 let kind =
6965 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
6966 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
6967 assert_eq!(json["measure"]["value"], "99.5");
6968 assert!(json["measure"].get("signature").is_none());
6969 }
6970
6971 #[test]
6972 fn value_kind_measure_roundtrips() {
6973 let original =
6974 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
6975 let api = crate::api::ValueKind::from(&original);
6976 let json = serde_json::to_string(&api).unwrap();
6977 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6978 assert_eq!(api, parsed);
6979 }
6980
6981 #[test]
6982 fn value_kind_measure_empty_roundtrips() {
6983 let original =
6984 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
6985 let api = crate::api::ValueKind::from(&original);
6986 let json = serde_json::to_string(&api).unwrap();
6987 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6988 assert_eq!(api, parsed);
6989 }
6990
6991 #[test]
6992 fn literal_value_number_serde_not_rational_array() {
6993 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
6994 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
6995 let number = json
6996 .get("value")
6997 .and_then(|v| v.get("number"))
6998 .expect("number field");
6999 assert!(number.is_string());
7000 assert_eq!(number.as_str(), Some("20"));
7001 assert!(
7002 !number.is_array(),
7003 "stored number must not serialize as [n,d]"
7004 );
7005 }
7006
7007 #[test]
7008 fn test_literal_value_to_primitive_type() {
7009 let one = rational_new(1, 1);
7010
7011 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
7012 assert_eq!(
7013 TypedLiteral::number(one.clone()).lemma_type.name(),
7014 "number"
7015 );
7016 assert_eq!(
7017 TypedLiteral::from_bool(bool::from(BooleanValue::True))
7018 .lemma_type
7019 .name(),
7020 "boolean"
7021 );
7022
7023 let dt = DateTimeValue {
7024 year: 2024,
7025 month: 1,
7026 day: 1,
7027 hour: 0,
7028 minute: 0,
7029 second: 0,
7030 microsecond: 0,
7031 timezone: None,
7032
7033 granularity: DateGranularity::Full,
7034 };
7035 assert_eq!(
7036 TypedLiteral::date(date_time_to_semantic(&dt))
7037 .lemma_type
7038 .name(),
7039 "date"
7040 );
7041 assert_eq!(
7042 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
7043 .lemma_type
7044 .name(),
7045 "ratio"
7046 );
7047 let dur_type = LemmaType::new(
7048 "duration".to_string(),
7049 TypeSpecification::Measure {
7050 minimum: None,
7051 maximum: None,
7052 decimals: None,
7053 units: MeasureUnits::from(vec![MeasureUnit {
7054 name: "second".to_string(),
7055 factor: crate::computation::rational::rational_one(),
7056 derived_measure_factors: Vec::new(),
7057 decomposition: BaseMeasureVector::new(),
7058 minimum: None,
7059 maximum: None,
7060 suggestion_magnitude: None,
7061 }]),
7062 traits: vec![MeasureTrait::Duration],
7063 decomposition: None,
7064 unit: None,
7065 help: String::new(),
7066 },
7067 TypeExtends::Primitive,
7068 );
7069 assert_eq!(
7070 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
7071 .lemma_type
7072 .name(),
7073 "duration"
7074 );
7075 }
7076
7077 #[test]
7078 fn test_type_display() {
7079 let specs = TypeSpecification::text();
7080 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
7081 assert_eq!(format!("{}", lemma_type), "name");
7082 }
7083
7084 #[test]
7085 fn test_type_serialization() {
7086 let specs = TypeSpecification::number();
7087 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
7088 let api = crate::api::LemmaType::from(&lemma_type);
7089 let serialized = serde_json::to_string(&api).unwrap();
7090 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
7091 assert_eq!(api, deserialized);
7092 }
7093
7094 #[test]
7095 fn test_literal_value_display_value() {
7096 let ten = rational_new(10, 1);
7097
7098 assert_eq!(
7099 LiteralValue::text("hello".to_string()).display_value(),
7100 "hello"
7101 );
7102 assert_eq!(LiteralValue::number(ten).display_value(), "10");
7103 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
7104 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
7105
7106 let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
7108 let ten_percent_ratio = LiteralValue::ratio(
7109 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
7110 .expect("ratio decimal"),
7111 );
7112 assert_eq!(
7113 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
7114 "10%"
7115 );
7116
7117 let time = TimeValue {
7118 hour: 14,
7119 minute: 30,
7120 second: 0,
7121 microsecond: 0,
7122 timezone: None,
7123 };
7124 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
7125 assert!(time_display.contains("14"));
7126 assert!(time_display.contains("30"));
7127 }
7128
7129 #[test]
7130 fn test_measure_display_respects_type_decimals() {
7131 let money_type = LemmaType {
7132 name: Some("money".to_string()),
7133 specifications: TypeSpecification::Measure {
7134 minimum: None,
7135 maximum: None,
7136 decimals: Some(2),
7137 units: MeasureUnits::from(vec![MeasureUnit {
7138 name: "eur".to_string(),
7139 factor: crate::computation::rational::rational_one(),
7140 derived_measure_factors: Vec::new(),
7141 decomposition: BaseMeasureVector::new(),
7142 minimum: None,
7143 maximum: None,
7144 suggestion_magnitude: None,
7145 }]),
7146 traits: Vec::new(),
7147 decomposition: None,
7148 unit: None,
7149 help: String::new(),
7150 },
7151 extends: TypeExtends::Primitive,
7152 };
7153 let money_type = Arc::new(money_type);
7154 let val =
7155 LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
7156 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.80 eur");
7157 let more_precision = LiteralValue::measure(
7158 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
7159 );
7160 assert_eq!(
7161 more_precision.display_value_with_type(money_type.as_ref()),
7162 "1.80 eur"
7163 );
7164 let measure_no_decimals = Arc::new(LemmaType {
7165 name: Some("count".to_string()),
7166 specifications: TypeSpecification::Measure {
7167 minimum: None,
7168 maximum: None,
7169 decimals: None,
7170 units: MeasureUnits::from(vec![MeasureUnit {
7171 name: "items".to_string(),
7172 factor: crate::computation::rational::rational_one(),
7173 derived_measure_factors: Vec::new(),
7174 decomposition: BaseMeasureVector::new(),
7175 minimum: None,
7176 maximum: None,
7177 suggestion_magnitude: None,
7178 }]),
7179 traits: Vec::new(),
7180 decomposition: None,
7181 unit: None,
7182 help: String::new(),
7183 },
7184 extends: TypeExtends::Primitive,
7185 });
7186 let val_any = LiteralValue::measure(
7187 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
7188 );
7189 assert_eq!(
7190 val_any.display_value_with_type(measure_no_decimals.as_ref()),
7191 "42.5 items"
7192 );
7193 }
7194
7195 #[test]
7196 fn test_literal_value_time_type() {
7197 let time = TimeValue {
7198 hour: 14,
7199 minute: 30,
7200 second: 0,
7201 microsecond: 0,
7202 timezone: None,
7203 };
7204 let lit = TypedLiteral::time(time_to_semantic(&time));
7205 assert_eq!(lit.lemma_type.name(), "time");
7206 }
7207
7208 #[test]
7209 fn test_measure_family_name_none_without_declaring_spec() {
7210 let money_primitive = LemmaType::new(
7211 "money".to_string(),
7212 TypeSpecification::measure(),
7213 TypeExtends::Primitive,
7214 );
7215 assert_eq!(money_primitive.measure_family_name(), None);
7216 }
7217
7218 #[test]
7219 fn test_measure_family_name_custom() {
7220 let measure_spec = TypeSpecification::measure();
7221 let money_custom = LemmaType::new(
7222 "money".to_string(),
7223 measure_spec,
7224 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7225 );
7226 assert_eq!(money_custom.measure_family_name(), Some("money"));
7227 }
7228
7229 #[test]
7230 fn test_same_named_roots_in_different_specs_are_different_families() {
7231 let declared_in = |spec: &str| TypeFamily {
7232 repository: None,
7233 spec: spec.to_string(),
7234 effective: crate::parsing::ast::EffectiveDate::Origin,
7235 name: "money".to_string(),
7236 };
7237 let pricing_money = LemmaType::new(
7238 "money".to_string(),
7239 TypeSpecification::measure(),
7240 TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
7241 );
7242 let shop_money = LemmaType::new(
7243 "money".to_string(),
7244 TypeSpecification::measure(),
7245 TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
7246 );
7247 assert!(!pricing_money.same_measure_family(&shop_money));
7248 assert!(!shop_money.same_measure_family(&pricing_money));
7249 }
7250
7251 #[test]
7252 fn test_same_measure_family_parent_and_child() {
7253 let measure_spec = TypeSpecification::measure();
7254 let type_x = LemmaType::new(
7255 "x".to_string(),
7256 measure_spec.clone(),
7257 TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
7258 );
7259 let type_x2 = LemmaType::new(
7260 "x2".to_string(),
7261 measure_spec,
7262 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7263 );
7264 assert_eq!(type_x.measure_family_name(), Some("x"));
7265 assert_eq!(type_x2.measure_family_name(), Some("x"));
7266 assert!(type_x.same_measure_family(&type_x2));
7267 assert!(type_x2.same_measure_family(&type_x));
7268 }
7269
7270 #[test]
7271 fn test_same_measure_family_siblings() {
7272 let measure_spec = TypeSpecification::measure();
7273 let type_x2_a = LemmaType::new(
7274 "x2a".to_string(),
7275 measure_spec.clone(),
7276 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7277 );
7278 let type_x2_b = LemmaType::new(
7279 "x2b".to_string(),
7280 measure_spec,
7281 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7282 );
7283 assert!(type_x2_a.same_measure_family(&type_x2_b));
7284 }
7285
7286 #[test]
7287 fn test_same_measure_family_different_families() {
7288 let measure_spec = TypeSpecification::measure();
7289 let money = LemmaType::new(
7290 "money".to_string(),
7291 measure_spec.clone(),
7292 TypeExtends::Primitive,
7293 );
7294 let temperature = LemmaType::new(
7295 "temperature".to_string(),
7296 measure_spec,
7297 TypeExtends::Primitive,
7298 );
7299 assert!(!money.same_measure_family(&temperature));
7300 assert!(!temperature.same_measure_family(&money));
7301 }
7302
7303 #[test]
7304 fn test_same_measure_family_measure_vs_non_measure() {
7305 let measure_spec = TypeSpecification::measure();
7306 let number_spec = TypeSpecification::number();
7307 let measure_type =
7308 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
7309 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7310 assert!(!measure_type.same_measure_family(&number_type));
7311 assert!(!number_type.same_measure_family(&measure_type));
7312 }
7313
7314 #[test]
7315 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
7316 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
7317 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
7318
7319 assert!(!left.same_measure_family(&right));
7320 assert!(left.compatible_with_anonymous_measure(&right));
7321 }
7322
7323 #[test]
7324 fn test_measure_family_name_non_measure_returns_none() {
7325 let number_spec = TypeSpecification::number();
7326 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7327 assert_eq!(number_type.measure_family_name(), None);
7328 }
7329
7330 #[test]
7331 fn test_lemma_type_inequality_local_vs_import_same_shape() {
7332 let measure_spec = TypeSpecification::measure();
7333 let local = LemmaType::new(
7334 "t".to_string(),
7335 measure_spec.clone(),
7336 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7337 );
7338 let imported = LemmaType::new(
7339 "t".to_string(),
7340 measure_spec,
7341 TypeExtends::Custom {
7342 parent: "money".to_string(),
7343 family: test_type_family("money"),
7344 defining_spec: TypeDefiningSpec::Import,
7345 },
7346 );
7347 assert_ne!(local, imported);
7348 }
7349
7350 #[test]
7351 fn test_lemma_type_equality_import_unit_variant() {
7352 let measure_spec = TypeSpecification::measure();
7353 let left = LemmaType::new(
7354 "t".to_string(),
7355 measure_spec.clone(),
7356 TypeExtends::Custom {
7357 parent: "money".to_string(),
7358 family: test_type_family("money"),
7359 defining_spec: TypeDefiningSpec::Import,
7360 },
7361 );
7362 let right = LemmaType::new(
7363 "t".to_string(),
7364 measure_spec,
7365 TypeExtends::Custom {
7366 parent: "money".to_string(),
7367 family: test_type_family("money"),
7368 defining_spec: TypeDefiningSpec::Import,
7369 },
7370 );
7371 assert_eq!(left, right);
7372 }
7373
7374 fn month_suggestion_arg() -> CommandArg {
7375 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7376 Decimal::ONE,
7377 "month".to_string(),
7378 ))
7379 }
7380
7381 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
7382 [
7383 CommandArg::Label(name.to_string()),
7384 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
7385 ]
7386 }
7387
7388 #[test]
7389 fn default_calendar_on_text_reports_hint() {
7390 let mut specs = TypeSpecification::text();
7391 let mut default = None;
7392 let err = specs
7393 .apply_constraint(
7394 "notes",
7395 TypeConstraintCommand::Suggest,
7396 &[month_suggestion_arg()],
7397 &mut default,
7398 &mut None,
7399 None,
7400 )
7401 .unwrap_err();
7402 assert!(err.contains("Unit 'month' is for calendar data"));
7403 assert!(err.contains("double quotes"));
7404 }
7405
7406 #[test]
7407 fn default_calendar_on_duration_reports_valid_units() {
7408 let mut specs = TypeSpecification::measure();
7409 specs
7410 .apply_constraint(
7411 "duration",
7412 TypeConstraintCommand::Unit,
7413 &unit_factor_arg("second", 1),
7414 &mut None,
7415 &mut None,
7416 None,
7417 )
7418 .unwrap();
7419 specs
7420 .apply_constraint(
7421 "duration",
7422 TypeConstraintCommand::Unit,
7423 &unit_factor_arg("week", 604_800),
7424 &mut None,
7425 &mut None,
7426 None,
7427 )
7428 .unwrap();
7429 specs
7430 .apply_constraint(
7431 "duration",
7432 TypeConstraintCommand::Trait,
7433 &[CommandArg::Label("duration".to_string())],
7434 &mut None,
7435 &mut None,
7436 None,
7437 )
7438 .unwrap();
7439 let mut default = None;
7440 let err = specs
7441 .apply_constraint(
7442 "duration",
7443 TypeConstraintCommand::Suggest,
7444 &[month_suggestion_arg()],
7445 &mut default,
7446 &mut None,
7447 None,
7448 )
7449 .unwrap_err();
7450 assert!(err.contains("Unit 'month' is for calendar data"));
7451 assert!(err.contains("Valid 'duration' units are"));
7452 assert!(err.contains("week"));
7453 }
7454
7455 #[test]
7456 fn default_valid_duration_weeks_accepted() {
7457 let mut specs = TypeSpecification::measure();
7458 specs
7459 .apply_constraint(
7460 "duration",
7461 TypeConstraintCommand::Unit,
7462 &unit_factor_arg("second", 1),
7463 &mut None,
7464 &mut None,
7465 None,
7466 )
7467 .unwrap();
7468 specs
7469 .apply_constraint(
7470 "duration",
7471 TypeConstraintCommand::Unit,
7472 &unit_factor_arg("week", 604_800),
7473 &mut None,
7474 &mut None,
7475 None,
7476 )
7477 .unwrap();
7478 specs
7479 .apply_constraint(
7480 "duration",
7481 TypeConstraintCommand::Trait,
7482 &[CommandArg::Label("duration".to_string())],
7483 &mut None,
7484 &mut None,
7485 None,
7486 )
7487 .unwrap();
7488 let mut default = None;
7489 specs
7490 .apply_constraint(
7491 "duration",
7492 TypeConstraintCommand::Suggest,
7493 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7494 Decimal::from(4),
7495 "week".to_string(),
7496 ))],
7497 &mut default,
7498 &mut None,
7499 None,
7500 )
7501 .unwrap();
7502 assert!(matches!(
7503 default,
7504 Some(RawSuggestion::Measure {
7505 unit_name,
7506 ..
7507 }) if unit_name == "week"
7508 ));
7509 }
7510
7511 #[test]
7512 fn default_unknown_unit_on_duration_lists_valid_units() {
7513 let mut specs = TypeSpecification::measure();
7514 specs
7515 .apply_constraint(
7516 "duration",
7517 TypeConstraintCommand::Unit,
7518 &unit_factor_arg("second", 1),
7519 &mut None,
7520 &mut None,
7521 None,
7522 )
7523 .unwrap();
7524 specs
7525 .apply_constraint(
7526 "duration",
7527 TypeConstraintCommand::Trait,
7528 &[CommandArg::Label("duration".to_string())],
7529 &mut None,
7530 &mut None,
7531 None,
7532 )
7533 .unwrap();
7534 let mut default = None;
7535 let err = specs
7536 .apply_constraint(
7537 "duration",
7538 TypeConstraintCommand::Suggest,
7539 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7540 Decimal::ONE,
7541 "fortnight".to_string(),
7542 ))],
7543 &mut default,
7544 &mut None,
7545 None,
7546 )
7547 .unwrap_err();
7548 assert!(err.contains("fortnight"));
7549 assert!(err.contains("not defined on 'duration'"));
7550 assert!(err.contains("Valid units are"));
7551 }
7552
7553 fn money_measure_type() -> LemmaType {
7554 LemmaType::new(
7555 "Money".to_string(),
7556 TypeSpecification::Measure {
7557 minimum: None,
7558 maximum: None,
7559 decimals: None,
7560 units: MeasureUnits::from(vec![
7561 MeasureUnit {
7562 name: "eur".to_string(),
7563 factor: crate::computation::rational::rational_one(),
7564 derived_measure_factors: Vec::new(),
7565 decomposition: BaseMeasureVector::new(),
7566 minimum: None,
7567 maximum: None,
7568 suggestion_magnitude: None,
7569 },
7570 MeasureUnit {
7571 name: "usd".to_string(),
7572 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
7573 91, 2,
7574 ))
7575 .expect("factor"),
7576 derived_measure_factors: Vec::new(),
7577 decomposition: BaseMeasureVector::new(),
7578 minimum: None,
7579 maximum: None,
7580 suggestion_magnitude: None,
7581 },
7582 ]),
7583 traits: Vec::new(),
7584 decomposition: None,
7585 unit: None,
7586 help: String::new(),
7587 },
7588 TypeExtends::Primitive,
7589 )
7590 }
7591
7592 #[test]
7593 fn measure_unit_names_for_named_measure() {
7594 let money = money_measure_type();
7595 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
7596 }
7597
7598 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
7603 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
7604 }
7605
7606 #[test]
7607 fn combine_signatures_multiply_adds_exponents() {
7608 let left = sig(&[("eur", 1)]);
7609 let right = sig(&[("hour", -1)]);
7610 let result = combine_signatures(&left, &right, true);
7611 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7612 }
7613
7614 #[test]
7615 fn combine_signatures_divide_subtracts_exponents() {
7616 let left = sig(&[("eur", 1)]);
7617 let right = sig(&[("hour", 1)]);
7618 let result = combine_signatures(&left, &right, false);
7619 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7620 }
7621
7622 #[test]
7623 fn combine_signatures_cancels_to_empty() {
7624 let left = sig(&[("ce", 1), ("minute", -1)]);
7625 let right = sig(&[("minute", 1)]);
7626 let result = combine_signatures(&left, &right, true);
7627 assert_eq!(result, sig(&[("ce", 1)]));
7629 }
7630
7631 #[test]
7632 fn combine_signatures_output_is_canonical_form() {
7633 let left = sig(&[("eur", 1), ("hour", 1)]);
7634 let right = sig(&[("minute", 1)]);
7635 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
7638 assert_eq!(result, expected);
7639 }
7640
7641 #[test]
7642 fn canonicalize_signature_drops_zero_exponents() {
7643 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
7644 let result = canonicalize_signature(&sig_with_zero);
7645 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7646 }
7647
7648 #[test]
7649 fn canonicalize_signature_sorts_by_name() {
7650 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
7651 let result = canonicalize_signature(&unsorted);
7652 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7653 }
7654
7655 #[test]
7661 fn format_signature_operator_style_numerator_only() {
7662 let signature = sig(&[("eur", 1)]);
7663 let result = format_signature_operator_style(&signature);
7664 assert_eq!(result, "eur");
7665 }
7666
7667 #[test]
7668 fn format_signature_operator_style_with_denominator() {
7669 let signature = sig(&[("eur", 1), ("hour", -1)]);
7670 let result = format_signature_operator_style(&signature);
7671 assert_eq!(result, "eur/hour");
7672 }
7673
7674 #[test]
7675 fn format_signature_operator_style_denominator_only() {
7676 let signature = sig(&[("meter", -1)]);
7677 let result = format_signature_operator_style(&signature);
7678 assert_eq!(result, "1/meter");
7679 }
7680
7681 #[test]
7682 fn format_signature_operator_style_with_exponents() {
7683 let signature = sig(&[("meter", 2), ("second", -2)]);
7684 let result = format_signature_operator_style(&signature);
7685 assert_eq!(result, "meter^2/second^2");
7686 }
7687
7688 #[test]
7693 fn calendar_unit_factor_table_completeness() {
7694 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
7697 let name = unit.to_string();
7698 assert!(
7699 calendar_unit_factor(&name).is_some(),
7700 "calendar_unit_factor('{}') must return Some",
7701 name
7702 );
7703 }
7704 }
7705
7706 #[test]
7707 fn semantic_calendar_unit_display_returns_singular() {
7708 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
7711 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
7712 }
7713
7714 #[test]
7719 fn signature_factor_with_calendar_units() {
7720 let calendar = test_calendar_type_for_signature_factor();
7721 let unit_index = crate::planning::unit_index::UnitIndex::new();
7722 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
7725 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
7726 .expect("must not overflow");
7727 let expected = rational_new(1, 12);
7728 assert_eq!(factor, expected, "month/year factor must be 1/12");
7729 }
7730
7731 fn test_calendar_type_for_signature_factor() -> LemmaType {
7732 use crate::computation::rational::{decimal_to_rational, rational_one};
7733 use crate::literals::{MeasureUnit, MeasureUnits};
7734 use rust_decimal::Decimal;
7735 LemmaType::new(
7736 "calendar".to_string(),
7737 TypeSpecification::Measure {
7738 minimum: None,
7739 maximum: None,
7740 decimals: None,
7741 units: MeasureUnits::from(vec![
7742 MeasureUnit {
7743 name: "month".to_string(),
7744 factor: rational_one(),
7745 minimum: None,
7746 maximum: None,
7747 suggestion_magnitude: None,
7748 decomposition: calendar_decomposition(),
7749 derived_measure_factors: Vec::new(),
7750 },
7751 MeasureUnit {
7752 name: "year".to_string(),
7753 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
7754 minimum: None,
7755 maximum: None,
7756 suggestion_magnitude: None,
7757 decomposition: calendar_decomposition(),
7758 derived_measure_factors: Vec::new(),
7759 },
7760 ]),
7761 traits: vec![MeasureTrait::Calendar],
7762 decomposition: Some(calendar_decomposition()),
7763 unit: None,
7764 help: String::new(),
7765 },
7766 TypeExtends::Primitive,
7767 )
7768 }
7769
7770 #[test]
7771 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
7772 fn signature_factor_panics_on_unresolved_name() {
7773 let unit_index = crate::planning::unit_index::UnitIndex::new();
7774 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
7775 signature_factor(&bad_sig, &unit_index, None)
7776 .expect("BUG: signature_factor must panic before returning a failure");
7777 }
7778
7779 #[test]
7780 fn signature_factor_uses_owner_when_expression_index_empty() {
7781 let money = test_money_type_for_signature_factor();
7782 let expression_units = crate::planning::unit_index::UnitIndex::new();
7783 let sig_usd = sig(&[("usd", 1)]);
7784 let factor =
7785 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
7786 assert_eq!(factor, rational_new(91, 100));
7787 }
7788
7789 fn test_money_type_for_signature_factor() -> LemmaType {
7790 use crate::computation::rational::decimal_to_rational;
7791 use crate::literals::{MeasureUnit, MeasureUnits};
7792 use rust_decimal::Decimal;
7793 LemmaType::new(
7794 "money".to_string(),
7795 TypeSpecification::Measure {
7796 minimum: None,
7797 maximum: None,
7798 decimals: Some(2),
7799 units: MeasureUnits::from(vec![
7800 MeasureUnit {
7801 name: "eur".to_string(),
7802 factor: crate::computation::rational::rational_one(),
7803 minimum: None,
7804 maximum: None,
7805 suggestion_magnitude: None,
7806 decomposition: BaseMeasureVector::new(),
7807 derived_measure_factors: Vec::new(),
7808 },
7809 MeasureUnit {
7810 name: "usd".to_string(),
7811 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
7812 minimum: None,
7813 maximum: None,
7814 suggestion_magnitude: None,
7815 decomposition: BaseMeasureVector::new(),
7816 derived_measure_factors: Vec::new(),
7817 },
7818 ]),
7819 traits: Vec::new(),
7820 decomposition: None,
7821 unit: None,
7822 help: String::new(),
7823 },
7824 TypeExtends::Primitive,
7825 )
7826 }
7827
7828 fn measure_type_with_kilogram() -> TypeSpecification {
7829 use crate::computation::rational::rational_one;
7830 use crate::literals::{MeasureUnit, MeasureUnits};
7831 let mut units = MeasureUnits::new();
7832 units.push(MeasureUnit {
7833 name: "kilogram".to_string(),
7834 factor: rational_one(),
7835 minimum: None,
7836 maximum: None,
7837 suggestion_magnitude: None,
7838 decomposition: BaseMeasureVector::new(),
7839 derived_measure_factors: Vec::new(),
7840 });
7841 TypeSpecification::Measure {
7842 minimum: None,
7843 maximum: None,
7844 decimals: None,
7845 units,
7846 traits: Vec::new(),
7847 decomposition: None,
7848 unit: None,
7849 help: String::new(),
7850 }
7851 }
7852
7853 #[test]
7854 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
7855 let ten = Value::Number(Decimal::from(10));
7856 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram(), None)
7857 .expect_err("bare number must not bind to measure");
7858 assert!(
7859 err.contains("kilogram"),
7860 "error must hint expected unit, got: {err}"
7861 );
7862 }
7863
7864 #[test]
7865 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
7866 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
7867 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram(), None)
7868 .expect("10 kilogram must bind to measure");
7869 assert!(matches!(kind, ValueKind::Measure(_)));
7870 }
7871
7872 #[test]
7873 fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
7874 let ten = Value::Number(Decimal::from(10));
7875 let err = parser_value_to_value_kind(&ten, &TypeSpecification::ratio(), None)
7876 .expect_err("bare number must not bind to ratio");
7877 assert!(
7878 err.contains("percent"),
7879 "error must hint expected unit, got: {err}"
7880 );
7881 }
7882
7883 #[test]
7884 fn value_kind_matches_spec_rejects_number_for_measure() {
7885 let n = ValueKind::Number(rational_new(10, 1));
7886 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
7887 }
7888
7889 #[test]
7890 fn value_kind_matches_spec_rejects_number_for_ratio() {
7891 let n = ValueKind::Number(rational_new(1, 2));
7892 assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
7893 }
7894
7895 #[test]
7896 fn apply_constraint_rejects_inherited_unit_factor_change() {
7897 let mut specs = TypeSpecification::measure();
7898 specs
7899 .apply_constraint(
7900 "money",
7901 TypeConstraintCommand::Unit,
7902 &unit_factor_arg("eur", 1),
7903 &mut None,
7904 &mut None,
7905 None,
7906 )
7907 .expect("seed eur");
7908 let err = specs
7909 .apply_constraint(
7910 "money",
7911 TypeConstraintCommand::Unit,
7912 &[
7913 CommandArg::Label("eur".to_string()),
7914 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
7915 ],
7916 &mut None,
7917 &mut None,
7918 None,
7919 )
7920 .expect_err("must not change inherited unit factor");
7921 assert!(err.contains("eur"), "error must name unit, got: {err}");
7922 assert!(
7923 err.contains("inherited") || err.contains("cannot change"),
7924 "error must reject factor change, got: {err}"
7925 );
7926 }
7927
7928 #[test]
7929 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
7930 let mut specs = TypeSpecification::measure();
7931 specs
7932 .apply_constraint(
7933 "money",
7934 TypeConstraintCommand::Unit,
7935 &unit_factor_arg("eur", 1),
7936 &mut None,
7937 &mut None,
7938 None,
7939 )
7940 .expect("seed eur");
7941 specs
7942 .apply_constraint(
7943 "money",
7944 TypeConstraintCommand::Unit,
7945 &unit_factor_arg("usd", 1),
7946 &mut None,
7947 &mut None,
7948 None,
7949 )
7950 .expect("add usd");
7951 match &specs {
7952 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
7953 other => panic!("expected Measure, got {other:?}"),
7954 }
7955 }
7956
7957 #[test]
7958 fn apply_constraint_idempotent_inherited_unit_redeclare() {
7959 let mut specs = TypeSpecification::measure();
7960 specs
7961 .apply_constraint(
7962 "money",
7963 TypeConstraintCommand::Unit,
7964 &unit_factor_arg("eur", 1),
7965 &mut None,
7966 &mut None,
7967 None,
7968 )
7969 .expect("seed eur");
7970 specs
7971 .apply_constraint(
7972 "money",
7973 TypeConstraintCommand::Unit,
7974 &unit_factor_arg("eur", 1),
7975 &mut None,
7976 &mut None,
7977 None,
7978 )
7979 .expect("idempotent eur");
7980 match &specs {
7981 TypeSpecification::Measure { units, .. } => {
7982 assert_eq!(units.len(), 1);
7983 assert_eq!(
7984 units.iter().find(|u| u.name == "eur").expect("eur").factor,
7985 crate::computation::rational::rational_one()
7986 );
7987 }
7988 other => panic!("expected Measure, got {other:?}"),
7989 }
7990 }
7991
7992 #[test]
7993 fn element_from_range_returns_element_for_every_range_primitive() {
7994 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
7995 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
7996 (PrimitiveKind::NumberRange, |element| {
7997 matches!(element, TypeSpecification::Number { .. })
7998 }),
7999 (PrimitiveKind::MeasureRange, |element| {
8000 matches!(element, TypeSpecification::Measure { .. })
8001 }),
8002 (PrimitiveKind::RatioRange, |element| {
8003 matches!(element, TypeSpecification::Ratio { .. })
8004 }),
8005 (PrimitiveKind::DateRange, |element| {
8006 matches!(element, TypeSpecification::Date { .. })
8007 }),
8008 (PrimitiveKind::TimeRange, |element| {
8009 matches!(element, TypeSpecification::Time { .. })
8010 }),
8011 ];
8012 for (kind, matches_element) in cases {
8013 let range_spec = type_spec_for_primitive(kind);
8014 let element = range_spec
8015 .element_from_range()
8016 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
8017 assert!(
8018 matches_element(&element),
8019 "{kind:?} element must match documented mapping, got {element:?}"
8020 );
8021 }
8022 }
8023
8024 #[test]
8025 fn element_from_range_returns_none_for_non_range_primitives() {
8026 let non_range = [
8027 type_spec_for_primitive(PrimitiveKind::Boolean),
8028 type_spec_for_primitive(PrimitiveKind::Measure),
8029 TypeSpecification::Undetermined,
8030 TypeSpecification::veto(),
8031 ];
8032 for spec in non_range {
8033 assert!(
8034 spec.element_from_range().is_none(),
8035 "{spec:?} must not define element_from_range"
8036 );
8037 }
8038 }
8039
8040 fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
8041 LemmaType::primitive(TypeSpecification::Spec {
8042 spec: SpecIdentity {
8043 repository: None,
8044 spec: name.to_string(),
8045 effective,
8046 },
8047 })
8048 }
8049
8050 #[test]
8051 fn same_value_type_spec_identity() {
8052 use crate::parsing::ast::EffectiveDate;
8053 let origin = EffectiveDate::Origin;
8054 let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
8055 year: 2026,
8056 month: 1,
8057 day: 1,
8058 hour: 0,
8059 minute: 0,
8060 second: 0,
8061 microsecond: 0,
8062 timezone: None,
8063 granularity: crate::literals::DateGranularity::Full,
8064 });
8065 let tax = spec_type("tax_bracket", origin.clone());
8066 let tax_again = spec_type("tax_bracket", origin.clone());
8067 let other = spec_type("menu_item", origin);
8068 let tax_dated = spec_type("tax_bracket", dated);
8069 assert!(tax.same_value_type(&tax_again));
8070 assert!(!tax.same_value_type(&other));
8071 assert!(!tax.same_value_type(&tax_dated));
8072 let mut other_repo = tax.clone();
8073 match &mut other_repo.specifications {
8074 TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
8075 other => panic!("expected Spec, got {other:?}"),
8076 }
8077 assert!(!tax.same_value_type(&other_repo));
8078 }
8079
8080 #[test]
8081 fn spec_identity_displays_repository_space_spec_space_effective() {
8082 use crate::parsing::ast::EffectiveDate;
8083 let workspace = SpecIdentity {
8084 repository: None,
8085 spec: "tax_bracket".to_string(),
8086 effective: EffectiveDate::Origin,
8087 };
8088 assert_eq!(workspace.to_string(), "tax_bracket");
8089 let dated = SpecIdentity {
8090 repository: Some("@jack/finance".to_string()),
8091 spec: "tax_bracket".to_string(),
8092 effective: EffectiveDate::DateTimeValue(
8093 DateTimeValue::from_str("2026-01-01").expect("date"),
8094 ),
8095 };
8096 assert_eq!(
8097 dated.to_string(),
8098 format!(
8099 "@jack/finance tax_bracket {}",
8100 DateTimeValue::from_str("2026-01-01").expect("date")
8101 )
8102 );
8103 }
8104
8105 #[test]
8106 fn type_family_displays_declaring_spec_then_type() {
8107 let workspace = TypeFamily {
8108 repository: None,
8109 spec: "pricing".to_string(),
8110 effective: crate::parsing::ast::EffectiveDate::Origin,
8111 name: "money".to_string(),
8112 };
8113 assert_eq!(workspace.to_string(), "pricing.money");
8114 let stdlib = TypeFamily {
8115 repository: Some("lemma".to_string()),
8116 spec: "units".to_string(),
8117 effective: crate::parsing::ast::EffectiveDate::Origin,
8118 name: "mass".to_string(),
8119 };
8120 assert_eq!(stdlib.to_string(), "lemma units.mass");
8121 let dated = TypeFamily {
8122 repository: None,
8123 spec: "pricing".to_string(),
8124 effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
8125 DateTimeValue::from_str("2024-01-01").expect("date"),
8126 ),
8127 name: "money".to_string(),
8128 };
8129 assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
8130 }
8131
8132 #[test]
8133 fn piecewise_type_keeps_spec_identity() {
8134 use crate::parsing::ast::EffectiveDate;
8135 let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
8136 let picked = crate::planning::typing::piecewise_type([
8137 Arc::new(LemmaType::veto_type()),
8138 Arc::clone(&tax),
8139 ]);
8140 assert!(picked.same_value_type(&tax));
8141 }
8142}