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 = value_to_semantic(value)?;
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 !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2558 return Err(
2559 "Please provide a ratio range, for example `-> suggest 10%...50%`."
2560 .to_string(),
2561 );
2562 }
2563 *target = Some(RawSuggestion::Value(ValueKind::Range(
2564 Box::new(left),
2565 Box::new(right),
2566 )));
2567 }
2568 _ => {
2569 return Err(format!(
2570 "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2571 command
2572 ));
2573 }
2574 },
2575 TypeSpecification::Text {
2576 length,
2577 options,
2578 help,
2579 } => match command {
2580 TypeConstraintCommand::Option => {
2581 if args.len() != 1 {
2582 return Err("option takes exactly one argument".to_string());
2583 }
2584 options.push(option_name(&args[0], "option")?);
2585 }
2586 TypeConstraintCommand::Options => {
2587 let mut collected = Vec::with_capacity(args.len());
2588 for arg in args {
2589 collected.push(option_name(arg, "options")?);
2590 }
2591 *options = collected;
2592 }
2593 TypeConstraintCommand::Length => {
2594 let d = require_decimal_literal(args, "length")?;
2595 *length = Some(decimal_to_usize(d, "length")?);
2596 }
2597 TypeConstraintCommand::Help => {
2598 apply_type_help_command(help, args)?;
2599 }
2600 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2601 let (target, cmd) = match command {
2602 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2603 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2604 _ => unreachable!("BUG: only Suggest or With in this arm"),
2605 };
2606 let lit = require_literal(args, cmd)?;
2607 reject_calendar_for_suggestion(
2608 lit,
2609 type_name,
2610 SuggestionExpectation::Text,
2611 None,
2612 )?;
2613 match lit {
2614 crate::literals::Value::Text(s) => {
2615 *target = Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2616 }
2617 _ => {
2618 return Err(
2619 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2620 .to_string(),
2621 );
2622 }
2623 }
2624 }
2625 _ => {
2626 return Err(format!(
2627 "Invalid command '{}' for text type. Valid commands: options, length, help, suggest, with",
2628 command
2629 ));
2630 }
2631 },
2632 TypeSpecification::Date {
2633 minimum,
2634 maximum,
2635 help,
2636 } => match command {
2637 TypeConstraintCommand::Minimum => {
2638 let dt = require_date_literal(args, "minimum")?;
2639 *minimum = Some(dt);
2640 }
2641 TypeConstraintCommand::Maximum => {
2642 let dt = require_date_literal(args, "maximum")?;
2643 *maximum = Some(dt);
2644 }
2645 TypeConstraintCommand::Help => {
2646 apply_type_help_command(help, args)?;
2647 }
2648 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2649 let (target, cmd) = match command {
2650 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2651 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2652 _ => unreachable!("BUG: only Suggest or With in this arm"),
2653 };
2654 let lit = require_literal(args, cmd)?;
2655 reject_calendar_for_suggestion(
2656 lit,
2657 type_name,
2658 SuggestionExpectation::Date,
2659 None,
2660 )?;
2661 match lit {
2662 crate::literals::Value::Date(dt) => {
2663 *target = Some(RawSuggestion::Value(ValueKind::Date(
2664 date_time_to_semantic(dt),
2665 )));
2666 }
2667 _ => {
2668 return Err(
2669 "Please provide a date, for example `-> suggest 2024-06-15`."
2670 .to_string(),
2671 );
2672 }
2673 }
2674 }
2675 _ => {
2676 return Err(format!(
2677 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
2678 command
2679 ));
2680 }
2681 },
2682 TypeSpecification::DateRange {
2683 lower,
2684 upper,
2685 minimum,
2686 maximum,
2687 help,
2688 } => match command {
2689 TypeConstraintCommand::Lower => {
2690 *lower = Some(require_date_literal(args, "lower")?);
2691 }
2692 TypeConstraintCommand::Upper => {
2693 *upper = Some(require_date_literal(args, "upper")?);
2694 }
2695 TypeConstraintCommand::Minimum => {
2696 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2697 }
2698 TypeConstraintCommand::Maximum => {
2699 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2700 }
2701 TypeConstraintCommand::Help => {
2702 apply_type_help_command(help, args)?;
2703 }
2704 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2705 let target = match command {
2706 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2707 TypeConstraintCommand::With => &mut *declared_value,
2708 _ => unreachable!("BUG: only Suggest or With in this arm"),
2709 };
2710 let (left, right) = require_suggestion_range_endpoints(
2711 args,
2712 type_name,
2713 SuggestionExpectation::DateRange,
2714 None,
2715 )?;
2716 let left = literal_value_from_parser_value(left)?;
2717 let right = literal_value_from_parser_value(right)?;
2718 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2719 return Err(
2720 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2721 .to_string(),
2722 );
2723 }
2724 *target = Some(RawSuggestion::Value(ValueKind::Range(
2725 Box::new(left),
2726 Box::new(right),
2727 )));
2728 }
2729 _ => {
2730 return Err(format!(
2731 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2732 command
2733 ));
2734 }
2735 },
2736 TypeSpecification::Time {
2737 minimum,
2738 maximum,
2739 help,
2740 } => match command {
2741 TypeConstraintCommand::Minimum => {
2742 let t = require_time_literal(args, "minimum")?;
2743 *minimum = Some(t);
2744 }
2745 TypeConstraintCommand::Maximum => {
2746 let t = require_time_literal(args, "maximum")?;
2747 *maximum = Some(t);
2748 }
2749 TypeConstraintCommand::Help => {
2750 apply_type_help_command(help, args)?;
2751 }
2752 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2753 let (target, cmd) = match command {
2754 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2755 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2756 _ => unreachable!("BUG: only Suggest or With in this arm"),
2757 };
2758 let lit = require_literal(args, cmd)?;
2759 reject_calendar_for_suggestion(
2760 lit,
2761 type_name,
2762 SuggestionExpectation::Time,
2763 None,
2764 )?;
2765 match lit {
2766 crate::literals::Value::Time(t) => {
2767 *target =
2768 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2769 }
2770 _ => {
2771 return Err(
2772 "Please provide a time, for example `-> suggest 09:00:00`."
2773 .to_string(),
2774 );
2775 }
2776 }
2777 }
2778 _ => {
2779 return Err(format!(
2780 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
2781 command
2782 ));
2783 }
2784 },
2785 TypeSpecification::TimeRange {
2786 lower,
2787 upper,
2788 minimum,
2789 maximum,
2790 help,
2791 } => match command {
2792 TypeConstraintCommand::Lower => {
2793 *lower = Some(require_time_literal(args, "lower")?);
2794 }
2795 TypeConstraintCommand::Upper => {
2796 *upper = Some(require_time_literal(args, "upper")?);
2797 }
2798 TypeConstraintCommand::Minimum => {
2799 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2800 }
2801 TypeConstraintCommand::Maximum => {
2802 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2803 }
2804 TypeConstraintCommand::Help => {
2805 apply_type_help_command(help, args)?;
2806 }
2807 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2808 let target = match command {
2809 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2810 TypeConstraintCommand::With => &mut *declared_value,
2811 _ => unreachable!("BUG: only Suggest or With in this arm"),
2812 };
2813 let (left, right) = require_suggestion_range_endpoints(
2814 args,
2815 type_name,
2816 SuggestionExpectation::TimeRange,
2817 None,
2818 )?;
2819 let left = literal_value_from_parser_value(left)?;
2820 let right = literal_value_from_parser_value(right)?;
2821 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2822 return Err(
2823 "Please provide a time range, for example `-> suggest 09:00...17:00`."
2824 .to_string(),
2825 );
2826 }
2827 *target = Some(RawSuggestion::Value(ValueKind::Range(
2828 Box::new(left),
2829 Box::new(right),
2830 )));
2831 }
2832 _ => {
2833 return Err(format!(
2834 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2835 command
2836 ));
2837 }
2838 },
2839 TypeSpecification::MeasureRange {
2840 lower,
2841 upper,
2842 minimum,
2843 maximum,
2844 units,
2845 decomposition,
2846 help,
2847 } => match command {
2848 TypeConstraintCommand::Unit => {
2849 return Err(format!(
2850 "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
2851 ));
2852 }
2853 TypeConstraintCommand::Lower => {
2854 *lower = Some(parse_measure_declared_bound(
2855 args, "lower", units, type_name,
2856 )?);
2857 }
2858 TypeConstraintCommand::Upper => {
2859 *upper = Some(parse_measure_declared_bound(
2860 args, "upper", units, type_name,
2861 )?);
2862 }
2863 TypeConstraintCommand::Minimum => {
2864 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2865 reject_negative_width_magnitude(&width.0, "minimum")?;
2866 *minimum = Some(width);
2867 }
2868 TypeConstraintCommand::Maximum => {
2869 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2870 reject_negative_width_magnitude(&width.0, "maximum")?;
2871 *maximum = Some(width);
2872 }
2873 TypeConstraintCommand::Help => {
2874 apply_type_help_command(help, args)?;
2875 }
2876 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2877 let target = match command {
2878 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2879 TypeConstraintCommand::With => &mut *declared_value,
2880 _ => unreachable!("BUG: only Suggest or With in this arm"),
2881 };
2882 let (left, right) = require_suggestion_range_endpoints(
2883 args,
2884 type_name,
2885 SuggestionExpectation::MeasureRange,
2886 Some(units),
2887 )?;
2888 let element_spec = TypeSpecification::MeasureRange {
2889 lower: lower.clone(),
2890 upper: upper.clone(),
2891 minimum: minimum.clone(),
2892 maximum: maximum.clone(),
2893 units: units.clone(),
2894 decomposition: decomposition.clone(),
2895 help: help.clone(),
2896 }
2897 .element_from_range()
2898 .expect("BUG: MeasureRange must define element_from_range");
2899 let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2900 let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2901 if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2902 return Err(format!(
2903 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2904 ));
2905 }
2906 *target = Some(RawSuggestion::Value(ValueKind::Range(
2907 Box::new(left),
2908 Box::new(right),
2909 )));
2910 }
2911 _ => {
2912 return Err(format!(
2913 "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2914 command
2915 ));
2916 }
2917 },
2918 TypeSpecification::Veto { .. } => {
2919 return Err(format!(
2920 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2921 command
2922 ));
2923 }
2924 TypeSpecification::Undetermined => {
2925 return Err(format!(
2926 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2927 command
2928 ));
2929 }
2930 TypeSpecification::Spec { .. } => {
2931 return Err(format!(
2932 "Invalid command '{command}' for spec type. A spec instance has no data constraints"
2933 ));
2934 }
2935 TypeSpecification::List { element, .. } => match command {
2936 TypeConstraintCommand::Help => {
2937 let mut unused_suggestion = None;
2938 let mut unused_value = None;
2939 Arc::make_mut(element).specifications.apply_constraint(
2940 type_name,
2941 command,
2942 args,
2943 &mut unused_suggestion,
2944 &mut unused_value,
2945 resolve_unit,
2946 )?;
2947 }
2948 TypeConstraintCommand::With => {
2949 let mut rows = match declared_value.take() {
2950 Some(RawSuggestion::List(existing)) => existing,
2951 None => Vec::new(),
2952 Some(other) => unreachable!(
2953 "BUG: list declared value must be RawSuggestion::List, got {other:?}"
2954 ),
2955 };
2956 for arg in args {
2957 let mut row_value = None;
2958 let mut unused_suggestion = None;
2959 Arc::make_mut(element).specifications.apply_constraint(
2960 type_name,
2961 TypeConstraintCommand::With,
2962 std::slice::from_ref(arg),
2963 &mut unused_suggestion,
2964 &mut row_value,
2965 resolve_unit,
2966 )?;
2967 rows.push(
2968 row_value.expect("BUG: -> with on a list row must produce a value"),
2969 );
2970 }
2971 *declared_value = Some(RawSuggestion::List(rows));
2972 }
2973 other => {
2974 return Err(format!(
2975 "Invalid command '{other}' for list type. Valid commands: with, help"
2976 ));
2977 }
2978 },
2979 }
2980 Ok(())
2981 }
2982}
2983
2984pub fn parse_number_unit(
2987 value_str: &str,
2988 type_spec: &TypeSpecification,
2989) -> Result<crate::parsing::ast::Value, String> {
2990 use crate::literals::{NumberWithUnit, RatioLiteral};
2991 use crate::parsing::ast::Value;
2992
2993 let trimmed = value_str.trim();
2994 match type_spec {
2995 TypeSpecification::Measure { units, .. } => {
2996 if units.is_empty() {
2997 unreachable!(
2998 "BUG: Measure type has no units; should have been validated during planning"
2999 );
3000 }
3001 match trimmed.parse::<NumberWithUnit>() {
3002 Ok(n) => {
3003 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3004 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3005 }
3006 Err(e) => {
3007 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3008 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3009 let example_unit = units
3010 .iter()
3011 .next()
3012 .expect("BUG: units non-empty after guard")
3013 .name
3014 .as_str();
3015 Err(format!(
3016 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3017 trimmed,
3018 example_unit,
3019 valid.join(", ")
3020 ))
3021 } else {
3022 Err(e)
3023 }
3024 }
3025 }
3026 }
3027 TypeSpecification::Ratio { units, .. } => {
3028 if units.is_empty() {
3029 unreachable!(
3030 "BUG: Ratio type has no units; should have been validated during planning"
3031 );
3032 }
3033 match trimmed.parse::<RatioLiteral>()? {
3034 RatioLiteral::Bare(_) => {
3035 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3036 }
3037 RatioLiteral::Percent(n) => {
3038 let unit = units.get("percent").map_err(|e| e.to_string())?;
3039 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3040 }
3041 RatioLiteral::Permille(n) => {
3042 let unit = units.get("permille").map_err(|e| e.to_string())?;
3043 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3044 }
3045 RatioLiteral::Named { value, unit } => {
3046 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3047 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3048 }
3049 }
3050 }
3051 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3052 }
3053}
3054
3055pub fn parse_value_from_string(
3058 value_str: &str,
3059 type_spec: &TypeSpecification,
3060 source: &Source,
3061) -> Result<crate::parsing::ast::Value, Error> {
3062 match type_spec {
3063 TypeSpecification::NumberRange { .. }
3064 | TypeSpecification::MeasureRange { .. }
3065 | TypeSpecification::DateRange { .. }
3066 | TypeSpecification::TimeRange { .. }
3067 | TypeSpecification::RatioRange { .. } => {
3068 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3069 unreachable!(
3070 "BUG: range_element_type_specification missing arm for known range type"
3071 )
3072 });
3073 parse_range_value_from_string(value_str, &element_spec, source)
3074 }
3075 _ => parse_scalar_value_from_string(value_str, type_spec, source),
3076 }
3077}
3078
3079fn parse_range_value_from_string(
3080 value_str: &str,
3081 element_spec: &TypeSpecification,
3082 source: &Source,
3083) -> Result<crate::parsing::ast::Value, Error> {
3084 use crate::parsing::ast::Value;
3085
3086 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3087
3088 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3089 to_err("Range value must use '...' between the two endpoints".to_string())
3090 })?;
3091 if left_str.trim().is_empty() || right_str.trim().is_empty() {
3092 return Err(to_err(
3093 "Range value must contain a non-empty left and right endpoint".to_string(),
3094 ));
3095 }
3096 let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3097 let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3098 Ok(Value::Range(Box::new(left), Box::new(right)))
3099}
3100
3101fn parse_scalar_value_from_string(
3102 value_str: &str,
3103 type_spec: &TypeSpecification,
3104 source: &Source,
3105) -> Result<crate::parsing::ast::Value, Error> {
3106 use crate::parsing::ast::Value;
3107
3108 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3109
3110 match type_spec {
3111 TypeSpecification::Text { .. } => value_str
3112 .parse::<crate::literals::TextLiteral>()
3113 .map(|t| Value::Text(t.0))
3114 .map_err(to_err),
3115 TypeSpecification::Number { .. } => value_str
3116 .parse::<crate::literals::NumberLiteral>()
3117 .map(|n| Value::Number(n.0))
3118 .map_err(to_err),
3119 TypeSpecification::Measure { .. } => {
3120 parse_number_unit(value_str, type_spec).map_err(to_err)
3121 }
3122 TypeSpecification::Boolean { .. } => value_str
3123 .parse::<BooleanValue>()
3124 .map(Value::Boolean)
3125 .map_err(to_err),
3126 TypeSpecification::Date { .. } => {
3127 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3128 Ok(Value::Date(date))
3129 }
3130 TypeSpecification::Time { .. } => {
3131 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3132 Ok(Value::Time(time))
3133 }
3134 TypeSpecification::Ratio { .. } => {
3135 parse_number_unit(value_str, type_spec).map_err(to_err)
3136 }
3137 TypeSpecification::NumberRange { .. }
3138 | TypeSpecification::MeasureRange { .. }
3139 | TypeSpecification::DateRange { .. }
3140 | TypeSpecification::TimeRange { .. }
3141 | TypeSpecification::RatioRange { .. } => {
3142 unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3143 }
3144 TypeSpecification::Veto { .. } => Err(to_err(
3145 "Veto type cannot be parsed from string".to_string(),
3146 )),
3147 TypeSpecification::Undetermined => unreachable!(
3148 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3149 ),
3150 TypeSpecification::Spec { .. } => Err(to_err(
3151 "spec values are not evaluation-data scalars".to_string(),
3152 )),
3153 TypeSpecification::List { .. } => Err(to_err(
3154 "list values are not evaluation-data scalars".to_string(),
3155 )),
3156 }
3157}
3158
3159#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3164#[serde(rename_all = "snake_case")]
3165pub enum SemanticCalendarUnit {
3166 Month,
3167 Year,
3168}
3169
3170impl fmt::Display for SemanticCalendarUnit {
3171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3172 let s = match self {
3173 SemanticCalendarUnit::Month => "month",
3174 SemanticCalendarUnit::Year => "year",
3175 };
3176 write!(f, "{}", s)
3177 }
3178}
3179
3180pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3181 match unit_name {
3182 "month" => SemanticCalendarUnit::Month,
3183 "year" => SemanticCalendarUnit::Year,
3184 other => unreachable!(
3185 "BUG: calendar measure signature unit must be month or year, got '{other}'"
3186 ),
3187 }
3188}
3189
3190pub fn semantic_calendar_unit_from_measure_signature(
3191 signature: &[(String, i32)],
3192) -> SemanticCalendarUnit {
3193 let unit_name = signature
3194 .first()
3195 .map(|(name, _)| name.as_str())
3196 .expect("BUG: calendar measure must carry a unit signature");
3197 semantic_calendar_unit_from_unit_name(unit_name)
3198}
3199
3200pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3201 if !lemma_type.is_calendar_like() {
3202 unreachable!(
3203 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3204 lemma_type.name()
3205 );
3206 }
3207 let signature = lemma_type.measure_runtime_signature();
3208 if signature.is_empty() {
3209 return SemanticCalendarUnit::Month;
3210 }
3211 semantic_calendar_unit_from_measure_signature(&signature)
3212}
3213
3214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3216pub enum SemanticConversionTarget {
3217 Type(PrimitiveKind),
3218 Unit {
3220 unit_name: String,
3221 #[serde(with = "crate::snapshot::shared::arc")]
3223 owning_type: Arc<LemmaType>,
3224 },
3225}
3226
3227impl std::hash::Hash for SemanticConversionTarget {
3228 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3229 match self {
3230 Self::Type(kind) => {
3231 0u8.hash(state);
3232 kind.hash(state);
3233 }
3234 Self::Unit {
3235 unit_name,
3236 owning_type,
3237 } => {
3238 1u8.hash(state);
3239 unit_name.hash(state);
3240 owning_type.hash(state);
3241 }
3242 }
3243 }
3244}
3245
3246impl SemanticConversionTarget {}
3247
3248impl fmt::Display for SemanticConversionTarget {
3249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3250 match self {
3251 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
3252 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
3253 }
3254 }
3255}
3256
3257#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3260pub struct SemanticTimezone {
3261 pub offset_minutes: i16,
3262}
3263
3264impl SemanticTimezone {
3265 pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
3267 TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
3268 }
3269
3270 pub fn offset_seconds(&self) -> i32 {
3271 i32::from(self.offset_minutes) * 60
3272 }
3273}
3274
3275impl From<&TimezoneValue> for SemanticTimezone {
3276 fn from(tz: &TimezoneValue) -> Self {
3277 Self {
3278 offset_minutes: tz.offset_minutes,
3279 }
3280 }
3281}
3282
3283impl From<&SemanticTimezone> for TimezoneValue {
3284 fn from(tz: &SemanticTimezone) -> Self {
3285 Self {
3286 offset_minutes: tz.offset_minutes,
3287 }
3288 }
3289}
3290
3291impl fmt::Display for SemanticTimezone {
3292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3293 TimezoneValue::from(self).fmt(f)
3294 }
3295}
3296
3297impl Serialize for SemanticTimezone {
3298 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3299 serializer.serialize_str(&self.to_string())
3300 }
3301}
3302
3303impl<'de> Deserialize<'de> for SemanticTimezone {
3304 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3305 let s = String::deserialize(deserializer)?;
3306 Self::from_str(&s).map_err(serde::de::Error::custom)
3307 }
3308}
3309
3310impl FromStr for SemanticTimezone {
3311 type Err = String;
3312
3313 fn from_str(s: &str) -> Result<Self, Self::Err> {
3314 Ok(Self::from(&TimezoneValue::from_str(s)?))
3315 }
3316}
3317
3318#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3320pub struct SemanticTime {
3321 pub hour: u32,
3322 pub minute: u32,
3323 pub second: u32,
3324 pub microsecond: u32,
3325 pub timezone: Option<SemanticTimezone>,
3326}
3327
3328impl fmt::Display for SemanticTime {
3329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3330 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
3331 if self.microsecond != 0 {
3332 write!(f, ".{:06}", self.microsecond)?;
3333 }
3334 if let Some(timezone) = &self.timezone {
3335 write!(f, "{}", timezone)?;
3336 }
3337 Ok(())
3338 }
3339}
3340
3341impl Serialize for SemanticTime {
3342 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3343 serializer.serialize_str(&self.to_string())
3344 }
3345}
3346
3347impl<'de> Deserialize<'de> for SemanticTime {
3348 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3349 let s = String::deserialize(deserializer)?;
3350 Self::from_str(&s).map_err(serde::de::Error::custom)
3351 }
3352}
3353
3354impl FromStr for SemanticTime {
3355 type Err = String;
3356
3357 fn from_str(s: &str) -> Result<Self, Self::Err> {
3358 Ok(time_to_semantic(&TimeValue::from_str(s)?))
3359 }
3360}
3361
3362#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3364pub struct SemanticDateTime {
3365 pub year: i32,
3366 pub month: u32,
3367 pub day: u32,
3368 pub hour: u32,
3369 pub minute: u32,
3370 pub second: u32,
3371 pub microsecond: u32,
3372 pub timezone: Option<SemanticTimezone>,
3373}
3374
3375impl fmt::Display for SemanticDateTime {
3376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3377 let has_time = self.hour != 0
3378 || self.minute != 0
3379 || self.second != 0
3380 || self.microsecond != 0
3381 || self.timezone.is_some();
3382 if !has_time {
3383 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
3384 } else {
3385 write!(
3386 f,
3387 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
3388 self.year, self.month, self.day, self.hour, self.minute, self.second
3389 )?;
3390 if self.microsecond != 0 {
3391 write!(f, ".{:06}", self.microsecond)?;
3392 }
3393 if let Some(tz) = &self.timezone {
3394 write!(f, "{}", tz)?;
3395 }
3396 Ok(())
3397 }
3398 }
3399}
3400
3401impl Serialize for SemanticDateTime {
3402 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3403 serializer.serialize_str(&self.to_string())
3404 }
3405}
3406
3407impl<'de> Deserialize<'de> for SemanticDateTime {
3408 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3409 let s = String::deserialize(deserializer)?;
3410 Self::from_str(&s).map_err(serde::de::Error::custom)
3411 }
3412}
3413
3414impl FromStr for SemanticDateTime {
3415 type Err = String;
3416
3417 fn from_str(s: &str) -> Result<Self, Self::Err> {
3418 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
3419 }
3420}
3421
3422#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3426pub enum RawSuggestion {
3427 Value(ValueKind),
3428 Measure {
3430 magnitude: RationalInteger,
3431 unit_name: String,
3432 },
3433 UnitBound {
3436 value: ValueKind,
3437 unit_name: String,
3438 },
3439 List(Vec<RawSuggestion>),
3440}
3441
3442#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3449pub struct BoundValueKind {
3450 pub value: ValueKind,
3451 pub unit: Option<Arc<str>>,
3452}
3453
3454impl BoundValueKind {
3455 pub fn unbound(value: ValueKind) -> Self {
3456 Self { value, unit: None }
3457 }
3458
3459 pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
3460 Self { value, unit }
3461 }
3462
3463 pub fn to_literal(&self) -> LiteralValue {
3464 LiteralValue {
3465 value: self.value.clone(),
3466 }
3467 }
3468
3469 #[must_use]
3472 pub fn agree_or_left_binding(
3473 left: Option<&Arc<str>>,
3474 right: Option<&Arc<str>>,
3475 ) -> Option<Arc<str>> {
3476 match (left, right) {
3477 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
3478 (Some(a), Some(_)) => Some(Arc::clone(a)),
3479 (Some(a), None) => Some(Arc::clone(a)),
3480 (None, Some(b)) => Some(Arc::clone(b)),
3481 (None, None) => None,
3482 }
3483 }
3484}
3485
3486pub fn bound_value_kind_from_raw_suggestion(
3487 raw: RawSuggestion,
3488 specifications: &TypeSpecification,
3489 type_name: &str,
3490) -> Result<BoundValueKind, String> {
3491 enum Step<'a> {
3492 Visit(RawSuggestion, &'a TypeSpecification),
3493 CollectList {
3494 element: &'a Arc<LemmaType>,
3495 rows: usize,
3496 },
3497 }
3498
3499 let mut steps = vec![Step::Visit(raw, specifications)];
3500 let mut bounds: Vec<BoundValueKind> = Vec::new();
3501 while let Some(step) = steps.pop() {
3502 match step {
3503 Step::Visit(raw, specifications) => match raw {
3504 RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
3505 RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
3506 value,
3507 unit: Some(Arc::from(unit_name)),
3508 }),
3509 RawSuggestion::Measure {
3510 magnitude,
3511 unit_name,
3512 } => {
3513 let TypeSpecification::Measure { units, .. } = specifications else {
3514 unreachable!(
3515 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
3516 );
3517 };
3518 let canonical = measure_declared_bound_to_canonical(
3519 &magnitude, &unit_name, units, type_name, "suggest",
3520 )?;
3521 bounds.push(BoundValueKind {
3522 value: ValueKind::Measure(canonical),
3523 unit: Some(Arc::from(unit_name)),
3524 });
3525 }
3526 RawSuggestion::List(rows) => {
3527 let TypeSpecification::List { element, .. } = specifications else {
3528 unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
3529 };
3530 steps.push(Step::CollectList {
3531 element,
3532 rows: rows.len(),
3533 });
3534 for row in rows.into_iter().rev() {
3535 steps.push(Step::Visit(row, &element.specifications));
3536 }
3537 }
3538 },
3539 Step::CollectList { element, rows } => {
3540 let start = bounds
3541 .len()
3542 .checked_sub(rows)
3543 .expect("BUG: list rows missing from bound stack");
3544 let converted: Vec<TypedLiteral> = bounds
3545 .split_off(start)
3546 .iter()
3547 .map(|bound| typed_literal_from_bound(bound, element))
3548 .collect();
3549 bounds.push(BoundValueKind::unbound(ValueKind::List(converted)));
3550 }
3551 }
3552 }
3553 Ok(bounds.pop().expect("BUG: bound walk left no result"))
3554}
3555
3556#[must_use]
3560pub fn display_binding_unit(
3561 schema: &LemmaType,
3562 settled: Option<&BoundValueKind>,
3563 fill: Option<&BoundValueKind>,
3564 suggestion: Option<&BoundValueKind>,
3565) -> Option<String> {
3566 if schema.is_measure_range() || schema.is_ratio_range() {
3567 return None;
3568 }
3569 if let Some(unit) = schema.unit() {
3570 return Some(unit.to_string());
3571 }
3572 if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
3573 return Some(unit.to_string());
3574 }
3575 if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
3576 return Some(unit.to_string());
3577 }
3578 suggestion
3579 .and_then(|bound| bound.unit.as_ref())
3580 .map(|unit| unit.to_string())
3581}
3582
3583#[must_use]
3588pub fn lemma_type_with_display_binding(
3589 schema: &LemmaType,
3590 settled: Option<&BoundValueKind>,
3591 fill: Option<&BoundValueKind>,
3592 suggestion: Option<&BoundValueKind>,
3593) -> LemmaType {
3594 if schema.is_measure_range() || schema.is_ratio_range() {
3595 return schema.clone();
3596 }
3597 match display_binding_unit(schema, settled, fill, suggestion) {
3598 Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
3599 Some(_) | None => schema.clone(),
3600 }
3601}
3602
3603#[must_use]
3605pub fn lemma_type_arc_with_display_binding(
3606 schema: &Arc<LemmaType>,
3607 settled: Option<&BoundValueKind>,
3608 fill: Option<&BoundValueKind>,
3609 suggestion: Option<&BoundValueKind>,
3610) -> Arc<LemmaType> {
3611 if schema.is_measure_range() || schema.is_ratio_range() {
3612 return Arc::clone(schema);
3613 }
3614 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
3615 Some(unit) if schema.unit() != Some(unit.as_str()) => {
3616 Arc::new(schema.as_ref().clone().with_unit(unit))
3617 }
3618 Some(_) | None => Arc::clone(schema),
3619 }
3620}
3621
3622#[must_use]
3627pub fn typed_literal_from_bound(
3628 bound: &BoundValueKind,
3629 lemma_type: &Arc<LemmaType>,
3630) -> TypedLiteral {
3631 let lemma_type = match (&bound.unit, &lemma_type.specifications) {
3632 (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
3633 if lemma_type.unit() != Some(unit.as_ref()) =>
3634 {
3635 Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
3636 }
3637 _ => Arc::clone(lemma_type),
3638 };
3639 TypedLiteral {
3640 value: bound.value.clone(),
3641 lemma_type,
3642 }
3643}
3644
3645#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3652pub enum ValueKind {
3653 Number(RationalInteger),
3654 Measure(RationalInteger),
3660 Text(String),
3661 Date(SemanticDateTime),
3662 Time(SemanticTime),
3663 Boolean(bool),
3664 Ratio(RationalInteger),
3666 Range(Box<TypedLiteral>, Box<TypedLiteral>),
3668 Spec(SpecValue),
3670 List(Vec<TypedLiteral>),
3672}
3673
3674impl ValueKind {
3675 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
3677 match self {
3678 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
3679 n.try_to_decimal().map_err(|failure| failure.to_string())
3680 }
3681 other => Err(format!("expected numeric value kind, got {other}")),
3682 }
3683 }
3684
3685 pub fn structural_byte_size(&self) -> usize {
3690 fn rational_byte_estimate(r: &RationalInteger) -> usize {
3691 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
3692 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
3693 numer_bytes.max(1) + denom_bytes.max(1)
3694 }
3695 match self {
3696 ValueKind::Number(r) => rational_byte_estimate(r),
3697 ValueKind::Measure(r) => rational_byte_estimate(r),
3698 ValueKind::Ratio(r) => rational_byte_estimate(r),
3699 ValueKind::Text(s) => s.len(),
3700 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
3704 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
3705 }
3706 ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
3707 ValueKind::Spec(SpecValue::Row(row)) => row
3708 .bindings
3709 .values()
3710 .map(|binding| match binding {
3711 crate::computation::OperationResult::Value(bound) => {
3712 bound.value.structural_byte_size()
3713 }
3714 crate::computation::OperationResult::Veto(_) => 0,
3715 })
3716 .sum(),
3717 ValueKind::List(rows) => rows
3718 .iter()
3719 .map(|row| row.value.structural_byte_size())
3720 .sum(),
3721 }
3722 }
3723}
3724
3725fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
3726 rational.display_str()
3727}
3728
3729fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
3730 use crate::parsing::ast::Value;
3731 match rational.try_to_decimal() {
3732 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
3733 Err(_) => format!("{} {}", rational.display_str(), unit),
3734 }
3735}
3736
3737impl fmt::Display for ValueKind {
3738 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3739 match self {
3740 ValueKind::Number(rational) => {
3741 write!(f, "{}", format_rational_magnitude_for_display(rational))
3742 }
3743 ValueKind::Measure(rational) => {
3744 write!(f, "{}", format_rational_magnitude_for_display(rational))
3745 }
3746 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3747 ValueKind::Ratio(rational) => {
3748 write!(f, "{}", format_rational_magnitude_for_display(rational))
3749 }
3750 ValueKind::Date(dt) => write!(f, "{}", dt),
3751 ValueKind::Time(t) => write!(
3752 f,
3753 "{}",
3754 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3755 hour: t.hour as u8,
3756 minute: t.minute as u8,
3757 second: t.second as u8,
3758 microsecond: t.microsecond,
3759 timezone: t
3760 .timezone
3761 .as_ref()
3762 .map(crate::parsing::ast::TimezoneValue::from),
3763 })
3764 ),
3765 ValueKind::Boolean(b) => write!(f, "{}", b),
3766 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3767 ValueKind::Spec(value) => write!(f, "{value}"),
3768 ValueKind::List(rows) => {
3769 write!(f, "list[")?;
3770 for (i, row) in rows.iter().enumerate() {
3771 if i > 0 {
3772 write!(f, ", ")?;
3773 }
3774 write!(f, "{row}")?;
3775 }
3776 write!(f, "]")
3777 }
3778 }
3779 }
3780}
3781
3782#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3793pub struct PathSegment {
3794 pub uses: String,
3796 pub repository: Option<String>,
3799 pub spec: String,
3801}
3802
3803#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3807pub struct SpecInstance {
3808 pub prefix: Vec<PathSegment>,
3809}
3810
3811impl SpecInstance {
3812 pub fn structural_byte_size(&self) -> usize {
3813 self.prefix
3814 .iter()
3815 .map(|segment| {
3816 segment.uses.len()
3817 + segment.spec.len()
3818 + segment.repository.as_ref().map_or(0, String::len)
3819 })
3820 .sum()
3821 }
3822}
3823
3824impl fmt::Display for SpecInstance {
3825 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3826 for (index, segment) in self.prefix.iter().enumerate() {
3827 if index > 0 {
3828 write!(f, ".")?;
3829 }
3830 write!(f, "{}", segment.uses)?;
3831 }
3832 Ok(())
3833 }
3834}
3835
3836#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3841pub enum SpecValue {
3842 Fixed(SpecInstance),
3843 #[serde(skip)]
3844 Row(Arc<RowInstance>),
3845}
3846
3847impl fmt::Display for SpecValue {
3848 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3849 match self {
3850 SpecValue::Fixed(instance) => write!(f, "{instance}"),
3851 SpecValue::Row(row) => write!(f, "{}", row.origin),
3852 }
3853 }
3854}
3855
3856#[derive(Debug, Clone, PartialEq, Eq)]
3866pub struct RowInstance {
3867 pub identity: SpecIdentity,
3868 pub origin: RowOrigin,
3869 pub bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3871}
3872
3873impl std::hash::Hash for RowInstance {
3874 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3875 self.identity.hash(state);
3876 self.origin.hash(state);
3877 self.bindings.len().hash(state);
3878 let mut entries: Vec<_> = self.bindings.iter().collect();
3879 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
3880 for (path, result) in entries {
3881 path.hash(state);
3882 result.hash(state);
3883 }
3884 }
3885}
3886
3887#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3889pub enum RowOrigin {
3890 Numbered { list: String, number: usize },
3892 Appended { number: usize },
3894}
3895
3896#[must_use]
3898pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
3899 format!("{list_key}.{number}")
3900}
3901
3902#[must_use]
3904pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
3905 format!("{}.{field_key}", numbered_row_origin(list_key, number))
3906}
3907
3908impl fmt::Display for RowOrigin {
3909 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3910 match self {
3911 RowOrigin::Numbered { list, number } => {
3912 write!(f, "{}", numbered_row_origin(list, *number))
3913 }
3914 RowOrigin::Appended { number } => write!(f, "appended row {number}"),
3915 }
3916 }
3917}
3918
3919#[must_use]
3921pub fn row_literal(
3922 identity: SpecIdentity,
3923 origin: RowOrigin,
3924 bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3925 lemma_type: Arc<LemmaType>,
3926) -> TypedLiteral {
3927 TypedLiteral {
3928 value: ValueKind::Spec(SpecValue::Row(Arc::new(RowInstance {
3929 identity,
3930 origin,
3931 bindings,
3932 }))),
3933 lemma_type,
3934 }
3935}
3936
3937#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3939pub enum RowEnd {
3940 Data(DataPath),
3941 Rule(RulePath),
3942}
3943
3944impl fmt::Display for RowEnd {
3945 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3946 match self {
3947 RowEnd::Data(path) => write!(f, "{}", path.input_key()),
3948 RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
3949 }
3950 }
3951}
3952
3953#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3955pub enum SpecMemberEnd {
3956 Rule(RulePath),
3957 Data(DataPath),
3958 Instance {
3960 instance: SpecInstance,
3961 identity: SpecIdentity,
3962 },
3963}
3964
3965#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3967pub struct SpecMemberTarget {
3968 pub instance: SpecInstance,
3969 pub end: SpecMemberEnd,
3970}
3971
3972#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3976pub struct DataPath {
3977 pub segments: Vec<PathSegment>,
3979 pub data: String,
3981}
3982
3983impl DataPath {
3984 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3986 Self { segments, data }
3987 }
3988
3989 pub fn local(data: String) -> Self {
3991 Self {
3992 segments: vec![],
3993 data,
3994 }
3995 }
3996
3997 pub fn input_key_segments(&self) -> impl Iterator<Item = &str> {
3999 self.segments
4000 .iter()
4001 .map(|segment| segment.uses.as_str())
4002 .chain(std::iter::once(self.data.as_str()))
4003 }
4004
4005 pub fn input_key(&self) -> String {
4008 let mut segments = self.input_key_segments();
4009 let mut s = segments
4010 .next()
4011 .expect("BUG: DataPath always has a data name segment")
4012 .to_string();
4013 for segment in segments {
4014 s.push('.');
4015 s.push_str(segment);
4016 }
4017 s
4018 }
4019}
4020
4021#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4025pub struct RulePath {
4026 pub segments: Vec<PathSegment>,
4028 pub rule: String,
4030}
4031
4032impl RulePath {
4033 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
4035 Self { segments, rule }
4036 }
4037
4038 pub fn input_key(&self) -> String {
4041 if self.segments.is_empty() {
4042 return self.rule.clone();
4043 }
4044 let mut s = String::new();
4045 for segment in &self.segments {
4046 s.push_str(&segment.uses);
4047 s.push('.');
4048 }
4049 s.push_str(&self.rule);
4050 s
4051 }
4052}
4053
4054pub type Expression = ExprTree<ExpressionKind>;
4067
4068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4070#[serde(rename_all = "snake_case")]
4071pub enum ExpressionKind {
4072 Literal(Box<TypedLiteral>),
4074 DataPath(DataPath),
4076 RulePath(RulePath),
4078 LogicalAnd {
4080 first: ExprId,
4081 steps: Vec<ChainStep<()>>,
4082 },
4083 Arithmetic {
4085 first: ExprId,
4086 steps: Vec<ChainStep<ArithmeticComputation>>,
4087 },
4088 Comparison(ExprId, ComparisonComputation, ExprId),
4089 UnitConversion(ExprId, SemanticConversionTarget),
4090 LogicalNegation(ExprId, NegationType),
4091 MathematicalComputation(MathematicalComputation, ExprId),
4092 Veto(VetoExpression),
4093 Now,
4095 DateRelative(DateRelativeKind, ExprId),
4097 DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
4099 RangeLiteral(ExprId, ExprId),
4100 PastFutureRange(DateRelativeKind, ExprId),
4101 RangeContainment(ExprId, ExprId),
4102 RangeBound(RangeBound, ExprId),
4104 ResultIsVeto(ExprId),
4106 SpecMember {
4113 base: ExprId,
4114 path: Vec<String>,
4115 name: String,
4116 targets: Vec<SpecMemberTarget>,
4117 },
4118 RowField {
4120 level: u32,
4121 identity: SpecIdentity,
4122 end: RowEnd,
4123 },
4124 Piecewise(Vec<(ExprId, ExprId)>),
4127 ListCommand {
4130 list: ExprId,
4131 op: ListOp,
4132 row_level: Option<u32>,
4133 },
4134}
4135
4136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4138pub enum ListOp {
4139 Find {
4140 keys: Vec<ExprId>,
4141 },
4142 First,
4143 Last,
4144 Count,
4145 Reduce {
4146 kind: ListReduce,
4147 argument: Option<ExprId>,
4148 },
4149 Predicate {
4150 kind: ListPredKind,
4151 op: crate::parsing::ast::RowCondition,
4152 right: ExprId,
4153 },
4154 InstancePredicate {
4155 kind: ListPredKind,
4156 expr: ExprId,
4157 },
4158 Sort {
4159 descending: bool,
4160 argument: Option<ExprId>,
4161 },
4162 Top {
4163 n: ExprId,
4164 },
4165 With {
4166 rows: Vec<ExprId>,
4167 },
4168 WithRow {
4170 fields: Vec<(DataPath, ExprId)>,
4171 },
4172}
4173
4174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4175pub enum ListReduce {
4176 Sum,
4177 Avg,
4178 Product,
4179 Min,
4180 Max,
4181}
4182
4183impl ListReduce {
4184 pub fn keyword(self) -> &'static str {
4185 match self {
4186 ListReduce::Sum => "sum",
4187 ListReduce::Avg => "avg",
4188 ListReduce::Product => "product",
4189 ListReduce::Min => "min",
4190 ListReduce::Max => "max",
4191 }
4192 }
4193}
4194
4195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4196pub enum ListPredKind {
4197 Any,
4198 All,
4199 Where,
4200}
4201
4202impl ListPredKind {
4203 pub fn keyword(self) -> &'static str {
4204 match self {
4205 ListPredKind::Any => "any",
4206 ListPredKind::All => "all",
4207 ListPredKind::Where => "where",
4208 }
4209 }
4210}
4211
4212impl ExprKind for ExpressionKind {
4213 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4214 match self {
4215 ExpressionKind::Literal(_)
4216 | ExpressionKind::DataPath(_)
4217 | ExpressionKind::RulePath(_)
4218 | ExpressionKind::Veto(_)
4219 | ExpressionKind::Now
4220 | ExpressionKind::RowField { .. } => {}
4221 ExpressionKind::LogicalAnd { first, steps } => {
4222 f(*first);
4223 steps.iter().for_each(|step| f(step.operand));
4224 }
4225 ExpressionKind::Arithmetic { first, steps } => {
4226 f(*first);
4227 steps.iter().for_each(|step| f(step.operand));
4228 }
4229 ExpressionKind::Comparison(left, _, right)
4230 | ExpressionKind::RangeLiteral(left, right)
4231 | ExpressionKind::RangeContainment(left, right) => {
4232 f(*left);
4233 f(*right);
4234 }
4235 ExpressionKind::UnitConversion(inner, _)
4236 | ExpressionKind::LogicalNegation(inner, _)
4237 | ExpressionKind::MathematicalComputation(_, inner)
4238 | ExpressionKind::DateRelative(_, inner)
4239 | ExpressionKind::DateCalendar(_, _, inner)
4240 | ExpressionKind::PastFutureRange(_, inner)
4241 | ExpressionKind::RangeBound(_, inner)
4242 | ExpressionKind::ResultIsVeto(inner)
4243 | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
4244 ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
4245 f(*condition);
4246 f(*result);
4247 }),
4248 ExpressionKind::ListCommand { list, op, .. } => {
4249 f(*list);
4250 op.for_each_child(f);
4251 }
4252 }
4253 }
4254
4255 fn children_mut(&mut self) -> Vec<&mut ExprId> {
4256 match self {
4257 ExpressionKind::Literal(_)
4258 | ExpressionKind::DataPath(_)
4259 | ExpressionKind::RulePath(_)
4260 | ExpressionKind::Veto(_)
4261 | ExpressionKind::Now
4262 | ExpressionKind::RowField { .. } => Vec::new(),
4263 ExpressionKind::LogicalAnd { first, steps } => std::iter::once(first)
4264 .chain(steps.iter_mut().map(|step| &mut step.operand))
4265 .collect(),
4266 ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
4267 .chain(steps.iter_mut().map(|step| &mut step.operand))
4268 .collect(),
4269 ExpressionKind::Comparison(left, _, right)
4270 | ExpressionKind::RangeLiteral(left, right)
4271 | ExpressionKind::RangeContainment(left, right) => vec![left, right],
4272 ExpressionKind::UnitConversion(inner, _)
4273 | ExpressionKind::LogicalNegation(inner, _)
4274 | ExpressionKind::MathematicalComputation(_, inner)
4275 | ExpressionKind::DateRelative(_, inner)
4276 | ExpressionKind::DateCalendar(_, _, inner)
4277 | ExpressionKind::PastFutureRange(_, inner)
4278 | ExpressionKind::RangeBound(_, inner)
4279 | ExpressionKind::ResultIsVeto(inner) => vec![inner],
4280 ExpressionKind::SpecMember { base, .. } => vec![base],
4281 ExpressionKind::Piecewise(arms) => arms
4282 .iter_mut()
4283 .flat_map(|(condition, result)| [condition, result])
4284 .collect(),
4285 ExpressionKind::ListCommand { list, op, .. } => {
4286 let mut children = vec![list];
4287 children.extend(op.children_mut());
4288 children
4289 }
4290 }
4291 }
4292}
4293
4294impl ListOp {
4295 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4296 match self {
4297 ListOp::First | ListOp::Last | ListOp::Count => {}
4298 ListOp::Find { keys } => keys.iter().copied().for_each(f),
4299 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4300 argument.iter().copied().for_each(f)
4301 }
4302 ListOp::Predicate { right, .. } => f(*right),
4303 ListOp::InstancePredicate { expr, .. } => f(*expr),
4304 ListOp::Top { n } => f(*n),
4305 ListOp::With { rows } => rows.iter().copied().for_each(f),
4306 ListOp::WithRow { fields } => fields.iter().for_each(|(_, value)| f(*value)),
4307 }
4308 }
4309
4310 fn children_mut(&mut self) -> Vec<&mut ExprId> {
4311 match self {
4312 ListOp::First | ListOp::Last | ListOp::Count => Vec::new(),
4313 ListOp::Find { keys } => keys.iter_mut().collect(),
4314 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4315 argument.iter_mut().collect()
4316 }
4317 ListOp::Predicate { right, .. } => vec![right],
4318 ListOp::InstancePredicate { expr, .. } => vec![expr],
4319 ListOp::Top { n } => vec![n],
4320 ListOp::With { rows } => rows.iter_mut().collect(),
4321 ListOp::WithRow { fields } => fields.iter_mut().map(|(_, value)| value).collect(),
4322 }
4323 }
4324}
4325
4326#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4332pub enum TypeDefiningSpec {
4333 Local,
4335 Import,
4337}
4338
4339#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4346pub struct TypeFamily {
4347 pub repository: Option<String>,
4348 pub spec: String,
4349 pub effective: crate::parsing::ast::EffectiveDate,
4350 pub name: String,
4351}
4352
4353impl TypeFamily {
4354 #[must_use]
4355 pub fn declared_in(spec: &SpecIdentity, name: &str) -> Self {
4356 Self {
4357 repository: spec.repository.clone(),
4358 spec: spec.spec.clone(),
4359 effective: spec.effective.clone(),
4360 name: name.to_string(),
4361 }
4362 }
4363
4364 #[must_use]
4366 pub fn is_declared_in(&self, spec: &SpecIdentity) -> bool {
4367 self.repository == spec.repository
4368 && self.spec == spec.spec
4369 && self.effective == spec.effective
4370 }
4371
4372 #[must_use]
4374 pub fn without_effective(&self) -> Self {
4375 Self {
4376 repository: self.repository.clone(),
4377 spec: self.spec.clone(),
4378 effective: crate::parsing::ast::EffectiveDate::Origin,
4379 name: self.name.clone(),
4380 }
4381 }
4382}
4383
4384impl fmt::Display for TypeFamily {
4386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4387 if let Some(repository) = &self.repository {
4388 write!(f, "{repository} ")?;
4389 }
4390 write!(f, "{}", self.spec)?;
4391 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
4392 write!(f, " {at}")?;
4393 }
4394 write!(f, ".{}", self.name)
4395 }
4396}
4397
4398#[derive(Clone, Debug, Serialize, Deserialize)]
4400pub enum TypeExtends {
4401 Primitive,
4403 Custom {
4406 parent: String,
4407 family: TypeFamily,
4408 defining_spec: TypeDefiningSpec,
4409 },
4410}
4411
4412impl PartialEq for TypeExtends {
4413 fn eq(&self, other: &Self) -> bool {
4414 match (self, other) {
4415 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
4416 (
4417 TypeExtends::Custom {
4418 parent: lp,
4419 family: lf,
4420 defining_spec: ld,
4421 },
4422 TypeExtends::Custom {
4423 parent: rp,
4424 family: rf,
4425 defining_spec: rd,
4426 },
4427 ) => lp == rp && lf == rf && ld == rd,
4428 _ => false,
4429 }
4430 }
4431}
4432
4433impl Eq for TypeExtends {}
4434
4435impl std::hash::Hash for TypeExtends {
4436 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4437 match self {
4438 TypeExtends::Primitive => {
4439 0u8.hash(state);
4440 }
4441 TypeExtends::Custom {
4442 parent,
4443 family,
4444 defining_spec,
4445 } => {
4446 1u8.hash(state);
4447 parent.hash(state);
4448 family.hash(state);
4449 defining_spec.hash(state);
4450 }
4451 }
4452 }
4453}
4454
4455impl TypeExtends {
4456 #[must_use]
4458 pub fn custom_local(parent: String, family: TypeFamily) -> Self {
4459 TypeExtends::Custom {
4460 parent,
4461 family,
4462 defining_spec: TypeDefiningSpec::Local,
4463 }
4464 }
4465
4466 #[must_use]
4468 pub fn parent_name(&self) -> Option<&str> {
4469 match self {
4470 TypeExtends::Primitive => None,
4471 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
4472 }
4473 }
4474
4475 #[must_use]
4476 fn with_families_without_effective(&self) -> Self {
4477 match self {
4478 TypeExtends::Primitive => TypeExtends::Primitive,
4479 TypeExtends::Custom {
4480 parent,
4481 family,
4482 defining_spec,
4483 } => TypeExtends::Custom {
4484 parent: parent.clone(),
4485 family: family.without_effective(),
4486 defining_spec: defining_spec.clone(),
4487 },
4488 }
4489 }
4490}
4491
4492#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4497pub struct LemmaType {
4498 pub name: Option<String>,
4500 pub specifications: TypeSpecification,
4502 pub extends: TypeExtends,
4504}
4505
4506impl LemmaType {
4507 pub fn map_measure<F>(self, f: F) -> Self
4511 where
4512 F: FnOnce(
4513 MeasureUnits,
4514 Option<BaseMeasureVector>,
4515 ) -> (MeasureUnits, Option<BaseMeasureVector>),
4516 {
4517 let LemmaType {
4518 name,
4519 specifications,
4520 extends,
4521 } = self;
4522 let specifications = match specifications {
4523 TypeSpecification::Measure {
4524 minimum,
4525 maximum,
4526 decimals,
4527 units,
4528 traits,
4529 decomposition,
4530 unit,
4531 help,
4532 } => {
4533 let (units, decomposition) = f(units, decomposition);
4534 TypeSpecification::Measure {
4535 minimum,
4536 maximum,
4537 decimals,
4538 units,
4539 traits,
4540 decomposition,
4541 unit,
4542 help,
4543 }
4544 }
4545 other => other,
4546 };
4547 LemmaType {
4548 name,
4549 specifications,
4550 extends,
4551 }
4552 }
4553
4554 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
4556 Self {
4557 name: Some(name),
4558 specifications,
4559 extends,
4560 }
4561 }
4562
4563 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
4565 Self {
4566 name: None,
4567 specifications,
4568 extends,
4569 }
4570 }
4571
4572 pub fn primitive(specifications: TypeSpecification) -> Self {
4574 Self {
4575 name: None,
4576 specifications,
4577 extends: TypeExtends::Primitive,
4578 }
4579 }
4580
4581 #[must_use]
4585 pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
4586 let unit_name = unit_name.into();
4587 match &mut self.specifications {
4588 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4589 *unit = Some(unit_name);
4590 }
4591 other => panic!(
4592 "BUG: with_unit called on non-measure/non-ratio type {}",
4593 other
4594 ),
4595 }
4596 self
4597 }
4598
4599 #[must_use]
4601 pub fn unit(&self) -> Option<&str> {
4602 match &self.specifications {
4603 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4604 unit.as_deref()
4605 }
4606 _ => None,
4607 }
4608 }
4609
4610 pub fn name(&self) -> String {
4612 self.name
4613 .clone()
4614 .unwrap_or_else(|| self.specifications.to_string())
4615 }
4616
4617 pub fn is_boolean(&self) -> bool {
4619 matches!(&self.specifications, TypeSpecification::Boolean { .. })
4620 }
4621
4622 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
4623 matches!(
4624 (kind, &self.specifications),
4625 (PrimitiveKind::Number, TypeSpecification::Number { .. })
4626 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
4627 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
4628 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
4629 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
4630 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
4631 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
4632 )
4633 }
4634
4635 pub fn is_measure(&self) -> bool {
4637 matches!(&self.specifications, TypeSpecification::Measure { .. })
4638 }
4639
4640 pub fn is_measure_range(&self) -> bool {
4641 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
4642 }
4643
4644 pub fn is_number(&self) -> bool {
4646 matches!(&self.specifications, TypeSpecification::Number { .. })
4647 }
4648
4649 pub fn is_number_range(&self) -> bool {
4650 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
4651 }
4652
4653 pub fn is_numeric(&self) -> bool {
4655 matches!(
4656 &self.specifications,
4657 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
4658 )
4659 }
4660
4661 pub fn is_text(&self) -> bool {
4663 matches!(&self.specifications, TypeSpecification::Text { .. })
4664 }
4665
4666 pub fn is_date(&self) -> bool {
4668 matches!(&self.specifications, TypeSpecification::Date { .. })
4669 }
4670
4671 pub fn is_date_range(&self) -> bool {
4672 matches!(&self.specifications, TypeSpecification::DateRange { .. })
4673 }
4674
4675 pub fn is_time_range(&self) -> bool {
4676 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
4677 }
4678
4679 pub fn is_time(&self) -> bool {
4681 matches!(&self.specifications, TypeSpecification::Time { .. })
4682 }
4683
4684 pub fn has_trait_duration(&self) -> bool {
4685 match &self.specifications {
4686 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
4687 _ => false,
4688 }
4689 }
4690
4691 pub fn is_duration_like_measure(&self) -> bool {
4692 if !self.is_measure() {
4693 return false;
4694 }
4695 if self.has_trait_duration() {
4696 return true;
4697 }
4698 self.is_anonymous_measure()
4699 && self
4700 .measure_type_decomposition()
4701 .is_some_and(|d| *d == duration_decomposition())
4702 }
4703
4704 pub fn is_duration_like(&self) -> bool {
4705 self.is_duration_like_measure()
4706 }
4707
4708 pub fn has_trait_calendar(&self) -> bool {
4709 match &self.specifications {
4710 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
4711 _ => false,
4712 }
4713 }
4714
4715 pub fn is_calendar_like_measure(&self) -> bool {
4716 if !self.is_measure() {
4717 return false;
4718 }
4719 if self.has_trait_calendar() {
4720 return true;
4721 }
4722 self.is_anonymous_measure()
4723 && self
4724 .measure_type_decomposition()
4725 .is_some_and(|d| *d == calendar_decomposition())
4726 }
4727
4728 pub fn is_calendar_like(&self) -> bool {
4729 self.is_calendar_like_measure()
4730 }
4731
4732 pub fn is_ratio(&self) -> bool {
4734 matches!(&self.specifications, TypeSpecification::Ratio { .. })
4735 }
4736
4737 pub fn is_ratio_range(&self) -> bool {
4738 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
4739 }
4740
4741 pub fn is_calendar_measure_range(&self) -> bool {
4742 matches!(
4743 &self.specifications,
4744 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
4745 if *decomposition == calendar_decomposition()
4746 )
4747 }
4748
4749 pub fn is_calendar_like_range(&self) -> bool {
4750 self.is_calendar_measure_range()
4751 }
4752
4753 pub fn is_range(&self) -> bool {
4754 matches!(
4755 &self.specifications,
4756 TypeSpecification::DateRange { .. }
4757 | TypeSpecification::TimeRange { .. }
4758 | TypeSpecification::NumberRange { .. }
4759 | TypeSpecification::MeasureRange { .. }
4760 | TypeSpecification::RatioRange { .. }
4761 )
4762 }
4763
4764 pub fn is_spec(&self) -> bool {
4766 matches!(&self.specifications, TypeSpecification::Spec { .. })
4767 }
4768
4769 pub fn is_list(&self) -> bool {
4770 matches!(&self.specifications, TypeSpecification::List { .. })
4771 }
4772
4773 pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
4774 match &self.specifications {
4775 TypeSpecification::List { element, .. } => Some(&element.specifications),
4776 _ => None,
4777 }
4778 }
4779
4780 pub fn list_of(element: Arc<LemmaType>) -> Self {
4781 Self {
4782 name: Some(format!("{} list", element.name())),
4783 specifications: TypeSpecification::List {
4784 element,
4785 key: ListKey::Numbered,
4786 },
4787 extends: TypeExtends::Primitive,
4788 }
4789 }
4790
4791 pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
4793 let TypeSpecification::List { element, .. } = &self.specifications else {
4794 panic!(
4795 "BUG: list_element_lemma_type on non-list type {}",
4796 self.name()
4797 );
4798 };
4799 element
4800 }
4801
4802 #[must_use]
4804 pub fn spec_identity(&self) -> Option<&SpecIdentity> {
4805 match &self.specifications {
4806 TypeSpecification::Spec { spec } => Some(spec),
4807 _ => None,
4808 }
4809 }
4810
4811 pub fn vetoed(&self) -> bool {
4813 matches!(&self.specifications, TypeSpecification::Veto { .. })
4814 }
4815
4816 pub fn is_undetermined(&self) -> bool {
4818 matches!(&self.specifications, TypeSpecification::Undetermined)
4819 }
4820
4821 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
4823 use TypeSpecification::*;
4824 matches!(
4825 (&self.specifications, &other.specifications),
4826 (Boolean { .. }, Boolean { .. })
4827 | (Number { .. }, Number { .. })
4828 | (NumberRange { .. }, NumberRange { .. })
4829 | (Measure { .. }, Measure { .. })
4830 | (MeasureRange { .. }, MeasureRange { .. })
4831 | (Text { .. }, Text { .. })
4832 | (Date { .. }, Date { .. })
4833 | (DateRange { .. }, DateRange { .. })
4834 | (Time { .. }, Time { .. })
4835 | (TimeRange { .. }, TimeRange { .. })
4836 | (Ratio { .. }, Ratio { .. })
4837 | (RatioRange { .. }, RatioRange { .. })
4838 | (Veto { .. }, Veto { .. })
4839 | (Undetermined, Undetermined)
4840 | (Spec { .. }, Spec { .. })
4841 | (List { .. }, List { .. })
4842 )
4843 }
4844
4845 #[must_use]
4847 pub fn family(&self) -> Option<&TypeFamily> {
4848 match &self.extends {
4849 TypeExtends::Custom { family, .. } => Some(family),
4850 TypeExtends::Primitive => None,
4851 }
4852 }
4853
4854 #[must_use]
4856 pub fn measure_family(&self) -> Option<&TypeFamily> {
4857 self.is_measure().then(|| self.family()).flatten()
4858 }
4859
4860 #[must_use]
4862 pub fn ratio_family(&self) -> Option<&TypeFamily> {
4863 self.is_ratio().then(|| self.family()).flatten()
4864 }
4865
4866 #[must_use]
4868 pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
4869 self.measure_family().or_else(|| self.ratio_family())
4870 }
4871
4872 #[must_use]
4874 pub fn measure_family_name(&self) -> Option<&str> {
4875 self.measure_family().map(|family| family.name.as_str())
4876 }
4877
4878 #[must_use]
4880 pub fn ratio_family_name(&self) -> Option<&str> {
4881 self.ratio_family().map(|family| family.name.as_str())
4882 }
4883
4884 #[must_use]
4888 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
4889 if !self.has_same_base_type(other) {
4890 return false;
4891 }
4892 if self.is_measure() {
4893 return self.measure_type_decomposition() == other.measure_type_decomposition();
4894 }
4895 if let (
4896 TypeSpecification::Spec { spec: self_spec },
4897 TypeSpecification::Spec { spec: other_spec },
4898 ) = (&self.specifications, &other.specifications)
4899 {
4900 return self_spec == other_spec;
4901 }
4902 if let (
4903 TypeSpecification::List {
4904 element: self_element,
4905 key: self_key,
4906 },
4907 TypeSpecification::List {
4908 element: other_element,
4909 key: other_key,
4910 },
4911 ) = (&self.specifications, &other.specifications)
4912 {
4913 return self_key == other_key && self_element.same_value_type(other_element);
4914 }
4915 if self.is_measure_range() {
4916 let self_element = self
4917 .specifications
4918 .element_from_range()
4919 .expect("BUG: MeasureRange always defines element_from_range");
4920 let other_element = other
4921 .specifications
4922 .element_from_range()
4923 .expect("BUG: MeasureRange always defines element_from_range");
4924 match (&self_element, &other_element) {
4925 (
4926 TypeSpecification::Measure {
4927 decomposition: self_decomp,
4928 ..
4929 },
4930 TypeSpecification::Measure {
4931 decomposition: other_decomp,
4932 ..
4933 },
4934 ) => self_decomp == other_decomp,
4935 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
4936 }
4937 } else {
4938 true
4939 }
4940 }
4941
4942 #[must_use]
4944 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
4945 if !self.is_measure() || !other.is_measure() {
4946 return false;
4947 }
4948 match (self.measure_family(), other.measure_family()) {
4949 (Some(self_family), Some(other_family)) => self_family == other_family,
4950 _ => false,
4951 }
4952 }
4953
4954 #[must_use]
4958 pub fn same_interface(&self, other: &Self) -> bool {
4959 self.with_families_without_effective() == other.with_families_without_effective()
4960 }
4961
4962 #[must_use]
4964 pub fn with_families_without_effective(&self) -> Self {
4965 Self {
4966 name: self.name.clone(),
4967 specifications: self.specifications.with_families_without_effective(),
4968 extends: self.extends.with_families_without_effective(),
4969 }
4970 }
4971
4972 #[must_use]
4973 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
4974 if !self.is_measure() || !other.is_measure() {
4975 return false;
4976 }
4977 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
4978 return false;
4979 }
4980 match (
4981 self.measure_type_decomposition(),
4982 other.measure_type_decomposition(),
4983 ) {
4984 (Some(a), Some(b)) => a == b,
4985 _ => false,
4986 }
4987 }
4988
4989 pub fn veto_type() -> Self {
4991 Self::primitive(TypeSpecification::veto())
4992 }
4993
4994 pub fn undetermined_type() -> Self {
4997 Self::primitive(TypeSpecification::Undetermined)
4998 }
4999
5000 pub fn decimal_places(&self) -> Option<u8> {
5003 match &self.specifications {
5004 TypeSpecification::Number { decimals, .. } => *decimals,
5005 TypeSpecification::Measure { decimals, .. } => *decimals,
5006 TypeSpecification::Ratio { decimals, .. } => *decimals,
5007 _ => None,
5008 }
5009 }
5010
5011 pub fn try_rational_as_decimal(
5016 &self,
5017 magnitude: &crate::computation::rational::RationalInteger,
5018 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5019 let decimal = magnitude.try_to_decimal()?;
5020 Ok(format_decimal_for_api(decimal, self.decimal_places()))
5021 }
5022
5023 pub fn try_measure_canonical_as_decimal_in_unit(
5025 &self,
5026 canonical_magnitude: &crate::computation::rational::RationalInteger,
5027 unit_name: &str,
5028 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5029 use crate::computation::rational::checked_div;
5030 let unit_factor = self.measure_unit_factor(unit_name);
5031 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
5032 self.try_rational_as_decimal(&magnitude_in_unit)
5033 }
5034
5035 pub fn try_ratio_canonical_as_decimal_in_unit(
5037 &self,
5038 canonical_magnitude: &crate::computation::rational::RationalInteger,
5039 unit_name: &str,
5040 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5041 use crate::computation::rational::checked_mul;
5042 let units = match &self.specifications {
5043 TypeSpecification::Ratio { units, .. } => units,
5044 _ => unreachable!(
5045 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
5046 self.name()
5047 ),
5048 };
5049 let ratio_unit = units
5050 .iter()
5051 .find(|unit| unit.name == unit_name)
5052 .unwrap_or_else(|| {
5053 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
5054 unreachable!(
5055 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
5056 unit_name,
5057 self.name(),
5058 valid.join(", ")
5059 )
5060 });
5061 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
5062 self.try_rational_as_decimal(&magnitude_in_unit)
5063 }
5064
5065 pub fn example_value(&self) -> &'static str {
5067 match &self.specifications {
5068 TypeSpecification::Text { .. } => "\"hello world\"",
5069 TypeSpecification::Measure { .. } => "12.50 eur",
5070 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
5071 TypeSpecification::Number { .. } => "3.14",
5072 TypeSpecification::NumberRange { .. } => "0...100",
5073 TypeSpecification::Boolean { .. } => "true",
5074 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
5075 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
5076 TypeSpecification::TimeRange { .. } => "09:00...17:00",
5077 TypeSpecification::Veto { .. } => "veto",
5078 TypeSpecification::Time { .. } => "14:30:00",
5079 TypeSpecification::Ratio { .. } => "50%",
5080 TypeSpecification::RatioRange { .. } => "10%...50%",
5081 TypeSpecification::Undetermined => unreachable!(
5082 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
5083 ),
5084 TypeSpecification::Spec { .. } => unreachable!(
5085 "BUG: example_value called on a spec type; spec instances are not prompted data"
5086 ),
5087 TypeSpecification::List { element, .. } => element.example_value(),
5088 }
5089 }
5090
5091 #[must_use]
5095 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
5099 match &self.specifications {
5100 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
5101 _ => unreachable!(
5102 "BUG: measure_type_decomposition called on non-measure type {}",
5103 self.name()
5104 ),
5105 }
5106 }
5107
5108 #[must_use]
5114 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
5115 if let Some(binding) = self.unit() {
5116 return vec![(binding.to_string(), 1)];
5117 }
5118 match &self.specifications {
5119 TypeSpecification::Measure {
5120 units,
5121 decomposition,
5122 ..
5123 } => {
5124 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5125 return vec![(canonical.name.clone(), 1)];
5126 }
5127 if let Some(first) = units.iter().next() {
5128 return vec![(first.name.clone(), 1)];
5129 }
5130 base_measure_vector_as_signature(
5131 decomposition
5132 .as_ref()
5133 .expect("BUG: measure type without units must carry a decomposition"),
5134 )
5135 }
5136 TypeSpecification::MeasureRange {
5137 units,
5138 decomposition,
5139 ..
5140 } => {
5141 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5142 return vec![(canonical.name.clone(), 1)];
5143 }
5144 if let Some(first) = units.iter().next() {
5145 return vec![(first.name.clone(), 1)];
5146 }
5147 base_measure_vector_as_signature(
5148 decomposition
5149 .as_ref()
5150 .expect("BUG: measure type without units must carry a decomposition"),
5151 )
5152 }
5153 _ => unreachable!(
5154 "BUG: measure_runtime_signature called on non-measure type {}",
5155 self.name()
5156 ),
5157 }
5158 }
5159
5160 #[must_use]
5162 pub fn ratio_primary_unit(&self) -> Option<&str> {
5163 match &self.specifications {
5164 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5165 units.iter().next().map(|unit| unit.name.as_str())
5166 }
5167 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5168 units.iter().next().map(|unit| unit.name.as_str())
5169 }
5170 _ => None,
5171 }
5172 }
5173
5174 pub fn is_anonymous_measure(&self) -> bool {
5177 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
5178 }
5179
5180 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
5185 Self {
5186 name: None,
5187 specifications: TypeSpecification::Measure {
5188 minimum: None,
5189 maximum: None,
5190 decimals: None,
5191 units: crate::literals::MeasureUnits::new(),
5192 traits: Vec::new(),
5193 decomposition: Some(decomposition),
5194 unit: None,
5195 help: String::new(),
5196 },
5197 extends: TypeExtends::Primitive,
5198 }
5199 }
5200
5201 #[must_use]
5203 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
5204 match &self.specifications {
5205 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5206 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5207 }
5208 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5209 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5210 }
5211 _ => None,
5212 }
5213 }
5214
5215 #[must_use]
5217 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
5218 match &self.specifications {
5219 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
5220 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5221 }
5222 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
5223 Some(units.iter().map(|unit| unit.name.as_str()).collect())
5224 }
5225 _ => None,
5226 }
5227 }
5228
5229 #[must_use]
5231 pub fn label_for_data_input(&self, input_key: &str) -> String {
5232 let type_label = if let Some(parent) = self.extends.parent_name() {
5233 parent.to_string()
5234 } else {
5235 let type_name = self.name();
5236 if type_name == input_key {
5237 self.specifications.to_string()
5238 } else {
5239 type_name
5240 }
5241 };
5242 if type_label == input_key {
5243 input_key.to_string()
5244 } else {
5245 format!("{input_key} [{type_label}]")
5246 }
5247 }
5248
5249 #[must_use]
5251 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
5252 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
5253 }
5254
5255 #[must_use]
5257 pub fn empty_runtime_input_vetoes(&self) -> bool {
5258 !matches!(self.specifications, TypeSpecification::Text { .. })
5259 }
5260
5261 pub fn measure_unit_factor(
5263 &self,
5264 unit_name: &str,
5265 ) -> &crate::computation::rational::RationalInteger {
5266 let units = match &self.specifications {
5267 TypeSpecification::Measure { units, .. } => units,
5268 TypeSpecification::MeasureRange { units, .. } => units,
5269 _ => unreachable!(
5270 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
5271 self.name()
5272 ),
5273 };
5274 match units.get(unit_name) {
5275 Ok(MeasureUnit { factor, .. }) => factor,
5276 Err(_) => {
5277 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5278 unreachable!(
5279 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
5280 unit_name,
5281 self.name(),
5282 valid.join(", ")
5283 );
5284 }
5285 }
5286 }
5287
5288 pub fn ratio_unit_factor(
5289 &self,
5290 unit_name: &str,
5291 ) -> &crate::computation::rational::RationalInteger {
5292 let units = match &self.specifications {
5293 TypeSpecification::Ratio { units, .. } => units,
5294 _ => unreachable!(
5295 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
5296 self.name()
5297 ),
5298 };
5299 match units.get(unit_name) {
5300 Ok(RatioUnit { value, .. }) => value,
5301 Err(_) => {
5302 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
5303 unreachable!(
5304 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
5305 unit_name,
5306 self.name(),
5307 valid.join(", ")
5308 );
5309 }
5310 }
5311 }
5312
5313 pub(crate) fn measure_literal_unit_map(
5315 &self,
5316 literal: &LiteralValue,
5317 unit_names: &[&str],
5318 factor_source: UnitFactorSource<'_>,
5319 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5320 use crate::computation::rational::checked_div;
5321
5322 let ValueKind::Measure(magnitude) = &literal.value else {
5323 panic!("BUG: measure_literal_unit_map called with non-measure value");
5324 };
5325 let mut map = BTreeMap::new();
5326 for &unit_name in unit_names {
5327 let unit_factor = factor_source.measure_unit_factor(unit_name);
5328 let magnitude_in_unit = checked_div(magnitude, unit_factor)
5329 .map_err(LiteralUnitMapFailure::UnitConversion)?;
5330 let decimal = self
5331 .try_rational_as_decimal(&magnitude_in_unit)
5332 .map_err(LiteralUnitMapFailure::Commit)?;
5333 map.insert(unit_name.to_string(), decimal);
5334 }
5335 Ok(map)
5336 }
5337
5338 pub(crate) fn ratio_literal_unit_map(
5340 &self,
5341 literal: &LiteralValue,
5342 unit_names: &[&str],
5343 factor_source: UnitFactorSource<'_>,
5344 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5345 use crate::computation::rational::checked_mul;
5346
5347 let ratio_api_type = match &self.specifications {
5348 TypeSpecification::Ratio { .. } => self,
5349 TypeSpecification::RatioRange { .. } => {
5350 return ratio_element_type_for_api(self).ratio_literal_unit_map(
5351 literal,
5352 unit_names,
5353 factor_source,
5354 );
5355 }
5356 _ => {
5357 panic!(
5358 "BUG: ratio_literal_unit_map called with non-ratio type {}",
5359 self.name()
5360 );
5361 }
5362 };
5363 let ValueKind::Ratio(canonical) = &literal.value else {
5364 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
5365 };
5366 if unit_names.is_empty() {
5367 panic!(
5368 "BUG: ratio literal type '{}' must have at least one unit name",
5369 self.name()
5370 );
5371 }
5372 let mut map = BTreeMap::new();
5373 for &unit_name in unit_names {
5374 let unit_factor = factor_source.ratio_unit_factor(unit_name);
5375 let magnitude_in_unit = checked_mul(canonical, unit_factor)
5376 .map_err(LiteralUnitMapFailure::UnitConversion)?;
5377 let decimal = ratio_api_type
5378 .try_rational_as_decimal(&magnitude_in_unit)
5379 .map_err(LiteralUnitMapFailure::Commit)?;
5380 map.insert(unit_name.to_string(), decimal);
5381 }
5382 Ok(map)
5383 }
5384}
5385
5386pub(crate) enum UnitFactorSource<'a> {
5388 DeclaredOn(&'a LemmaType),
5389 Merged {
5390 measure: Option<&'a MeasureUnits>,
5391 ratio: Option<&'a RatioUnits>,
5392 },
5393}
5394
5395impl UnitFactorSource<'_> {
5396 fn measure_unit_factor(
5397 &self,
5398 unit_name: &str,
5399 ) -> &crate::computation::rational::RationalInteger {
5400 match self {
5401 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.measure_unit_factor(unit_name),
5402 UnitFactorSource::Merged { measure, .. } => {
5403 let units = measure.unwrap_or_else(|| {
5404 panic!(
5405 "BUG: family measure expansion missing merged table for unit '{unit_name}'"
5406 )
5407 });
5408 &units
5409 .get(unit_name)
5410 .unwrap_or_else(|_| {
5411 panic!("BUG: family unit '{unit_name}' missing from merged measure table")
5412 })
5413 .factor
5414 }
5415 }
5416 }
5417
5418 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
5419 match self {
5420 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
5421 UnitFactorSource::Merged { ratio, .. } => {
5422 let units = ratio.unwrap_or_else(|| {
5423 panic!(
5424 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
5425 )
5426 });
5427 &units
5428 .get(unit_name)
5429 .unwrap_or_else(|_| {
5430 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
5431 })
5432 .value
5433 }
5434 }
5435 }
5436}
5437
5438pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
5440 match &lemma_type.specifications {
5441 TypeSpecification::Ratio { .. } => lemma_type.clone(),
5442 TypeSpecification::RatioRange { .. } => {
5443 let element = range_element_type_specification(&lemma_type.specifications)
5444 .expect("BUG: ratio range type must have ratio element specification");
5445 let TypeSpecification::Ratio {
5446 units, decimals, ..
5447 } = element
5448 else {
5449 panic!("BUG: ratio range element spec must be Ratio");
5450 };
5451 LemmaType::primitive(TypeSpecification::Ratio {
5452 minimum: None,
5453 maximum: None,
5454 decimals,
5455 units,
5456 unit: None,
5457 help: String::new(),
5458 })
5459 }
5460 _ => panic!(
5461 "BUG: ratio_element_type_for_api called with non-ratio type {}",
5462 lemma_type.name()
5463 ),
5464 }
5465}
5466
5467#[derive(Debug, Clone, PartialEq, Eq)]
5469pub(crate) enum LiteralUnitMapFailure {
5470 Commit(crate::computation::rational::NumericFailure),
5471 UnitConversion(crate::computation::rational::NumericFailure),
5472}
5473
5474#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5477pub struct LiteralValue {
5478 pub value: ValueKind,
5479}
5480
5481impl LiteralValue {
5482 #[inline]
5483 pub fn new(value: ValueKind) -> Self {
5484 Self { value }
5485 }
5486
5487 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
5488 match signature {
5489 [(unit_name, 1)] => Some(unit_name.as_str()),
5490 _ => None,
5491 }
5492 }
5493}
5494
5495#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5501pub struct TypedLiteral {
5502 pub value: ValueKind,
5503 #[serde(with = "crate::snapshot::shared::arc")]
5504 pub lemma_type: Arc<LemmaType>,
5505}
5506
5507impl TypedLiteral {
5508 #[inline]
5509 pub fn to_literal(&self) -> LiteralValue {
5510 LiteralValue {
5511 value: self.value.clone(),
5512 }
5513 }
5514
5515 #[inline]
5516 pub fn into_literal(self) -> LiteralValue {
5517 LiteralValue { value: self.value }
5518 }
5519}
5520
5521impl From<TypedLiteral> for LiteralValue {
5522 fn from(typed: TypedLiteral) -> Self {
5523 typed.into_literal()
5524 }
5525}
5526
5527impl From<&TypedLiteral> for LiteralValue {
5528 fn from(typed: &TypedLiteral) -> Self {
5529 typed.to_literal()
5530 }
5531}
5532
5533impl LiteralValue {
5534 pub fn text(s: String) -> Self {
5535 Self {
5536 value: ValueKind::Text(s),
5537 }
5538 }
5539
5540 pub fn number(n: RationalInteger) -> Self {
5541 Self {
5542 value: ValueKind::Number(n),
5543 }
5544 }
5545
5546 pub fn number_from_decimal(decimal: Decimal) -> Self {
5547 Self::number(
5548 crate::literals::rational_from_parsed_decimal(decimal)
5549 .expect("BUG: literal number from decimal must lift at boundary"),
5550 )
5551 }
5552
5553 pub fn measure(n: RationalInteger) -> Self {
5554 Self {
5555 value: ValueKind::Measure(n),
5556 }
5557 }
5558
5559 pub fn from_bool(b: bool) -> Self {
5560 Self {
5561 value: ValueKind::Boolean(b),
5562 }
5563 }
5564
5565 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5566 Self::date(date_time_to_semantic(dt))
5567 }
5568
5569 pub fn date(dt: SemanticDateTime) -> Self {
5570 Self {
5571 value: ValueKind::Date(dt),
5572 }
5573 }
5574
5575 pub fn time(t: SemanticTime) -> Self {
5576 Self {
5577 value: ValueKind::Time(t),
5578 }
5579 }
5580
5581 pub fn ratio(r: RationalInteger) -> Self {
5582 Self {
5583 value: ValueKind::Ratio(r),
5584 }
5585 }
5586
5587 pub fn ratio_from_decimal(r: Decimal) -> Self {
5588 Self::ratio(
5589 crate::literals::rational_from_parsed_decimal(r)
5590 .expect("BUG: ratio literal from decimal must lift at boundary"),
5591 )
5592 }
5593
5594 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5595 Self {
5596 value: ValueKind::Range(Box::new(left), Box::new(right)),
5597 }
5598 }
5599
5600 pub fn display_value(&self) -> String {
5602 match &self.value {
5603 ValueKind::Measure(_) | ValueKind::Ratio(_) => format!("{}", self.value),
5604 ValueKind::Range(left, right) => {
5605 format!("{}...{}", left.display_value(), right.display_value())
5606 }
5607 ValueKind::List(rows) => rows
5608 .iter()
5609 .map(|row| row.display_value())
5610 .collect::<Vec<_>>()
5611 .join(", "),
5612 _ => format!("{}", self.value),
5613 }
5614 }
5615
5616 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
5618 match &self.value {
5619 ValueKind::Measure(n) => {
5620 let signature = lemma_type.measure_runtime_signature();
5621 format_measure_canonical_for_display(n, lemma_type, &signature)
5622 }
5623 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
5624 ValueKind::Range(left, right) => {
5625 format!("{}...{}", left.display_value(), right.display_value())
5626 }
5627 ValueKind::List(rows) => rows
5628 .iter()
5629 .map(|row| row.display_value())
5630 .collect::<Vec<_>>()
5631 .join(", "),
5632 _ => format!("{}", self.value),
5633 }
5634 }
5635
5636 pub fn byte_size(&self) -> usize {
5638 self.value.structural_byte_size()
5639 }
5640
5641 #[must_use]
5643 pub fn magnitude_suggestion_for_decimal_prompt(
5644 &self,
5645 lemma_type: &LemmaType,
5646 ) -> Option<rust_decimal::Decimal> {
5647 match &self.value {
5648 ValueKind::Number(n) => Some(
5649 lemma_type
5650 .try_rational_as_decimal(n)
5651 .expect("BUG: stored number literal must convert to decimal for prompt"),
5652 ),
5653 ValueKind::Measure(n) => {
5654 let signature = lemma_type.measure_runtime_signature();
5655 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
5656 "BUG: measure prompt requires exactly one signature unit with exponent 1",
5657 );
5658 Some(
5659 lemma_type
5660 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
5661 .expect("BUG: stored measure literal must convert to decimal for prompt"),
5662 )
5663 }
5664 ValueKind::Ratio(n) => {
5665 if let Some(unit_name) = lemma_type.unit() {
5666 Some(
5667 lemma_type
5668 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
5669 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
5670 )
5671 } else {
5672 Some(lemma_type.try_rational_as_decimal(n).expect(
5673 "BUG: stored bare ratio literal must convert to decimal for prompt",
5674 ))
5675 }
5676 }
5677 _ => None,
5678 }
5679 }
5680
5681 #[must_use]
5683 pub fn measure_units(
5684 &self,
5685 lemma_type: &LemmaType,
5686 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5687 if !matches!(self.value, ValueKind::Measure(_)) {
5688 return None;
5689 }
5690 lemma_type.measure_unit_names()?;
5691 let declared: Vec<&str> = lemma_type
5692 .measure_unit_names()
5693 .expect("BUG: measure_unit_names checked above");
5694 lemma_type
5695 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5696 .ok()
5697 }
5698
5699 #[must_use]
5701 pub fn ratio_units(
5702 &self,
5703 lemma_type: &LemmaType,
5704 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5705 if !matches!(self.value, ValueKind::Ratio(_)) {
5706 return None;
5707 }
5708 let has_declared_units = match &lemma_type.specifications {
5709 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
5710 TypeSpecification::RatioRange { .. } => true,
5711 _ => return None,
5712 };
5713 if !has_declared_units {
5714 return None;
5715 }
5716 let declared: Vec<&str> = lemma_type
5717 .ratio_unit_names()
5718 .expect("BUG: ratio units checked above");
5719 lemma_type
5720 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5721 .ok()
5722 }
5723
5724 #[must_use]
5726 pub fn magnitude_in_unit(
5727 &self,
5728 lemma_type: &LemmaType,
5729 unit: &str,
5730 ) -> Option<rust_decimal::Decimal> {
5731 self.measure_units(lemma_type)
5732 .and_then(|map| map.get(unit).copied())
5733 .or_else(|| {
5734 self.ratio_units(lemma_type)
5735 .and_then(|map| map.get(unit).copied())
5736 })
5737 }
5738
5739 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
5741 let ValueKind::Measure(magnitude) = &self.value else {
5742 unreachable!(
5743 "BUG: duration_canonical_seconds called with {:?}",
5744 self.value
5745 );
5746 };
5747 if !lemma_type.is_duration_like_measure() {
5748 unreachable!(
5749 "BUG: duration_canonical_seconds called with type {}",
5750 lemma_type.name()
5751 );
5752 }
5753 let factor = lemma_type.measure_unit_factor("second");
5754 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
5755 }
5756
5757 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
5759 let ValueKind::Measure(magnitude) = &self.value else {
5760 unreachable!(
5761 "BUG: calendar_canonical_months called with {:?}",
5762 self.value
5763 );
5764 };
5765 if !lemma_type.is_calendar_like() {
5766 unreachable!(
5767 "BUG: calendar_canonical_months called with type {}",
5768 lemma_type.name()
5769 );
5770 }
5771 let factor = lemma_type.measure_unit_factor("month");
5772 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
5773 }
5774}
5775
5776impl TypedLiteral {
5777 pub fn text(s: String) -> Self {
5778 Self {
5779 value: ValueKind::Text(s),
5780 lemma_type: primitive_text_arc().clone(),
5781 }
5782 }
5783
5784 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
5785 Self {
5786 value: ValueKind::Text(s),
5787 lemma_type,
5788 }
5789 }
5790
5791 pub fn number(n: RationalInteger) -> Self {
5792 Self {
5793 value: ValueKind::Number(n),
5794 lemma_type: primitive_number_arc().clone(),
5795 }
5796 }
5797
5798 pub fn number_from_decimal(decimal: Decimal) -> Self {
5799 Self::number(
5800 crate::literals::rational_from_parsed_decimal(decimal)
5801 .expect("BUG: literal number from decimal must lift at boundary"),
5802 )
5803 }
5804
5805 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5806 Self {
5807 value: ValueKind::Number(n),
5808 lemma_type,
5809 }
5810 }
5811
5812 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
5813 Self::number_with_type(
5814 crate::literals::rational_from_parsed_decimal(decimal)
5815 .expect("BUG: literal number from decimal must lift at boundary"),
5816 lemma_type,
5817 )
5818 }
5819
5820 pub fn measure_with_bound_unit(
5822 n: RationalInteger,
5823 unit_name: impl Into<String>,
5824 lemma_type: Arc<LemmaType>,
5825 ) -> Self {
5826 Self::measure_with_type(
5827 n,
5828 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5829 )
5830 }
5831
5832 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5833 Self {
5834 value: ValueKind::Measure(n),
5835 lemma_type,
5836 }
5837 }
5838
5839 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5840 Self {
5841 value: ValueKind::Measure(n),
5842 lemma_type,
5843 }
5844 }
5845
5846 pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
5848 Self {
5849 value: ValueKind::Measure(value),
5850 lemma_type: Arc::new(anonymous_measure_type()),
5851 }
5852 }
5853
5854 pub fn from_bool(b: bool) -> Self {
5855 Self {
5856 value: ValueKind::Boolean(b),
5857 lemma_type: primitive_boolean_arc().clone(),
5858 }
5859 }
5860
5861 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5862 Self::date(date_time_to_semantic(dt))
5863 }
5864
5865 pub fn date(dt: SemanticDateTime) -> Self {
5866 Self {
5867 value: ValueKind::Date(dt),
5868 lemma_type: primitive_date_arc().clone(),
5869 }
5870 }
5871
5872 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
5873 Self {
5874 value: ValueKind::Date(dt),
5875 lemma_type,
5876 }
5877 }
5878
5879 pub fn time(t: SemanticTime) -> Self {
5880 Self {
5881 value: ValueKind::Time(t),
5882 lemma_type: primitive_time_arc().clone(),
5883 }
5884 }
5885
5886 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
5887 Self {
5888 value: ValueKind::Time(t),
5889 lemma_type,
5890 }
5891 }
5892
5893 pub fn calendar(
5894 value: RationalInteger,
5895 unit: SemanticCalendarUnit,
5896 lemma_type: Arc<LemmaType>,
5897 ) -> Self {
5898 let unit_name = unit.to_string();
5899 debug_assert_eq!(
5900 semantic_calendar_unit_from_unit_name(&unit_name),
5901 unit,
5902 "BUG: calendar unit name must round-trip"
5903 );
5904 Self::measure_with_bound_unit(value, unit_name, lemma_type)
5905 }
5906
5907 pub fn calendar_from_decimal(
5908 value: Decimal,
5909 unit: SemanticCalendarUnit,
5910 lemma_type: Arc<LemmaType>,
5911 ) -> Self {
5912 Self::calendar(
5913 crate::literals::rational_from_parsed_decimal(value)
5914 .expect("BUG: calendar literal from decimal must lift at boundary"),
5915 unit,
5916 lemma_type,
5917 )
5918 }
5919
5920 pub fn calendar_with_type(
5921 value: RationalInteger,
5922 unit: SemanticCalendarUnit,
5923 lemma_type: Arc<LemmaType>,
5924 ) -> Self {
5925 Self::calendar(value, unit, lemma_type)
5926 }
5927
5928 pub fn ratio(r: RationalInteger) -> Self {
5929 Self {
5930 value: ValueKind::Ratio(r),
5931 lemma_type: primitive_ratio_arc().clone(),
5932 }
5933 }
5934
5935 pub fn ratio_from_decimal(r: Decimal) -> Self {
5936 Self::ratio(
5937 crate::literals::rational_from_parsed_decimal(r)
5938 .expect("BUG: ratio literal from decimal must lift at boundary"),
5939 )
5940 }
5941
5942 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5943 Self {
5944 value: ValueKind::Ratio(r),
5945 lemma_type,
5946 }
5947 }
5948
5949 pub fn ratio_with_bound_unit(
5950 r: RationalInteger,
5951 unit_name: impl Into<String>,
5952 lemma_type: Arc<LemmaType>,
5953 ) -> Self {
5954 Self::ratio_with_type(
5955 r,
5956 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5957 )
5958 }
5959
5960 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5961 let specifications =
5962 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
5963 .unwrap_or_else(|| {
5964 unreachable!(
5965 "BUG: attempted to construct a range literal from incompatible endpoint types"
5966 )
5967 });
5968
5969 Self {
5970 value: ValueKind::Range(Box::new(left), Box::new(right)),
5971 lemma_type: Arc::new(LemmaType::primitive(specifications)),
5972 }
5973 }
5974
5975 pub fn display_value(&self) -> String {
5976 self.to_literal().display_value_with_type(&self.lemma_type)
5977 }
5978
5979 pub fn get_type(&self) -> &LemmaType {
5980 &self.lemma_type
5981 }
5982
5983 pub fn byte_size(&self) -> usize {
5984 self.value.structural_byte_size()
5985 }
5986}
5987
5988impl fmt::Display for TypedLiteral {
5989 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5990 write!(f, "{}", self.display_value())
5991 }
5992}
5993
5994#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5997pub enum ReferenceTarget {
5998 Data(DataPath),
5999 Rule(RulePath),
6000}
6001
6002#[derive(Clone, Debug, Serialize, Deserialize)]
6007pub enum ImportOverlay {
6008 Literal(BoundValueKind),
6009 Reference {
6010 target: ReferenceTarget,
6011 constraints: Option<Vec<Constraint>>,
6012 },
6013}
6014
6015#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6019pub(crate) enum ReferenceEnd {
6020 Promptable(DataPath),
6021 Rule(RulePath),
6022 Import,
6023}
6024
6025#[derive(Clone, Debug, Serialize, Deserialize)]
6027pub enum DataDefinition {
6028 Value {
6030 value: LiteralValue,
6031 #[serde(with = "crate::snapshot::shared::arc")]
6032 resolved_type: Arc<LemmaType>,
6033 source: Source,
6034 },
6035 TypeDeclaration {
6041 #[serde(with = "crate::snapshot::shared::arc")]
6042 resolved_type: Arc<LemmaType>,
6043 declared_suggestion: Option<BoundValueKind>,
6044 declared_value: Option<BoundValueKind>,
6045 source: Source,
6046 },
6047 Import {
6054 identity: SpecIdentity,
6055 overlay: IndexMap<DataPath, ImportOverlay>,
6057 source: Source,
6058 },
6059 InstanceList {
6063 #[serde(with = "crate::snapshot::shared::arc")]
6064 resolved_type: Arc<LemmaType>,
6065 source: Source,
6066 },
6067 Reference {
6092 target: ReferenceTarget,
6093 #[serde(with = "crate::snapshot::shared::arc")]
6094 resolved_type: Arc<LemmaType>,
6095 local_constraints: Option<Vec<Constraint>>,
6096 local_suggestion: Option<BoundValueKind>,
6097 local_value: Option<BoundValueKind>,
6098 source: Source,
6099 },
6100}
6101
6102impl DataDefinition {
6103 pub fn lemma_type(&self) -> Option<&LemmaType> {
6105 self.resolved_type_arc().map(|arc| arc.as_ref())
6106 }
6107
6108 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
6110 match self {
6111 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
6112 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
6113 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
6114 DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
6115 DataDefinition::Import { .. } => None,
6116 }
6117 }
6118
6119 #[inline]
6121 pub fn schema_type(&self) -> Option<&LemmaType> {
6122 self.lemma_type()
6123 }
6124
6125 pub fn value(&self) -> Option<LiteralValue> {
6129 match self {
6130 DataDefinition::Value { value, .. } => Some(value.clone()),
6131 DataDefinition::TypeDeclaration {
6132 declared_value: Some(dv),
6133 ..
6134 } => Some(dv.to_literal()),
6135 DataDefinition::Reference {
6136 local_value: Some(dv),
6137 resolved_type: _,
6138 ..
6139 } => Some(dv.to_literal()),
6140 DataDefinition::TypeDeclaration { .. }
6141 | DataDefinition::Import { .. }
6142 | DataDefinition::InstanceList { .. }
6143 | DataDefinition::Reference { .. } => None,
6144 }
6145 }
6146
6147 pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
6149 match self {
6150 DataDefinition::TypeDeclaration {
6151 declared_value: Some(dv),
6152 ..
6153 }
6154 | DataDefinition::Reference {
6155 local_value: Some(dv),
6156 ..
6157 } => Some(dv),
6158 DataDefinition::Value { .. }
6159 | DataDefinition::TypeDeclaration { .. }
6160 | DataDefinition::Reference { .. }
6161 | DataDefinition::InstanceList { .. }
6162 | DataDefinition::Import { .. } => None,
6163 }
6164 }
6165
6166 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
6170 match self {
6171 DataDefinition::TypeDeclaration {
6172 declared_suggestion: Some(dv),
6173 ..
6174 }
6175 | DataDefinition::Reference {
6176 local_suggestion: Some(dv),
6177 ..
6178 } => Some(dv),
6179 DataDefinition::Value { .. }
6180 | DataDefinition::TypeDeclaration { .. }
6181 | DataDefinition::Reference { .. }
6182 | DataDefinition::InstanceList { .. }
6183 | DataDefinition::Import { .. } => None,
6184 }
6185 }
6186
6187 pub fn source(&self) -> &Source {
6189 match self {
6190 DataDefinition::Value { source, .. } => source,
6191 DataDefinition::TypeDeclaration { source, .. } => source,
6192 DataDefinition::Import { source, .. } => source,
6193 DataDefinition::InstanceList { source, .. } => source,
6194 DataDefinition::Reference { source, .. } => source,
6195 }
6196 }
6197}
6198
6199pub fn number_with_unit_to_value_kind(
6201 magnitude: rust_decimal::Decimal,
6202 unit_name: &str,
6203 lemma_type: &LemmaType,
6204) -> Result<ValueKind, String> {
6205 match &lemma_type.specifications {
6206 TypeSpecification::Ratio { units, .. } => {
6207 use crate::computation::rational::{checked_div, decimal_to_rational};
6208 let unit = units.get(unit_name)?;
6209 let magnitude_rational = decimal_to_rational(magnitude)
6210 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6211 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6212 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6213 Ok(ValueKind::Ratio(canonical_rational))
6214 }
6215 TypeSpecification::Measure { units, .. } => {
6216 use crate::computation::rational::checked_mul;
6217 let rational = lift_parser_decimal(magnitude)?;
6218 let unit = units.get(unit_name)?;
6219 let canonical = checked_mul(&rational, &unit.factor)
6220 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6221 Ok(ValueKind::Measure(canonical))
6222 }
6223 _ => Err(format!(
6224 "Unit '{}' is defined on type '{}' which is not measure or ratio",
6225 unit_name,
6226 lemma_type.name()
6227 )),
6228 }
6229}
6230
6231pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
6234 matches!(
6235 (type_spec, value),
6236 (TypeSpecification::Number { .. }, ValueKind::Number(_))
6237 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
6238 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
6239 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
6240 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
6241 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
6242 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
6243 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
6244 | (
6245 TypeSpecification::NumberRange { .. },
6246 ValueKind::Range(_, _)
6247 )
6248 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
6249 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
6250 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
6251 | (
6252 TypeSpecification::MeasureRange { .. },
6253 ValueKind::Range(_, _)
6254 )
6255 | (TypeSpecification::Veto { .. }, _)
6256 | (TypeSpecification::Undetermined, _)
6257 | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
6258 | (TypeSpecification::List { .. }, ValueKind::List(_))
6259 )
6260}
6261
6262fn parser_value_type_mismatch(
6263 value: &crate::literals::Value,
6264 type_spec: &TypeSpecification,
6265) -> String {
6266 use crate::parsing::ast::AsLemmaSource;
6267 let value_str = format!("{}", AsLemmaSource(value));
6268 match type_spec {
6269 TypeSpecification::Measure { units, .. } => {
6270 let unit_hint = units
6271 .iter()
6272 .find(|u| u.factor == crate::computation::rational::rational_one())
6273 .map(|u| u.name.as_str())
6274 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
6275 .unwrap_or("unit");
6276 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
6277 }
6278 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
6279 let unit_hint = units
6280 .iter()
6281 .next()
6282 .map(|u| u.name.as_str())
6283 .unwrap_or("unit");
6284 format!(
6285 "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
6286 )
6287 }
6288 _ => format!("cannot use {value_str} as {type_spec}"),
6289 }
6290}
6291
6292pub fn refresh_measure_literal_canonical_magnitude(
6297 lit: &mut LiteralValue,
6298 previous_type: &LemmaType,
6299 resolved_type: &LemmaType,
6300) -> Result<(), crate::computation::rational::NumericFailure> {
6301 use crate::computation::rational::{checked_div, checked_mul};
6302 let ValueKind::Measure(magnitude) = &mut lit.value else {
6303 return Ok(());
6304 };
6305 let signature = previous_type.measure_runtime_signature();
6309 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
6310 return Ok(());
6311 };
6312 if signature.len() != 1 {
6313 return Ok(());
6314 }
6315 let stored_factor = previous_type.measure_unit_factor(unit_name);
6316 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
6317 if stored_factor == resolved_factor {
6318 return Ok(());
6319 }
6320 let scaled = checked_mul(magnitude, resolved_factor)?;
6321 *magnitude = checked_div(&scaled, stored_factor)?;
6322 Ok(())
6323}
6324
6325fn number_with_unit_value_kind(
6326 magnitude: rust_decimal::Decimal,
6327 unit_name: &str,
6328 type_spec: &TypeSpecification,
6329) -> Result<ValueKind, String> {
6330 use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
6331 match type_spec {
6332 TypeSpecification::Ratio { units, .. } => {
6333 let unit = units.get(unit_name)?;
6334 let magnitude_rational = decimal_to_rational(magnitude)
6335 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6336 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6337 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6338 Ok(ValueKind::Ratio(canonical_rational))
6339 }
6340 TypeSpecification::Measure { units, .. } => {
6341 let rational = lift_parser_decimal(magnitude)?;
6342 let unit = units.get(unit_name)?;
6343 let canonical = checked_mul(&rational, &unit.factor)
6344 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6345 Ok(ValueKind::Measure(canonical))
6346 }
6347 _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
6348 }
6349}
6350
6351pub fn parser_value_to_value_kind(
6356 value: &crate::literals::Value,
6357 type_spec: &TypeSpecification,
6358 resolve_unit: Option<&UnitTokenResolver<'_>>,
6359) -> Result<ValueKind, String> {
6360 use crate::computation::rational::decimal_to_rational;
6361 use crate::literals::Value;
6362 match (value, type_spec) {
6363 (Value::NumberWithUnit(magnitude, unit_name), _) => {
6364 number_with_unit_value_kind(*magnitude, unit_name, type_spec)
6365 }
6366 (Value::Number(n), TypeSpecification::Number { .. }) => {
6367 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
6368 }
6369 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
6370 let r = decimal_to_rational(*n)
6371 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6372 Ok(ValueKind::Ratio(r))
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 (
6383 Value::Range(left, right),
6384 range_spec @ (TypeSpecification::NumberRange { .. }
6385 | TypeSpecification::DateRange { .. }
6386 | TypeSpecification::TimeRange { .. }
6387 | TypeSpecification::RatioRange { .. }
6388 | TypeSpecification::MeasureRange { .. }),
6389 ) => {
6390 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
6391 "BUG: range_element_type_specification missing arm for range type".to_string()
6392 })?;
6393 let left_lit = lift_range_endpoint(left, &endpoint, resolve_unit)?;
6394 let right_lit = lift_range_endpoint(right, &endpoint, resolve_unit)?;
6395 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
6396 }
6397 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
6398 }
6399}
6400
6401pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
6405 use crate::parsing::ast::Value;
6406 Ok(match value {
6407 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
6408 Value::Text(s) => ValueKind::Text(s.clone()),
6409 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
6410 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
6411 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
6412 Value::NumberWithUnit(_, _) => {
6413 return Err(
6414 "number_with_unit literal requires type context (measure or ratio)".to_string(),
6415 );
6416 }
6417 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
6418 })
6419}
6420
6421pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
6423 SemanticDateTime {
6424 year: dt.year,
6425 month: dt.month,
6426 day: dt.day,
6427 hour: dt.hour,
6428 minute: dt.minute,
6429 second: dt.second,
6430 microsecond: dt.microsecond,
6431 timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
6432 }
6433}
6434
6435pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
6437 SemanticTime {
6438 hour: t.hour.into(),
6439 minute: t.minute.into(),
6440 second: t.second.into(),
6441 microsecond: t.microsecond,
6442 timezone: t.timezone.as_ref().map(SemanticTimezone::from),
6443 }
6444}
6445
6446pub(crate) fn compare_semantic_dates(
6450 left: &SemanticDateTime,
6451 right: &SemanticDateTime,
6452) -> std::cmp::Ordering {
6453 left.year
6454 .cmp(&right.year)
6455 .then_with(|| left.month.cmp(&right.month))
6456 .then_with(|| left.day.cmp(&right.day))
6457 .then_with(|| left.hour.cmp(&right.hour))
6458 .then_with(|| left.minute.cmp(&right.minute))
6459 .then_with(|| left.second.cmp(&right.second))
6460 .then_with(|| left.microsecond.cmp(&right.microsecond))
6461}
6462
6463pub(crate) fn compare_semantic_times(
6466 left: &SemanticTime,
6467 right: &SemanticTime,
6468) -> std::cmp::Ordering {
6469 left.hour
6470 .cmp(&right.hour)
6471 .then_with(|| left.minute.cmp(&right.minute))
6472 .then_with(|| left.second.cmp(&right.second))
6473 .then_with(|| left.microsecond.cmp(&right.microsecond))
6474}
6475
6476pub fn conversion_target_to_semantic(
6478 ct: &ConversionTarget,
6479 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
6480 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
6481) -> Result<SemanticConversionTarget, String> {
6482 match ct {
6483 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
6484 ConversionTarget::Unit { unit_name } => {
6485 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
6486 let (bare, owning_type) = match resolved_types {
6487 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
6488 None => index.resolve(unit_name)?,
6489 };
6490 Ok(SemanticConversionTarget::Unit {
6491 unit_name: bare,
6492 owning_type,
6493 })
6494 }
6495 }
6496}
6497
6498static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
6504static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
6505static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
6506static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6507static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6508static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
6509static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
6510static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6511static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6512static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6513static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6514static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6515
6516#[must_use]
6517pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
6518 lemma_type_for_primitive(PrimitiveKind::Boolean)
6519}
6520
6521#[must_use]
6522pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
6523 lemma_type_for_primitive(PrimitiveKind::Number)
6524}
6525
6526#[must_use]
6527pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
6528 lemma_type_for_primitive(PrimitiveKind::Text)
6529}
6530
6531#[must_use]
6532pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
6533 lemma_type_for_primitive(PrimitiveKind::Date)
6534}
6535
6536#[must_use]
6537pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
6538 lemma_type_for_primitive(PrimitiveKind::DateRange)
6539}
6540
6541#[must_use]
6542pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
6543 lemma_type_for_primitive(PrimitiveKind::Time)
6544}
6545
6546#[must_use]
6547pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
6548 lemma_type_for_primitive(PrimitiveKind::Ratio)
6549}
6550
6551#[must_use]
6553pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
6554 let cell = match kind {
6555 PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
6556 PrimitiveKind::Number => &PRIMITIVE_NUMBER,
6557 PrimitiveKind::Text => &PRIMITIVE_TEXT,
6558 PrimitiveKind::Date => &PRIMITIVE_DATE,
6559 PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
6560 PrimitiveKind::Time => &PRIMITIVE_TIME,
6561 PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
6562 PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
6563 PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
6564 PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
6565 PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
6566 PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
6567 };
6568 cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
6569}
6570
6571#[must_use]
6573pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
6574 match kind {
6575 PrimitiveKind::Boolean => TypeSpecification::boolean(),
6576 PrimitiveKind::Measure => TypeSpecification::measure(),
6577 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
6578 PrimitiveKind::Number => TypeSpecification::number(),
6579 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
6580 PrimitiveKind::Ratio => TypeSpecification::ratio(),
6581 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
6582 PrimitiveKind::Text => TypeSpecification::text(),
6583 PrimitiveKind::Date => TypeSpecification::date(),
6584 PrimitiveKind::DateRange => TypeSpecification::date_range(),
6585 PrimitiveKind::Time => TypeSpecification::time(),
6586 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
6587 }
6588}
6589
6590impl fmt::Display for PathSegment {
6595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6596 match &self.repository {
6597 Some(repository) => write!(f, "{} → {} {}", self.uses, repository, self.spec),
6598 None => write!(f, "{} → {}", self.uses, self.spec),
6599 }
6600 }
6601}
6602
6603impl fmt::Display for DataPath {
6604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6605 write!(f, "{}", self.input_key())
6606 }
6607}
6608
6609impl fmt::Display for RulePath {
6610 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6611 write!(f, "{}", self.input_key())
6612 }
6613}
6614
6615impl fmt::Display for LemmaType {
6616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6617 write!(f, "{}", self.name())
6618 }
6619}
6620
6621pub(crate) fn format_decimal_for_api(
6622 decimal: rust_decimal::Decimal,
6623 decimal_places: Option<u8>,
6624) -> rust_decimal::Decimal {
6625 match decimal_places {
6626 Some(decimal_places) => {
6627 let places = u32::from(decimal_places);
6628 let mut rounded = decimal.round_dp(places);
6629 rounded.rescale(places);
6630 rounded
6631 }
6632 None => {
6633 let normalized = decimal.normalize();
6634 if normalized.fract().is_zero() {
6635 normalized.trunc()
6636 } else {
6637 normalized
6638 }
6639 }
6640 }
6641}
6642
6643fn format_decimal_for_human_display(
6644 decimal: rust_decimal::Decimal,
6645 decimal_places: Option<u8>,
6646) -> String {
6647 match decimal_places {
6648 Some(decimal_places) => {
6649 let rounded = decimal.round_dp(u32::from(decimal_places));
6650 format!("{:.prec$}", rounded, prec = decimal_places as usize)
6651 }
6652 None => decimal.normalize().to_string(),
6653 }
6654}
6655
6656fn format_rational_for_human_display(
6657 magnitude: &crate::computation::rational::RationalInteger,
6658 decimal_places: Option<u8>,
6659) -> String {
6660 match magnitude.try_to_decimal() {
6661 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
6662 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
6663 Err(_) => magnitude.display_str(),
6664 }
6665}
6666
6667fn format_measure_canonical_for_display(
6668 canonical: &crate::computation::rational::RationalInteger,
6669 lemma_type: &LemmaType,
6670 signature: &[(String, i32)],
6671) -> String {
6672 use crate::computation::rational::checked_div;
6673
6674 let decimals = lemma_type.decimal_places();
6675
6676 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
6677 if !units.is_empty() {
6678 let unit = match lemma_type.unit() {
6680 Some(binding) => units
6681 .iter()
6682 .find(|unit| unit.name == binding)
6683 .unwrap_or_else(|| {
6684 panic!(
6685 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
6686 lemma_type.name()
6687 )
6688 }),
6689 None => units
6690 .iter()
6691 .next()
6692 .expect("BUG: measure type with non-empty units must have a first unit"),
6693 };
6694 let in_unit = checked_div(canonical, &unit.factor)
6695 .expect("BUG: de-canonicalization for measure display must not fail");
6696 let formatted = format_rational_for_human_display(&in_unit, decimals);
6697 return format!("{} {}", formatted, unit.name);
6698 }
6699 }
6700
6701 let unit_label = match signature {
6702 [] => String::new(),
6703 [(name, 1)] => name.clone(),
6704 _ => format_signature_operator_style(signature),
6705 };
6706 let formatted = format_rational_for_human_display(canonical, decimals);
6707 if unit_label.is_empty() {
6708 formatted
6709 } else {
6710 format!("{formatted} {unit_label}")
6711 }
6712}
6713
6714fn format_ratio_canonical_for_display(
6715 canonical: &crate::computation::rational::RationalInteger,
6716 lemma_type: &LemmaType,
6717) -> String {
6718 use crate::computation::rational::checked_mul;
6719
6720 let units = match &lemma_type.specifications {
6721 TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
6722 units
6723 }
6724 _ => unreachable!(
6725 "BUG: ratio display with non-ratio type {}",
6726 lemma_type.name()
6727 ),
6728 };
6729 let display_unit = lemma_type
6730 .unit()
6731 .or_else(|| lemma_type.ratio_primary_unit());
6732 let Some(unit_name) = display_unit else {
6733 return format_rational_magnitude_for_display(canonical);
6734 };
6735 let unit = units.get(unit_name).unwrap_or_else(|_| {
6736 panic!(
6737 "BUG: ratio display unit '{unit_name}' is not declared on type {}",
6738 lemma_type.name()
6739 )
6740 });
6741 let in_unit = checked_mul(canonical, &unit.value)
6742 .expect("BUG: de-canonicalization for ratio display must not fail");
6743 format_number_with_unit_for_display(&in_unit, unit_name)
6744}
6745
6746impl fmt::Display for LiteralValue {
6747 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6748 match &self.value {
6752 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
6753 write!(f, "{}", self.value)
6754 }
6755 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
6756 _ => write!(f, "{}", self.value),
6757 }
6758 }
6759}
6760
6761#[cfg(test)]
6766pub(crate) mod tests {
6767 use super::*;
6768 use crate::computation::rational::decimal_to_rational;
6769 use crate::literals::DateGranularity;
6770 use crate::literals::Value;
6771 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
6772 use rust_decimal::Decimal;
6773 use std::str::FromStr;
6774
6775 #[must_use]
6776 pub(crate) fn test_type_family(name: &str) -> TypeFamily {
6777 TypeFamily {
6778 repository: None,
6779 spec: "test".to_string(),
6780 effective: crate::parsing::ast::EffectiveDate::Origin,
6781 name: name.to_string(),
6782 }
6783 }
6784
6785 #[must_use]
6786 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
6787 crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
6788 }
6789
6790 #[must_use]
6791 pub(crate) fn primitive_measure() -> &'static LemmaType {
6792 primitive_measure_arc().as_ref()
6793 }
6794
6795 #[test]
6796 fn default_primitive_help_is_goal_oriented() {
6797 let kinds = [
6798 PrimitiveKind::Boolean,
6799 PrimitiveKind::Measure,
6800 PrimitiveKind::MeasureRange,
6801 PrimitiveKind::Number,
6802 PrimitiveKind::NumberRange,
6803 PrimitiveKind::Ratio,
6804 PrimitiveKind::RatioRange,
6805 PrimitiveKind::Text,
6806 PrimitiveKind::Date,
6807 PrimitiveKind::DateRange,
6808 PrimitiveKind::Time,
6809 PrimitiveKind::TimeRange,
6810 ];
6811 for kind in kinds {
6812 let spec = type_spec_for_primitive(kind);
6813 let help = match &spec {
6814 TypeSpecification::Boolean { help, .. }
6815 | TypeSpecification::Number { help, .. }
6816 | TypeSpecification::NumberRange { help, .. }
6817 | TypeSpecification::Text { help, .. }
6818 | TypeSpecification::Measure { help, .. }
6819 | TypeSpecification::MeasureRange { help, .. }
6820 | TypeSpecification::Ratio { help, .. }
6821 | TypeSpecification::RatioRange { help, .. }
6822 | TypeSpecification::Date { help, .. }
6823 | TypeSpecification::DateRange { help, .. }
6824 | TypeSpecification::TimeRange { help, .. }
6825 | TypeSpecification::Time { help, .. } => help,
6826 TypeSpecification::Veto { .. }
6827 | TypeSpecification::Undetermined
6828 | TypeSpecification::Spec { .. }
6829 | TypeSpecification::List { .. } => {
6830 unreachable!(
6831 "BUG: primitive kind {:?} mapped to non-primitive spec",
6832 kind
6833 )
6834 }
6835 };
6836 assert!(!help.is_empty(), "help for {:?}", kind);
6837 assert!(
6838 !help.to_ascii_lowercase().contains("format:"),
6839 "help for {:?} must not describe syntax: {:?}",
6840 kind,
6841 help
6842 );
6843 assert_eq!(help, default_help_for_primitive(kind));
6844 }
6845 }
6846
6847 #[test]
6848 fn data_path_and_rule_path_display_are_input_key() {
6849 let implicit = PathSegment {
6850 uses: "bag".to_string(),
6851 repository: None,
6852 spec: "bag".to_string(),
6853 };
6854 let explicit = PathSegment {
6855 uses: "bag".to_string(),
6856 repository: None,
6857 spec: "nut_bag".to_string(),
6858 };
6859 let named_repo = PathSegment {
6860 uses: "calc".to_string(),
6861 repository: Some("alpha".to_string()),
6862 spec: "pricing".to_string(),
6863 };
6864
6865 assert_eq!(
6866 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
6867 "bag.weight"
6868 );
6869 assert_eq!(
6870 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
6871 "bag.weight"
6872 );
6873 assert_eq!(
6874 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
6875 "calc.price"
6876 );
6877 assert_eq!(
6878 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
6879 "bag.cost"
6880 );
6881 assert_eq!(
6882 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
6883 "bag.cost"
6884 );
6885 assert_eq!(
6886 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
6887 "calc.price"
6888 );
6889
6890 assert_eq!(
6891 PathSegment {
6892 uses: "bag".to_string(),
6893 repository: None,
6894 spec: "nut_bag".to_string(),
6895 }
6896 .to_string(),
6897 "bag → nut_bag"
6898 );
6899 assert_eq!(
6900 PathSegment {
6901 uses: "calc".to_string(),
6902 repository: Some("alpha".to_string()),
6903 spec: "pricing".to_string(),
6904 }
6905 .to_string(),
6906 "calc → alpha pricing"
6907 );
6908 }
6909
6910 #[test]
6911 fn test_negated_comparison() {
6912 assert_eq!(
6913 negated_comparison(ComparisonComputation::LessThan),
6914 ComparisonComputation::GreaterThanOrEqual
6915 );
6916 assert_eq!(
6917 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
6918 ComparisonComputation::LessThan
6919 );
6920 assert_eq!(
6921 negated_comparison(ComparisonComputation::Is),
6922 ComparisonComputation::IsNot
6923 );
6924 assert_eq!(
6925 negated_comparison(ComparisonComputation::IsNot),
6926 ComparisonComputation::Is
6927 );
6928 }
6929
6930 #[test]
6931 fn value_to_semantic_number_is_decimal() {
6932 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
6933 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
6934 }
6935
6936 #[test]
6937 fn value_kind_measure_serializes_magnitude_only() {
6938 let kind =
6939 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
6940 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
6941 assert_eq!(json["measure"]["value"], "99.5");
6942 assert!(json["measure"].get("signature").is_none());
6943 }
6944
6945 #[test]
6946 fn value_kind_measure_roundtrips() {
6947 let original =
6948 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
6949 let api = crate::api::ValueKind::from(&original);
6950 let json = serde_json::to_string(&api).unwrap();
6951 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6952 assert_eq!(api, parsed);
6953 }
6954
6955 #[test]
6956 fn value_kind_measure_empty_roundtrips() {
6957 let original =
6958 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
6959 let api = crate::api::ValueKind::from(&original);
6960 let json = serde_json::to_string(&api).unwrap();
6961 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6962 assert_eq!(api, parsed);
6963 }
6964
6965 #[test]
6966 fn literal_value_number_serde_not_rational_array() {
6967 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
6968 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
6969 let number = json
6970 .get("value")
6971 .and_then(|v| v.get("number"))
6972 .expect("number field");
6973 assert!(number.is_string());
6974 assert_eq!(number.as_str(), Some("20"));
6975 assert!(
6976 !number.is_array(),
6977 "stored number must not serialize as [n,d]"
6978 );
6979 }
6980
6981 #[test]
6982 fn test_literal_value_to_primitive_type() {
6983 let one = rational_new(1, 1);
6984
6985 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
6986 assert_eq!(
6987 TypedLiteral::number(one.clone()).lemma_type.name(),
6988 "number"
6989 );
6990 assert_eq!(
6991 TypedLiteral::from_bool(bool::from(BooleanValue::True))
6992 .lemma_type
6993 .name(),
6994 "boolean"
6995 );
6996
6997 let dt = DateTimeValue {
6998 year: 2024,
6999 month: 1,
7000 day: 1,
7001 hour: 0,
7002 minute: 0,
7003 second: 0,
7004 microsecond: 0,
7005 timezone: None,
7006
7007 granularity: DateGranularity::Full,
7008 };
7009 assert_eq!(
7010 TypedLiteral::date(date_time_to_semantic(&dt))
7011 .lemma_type
7012 .name(),
7013 "date"
7014 );
7015 assert_eq!(
7016 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
7017 .lemma_type
7018 .name(),
7019 "ratio"
7020 );
7021 let dur_type = LemmaType::new(
7022 "duration".to_string(),
7023 TypeSpecification::Measure {
7024 minimum: None,
7025 maximum: None,
7026 decimals: None,
7027 units: MeasureUnits::from(vec![MeasureUnit {
7028 name: "second".to_string(),
7029 factor: crate::computation::rational::rational_one(),
7030 derived_measure_factors: Vec::new(),
7031 decomposition: BaseMeasureVector::new(),
7032 minimum: None,
7033 maximum: None,
7034 suggestion_magnitude: None,
7035 }]),
7036 traits: vec![MeasureTrait::Duration],
7037 decomposition: None,
7038 unit: None,
7039 help: String::new(),
7040 },
7041 TypeExtends::Primitive,
7042 );
7043 assert_eq!(
7044 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
7045 .lemma_type
7046 .name(),
7047 "duration"
7048 );
7049 }
7050
7051 #[test]
7052 fn test_type_display() {
7053 let specs = TypeSpecification::text();
7054 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
7055 assert_eq!(format!("{}", lemma_type), "name");
7056 }
7057
7058 #[test]
7059 fn test_type_serialization() {
7060 let specs = TypeSpecification::number();
7061 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
7062 let api = crate::api::LemmaType::from(&lemma_type);
7063 let serialized = serde_json::to_string(&api).unwrap();
7064 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
7065 assert_eq!(api, deserialized);
7066 }
7067
7068 #[test]
7069 fn test_literal_value_display_value() {
7070 let ten = rational_new(10, 1);
7071
7072 assert_eq!(
7073 LiteralValue::text("hello".to_string()).display_value(),
7074 "hello"
7075 );
7076 assert_eq!(LiteralValue::number(ten).display_value(), "10");
7077 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
7078 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
7079
7080 let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
7082 let ten_percent_ratio = LiteralValue::ratio(
7083 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
7084 .expect("ratio decimal"),
7085 );
7086 assert_eq!(
7087 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
7088 "10%"
7089 );
7090
7091 let time = TimeValue {
7092 hour: 14,
7093 minute: 30,
7094 second: 0,
7095 microsecond: 0,
7096 timezone: None,
7097 };
7098 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
7099 assert!(time_display.contains("14"));
7100 assert!(time_display.contains("30"));
7101 }
7102
7103 #[test]
7104 fn test_measure_display_respects_type_decimals() {
7105 let money_type = LemmaType {
7106 name: Some("money".to_string()),
7107 specifications: TypeSpecification::Measure {
7108 minimum: None,
7109 maximum: None,
7110 decimals: Some(2),
7111 units: MeasureUnits::from(vec![MeasureUnit {
7112 name: "eur".to_string(),
7113 factor: crate::computation::rational::rational_one(),
7114 derived_measure_factors: Vec::new(),
7115 decomposition: BaseMeasureVector::new(),
7116 minimum: None,
7117 maximum: None,
7118 suggestion_magnitude: None,
7119 }]),
7120 traits: Vec::new(),
7121 decomposition: None,
7122 unit: None,
7123 help: String::new(),
7124 },
7125 extends: TypeExtends::Primitive,
7126 };
7127 let money_type = Arc::new(money_type);
7128 let val =
7129 LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
7130 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.80 eur");
7131 let more_precision = LiteralValue::measure(
7132 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
7133 );
7134 assert_eq!(
7135 more_precision.display_value_with_type(money_type.as_ref()),
7136 "1.80 eur"
7137 );
7138 let measure_no_decimals = Arc::new(LemmaType {
7139 name: Some("count".to_string()),
7140 specifications: TypeSpecification::Measure {
7141 minimum: None,
7142 maximum: None,
7143 decimals: None,
7144 units: MeasureUnits::from(vec![MeasureUnit {
7145 name: "items".to_string(),
7146 factor: crate::computation::rational::rational_one(),
7147 derived_measure_factors: Vec::new(),
7148 decomposition: BaseMeasureVector::new(),
7149 minimum: None,
7150 maximum: None,
7151 suggestion_magnitude: None,
7152 }]),
7153 traits: Vec::new(),
7154 decomposition: None,
7155 unit: None,
7156 help: String::new(),
7157 },
7158 extends: TypeExtends::Primitive,
7159 });
7160 let val_any = LiteralValue::measure(
7161 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
7162 );
7163 assert_eq!(
7164 val_any.display_value_with_type(measure_no_decimals.as_ref()),
7165 "42.5 items"
7166 );
7167 }
7168
7169 #[test]
7170 fn test_literal_value_time_type() {
7171 let time = TimeValue {
7172 hour: 14,
7173 minute: 30,
7174 second: 0,
7175 microsecond: 0,
7176 timezone: None,
7177 };
7178 let lit = TypedLiteral::time(time_to_semantic(&time));
7179 assert_eq!(lit.lemma_type.name(), "time");
7180 }
7181
7182 #[test]
7183 fn test_measure_family_name_none_without_declaring_spec() {
7184 let money_primitive = LemmaType::new(
7185 "money".to_string(),
7186 TypeSpecification::measure(),
7187 TypeExtends::Primitive,
7188 );
7189 assert_eq!(money_primitive.measure_family_name(), None);
7190 }
7191
7192 #[test]
7193 fn test_measure_family_name_custom() {
7194 let measure_spec = TypeSpecification::measure();
7195 let money_custom = LemmaType::new(
7196 "money".to_string(),
7197 measure_spec,
7198 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7199 );
7200 assert_eq!(money_custom.measure_family_name(), Some("money"));
7201 }
7202
7203 #[test]
7204 fn test_same_named_roots_in_different_specs_are_different_families() {
7205 let declared_in = |spec: &str| TypeFamily {
7206 repository: None,
7207 spec: spec.to_string(),
7208 effective: crate::parsing::ast::EffectiveDate::Origin,
7209 name: "money".to_string(),
7210 };
7211 let pricing_money = LemmaType::new(
7212 "money".to_string(),
7213 TypeSpecification::measure(),
7214 TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
7215 );
7216 let shop_money = LemmaType::new(
7217 "money".to_string(),
7218 TypeSpecification::measure(),
7219 TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
7220 );
7221 assert!(!pricing_money.same_measure_family(&shop_money));
7222 assert!(!shop_money.same_measure_family(&pricing_money));
7223 }
7224
7225 #[test]
7226 fn test_same_measure_family_parent_and_child() {
7227 let measure_spec = TypeSpecification::measure();
7228 let type_x = LemmaType::new(
7229 "x".to_string(),
7230 measure_spec.clone(),
7231 TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
7232 );
7233 let type_x2 = LemmaType::new(
7234 "x2".to_string(),
7235 measure_spec,
7236 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7237 );
7238 assert_eq!(type_x.measure_family_name(), Some("x"));
7239 assert_eq!(type_x2.measure_family_name(), Some("x"));
7240 assert!(type_x.same_measure_family(&type_x2));
7241 assert!(type_x2.same_measure_family(&type_x));
7242 }
7243
7244 #[test]
7245 fn test_same_measure_family_siblings() {
7246 let measure_spec = TypeSpecification::measure();
7247 let type_x2_a = LemmaType::new(
7248 "x2a".to_string(),
7249 measure_spec.clone(),
7250 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7251 );
7252 let type_x2_b = LemmaType::new(
7253 "x2b".to_string(),
7254 measure_spec,
7255 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7256 );
7257 assert!(type_x2_a.same_measure_family(&type_x2_b));
7258 }
7259
7260 #[test]
7261 fn test_same_measure_family_different_families() {
7262 let measure_spec = TypeSpecification::measure();
7263 let money = LemmaType::new(
7264 "money".to_string(),
7265 measure_spec.clone(),
7266 TypeExtends::Primitive,
7267 );
7268 let temperature = LemmaType::new(
7269 "temperature".to_string(),
7270 measure_spec,
7271 TypeExtends::Primitive,
7272 );
7273 assert!(!money.same_measure_family(&temperature));
7274 assert!(!temperature.same_measure_family(&money));
7275 }
7276
7277 #[test]
7278 fn test_same_measure_family_measure_vs_non_measure() {
7279 let measure_spec = TypeSpecification::measure();
7280 let number_spec = TypeSpecification::number();
7281 let measure_type =
7282 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
7283 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7284 assert!(!measure_type.same_measure_family(&number_type));
7285 assert!(!number_type.same_measure_family(&measure_type));
7286 }
7287
7288 #[test]
7289 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
7290 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
7291 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
7292
7293 assert!(!left.same_measure_family(&right));
7294 assert!(left.compatible_with_anonymous_measure(&right));
7295 }
7296
7297 #[test]
7298 fn test_measure_family_name_non_measure_returns_none() {
7299 let number_spec = TypeSpecification::number();
7300 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7301 assert_eq!(number_type.measure_family_name(), None);
7302 }
7303
7304 #[test]
7305 fn test_lemma_type_inequality_local_vs_import_same_shape() {
7306 let measure_spec = TypeSpecification::measure();
7307 let local = LemmaType::new(
7308 "t".to_string(),
7309 measure_spec.clone(),
7310 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7311 );
7312 let imported = LemmaType::new(
7313 "t".to_string(),
7314 measure_spec,
7315 TypeExtends::Custom {
7316 parent: "money".to_string(),
7317 family: test_type_family("money"),
7318 defining_spec: TypeDefiningSpec::Import,
7319 },
7320 );
7321 assert_ne!(local, imported);
7322 }
7323
7324 #[test]
7325 fn test_lemma_type_equality_import_unit_variant() {
7326 let measure_spec = TypeSpecification::measure();
7327 let left = LemmaType::new(
7328 "t".to_string(),
7329 measure_spec.clone(),
7330 TypeExtends::Custom {
7331 parent: "money".to_string(),
7332 family: test_type_family("money"),
7333 defining_spec: TypeDefiningSpec::Import,
7334 },
7335 );
7336 let right = LemmaType::new(
7337 "t".to_string(),
7338 measure_spec,
7339 TypeExtends::Custom {
7340 parent: "money".to_string(),
7341 family: test_type_family("money"),
7342 defining_spec: TypeDefiningSpec::Import,
7343 },
7344 );
7345 assert_eq!(left, right);
7346 }
7347
7348 fn month_suggestion_arg() -> CommandArg {
7349 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7350 Decimal::ONE,
7351 "month".to_string(),
7352 ))
7353 }
7354
7355 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
7356 [
7357 CommandArg::Label(name.to_string()),
7358 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
7359 ]
7360 }
7361
7362 #[test]
7363 fn default_calendar_on_text_reports_hint() {
7364 let mut specs = TypeSpecification::text();
7365 let mut default = None;
7366 let err = specs
7367 .apply_constraint(
7368 "notes",
7369 TypeConstraintCommand::Suggest,
7370 &[month_suggestion_arg()],
7371 &mut default,
7372 &mut None,
7373 None,
7374 )
7375 .unwrap_err();
7376 assert!(err.contains("Unit 'month' is for calendar data"));
7377 assert!(err.contains("double quotes"));
7378 }
7379
7380 #[test]
7381 fn default_calendar_on_duration_reports_valid_units() {
7382 let mut specs = TypeSpecification::measure();
7383 specs
7384 .apply_constraint(
7385 "duration",
7386 TypeConstraintCommand::Unit,
7387 &unit_factor_arg("second", 1),
7388 &mut None,
7389 &mut None,
7390 None,
7391 )
7392 .unwrap();
7393 specs
7394 .apply_constraint(
7395 "duration",
7396 TypeConstraintCommand::Unit,
7397 &unit_factor_arg("week", 604_800),
7398 &mut None,
7399 &mut None,
7400 None,
7401 )
7402 .unwrap();
7403 specs
7404 .apply_constraint(
7405 "duration",
7406 TypeConstraintCommand::Trait,
7407 &[CommandArg::Label("duration".to_string())],
7408 &mut None,
7409 &mut None,
7410 None,
7411 )
7412 .unwrap();
7413 let mut default = None;
7414 let err = specs
7415 .apply_constraint(
7416 "duration",
7417 TypeConstraintCommand::Suggest,
7418 &[month_suggestion_arg()],
7419 &mut default,
7420 &mut None,
7421 None,
7422 )
7423 .unwrap_err();
7424 assert!(err.contains("Unit 'month' is for calendar data"));
7425 assert!(err.contains("Valid 'duration' units are"));
7426 assert!(err.contains("week"));
7427 }
7428
7429 #[test]
7430 fn default_valid_duration_weeks_accepted() {
7431 let mut specs = TypeSpecification::measure();
7432 specs
7433 .apply_constraint(
7434 "duration",
7435 TypeConstraintCommand::Unit,
7436 &unit_factor_arg("second", 1),
7437 &mut None,
7438 &mut None,
7439 None,
7440 )
7441 .unwrap();
7442 specs
7443 .apply_constraint(
7444 "duration",
7445 TypeConstraintCommand::Unit,
7446 &unit_factor_arg("week", 604_800),
7447 &mut None,
7448 &mut None,
7449 None,
7450 )
7451 .unwrap();
7452 specs
7453 .apply_constraint(
7454 "duration",
7455 TypeConstraintCommand::Trait,
7456 &[CommandArg::Label("duration".to_string())],
7457 &mut None,
7458 &mut None,
7459 None,
7460 )
7461 .unwrap();
7462 let mut default = None;
7463 specs
7464 .apply_constraint(
7465 "duration",
7466 TypeConstraintCommand::Suggest,
7467 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7468 Decimal::from(4),
7469 "week".to_string(),
7470 ))],
7471 &mut default,
7472 &mut None,
7473 None,
7474 )
7475 .unwrap();
7476 assert!(matches!(
7477 default,
7478 Some(RawSuggestion::Measure {
7479 unit_name,
7480 ..
7481 }) if unit_name == "week"
7482 ));
7483 }
7484
7485 #[test]
7486 fn default_unknown_unit_on_duration_lists_valid_units() {
7487 let mut specs = TypeSpecification::measure();
7488 specs
7489 .apply_constraint(
7490 "duration",
7491 TypeConstraintCommand::Unit,
7492 &unit_factor_arg("second", 1),
7493 &mut None,
7494 &mut None,
7495 None,
7496 )
7497 .unwrap();
7498 specs
7499 .apply_constraint(
7500 "duration",
7501 TypeConstraintCommand::Trait,
7502 &[CommandArg::Label("duration".to_string())],
7503 &mut None,
7504 &mut None,
7505 None,
7506 )
7507 .unwrap();
7508 let mut default = None;
7509 let err = specs
7510 .apply_constraint(
7511 "duration",
7512 TypeConstraintCommand::Suggest,
7513 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7514 Decimal::ONE,
7515 "fortnight".to_string(),
7516 ))],
7517 &mut default,
7518 &mut None,
7519 None,
7520 )
7521 .unwrap_err();
7522 assert!(err.contains("fortnight"));
7523 assert!(err.contains("not defined on 'duration'"));
7524 assert!(err.contains("Valid units are"));
7525 }
7526
7527 fn money_measure_type() -> LemmaType {
7528 LemmaType::new(
7529 "Money".to_string(),
7530 TypeSpecification::Measure {
7531 minimum: None,
7532 maximum: None,
7533 decimals: None,
7534 units: MeasureUnits::from(vec![
7535 MeasureUnit {
7536 name: "eur".to_string(),
7537 factor: crate::computation::rational::rational_one(),
7538 derived_measure_factors: Vec::new(),
7539 decomposition: BaseMeasureVector::new(),
7540 minimum: None,
7541 maximum: None,
7542 suggestion_magnitude: None,
7543 },
7544 MeasureUnit {
7545 name: "usd".to_string(),
7546 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
7547 91, 2,
7548 ))
7549 .expect("factor"),
7550 derived_measure_factors: Vec::new(),
7551 decomposition: BaseMeasureVector::new(),
7552 minimum: None,
7553 maximum: None,
7554 suggestion_magnitude: None,
7555 },
7556 ]),
7557 traits: Vec::new(),
7558 decomposition: None,
7559 unit: None,
7560 help: String::new(),
7561 },
7562 TypeExtends::Primitive,
7563 )
7564 }
7565
7566 #[test]
7567 fn measure_unit_names_for_named_measure() {
7568 let money = money_measure_type();
7569 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
7570 }
7571
7572 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
7577 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
7578 }
7579
7580 #[test]
7581 fn combine_signatures_multiply_adds_exponents() {
7582 let left = sig(&[("eur", 1)]);
7583 let right = sig(&[("hour", -1)]);
7584 let result = combine_signatures(&left, &right, true);
7585 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7586 }
7587
7588 #[test]
7589 fn combine_signatures_divide_subtracts_exponents() {
7590 let left = sig(&[("eur", 1)]);
7591 let right = sig(&[("hour", 1)]);
7592 let result = combine_signatures(&left, &right, false);
7593 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7594 }
7595
7596 #[test]
7597 fn combine_signatures_cancels_to_empty() {
7598 let left = sig(&[("ce", 1), ("minute", -1)]);
7599 let right = sig(&[("minute", 1)]);
7600 let result = combine_signatures(&left, &right, true);
7601 assert_eq!(result, sig(&[("ce", 1)]));
7603 }
7604
7605 #[test]
7606 fn combine_signatures_output_is_canonical_form() {
7607 let left = sig(&[("eur", 1), ("hour", 1)]);
7608 let right = sig(&[("minute", 1)]);
7609 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
7612 assert_eq!(result, expected);
7613 }
7614
7615 #[test]
7616 fn canonicalize_signature_drops_zero_exponents() {
7617 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
7618 let result = canonicalize_signature(&sig_with_zero);
7619 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7620 }
7621
7622 #[test]
7623 fn canonicalize_signature_sorts_by_name() {
7624 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
7625 let result = canonicalize_signature(&unsorted);
7626 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7627 }
7628
7629 #[test]
7635 fn format_signature_operator_style_numerator_only() {
7636 let signature = sig(&[("eur", 1)]);
7637 let result = format_signature_operator_style(&signature);
7638 assert_eq!(result, "eur");
7639 }
7640
7641 #[test]
7642 fn format_signature_operator_style_with_denominator() {
7643 let signature = sig(&[("eur", 1), ("hour", -1)]);
7644 let result = format_signature_operator_style(&signature);
7645 assert_eq!(result, "eur/hour");
7646 }
7647
7648 #[test]
7649 fn format_signature_operator_style_denominator_only() {
7650 let signature = sig(&[("meter", -1)]);
7651 let result = format_signature_operator_style(&signature);
7652 assert_eq!(result, "1/meter");
7653 }
7654
7655 #[test]
7656 fn format_signature_operator_style_with_exponents() {
7657 let signature = sig(&[("meter", 2), ("second", -2)]);
7658 let result = format_signature_operator_style(&signature);
7659 assert_eq!(result, "meter^2/second^2");
7660 }
7661
7662 #[test]
7667 fn calendar_unit_factor_table_completeness() {
7668 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
7671 let name = unit.to_string();
7672 assert!(
7673 calendar_unit_factor(&name).is_some(),
7674 "calendar_unit_factor('{}') must return Some",
7675 name
7676 );
7677 }
7678 }
7679
7680 #[test]
7681 fn semantic_calendar_unit_display_returns_singular() {
7682 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
7685 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
7686 }
7687
7688 #[test]
7693 fn signature_factor_with_calendar_units() {
7694 let calendar = test_calendar_type_for_signature_factor();
7695 let unit_index = crate::planning::unit_index::UnitIndex::new();
7696 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
7699 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
7700 .expect("must not overflow");
7701 let expected = rational_new(1, 12);
7702 assert_eq!(factor, expected, "month/year factor must be 1/12");
7703 }
7704
7705 fn test_calendar_type_for_signature_factor() -> LemmaType {
7706 use crate::computation::rational::{decimal_to_rational, rational_one};
7707 use crate::literals::{MeasureUnit, MeasureUnits};
7708 use rust_decimal::Decimal;
7709 LemmaType::new(
7710 "calendar".to_string(),
7711 TypeSpecification::Measure {
7712 minimum: None,
7713 maximum: None,
7714 decimals: None,
7715 units: MeasureUnits::from(vec![
7716 MeasureUnit {
7717 name: "month".to_string(),
7718 factor: rational_one(),
7719 minimum: None,
7720 maximum: None,
7721 suggestion_magnitude: None,
7722 decomposition: calendar_decomposition(),
7723 derived_measure_factors: Vec::new(),
7724 },
7725 MeasureUnit {
7726 name: "year".to_string(),
7727 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
7728 minimum: None,
7729 maximum: None,
7730 suggestion_magnitude: None,
7731 decomposition: calendar_decomposition(),
7732 derived_measure_factors: Vec::new(),
7733 },
7734 ]),
7735 traits: vec![MeasureTrait::Calendar],
7736 decomposition: Some(calendar_decomposition()),
7737 unit: None,
7738 help: String::new(),
7739 },
7740 TypeExtends::Primitive,
7741 )
7742 }
7743
7744 #[test]
7745 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
7746 fn signature_factor_panics_on_unresolved_name() {
7747 let unit_index = crate::planning::unit_index::UnitIndex::new();
7748 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
7749 signature_factor(&bad_sig, &unit_index, None)
7750 .expect("BUG: signature_factor must panic before returning a failure");
7751 }
7752
7753 #[test]
7754 fn signature_factor_uses_owner_when_expression_index_empty() {
7755 let money = test_money_type_for_signature_factor();
7756 let expression_units = crate::planning::unit_index::UnitIndex::new();
7757 let sig_usd = sig(&[("usd", 1)]);
7758 let factor =
7759 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
7760 assert_eq!(factor, rational_new(91, 100));
7761 }
7762
7763 fn test_money_type_for_signature_factor() -> LemmaType {
7764 use crate::computation::rational::decimal_to_rational;
7765 use crate::literals::{MeasureUnit, MeasureUnits};
7766 use rust_decimal::Decimal;
7767 LemmaType::new(
7768 "money".to_string(),
7769 TypeSpecification::Measure {
7770 minimum: None,
7771 maximum: None,
7772 decimals: Some(2),
7773 units: MeasureUnits::from(vec![
7774 MeasureUnit {
7775 name: "eur".to_string(),
7776 factor: crate::computation::rational::rational_one(),
7777 minimum: None,
7778 maximum: None,
7779 suggestion_magnitude: None,
7780 decomposition: BaseMeasureVector::new(),
7781 derived_measure_factors: Vec::new(),
7782 },
7783 MeasureUnit {
7784 name: "usd".to_string(),
7785 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
7786 minimum: None,
7787 maximum: None,
7788 suggestion_magnitude: None,
7789 decomposition: BaseMeasureVector::new(),
7790 derived_measure_factors: Vec::new(),
7791 },
7792 ]),
7793 traits: Vec::new(),
7794 decomposition: None,
7795 unit: None,
7796 help: String::new(),
7797 },
7798 TypeExtends::Primitive,
7799 )
7800 }
7801
7802 fn measure_type_with_kilogram() -> TypeSpecification {
7803 use crate::computation::rational::rational_one;
7804 use crate::literals::{MeasureUnit, MeasureUnits};
7805 let mut units = MeasureUnits::new();
7806 units.push(MeasureUnit {
7807 name: "kilogram".to_string(),
7808 factor: rational_one(),
7809 minimum: None,
7810 maximum: None,
7811 suggestion_magnitude: None,
7812 decomposition: BaseMeasureVector::new(),
7813 derived_measure_factors: Vec::new(),
7814 });
7815 TypeSpecification::Measure {
7816 minimum: None,
7817 maximum: None,
7818 decimals: None,
7819 units,
7820 traits: Vec::new(),
7821 decomposition: None,
7822 unit: None,
7823 help: String::new(),
7824 }
7825 }
7826
7827 #[test]
7828 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
7829 let ten = Value::Number(Decimal::from(10));
7830 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram(), None)
7831 .expect_err("bare number must not bind to measure");
7832 assert!(
7833 err.contains("kilogram"),
7834 "error must hint expected unit, got: {err}"
7835 );
7836 }
7837
7838 #[test]
7839 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
7840 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
7841 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram(), None)
7842 .expect("10 kilogram must bind to measure");
7843 assert!(matches!(kind, ValueKind::Measure(_)));
7844 }
7845
7846 #[test]
7847 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
7848 let ten = Value::Number(Decimal::from(10));
7849 let kind = parser_value_to_value_kind(&ten, &TypeSpecification::ratio(), None)
7850 .expect("number -> ratio");
7851 assert!(matches!(kind, ValueKind::Ratio(_)));
7852 }
7853
7854 #[test]
7855 fn value_kind_matches_spec_rejects_number_for_measure() {
7856 let n = ValueKind::Number(rational_new(10, 1));
7857 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
7858 }
7859
7860 #[test]
7861 fn apply_constraint_rejects_inherited_unit_factor_change() {
7862 let mut specs = TypeSpecification::measure();
7863 specs
7864 .apply_constraint(
7865 "money",
7866 TypeConstraintCommand::Unit,
7867 &unit_factor_arg("eur", 1),
7868 &mut None,
7869 &mut None,
7870 None,
7871 )
7872 .expect("seed eur");
7873 let err = specs
7874 .apply_constraint(
7875 "money",
7876 TypeConstraintCommand::Unit,
7877 &[
7878 CommandArg::Label("eur".to_string()),
7879 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
7880 ],
7881 &mut None,
7882 &mut None,
7883 None,
7884 )
7885 .expect_err("must not change inherited unit factor");
7886 assert!(err.contains("eur"), "error must name unit, got: {err}");
7887 assert!(
7888 err.contains("inherited") || err.contains("cannot change"),
7889 "error must reject factor change, got: {err}"
7890 );
7891 }
7892
7893 #[test]
7894 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
7895 let mut specs = TypeSpecification::measure();
7896 specs
7897 .apply_constraint(
7898 "money",
7899 TypeConstraintCommand::Unit,
7900 &unit_factor_arg("eur", 1),
7901 &mut None,
7902 &mut None,
7903 None,
7904 )
7905 .expect("seed eur");
7906 specs
7907 .apply_constraint(
7908 "money",
7909 TypeConstraintCommand::Unit,
7910 &unit_factor_arg("usd", 1),
7911 &mut None,
7912 &mut None,
7913 None,
7914 )
7915 .expect("add usd");
7916 match &specs {
7917 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
7918 other => panic!("expected Measure, got {other:?}"),
7919 }
7920 }
7921
7922 #[test]
7923 fn apply_constraint_idempotent_inherited_unit_redeclare() {
7924 let mut specs = TypeSpecification::measure();
7925 specs
7926 .apply_constraint(
7927 "money",
7928 TypeConstraintCommand::Unit,
7929 &unit_factor_arg("eur", 1),
7930 &mut None,
7931 &mut None,
7932 None,
7933 )
7934 .expect("seed eur");
7935 specs
7936 .apply_constraint(
7937 "money",
7938 TypeConstraintCommand::Unit,
7939 &unit_factor_arg("eur", 1),
7940 &mut None,
7941 &mut None,
7942 None,
7943 )
7944 .expect("idempotent eur");
7945 match &specs {
7946 TypeSpecification::Measure { units, .. } => {
7947 assert_eq!(units.len(), 1);
7948 assert_eq!(
7949 units.iter().find(|u| u.name == "eur").expect("eur").factor,
7950 crate::computation::rational::rational_one()
7951 );
7952 }
7953 other => panic!("expected Measure, got {other:?}"),
7954 }
7955 }
7956
7957 #[test]
7958 fn element_from_range_returns_element_for_every_range_primitive() {
7959 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
7960 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
7961 (PrimitiveKind::NumberRange, |element| {
7962 matches!(element, TypeSpecification::Number { .. })
7963 }),
7964 (PrimitiveKind::MeasureRange, |element| {
7965 matches!(element, TypeSpecification::Measure { .. })
7966 }),
7967 (PrimitiveKind::RatioRange, |element| {
7968 matches!(element, TypeSpecification::Ratio { .. })
7969 }),
7970 (PrimitiveKind::DateRange, |element| {
7971 matches!(element, TypeSpecification::Date { .. })
7972 }),
7973 (PrimitiveKind::TimeRange, |element| {
7974 matches!(element, TypeSpecification::Time { .. })
7975 }),
7976 ];
7977 for (kind, matches_element) in cases {
7978 let range_spec = type_spec_for_primitive(kind);
7979 let element = range_spec
7980 .element_from_range()
7981 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
7982 assert!(
7983 matches_element(&element),
7984 "{kind:?} element must match documented mapping, got {element:?}"
7985 );
7986 }
7987 }
7988
7989 #[test]
7990 fn element_from_range_returns_none_for_non_range_primitives() {
7991 let non_range = [
7992 type_spec_for_primitive(PrimitiveKind::Boolean),
7993 type_spec_for_primitive(PrimitiveKind::Measure),
7994 TypeSpecification::Undetermined,
7995 TypeSpecification::veto(),
7996 ];
7997 for spec in non_range {
7998 assert!(
7999 spec.element_from_range().is_none(),
8000 "{spec:?} must not define element_from_range"
8001 );
8002 }
8003 }
8004
8005 fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
8006 LemmaType::primitive(TypeSpecification::Spec {
8007 spec: SpecIdentity {
8008 repository: None,
8009 spec: name.to_string(),
8010 effective,
8011 },
8012 })
8013 }
8014
8015 #[test]
8016 fn same_value_type_spec_identity() {
8017 use crate::parsing::ast::EffectiveDate;
8018 let origin = EffectiveDate::Origin;
8019 let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
8020 year: 2026,
8021 month: 1,
8022 day: 1,
8023 hour: 0,
8024 minute: 0,
8025 second: 0,
8026 microsecond: 0,
8027 timezone: None,
8028 granularity: crate::literals::DateGranularity::Full,
8029 });
8030 let tax = spec_type("tax_bracket", origin.clone());
8031 let tax_again = spec_type("tax_bracket", origin.clone());
8032 let other = spec_type("menu_item", origin);
8033 let tax_dated = spec_type("tax_bracket", dated);
8034 assert!(tax.same_value_type(&tax_again));
8035 assert!(!tax.same_value_type(&other));
8036 assert!(!tax.same_value_type(&tax_dated));
8037 let mut other_repo = tax.clone();
8038 match &mut other_repo.specifications {
8039 TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
8040 other => panic!("expected Spec, got {other:?}"),
8041 }
8042 assert!(!tax.same_value_type(&other_repo));
8043 }
8044
8045 #[test]
8046 fn spec_identity_displays_repository_space_spec_space_effective() {
8047 use crate::parsing::ast::EffectiveDate;
8048 let workspace = SpecIdentity {
8049 repository: None,
8050 spec: "tax_bracket".to_string(),
8051 effective: EffectiveDate::Origin,
8052 };
8053 assert_eq!(workspace.to_string(), "tax_bracket");
8054 let dated = SpecIdentity {
8055 repository: Some("@jack/finance".to_string()),
8056 spec: "tax_bracket".to_string(),
8057 effective: EffectiveDate::DateTimeValue(
8058 DateTimeValue::from_str("2026-01-01").expect("date"),
8059 ),
8060 };
8061 assert_eq!(
8062 dated.to_string(),
8063 format!(
8064 "@jack/finance tax_bracket {}",
8065 DateTimeValue::from_str("2026-01-01").expect("date")
8066 )
8067 );
8068 }
8069
8070 #[test]
8071 fn type_family_displays_declaring_spec_then_type() {
8072 let workspace = TypeFamily {
8073 repository: None,
8074 spec: "pricing".to_string(),
8075 effective: crate::parsing::ast::EffectiveDate::Origin,
8076 name: "money".to_string(),
8077 };
8078 assert_eq!(workspace.to_string(), "pricing.money");
8079 let stdlib = TypeFamily {
8080 repository: Some("lemma".to_string()),
8081 spec: "units".to_string(),
8082 effective: crate::parsing::ast::EffectiveDate::Origin,
8083 name: "mass".to_string(),
8084 };
8085 assert_eq!(stdlib.to_string(), "lemma units.mass");
8086 let dated = TypeFamily {
8087 repository: None,
8088 spec: "pricing".to_string(),
8089 effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
8090 DateTimeValue::from_str("2024-01-01").expect("date"),
8091 ),
8092 name: "money".to_string(),
8093 };
8094 assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
8095 }
8096
8097 #[test]
8098 fn piecewise_type_keeps_spec_identity() {
8099 use crate::parsing::ast::EffectiveDate;
8100 let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
8101 let picked = crate::planning::typing::piecewise_type([
8102 Arc::new(LemmaType::veto_type()),
8103 Arc::clone(&tax),
8104 ]);
8105 assert!(picked.same_value_type(&tax));
8106 }
8107}