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