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]
926 fn with_families_without_effective(&self) -> Self {
927 match self {
928 Self::Measure {
929 minimum,
930 maximum,
931 units,
932 traits,
933 decomposition,
934 unit,
935 help,
936 } => Self::Measure {
937 minimum: minimum.clone(),
938 maximum: maximum.clone(),
939 units: measure_units_without_effective(units),
940 traits: traits.clone(),
941 decomposition: decomposition
942 .as_ref()
943 .map(base_measure_vector_without_effective),
944 unit: unit.clone(),
945 help: help.clone(),
946 },
947 Self::MeasureRange {
948 lower,
949 upper,
950 minimum,
951 maximum,
952 units,
953 decomposition,
954 help,
955 } => Self::MeasureRange {
956 lower: lower.clone(),
957 upper: upper.clone(),
958 minimum: minimum.clone(),
959 maximum: maximum.clone(),
960 units: measure_units_without_effective(units),
961 decomposition: decomposition
962 .as_ref()
963 .map(base_measure_vector_without_effective),
964 help: help.clone(),
965 },
966 Self::List { element, key, help } => Self::List {
967 element: Arc::new(element.with_families_without_effective()),
968 key: key.clone(),
969 help: help.clone(),
970 },
971 other => other.clone(),
972 }
973 }
974
975 #[must_use]
979 pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
980 match (self, other) {
981 (
982 Self::Measure {
983 minimum: a_min,
984 maximum: a_max,
985 units: a_units,
986 traits: a_traits,
987 decomposition: a_decomp,
988 help: a_help,
989 unit: _,
990 },
991 Self::Measure {
992 minimum: b_min,
993 maximum: b_max,
994 units: b_units,
995 traits: b_traits,
996 decomposition: b_decomp,
997 help: b_help,
998 unit: _,
999 },
1000 ) => {
1001 a_min == b_min
1002 && a_max == b_max
1003 && a_units == b_units
1004 && a_traits == b_traits
1005 && a_decomp == b_decomp
1006 && a_help == b_help
1007 }
1008 (
1009 Self::Ratio {
1010 minimum: a_min,
1011 maximum: a_max,
1012 units: a_units,
1013 help: a_help,
1014 unit: _,
1015 },
1016 Self::Ratio {
1017 minimum: b_min,
1018 maximum: b_max,
1019 units: b_units,
1020 help: b_help,
1021 unit: _,
1022 },
1023 ) => a_min == b_min && a_max == b_max && a_units == b_units && a_help == b_help,
1024 _ => self == other,
1025 }
1026 }
1027}
1028
1029fn require_literal<'a>(
1035 args: &'a [CommandArg],
1036 cmd: &str,
1037) -> Result<&'a crate::literals::Value, String> {
1038 if args.len() > 1 {
1039 return Err(format!("{cmd} takes exactly one value"));
1040 }
1041 let arg = args
1042 .first()
1043 .ok_or_else(|| format!("{} requires an argument", cmd))?;
1044 match arg {
1045 CommandArg::Literal(v) => Ok(v),
1046 CommandArg::Label(name) => Err(format!(
1047 "{} requires a literal value, got identifier '{}'",
1048 cmd, name
1049 )),
1050 CommandArg::UnitExpr(_) => Err(format!(
1051 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
1052 cmd
1053 )),
1054 }
1055}
1056
1057fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
1058 match require_literal(args, "help")? {
1059 crate::literals::Value::Text(s) => {
1060 *help = s.clone();
1061 Ok(())
1062 }
1063 other => Err(format!(
1064 "help requires a text literal (quoted string), got {}",
1065 value_kind_name(other)
1066 )),
1067 }
1068}
1069
1070fn format_measure_units_list(units: &MeasureUnits) -> String {
1071 units
1072 .iter()
1073 .map(|u| u.name.as_str())
1074 .collect::<Vec<_>>()
1075 .join(", ")
1076}
1077
1078#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1080pub(crate) enum SuggestionExpectation {
1081 MeasureUnits,
1082 Text,
1083 Number,
1084 Boolean,
1085 Date,
1086 Time,
1087 Ratio,
1088 NumberRange,
1089 DateRange,
1090 TimeRange,
1091 MeasureRange,
1092 RatioRange,
1093}
1094
1095pub(crate) fn suggestion_value_mismatch_error(
1096 calendar_unit: &str,
1097 type_name: &str,
1098 expectation: SuggestionExpectation,
1099 measure_units: Option<&MeasureUnits>,
1100) -> String {
1101 let unit_label = calendar_unit;
1102 let first = format!("Unit '{unit_label}' is for calendar data.");
1103 match expectation {
1104 SuggestionExpectation::MeasureUnits => {
1105 let list = measure_units
1106 .map(format_measure_units_list)
1107 .unwrap_or_default();
1108 format!("{first} Valid '{type_name}' units are: {list}.")
1109 }
1110 SuggestionExpectation::Text => format!(
1111 "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
1112 ),
1113 SuggestionExpectation::Number => format!(
1114 "{first} Please provide a number, for example `-> suggest 42`."
1115 ),
1116 SuggestionExpectation::Boolean => format!(
1117 "{first} Please provide true or false, for example `-> suggest true`."
1118 ),
1119 SuggestionExpectation::Date => format!(
1120 "{first} Please provide a date, for example `-> suggest 2024-06-15`."
1121 ),
1122 SuggestionExpectation::Time => format!(
1123 "{first} Please provide a time, for example `-> suggest 09:00:00`."
1124 ),
1125 SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
1126 "{first} Please provide a ratio, for example `-> suggest 25%`."
1127 ),
1128 SuggestionExpectation::NumberRange => format!(
1129 "{first} Please provide a number range, for example `-> suggest 10...100`."
1130 ),
1131 SuggestionExpectation::DateRange => format!(
1132 "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1133 ),
1134 SuggestionExpectation::TimeRange => format!(
1135 "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
1136 ),
1137 SuggestionExpectation::MeasureRange => format!(
1138 "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1139 ),
1140 }
1141}
1142
1143fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
1144 let example = if traits.contains(&MeasureTrait::Duration) {
1145 "4 week"
1146 } else if traits.contains(&MeasureTrait::Calendar) {
1147 "3 month"
1148 } else {
1149 "30 kilogram"
1150 };
1151 format!(
1152 "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
1153 )
1154}
1155
1156#[derive(Clone, Copy, Debug)]
1159pub struct ConstraintNames<'a> {
1160 pub type_name: &'a str,
1161 pub data_name: &'a str,
1162}
1163
1164fn decimals_depend_on_the_unit_error(
1165 kind: &str,
1166 names: ConstraintNames<'_>,
1167 args: &[CommandArg],
1168 first_unit: Option<&str>,
1169) -> Result<String, String> {
1170 let ConstraintNames {
1171 type_name,
1172 data_name,
1173 } = names;
1174 let places = decimal_to_u8(require_decimal_literal(args, "decimals")?, "decimals")?;
1175 let step = match places {
1176 0 => "1".to_string(),
1177 _ => format!("0.{}1", "0".repeat(usize::from(places) - 1)),
1178 };
1179 let advice = match first_unit {
1180 Some(unit) => {
1181 format!("Round in a rule instead, for example '{data_name} -> round {step} {unit}'.")
1182 }
1183 None => format!(
1184 "Round in a rule instead, for example '{data_name} -> round' followed by a step of {step} in one of the type's units."
1185 ),
1186 };
1187 Ok(format!("'decimals' is not valid on {kind} type '{type_name}': precision depends on the unit. {advice}"))
1188}
1189
1190fn reject_calendar_for_suggestion(
1191 value: &crate::literals::Value,
1192 type_name: &str,
1193 expectation: SuggestionExpectation,
1194 measure_units: Option<&MeasureUnits>,
1195) -> Result<(), String> {
1196 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
1197 if calendar_unit_factor(unit).is_some() {
1198 return Err(suggestion_value_mismatch_error(
1199 unit,
1200 type_name,
1201 expectation,
1202 measure_units,
1203 ));
1204 }
1205 }
1206 Ok(())
1207}
1208
1209fn value_kind_name(v: &crate::literals::Value) -> &'static str {
1211 use crate::literals::Value;
1212 match v {
1213 Value::Number(_) => "number",
1214 Value::NumberWithUnit(_, _) => "number_with_unit",
1215 Value::Text(_) => "text",
1216 Value::Date(_) => "date",
1217 Value::Time(_) => "time",
1218 Value::Boolean(_) => "boolean",
1219 Value::Range(_, _) => "range",
1220 }
1221}
1222
1223fn require_suggestion_range_endpoints<'a>(
1224 args: &'a [CommandArg],
1225 type_name: &str,
1226 expectation: SuggestionExpectation,
1227 measure_units: Option<&MeasureUnits>,
1228) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
1229 match require_literal(args, "suggest")? {
1230 crate::literals::Value::NumberWithUnit(_, unit)
1231 if calendar_unit_factor(unit).is_some() =>
1232 {
1233 Err(suggestion_value_mismatch_error(
1234 unit,
1235 type_name,
1236 expectation,
1237 measure_units,
1238 ))
1239 }
1240 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
1241 _ => Err(match expectation {
1242 SuggestionExpectation::NumberRange => {
1243 "Please provide a number range, for example `-> suggest 10...100`.".to_string()
1244 }
1245 SuggestionExpectation::DateRange => {
1246 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1247 .to_string()
1248 }
1249 SuggestionExpectation::RatioRange => {
1250 "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
1251 }
1252 SuggestionExpectation::MeasureRange => format!(
1253 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1254 ),
1255 _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
1256 }),
1257 }
1258}
1259
1260fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
1261 crate::computation::rational::decimal_to_rational(decimal)
1262 .map_err(|failure| format!("literal failed rational lift: {failure}"))
1263}
1264
1265pub fn range_element_type_specification(
1267 range_spec: &TypeSpecification,
1268) -> Option<TypeSpecification> {
1269 range_spec.element_from_range()
1270}
1271
1272fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
1273 match (&left.specifications, &right.specifications) {
1274 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1275 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1276 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1277 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1278 left.same_measure_family(right)
1279 }
1280 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1281 _ => false,
1282 }
1283}
1284
1285pub fn range_type_specification_from_endpoints(
1287 left: &LemmaType,
1288 right: &LemmaType,
1289) -> Option<TypeSpecification> {
1290 if !range_endpoints_compatible(left, right) {
1291 return None;
1292 }
1293 left.specifications.range_from_element()
1294}
1295
1296pub enum UnitTokenResolver<'a> {
1299 Declared {
1301 unit_index: &'a UnitIndex,
1302 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1303 },
1304 Undeclared,
1306 Planning {
1308 unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
1309 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1310 },
1311 Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
1313 Typed {
1316 unit_index: &'a UnitIndex,
1317 named_types: &'a IndexMap<String, Arc<LemmaType>>,
1318 lemma_type: &'a LemmaType,
1319 },
1320}
1321
1322impl UnitTokenResolver<'_> {
1323 pub fn resolve(&self, unit: &str) -> Result<String, String> {
1324 match self {
1325 UnitTokenResolver::Declared {
1326 unit_index,
1327 named_types,
1328 } => unit_index
1329 .resolve_with_named_types(unit, named_types)
1330 .map(|(bare, _)| bare),
1331 UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
1332 UnitTokenResolver::Planning {
1333 unit_index,
1334 named_types,
1335 } => unit_index
1336 .resolve_with_named_types(unit, named_types)
1337 .map(|(bare, _)| bare),
1338 UnitTokenResolver::Search(scopes) => scopes
1339 .iter()
1340 .find_map(|(unit_index, named_types)| {
1341 unit_index
1342 .resolve_with_named_types(unit, named_types)
1343 .ok()
1344 .map(|(bare, _)| bare)
1345 })
1346 .ok_or_else(|| unknown_unit_message(unit)),
1347 UnitTokenResolver::Typed {
1348 unit_index,
1349 named_types,
1350 lemma_type,
1351 } => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
1352 }
1353 }
1354}
1355
1356fn unknown_unit_message(unit: &str) -> String {
1357 format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
1358}
1359
1360fn resolve_declared_on_type(
1361 written: &str,
1362 lemma_type: &LemmaType,
1363 unit_index: &UnitIndex,
1364 named_types: &IndexMap<String, Arc<LemmaType>>,
1365) -> Result<String, String> {
1366 match unit_index.resolve_with_named_types(written, named_types) {
1367 Ok((bare, _)) => {
1368 if unit_name_declared_on_type(lemma_type, &bare)
1369 || range_element_declares_unit(lemma_type, &bare)
1370 {
1371 Ok(bare)
1372 } else {
1373 Err(format!(
1374 "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
1375 lemma_type.name()
1376 ))
1377 }
1378 }
1379 Err(index_err) => {
1380 if unit_name_declared_on_type(lemma_type, written)
1381 || range_element_declares_unit(lemma_type, written)
1382 {
1383 Ok(written.to_string())
1384 } else {
1385 Err(index_err)
1386 }
1387 }
1388 }
1389}
1390
1391fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
1392 let Some(element) = lemma_type.specifications.element_from_range() else {
1393 return false;
1394 };
1395 unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
1396}
1397
1398fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
1399 match &lemma_type.specifications {
1400 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
1401 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
1402 _ => false,
1403 }
1404}
1405
1406fn bare_declared_unit_on_element(
1408 element_spec: &TypeSpecification,
1409 token: &str,
1410) -> Result<String, String> {
1411 match element_spec {
1412 TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1413 TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1414 other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1415 }
1416}
1417
1418pub fn resolve_endpoint_unit_token(
1424 token: &str,
1425 element_spec: &TypeSpecification,
1426 resolve_unit: Option<&UnitTokenResolver<'_>>,
1427) -> Result<String, String> {
1428 match bare_declared_unit_on_element(element_spec, token) {
1429 Ok(bare) => Ok(bare),
1430 Err(declared_err) => {
1431 let Some(resolve) = resolve_unit else {
1432 return Err(declared_err);
1433 };
1434 let bare = resolve.resolve(token)?;
1435 bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1436 format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1437 })
1438 }
1439 }
1440}
1441
1442pub(crate) fn lift_range_endpoint(
1451 value: &crate::parsing::ast::Value,
1452 element_type: &LemmaType,
1453 resolve_unit: Option<&UnitTokenResolver<'_>>,
1454) -> Result<TypedLiteral, String> {
1455 use crate::parsing::ast::Value;
1456 let element_spec = &element_type.specifications;
1457 match value {
1458 Value::NumberWithUnit(magnitude, unit_token) => {
1459 let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1460 let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
1461 let lemma_type = match &kind {
1462 ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
1463 Arc::new(element_type.clone().with_unit(bare))
1464 }
1465 _ => Arc::new(element_type.clone()),
1466 };
1467 Ok(TypedLiteral {
1468 value: kind,
1469 lemma_type,
1470 })
1471 }
1472 _ => {
1473 let kind = parser_scalar_to_value_kind(value, element_spec)?;
1474 Ok(TypedLiteral {
1475 value: kind,
1476 lemma_type: Arc::new(element_type.clone()),
1477 })
1478 }
1479 }
1480}
1481
1482fn literal_value_from_parser_value(
1483 value: &crate::parsing::ast::Value,
1484) -> Result<TypedLiteral, String> {
1485 use crate::parsing::ast::Value;
1486
1487 enum Step<'a> {
1488 Visit(&'a Value),
1489 CombineRange,
1490 }
1491
1492 let mut steps = vec![Step::Visit(value)];
1493 let mut literals: Vec<TypedLiteral> = Vec::new();
1494 while let Some(step) = steps.pop() {
1495 match step {
1496 Step::Visit(Value::Range(left, right)) => {
1497 steps.push(Step::CombineRange);
1498 steps.push(Step::Visit(right));
1499 steps.push(Step::Visit(left));
1500 }
1501 Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
1502 Step::CombineRange => {
1503 let right = literals
1504 .pop()
1505 .expect("BUG: range right endpoint missing from literal stack");
1506 let left = literals
1507 .pop()
1508 .expect("BUG: range left endpoint missing from literal stack");
1509 let compatible = match (
1510 &left.lemma_type.specifications,
1511 &right.lemma_type.specifications,
1512 ) {
1513 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1514 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1515 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1516 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1517 left.lemma_type.same_measure_family(&right.lemma_type)
1518 || left
1519 .lemma_type
1520 .compatible_with_anonymous_measure(&right.lemma_type)
1521 || right
1522 .lemma_type
1523 .compatible_with_anonymous_measure(&left.lemma_type)
1524 }
1525 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1526 _ => false,
1527 };
1528 if !compatible {
1529 return Err(format!(
1530 "range endpoints must have the same supported base type, got {} and {}",
1531 left.lemma_type.name(),
1532 right.lemma_type.name()
1533 ));
1534 }
1535 literals.push(TypedLiteral::range(left, right));
1536 }
1537 }
1538 }
1539 Ok(literals.pop().expect("BUG: literal walk left no result"))
1540}
1541
1542fn scalar_literal_from_parser_value(
1543 value: &crate::parsing::ast::Value,
1544) -> Result<TypedLiteral, String> {
1545 use crate::parsing::ast::Value;
1546
1547 match value {
1548 Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1549 Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1550 Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1551 Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1552 Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1553 Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
1554 lift_parser_decimal(*n)?,
1555 )),
1556 Value::Range(_, _) => {
1557 unreachable!("BUG: range values are walked by literal_value_from_parser_value")
1558 }
1559 }
1560}
1561
1562fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1564 if !d.is_integer() {
1565 return Err(format!(
1566 "{} requires a whole number, got fractional value",
1567 ctx
1568 ));
1569 }
1570 d.numer_to_u8()
1571 .ok_or_else(|| format!("{} value out of range for u8", ctx))
1572}
1573
1574fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1576 if !d.is_integer() {
1577 return Err(format!(
1578 "{} requires a whole number, got fractional value",
1579 ctx
1580 ));
1581 }
1582 d.numer_to_usize()
1583 .ok_or_else(|| format!("{} value out of range for usize", ctx))
1584}
1585
1586fn ratio_bound_to_canonical_rational(
1592 args: &[CommandArg],
1593 cmd: &str,
1594 units: &RatioUnits,
1595) -> Result<RationalInteger, String> {
1596 use crate::computation::rational::{checked_div, decimal_to_rational};
1597 let lit = require_literal(args, cmd)?;
1598 match lit {
1599 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1600 let unit = units.get(unit_name.as_str())?;
1601 let magnitude_rational = decimal_to_rational(*magnitude)
1602 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1603 checked_div(&magnitude_rational, &unit.value)
1604 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1605 }
1606 other => Err(format!(
1607 "{cmd} requires a ratio literal with a unit, got {}",
1608 value_kind_name(other)
1609 )),
1610 }
1611}
1612
1613fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1614 use crate::computation::rational::decimal_to_rational;
1615 match require_literal(args, cmd)? {
1616 crate::literals::Value::Number(d) => decimal_to_rational(*d)
1617 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1618 other => Err(format!(
1619 "{} requires a number literal, got {}",
1620 cmd,
1621 value_kind_name(other)
1622 )),
1623 }
1624}
1625
1626enum UnitConstraintField {
1627 Minimum,
1628 Maximum,
1629 SuggestionMagnitude,
1630}
1631
1632fn apply_measure_unit_factor(
1633 units: &mut MeasureUnits,
1634 unit_name: String,
1635 value: rust_decimal::Decimal,
1636 derived_measure_factors: Vec<(String, i32)>,
1637) -> Result<(), String> {
1638 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1639 let new_factor = crate::computation::rational::decimal_to_rational(value)
1640 .map_err(|failure| failure.to_string())?;
1641 if existing.literal_factor() != Some(&new_factor)
1642 || existing.derived_measure_factors != derived_measure_factors
1643 {
1644 return Err(format!(
1645 "Unit '{unit_name}' is already defined in this type's inherited units; \
1646 cannot change factor or decomposition. Add a new unit name instead."
1647 ));
1648 }
1649 return Ok(());
1650 }
1651 units.0.push(MeasureUnit::from_decimal_factor(
1652 unit_name,
1653 value,
1654 derived_measure_factors,
1655 )?);
1656 Ok(())
1657}
1658
1659pub(crate) fn measure_declared_bound_to_canonical(
1660 magnitude: &RationalInteger,
1661 unit_name: &str,
1662 units: &MeasureUnits,
1663 type_name: &str,
1664 command: &str,
1665) -> Result<RationalInteger, String> {
1666 use crate::computation::rational::checked_mul;
1667 let unit = units.get(unit_name).map_err(|_| {
1668 format!(
1669 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1670 format_measure_units_list(units)
1671 )
1672 })?;
1673 let factor = unit.literal_factor().ok_or_else(|| {
1674 format!(
1675 "{command} cannot use unit '{unit_name}' on '{type_name}': its conversion factor is a reference and is not known at planning."
1676 )
1677 })?;
1678 checked_mul(magnitude, factor)
1679 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1680}
1681
1682fn parse_measure_declared_bound(
1683 args: &[CommandArg],
1684 cmd: &str,
1685 units: &MeasureUnits,
1686 type_name: &str,
1687) -> Result<(RationalInteger, String), String> {
1688 use crate::computation::rational::decimal_to_rational;
1689 let lit = require_literal(args, cmd)?;
1690 let (magnitude, unit_name) = match lit {
1691 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1692 other => {
1693 return Err(format!(
1694 "{cmd} requires a measure literal with a unit, got {}",
1695 value_kind_name(other)
1696 ));
1697 }
1698 };
1699 units.get(unit_name.as_str()).map_err(|_| {
1700 format!(
1701 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1702 format_measure_units_list(units)
1703 )
1704 })?;
1705 let magnitude_rational = decimal_to_rational(magnitude)
1706 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1707 Ok((magnitude_rational, unit_name))
1708}
1709
1710fn sync_measure_units_from_canonical(
1711 units: &mut MeasureUnits,
1712 canonical: &RationalInteger,
1713 field: UnitConstraintField,
1714) -> Result<(), String> {
1715 use crate::computation::rational::checked_div;
1716 for unit in &mut units.0 {
1717 let Some(factor) = unit.literal_factor() else {
1718 continue;
1719 };
1720 let magnitude = checked_div(canonical, factor).map_err(|failure| {
1721 format!(
1722 "cannot derive per-unit constraint for unit '{}': {failure}",
1723 unit.name
1724 )
1725 })?;
1726 match field {
1727 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1728 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1729 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1730 }
1731 }
1732 Ok(())
1733}
1734
1735fn sync_ratio_units_from_canonical(
1736 units: &mut RatioUnits,
1737 canonical: &RationalInteger,
1738 field: UnitConstraintField,
1739) -> Result<(), String> {
1740 use crate::computation::rational::checked_mul;
1741 for unit in &mut units.0 {
1742 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1743 format!(
1744 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1745 unit.name
1746 )
1747 })?;
1748 match field {
1749 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1750 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1751 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1752 }
1753 }
1754 Ok(())
1755}
1756
1757fn sync_measure_suggestion_units(
1758 units: &mut MeasureUnits,
1759 default: &ValueKind,
1760 type_name: &str,
1761) -> Result<(), String> {
1762 let ValueKind::Measure(magnitude) = default else {
1763 return Ok(());
1764 };
1765 let unit_name = units
1766 .iter()
1767 .find(|unit| unit.is_canonical_factor())
1768 .or_else(|| units.iter().next())
1769 .map(|unit| unit.name.as_str())
1770 .expect(
1771 "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1772 );
1773 units.get(unit_name).map_err(|_| {
1774 format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1775 })?;
1776 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1777}
1778
1779pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1780 specification: &mut TypeSpecification,
1781 declared_suggestion: Option<&ValueKind>,
1782 type_name: &str,
1783) -> Result<(), String> {
1784 let TypeSpecification::Measure {
1785 minimum,
1786 maximum,
1787 units,
1788 ..
1789 } = specification
1790 else {
1791 return Ok(());
1792 };
1793
1794 if let Some(bound) = minimum.as_ref() {
1795 let canonical =
1796 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "minimum")?;
1797 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1798 }
1799 if let Some(bound) = maximum.as_ref() {
1800 let canonical =
1801 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "maximum")?;
1802 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1803 }
1804 if let Some(default) = declared_suggestion {
1805 sync_measure_suggestion_units(units, default, type_name)?;
1806 }
1807
1808 if minimum.is_some() {
1809 for unit in units.iter() {
1810 assert!(
1811 unit.minimum.is_some(),
1812 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1813 unit.name
1814 );
1815 }
1816 }
1817 if maximum.is_some() {
1818 for unit in units.iter() {
1819 assert!(
1820 unit.maximum.is_some(),
1821 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1822 unit.name
1823 );
1824 }
1825 }
1826 if declared_suggestion.is_some() {
1827 for unit in units.iter() {
1828 assert!(
1829 unit.suggestion_magnitude.is_some(),
1830 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1831 unit.name
1832 );
1833 }
1834 }
1835
1836 Ok(())
1837}
1838
1839fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1840 let ValueKind::Ratio(canonical) = default else {
1841 return Ok(());
1842 };
1843 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1844}
1845
1846pub(crate) fn apply_option_constraint(
1852 lemma_type: &mut LemmaType,
1853 command: TypeConstraintCommand,
1854 args: &[CommandArg],
1855 resolve_unit: Option<&UnitTokenResolver<'_>>,
1856 spec: &crate::parsing::ast::LemmaSpec,
1857) -> Result<(), String> {
1858 let mut hops = 0;
1859 {
1860 let mut probe = &*lemma_type;
1861 while let TypeSpecification::List { element, .. } = &probe.specifications {
1862 hops += 1;
1863 probe = element.as_ref();
1864 }
1865 }
1866 let mut current = lemma_type;
1867 for _ in 0..hops {
1868 let TypeSpecification::List { element, .. } = &mut current.specifications else {
1869 unreachable!("BUG: option forwarding counted a list");
1870 };
1871 current = Arc::make_mut(element);
1872 }
1873 apply_options_command(current, command, args, resolve_unit, spec)
1874}
1875
1876pub(crate) fn apply_spec_member_command(
1878 lemma_type: &LemmaType,
1879 command: TypeConstraintCommand,
1880 args: &[CommandArg],
1881 spec: &crate::parsing::ast::LemmaSpec,
1882 target: &mut Option<RawSuggestion>,
1883) -> Result<(), String> {
1884 let label = match args {
1885 [CommandArg::Label(name)] => name,
1886 _ => return Err(format!("{command} on a spec takes one instance name")),
1887 };
1888 let option = spec_alias_option(
1889 lemma_type,
1890 uses_alias(spec, label)
1891 .ok_or_else(|| format!("'{label}' is not an instance of this spec"))?,
1892 label,
1893 )?;
1894 *target = Some(RawSuggestion::Value(option.value.value));
1895 Ok(())
1896}
1897
1898pub(crate) fn apply_list_with_rows(
1900 lemma_type: &mut LemmaType,
1901 names: ConstraintNames<'_>,
1902 args: &[CommandArg],
1903 declared_value: &mut Option<RawSuggestion>,
1904 resolve_unit: Option<&UnitTokenResolver<'_>>,
1905 spec: &crate::parsing::ast::LemmaSpec,
1906) -> Result<(), String> {
1907 let mut rows = match declared_value.take() {
1908 Some(RawSuggestion::List(existing)) => existing,
1909 None => Vec::new(),
1910 Some(other) => {
1911 unreachable!("BUG: list declared value must be RawSuggestion::List, got {other:?}")
1912 }
1913 };
1914 let TypeSpecification::List { element, .. } = &mut lemma_type.specifications else {
1915 unreachable!("BUG: apply_list_with_rows on a non-list type");
1916 };
1917 for arg in args {
1918 rows.push(list_with_row(
1919 Arc::make_mut(element),
1920 names,
1921 arg,
1922 resolve_unit,
1923 spec,
1924 )?);
1925 }
1926 *declared_value = Some(RawSuggestion::List(rows));
1927 Ok(())
1928}
1929
1930fn list_with_row(
1931 element: &mut LemmaType,
1932 names: ConstraintNames<'_>,
1933 arg: &CommandArg,
1934 resolve_unit: Option<&UnitTokenResolver<'_>>,
1935 spec: &crate::parsing::ast::LemmaSpec,
1936) -> Result<RawSuggestion, String> {
1937 let mut hops = 0;
1938 {
1939 let mut probe = &*element;
1940 while let TypeSpecification::List { element, .. } = &probe.specifications {
1941 hops += 1;
1942 probe = element.as_ref();
1943 }
1944 }
1945 let mut current = element;
1946 for _ in 0..hops {
1947 let TypeSpecification::List { element, .. } = &mut current.specifications else {
1948 unreachable!("BUG: list -> with counted a list");
1949 };
1950 current = Arc::make_mut(element);
1951 }
1952 let mut row_value = None;
1953 if matches!(current.specifications, TypeSpecification::Spec { .. }) {
1954 apply_spec_member_command(
1955 current,
1956 TypeConstraintCommand::With,
1957 std::slice::from_ref(arg),
1958 spec,
1959 &mut row_value,
1960 )?;
1961 } else {
1962 let mut unused_suggestion = None;
1963 current.apply_constraint(
1964 names,
1965 TypeConstraintCommand::With,
1966 std::slice::from_ref(arg),
1967 &mut unused_suggestion,
1968 &mut row_value,
1969 resolve_unit,
1970 )?;
1971 }
1972 let mut value = row_value.expect("BUG: -> with on a list row must produce a value");
1973 for _ in 0..hops {
1974 value = RawSuggestion::List(vec![value]);
1975 }
1976 Ok(value)
1977}
1978
1979fn apply_options_command(
1980 lemma_type: &mut LemmaType,
1981 command: TypeConstraintCommand,
1982 args: &[CommandArg],
1983 resolve_unit: Option<&UnitTokenResolver<'_>>,
1984 spec: &crate::parsing::ast::LemmaSpec,
1985) -> Result<(), String> {
1986 if command == TypeConstraintCommand::Option && args.len() != 1 {
1987 return Err("option takes exactly one argument".to_string());
1988 }
1989 if args.is_empty() {
1990 return Err("options takes at least one value".to_string());
1991 }
1992 let mut literals: Vec<OptionValue> = Vec::new();
1993 let mut names: Vec<String> = Vec::new();
1994 for arg in args {
1995 match arg {
1996 CommandArg::Literal(value) => {
1997 literals.push(OptionValue {
1998 value: option_bound_from_literal(value, lemma_type, resolve_unit)?,
1999 });
2000 }
2001 CommandArg::Label(name) => names.push(name.clone()),
2002 CommandArg::UnitExpr(_) => {
2003 return Err(format!(
2004 "{command} requires a value of this type, got a unit expression"
2005 ));
2006 }
2007 }
2008 }
2009 if !literals.is_empty() && !names.is_empty() {
2010 return Err(format!(
2011 "{command} mixes values and a rule. Write the values, or name one rule."
2012 ));
2013 }
2014 if matches!(lemma_type.specifications, TypeSpecification::Spec { .. }) {
2015 return apply_spec_list_options(lemma_type, command, args, spec);
2016 }
2017 if !names.is_empty() {
2018 return apply_option_names(lemma_type, &names, spec);
2019 }
2020 append_option_literals(lemma_type, literals)?;
2021 Ok(())
2022}
2023
2024fn option_bound_from_literal(
2025 value: &crate::literals::Value,
2026 lemma_type: &LemmaType,
2027 resolve_unit: Option<&UnitTokenResolver<'_>>,
2028) -> Result<BoundValueKind, String> {
2029 let kind = parser_value_to_value_kind(value, lemma_type, resolve_unit)?;
2030 if let ValueKind::UnscaledMeasure { unit, .. } = &kind {
2031 return Err(format!(
2032 "option cannot use unit '{unit}': its conversion factor is a reference and is not known at planning."
2033 ));
2034 }
2035 let token = match value {
2036 crate::literals::Value::NumberWithUnit(_, unit) => Some(unit.as_str()),
2037 crate::literals::Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
2038 (
2039 crate::literals::Value::NumberWithUnit(_, left_unit),
2040 crate::literals::Value::NumberWithUnit(_, right_unit),
2041 ) if left_unit == right_unit => Some(left_unit.as_str()),
2042 _ => None,
2043 },
2044 _ => None,
2045 };
2046 let unit = match token {
2047 Some(token) => Some(Arc::<str>::from(resolve_endpoint_unit_token(
2048 token,
2049 &lemma_type.specifications,
2050 resolve_unit,
2051 )?)),
2052 None => None,
2053 };
2054 Ok(BoundValueKind { value: kind, unit })
2055}
2056
2057fn apply_spec_list_options(
2058 _lemma_type: &mut LemmaType,
2059 _command: TypeConstraintCommand,
2060 _args: &[CommandArg],
2061 _spec: &crate::parsing::ast::LemmaSpec,
2062) -> Result<(), String> {
2063 Err(SPEC_LIST_HAS_NO_OPTIONS.to_string())
2064}
2065
2066enum OptionName {
2067 Text(OptionValue),
2068 Instance(OptionValue),
2069 Rule,
2070}
2071
2072fn apply_option_names(
2073 lemma_type: &mut LemmaType,
2074 names: &[String],
2075 spec: &crate::parsing::ast::LemmaSpec,
2076) -> Result<(), String> {
2077 let mut meanings = Vec::with_capacity(names.len());
2078 for name in names {
2079 meanings.push(option_name_meaning(lemma_type, spec, name)?);
2080 }
2081 let rule_count = meanings
2082 .iter()
2083 .filter(|meaning| matches!(meaning, OptionName::Rule))
2084 .count();
2085 if rule_count > 0 {
2086 if names.len() != 1 || rule_count != 1 {
2087 return Err(
2088 "options names one rule, or lists values. A rule is not mixed with values."
2089 .to_string(),
2090 );
2091 }
2092 if let Some(existing) = &lemma_type.options.rule {
2093 return Err(format!(
2094 "`-> options` already names rule '{existing}', not '{}'",
2095 names[0]
2096 ));
2097 }
2098 lemma_type.options.rule = Some(names[0].clone());
2099 return Ok(());
2100 }
2101 let mut literals = Vec::with_capacity(meanings.len());
2102 for meaning in meanings {
2103 match meaning {
2104 OptionName::Text(value) | OptionName::Instance(value) => literals.push(value),
2105 OptionName::Rule => unreachable!("BUG: rule meanings are returned above"),
2106 }
2107 }
2108 reject_duplicate_options(lemma_type, &literals)?;
2109 append_option_literals(lemma_type, literals)?;
2110 Ok(())
2111}
2112
2113fn option_name_meaning(
2115 lemma_type: &LemmaType,
2116 spec: &crate::parsing::ast::LemmaSpec,
2117 name: &str,
2118) -> Result<OptionName, String> {
2119 let mut found: Vec<OptionName> = Vec::new();
2120 if matches!(lemma_type.specifications, TypeSpecification::Text { .. }) {
2121 found.push(OptionName::Text(OptionValue {
2122 value: BoundValueKind::unbound(ValueKind::Text(Arc::from(name))),
2123 }));
2124 }
2125 if let Some(data) = uses_alias(spec, name) {
2126 if let Ok(instance) = spec_alias_option(lemma_type, data, name) {
2127 found.push(OptionName::Instance(instance));
2128 }
2129 }
2130 if spec.rules.iter().any(|rule| rule.name == name) {
2131 found.push(OptionName::Rule);
2132 }
2133 match found.len() {
2134 1 => Ok(found.pop().expect("BUG: one option meaning")),
2135 0 => Err(format!("'{name}' is not a value of this type or a rule")),
2136 _ => Err(format!(
2137 "'{name}' is more than one of a text option, an instance, and a rule"
2138 )),
2139 }
2140}
2141
2142fn uses_alias<'a>(
2143 spec: &'a crate::parsing::ast::LemmaSpec,
2144 name: &str,
2145) -> Option<&'a crate::parsing::ast::LemmaData> {
2146 spec.data.iter().find(|data| {
2147 data.reference.name == name
2148 && matches!(data.value, crate::parsing::ast::DataValue::Import { .. })
2149 })
2150}
2151
2152fn spec_alias_option(
2153 lemma_type: &LemmaType,
2154 data: &crate::parsing::ast::LemmaData,
2155 name: &str,
2156) -> Result<OptionValue, String> {
2157 let TypeSpecification::Spec { spec } = &lemma_type.specifications else {
2158 return Err(format!("'{name}' is not a value of this type or a rule"));
2159 };
2160 let crate::parsing::ast::DataValue::Import { spec_ref, .. } = &data.value else {
2161 unreachable!("BUG: uses_alias returns an import");
2162 };
2163 if spec_ref.name != spec.spec {
2164 return Err(format!(
2165 "'{name}' is an instance of '{}', not '{}'",
2166 spec_ref.name, spec.spec
2167 ));
2168 }
2169 unreachable!(
2170 "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"
2171 )
2172}
2173
2174fn append_option_literals(
2175 lemma_type: &mut LemmaType,
2176 incoming: Vec<OptionValue>,
2177) -> Result<(), String> {
2178 for option in incoming {
2179 if lemma_type
2180 .options
2181 .values
2182 .iter()
2183 .any(|existing| existing.value == option.value)
2184 {
2185 return Err(format!(
2186 "duplicate option '{}'",
2187 lemma_type.display_bound(&option.value)
2188 ));
2189 }
2190 lemma_type.options.values.push(option);
2191 }
2192 Ok(())
2193}
2194
2195fn static_option_match(options: &TypeOptions, value: &ValueKind) -> bool {
2196 options
2197 .values
2198 .iter()
2199 .any(|option| option_values_match(&option.value.value, value))
2200}
2201
2202pub(crate) fn disallowed_message(
2203 lemma_type: &LemmaType,
2204 options: &TypeOptions,
2205 value: &ValueKind,
2206 rule_rows: Option<&ListValue>,
2207) -> String {
2208 let mut shown = options
2209 .values
2210 .iter()
2211 .map(|option| lemma_type.display_bound(&option.value))
2212 .collect::<Vec<_>>()
2213 .join(", ");
2214 if let Some(rows) = rule_rows {
2215 let rule_shown = rows
2216 .iter()
2217 .map(|row| row.value.to_string())
2218 .collect::<Vec<_>>()
2219 .join(", ");
2220 if !shown.is_empty() && !rule_shown.is_empty() {
2221 shown.push_str(", ");
2222 }
2223 shown.push_str(&rule_shown);
2224 }
2225 format!("'{value}' is not one of the allowed options: {shown}.")
2226}
2227
2228fn option_values_match(option: &ValueKind, value: &ValueKind) -> bool {
2231 let mut values: Vec<(ValueKind, ValueKind)> = vec![(option.clone(), value.clone())];
2232 while let Some((left, right)) = values.pop() {
2233 match (left, right) {
2234 (ValueKind::Spec(_), _) | (_, ValueKind::Spec(_)) => {
2235 unreachable!(
2236 "BUG: option matching saw a spec value; {}",
2237 SPEC_LIST_HAS_NO_OPTIONS
2238 );
2239 }
2240 (ValueKind::List(left), ValueKind::List(right)) => {
2241 if left.len() != right.len() {
2242 return false;
2243 }
2244 for (a, b) in left.iter().zip(right.iter()) {
2245 if a.lemma_type != b.lemma_type {
2246 return false;
2247 }
2248 values.push((a.into_owned().value, b.into_owned().value));
2249 }
2250 }
2251 (left, right) => {
2252 if left != right {
2253 return false;
2254 }
2255 }
2256 }
2257 }
2258 true
2259}
2260
2261pub(crate) fn option_detail_line(lemma_type: &LemmaType) -> Option<String> {
2262 let options = &lemma_type.options;
2263 let mut parts = Vec::new();
2264 if !options.values.is_empty() {
2265 parts.push(
2266 options
2267 .values
2268 .iter()
2269 .map(|option| lemma_type.display_bound(&option.value))
2270 .collect::<Vec<_>>()
2271 .join(", "),
2272 );
2273 }
2274 if let Some(name) = &options.rule {
2275 parts.push(name.clone());
2276 }
2277 if parts.is_empty() {
2278 None
2279 } else {
2280 Some(format!("options: {}", parts.join(", ")))
2281 }
2282}
2283
2284fn reject_duplicate_options(lemma_type: &LemmaType, values: &[OptionValue]) -> Result<(), String> {
2285 for (index, option) in values.iter().enumerate() {
2286 if values[..index]
2287 .iter()
2288 .any(|earlier| earlier.value == option.value)
2289 {
2290 return Err(format!(
2291 "duplicate option '{}'",
2292 lemma_type.display_bound(&option.value)
2293 ));
2294 }
2295 }
2296 Ok(())
2297}
2298
2299fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
2300 match arg {
2301 CommandArg::Label(name) => Ok(name.clone()),
2302 CommandArg::Literal(other) => Err(format!(
2303 "{} requires an identifier, got {}",
2304 cmd,
2305 value_kind_name(other)
2306 )),
2307 CommandArg::UnitExpr(_) => Err(format!(
2308 "{} requires an identifier, got a unit expression",
2309 cmd
2310 )),
2311 }
2312}
2313
2314fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
2315 match measure_trait {
2316 MeasureTrait::Duration => "duration",
2317 MeasureTrait::Calendar => "calendar",
2318 }
2319}
2320
2321fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
2322 if args.len() != 1 {
2323 return Err("trait requires exactly one identifier argument".to_string());
2324 }
2325 match label_name(&args[0], "trait")?
2326 .trim()
2327 .to_lowercase()
2328 .as_str()
2329 {
2330 "duration" => Ok(MeasureTrait::Duration),
2331 "calendar" => Ok(MeasureTrait::Calendar),
2332 other => Err(format!("Unknown measure trait '{}'", other)),
2333 }
2334}
2335
2336fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2337 let month_unit = units
2338 .iter()
2339 .find(|unit| unit.name == "month")
2340 .ok_or_else(|| {
2341 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
2342 .to_string()
2343 })?;
2344 if !month_unit.is_canonical_factor() {
2345 return Err("trait calendar requires unit month 1".to_string());
2346 }
2347 Ok(())
2348}
2349
2350fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2351 let second_unit = units
2352 .iter()
2353 .find(|unit| unit.name == "second")
2354 .ok_or_else(|| {
2355 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
2356 .to_string()
2357 })?;
2358 if !second_unit.is_canonical_factor() {
2359 return Err("trait duration requires unit second 1".to_string());
2360 }
2361 Ok(())
2362}
2363
2364fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
2366 match require_literal(args, cmd)? {
2367 crate::literals::Value::Date(dt) => Ok(dt.clone()),
2368 other => Err(format!(
2369 "{} requires a date literal (e.g. 2024-01-01), got {}",
2370 cmd,
2371 value_kind_name(other)
2372 )),
2373 }
2374}
2375
2376fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
2378 match require_literal(args, cmd)? {
2379 crate::literals::Value::Time(t) => Ok(t.clone()),
2380 other => Err(format!(
2381 "{} requires a time literal (e.g. 12:30:00), got {}",
2382 cmd,
2383 value_kind_name(other)
2384 )),
2385 }
2386}
2387
2388#[must_use]
2390pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
2391 use PrimitiveKind::*;
2392 match kind {
2393 Boolean => "Whether this holds (true or false).",
2394 Number => "A dimensionless number.",
2395 NumberRange => "The lower and upper bound of the number range.",
2396 Text => "A text value.",
2397 Measure => "A numeric amount in one of this type's units.",
2398 MeasureRange => "The lower and upper bound of the measure range.",
2399 Ratio => "A ratio in one of this type's units (e.g. percent).",
2400 RatioRange => "The lower and upper bound of the ratio range.",
2401 Date => "A date, or a date and time with optional timezone.",
2402 DateRange => "The start date and end date of the date range.",
2403 Time => "A time of day, with optional timezone.",
2404 TimeRange => "The start time and end time of the time range.",
2405 }
2406}
2407
2408impl TypeSpecification {
2409 pub fn boolean() -> Self {
2410 TypeSpecification::Boolean {
2411 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
2412 }
2413 }
2414 pub fn measure() -> Self {
2415 TypeSpecification::Measure {
2416 minimum: None,
2417 maximum: None,
2418 units: MeasureUnits::new(),
2419 traits: Vec::new(),
2420 decomposition: None,
2421 unit: None,
2422 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
2423 }
2424 }
2425 pub fn number() -> Self {
2426 TypeSpecification::Number {
2427 minimum: None,
2428 maximum: None,
2429 decimals: None,
2430 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
2431 }
2432 }
2433 pub fn number_range() -> Self {
2434 TypeSpecification::NumberRange {
2435 lower: None,
2436 upper: None,
2437 minimum: None,
2438 maximum: None,
2439 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2440 }
2441 }
2442 pub fn ratio() -> Self {
2443 TypeSpecification::Ratio {
2444 minimum: None,
2445 maximum: None,
2446 units: RatioUnits(vec![
2447 RatioUnit {
2448 name: "percent".to_string(),
2449 value: crate::computation::rational::rational_new(100, 1),
2450 minimum: None,
2451 maximum: None,
2452 suggestion_magnitude: None,
2453 },
2454 RatioUnit {
2455 name: "permille".to_string(),
2456 value: crate::computation::rational::rational_new(1000, 1),
2457 minimum: None,
2458 maximum: None,
2459 suggestion_magnitude: None,
2460 },
2461 ]),
2462 unit: None,
2463 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
2464 }
2465 }
2466 pub fn ratio_range() -> Self {
2467 TypeSpecification::RatioRange {
2468 lower: None,
2469 upper: None,
2470 minimum: None,
2471 maximum: None,
2472 units: match TypeSpecification::ratio() {
2473 TypeSpecification::Ratio { units, .. } => units,
2474 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
2475 },
2476 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2477 }
2478 }
2479 pub fn text() -> Self {
2480 TypeSpecification::Text {
2481 length: None,
2482 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
2483 }
2484 }
2485 pub fn date() -> Self {
2486 TypeSpecification::Date {
2487 minimum: None,
2488 maximum: None,
2489 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
2490 }
2491 }
2492 pub fn date_range() -> Self {
2493 TypeSpecification::DateRange {
2494 lower: None,
2495 upper: None,
2496 minimum: None,
2497 maximum: None,
2498 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2499 }
2500 }
2501 pub fn time() -> Self {
2502 TypeSpecification::Time {
2503 minimum: None,
2504 maximum: None,
2505 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
2506 }
2507 }
2508 pub fn time_range() -> Self {
2509 TypeSpecification::TimeRange {
2510 lower: None,
2511 upper: None,
2512 minimum: None,
2513 maximum: None,
2514 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2515 }
2516 }
2517 pub fn measure_range() -> Self {
2518 TypeSpecification::MeasureRange {
2519 lower: None,
2520 upper: None,
2521 minimum: None,
2522 maximum: None,
2523 units: MeasureUnits::new(),
2524 decomposition: None,
2525 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2526 }
2527 }
2528
2529 #[must_use]
2531 pub fn element_from_range(&self) -> Option<Self> {
2532 match self {
2533 TypeSpecification::NumberRange { lower, upper, .. } => {
2534 Some(TypeSpecification::Number {
2535 minimum: lower.clone(),
2536 maximum: upper.clone(),
2537 decimals: None,
2538 help: String::new(),
2539 })
2540 }
2541 TypeSpecification::MeasureRange {
2542 lower,
2543 upper,
2544 units,
2545 decomposition,
2546 ..
2547 } => Some(TypeSpecification::Measure {
2548 minimum: lower.clone(),
2549 maximum: upper.clone(),
2550 units: units.clone(),
2551 traits: Vec::new(),
2552 decomposition: decomposition.clone(),
2553 unit: None,
2554 help: String::new(),
2555 }),
2556 TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
2557 minimum: lower.clone(),
2558 maximum: upper.clone(),
2559 help: String::new(),
2560 }),
2561 TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
2562 minimum: lower.clone(),
2563 maximum: upper.clone(),
2564 help: String::new(),
2565 }),
2566 TypeSpecification::RatioRange {
2567 lower,
2568 upper,
2569 units,
2570 ..
2571 } => Some(TypeSpecification::Ratio {
2572 minimum: lower.clone(),
2573 maximum: upper.clone(),
2574 units: units.clone(),
2575 unit: None,
2576 help: String::new(),
2577 }),
2578 _ => None,
2579 }
2580 }
2581
2582 #[must_use]
2584 pub fn range_from_element(&self) -> Option<Self> {
2585 match self {
2586 TypeSpecification::Number {
2587 minimum, maximum, ..
2588 } => Some(TypeSpecification::NumberRange {
2589 lower: minimum.clone(),
2590 upper: maximum.clone(),
2591 minimum: None,
2592 maximum: None,
2593 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2594 }),
2595 TypeSpecification::Measure {
2596 minimum,
2597 maximum,
2598 units,
2599 decomposition,
2600 ..
2601 } => Some(TypeSpecification::MeasureRange {
2602 lower: minimum.clone(),
2603 upper: maximum.clone(),
2604 minimum: None,
2605 maximum: None,
2606 units: units.clone(),
2607 decomposition: decomposition.clone(),
2608 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2609 }),
2610 TypeSpecification::Date {
2611 minimum, maximum, ..
2612 } => Some(TypeSpecification::DateRange {
2613 lower: minimum.clone(),
2614 upper: maximum.clone(),
2615 minimum: None,
2616 maximum: None,
2617 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2618 }),
2619 TypeSpecification::Time {
2620 minimum, maximum, ..
2621 } => Some(TypeSpecification::TimeRange {
2622 lower: minimum.clone(),
2623 upper: maximum.clone(),
2624 minimum: None,
2625 maximum: None,
2626 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2627 }),
2628 TypeSpecification::Ratio {
2629 minimum,
2630 maximum,
2631 units,
2632 ..
2633 } => Some(TypeSpecification::RatioRange {
2634 lower: minimum.clone(),
2635 upper: maximum.clone(),
2636 minimum: None,
2637 maximum: None,
2638 units: units.clone(),
2639 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2640 }),
2641 _ => None,
2642 }
2643 }
2644
2645 #[must_use]
2650 pub fn minimum_decimal(&self) -> Option<Decimal> {
2651 match self {
2652 TypeSpecification::Number { minimum, .. }
2653 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
2654 bound
2655 .try_to_decimal()
2656 .expect("BUG: planned minimum must convert to decimal")
2657 }),
2658 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
2659 bound
2660 .try_to_decimal()
2661 .expect("BUG: planned minimum must convert to decimal")
2662 }),
2663 _ => None,
2664 }
2665 }
2666
2667 #[must_use]
2672 pub fn maximum_decimal(&self) -> Option<Decimal> {
2673 match self {
2674 TypeSpecification::Number { maximum, .. }
2675 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
2676 bound
2677 .try_to_decimal()
2678 .expect("BUG: planned maximum must convert to decimal")
2679 }),
2680 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
2681 bound
2682 .try_to_decimal()
2683 .expect("BUG: planned maximum must convert to decimal")
2684 }),
2685 _ => None,
2686 }
2687 }
2688
2689 pub fn veto() -> Self {
2690 TypeSpecification::Veto { message: None }
2691 }
2692
2693 pub fn apply_constraint(
2705 &mut self,
2706 names: ConstraintNames<'_>,
2707 command: TypeConstraintCommand,
2708 args: &[CommandArg],
2709 declared_suggestion: &mut Option<RawSuggestion>,
2710 declared_value: &mut Option<RawSuggestion>,
2711 resolve_unit: Option<&UnitTokenResolver<'_>>,
2712 ) -> Result<(), String> {
2713 let type_name = names.type_name;
2714 if command == TypeConstraintCommand::With
2715 && args.len() > 1
2716 && !matches!(self, TypeSpecification::List { .. })
2717 {
2718 return Err(format!("`-> with` on {self} takes exactly one value"));
2719 }
2720 if command == TypeConstraintCommand::Trait
2721 && !matches!(&self, TypeSpecification::Measure { .. })
2722 {
2723 return Err("trait command is only valid on measure types".to_string());
2724 }
2725 match self {
2726 TypeSpecification::Boolean { help } => match command {
2727 TypeConstraintCommand::Help => {
2728 apply_type_help_command(help, args)?;
2729 }
2730 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2731 let (target, cmd) = match command {
2732 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2733 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2734 _ => unreachable!("BUG: only Suggest or With in this arm"),
2735 };
2736 let lit = require_literal(args, cmd)?;
2737 reject_calendar_for_suggestion(
2738 lit,
2739 type_name,
2740 SuggestionExpectation::Boolean,
2741 None,
2742 )?;
2743 match lit {
2744 crate::literals::Value::Boolean(bv) => {
2745 *target =
2746 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
2747 }
2748 _ => {
2749 return Err(
2750 "Please provide true or false, for example `-> suggest true`."
2751 .to_string(),
2752 );
2753 }
2754 }
2755 }
2756 other => {
2757 return Err(format!(
2758 "Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
2759 other
2760 ));
2761 }
2762 },
2763 TypeSpecification::Measure {
2764 minimum,
2765 maximum,
2766 units,
2767 traits,
2768 help,
2769 ..
2770 } => match command {
2771 TypeConstraintCommand::Decimals => {
2772 return Err(decimals_depend_on_the_unit_error(
2773 "measure",
2774 names,
2775 args,
2776 units.0.first().map(|unit| unit.name.as_str()),
2777 )?);
2778 }
2779 TypeConstraintCommand::Unit => match args {
2780 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Reference(reference))] =>
2781 {
2782 let unit_name = name.clone();
2783 let written = reference.to_string();
2784 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2785 let same = match &existing.scale {
2786 UnitScale::Unresolved {
2787 segments,
2788 name: target_name,
2789 ..
2790 } => {
2791 segments == &reference.segments
2792 && target_name == &reference.name
2793 }
2794 UnitScale::Literal(_) | UnitScale::Reference(_) => false,
2795 };
2796 if !same {
2797 return Err(format!(
2798 "Unit '{unit_name}' is already defined in this type's inherited units; \
2799 cannot change factor or decomposition. Add a new unit name instead."
2800 ));
2801 }
2802 } else {
2803 units.0.push(MeasureUnit::from_reference(
2804 unit_name,
2805 written,
2806 reference.segments.clone(),
2807 reference.name.clone(),
2808 ));
2809 }
2810 }
2811 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(value))] =>
2812 {
2813 apply_measure_unit_factor(units, name.clone(), *value, Vec::new())?;
2814 }
2815 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(prefix, factors))] =>
2816 {
2817 let derived_measure_factors: Vec<(String, i32)> = factors
2818 .iter()
2819 .map(|f| (f.measure_ref.clone(), f.exp))
2820 .collect();
2821 apply_measure_unit_factor(
2822 units,
2823 name.clone(),
2824 *prefix,
2825 derived_measure_factors,
2826 )?;
2827 }
2828 _ => {
2829 return Err(
2830 "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')"
2831 .to_string(),
2832 );
2833 }
2834 },
2835 TypeConstraintCommand::Trait => {
2836 let measure_trait = parse_measure_trait(args)?;
2837 if traits.contains(&measure_trait) {
2838 return Err(format!(
2839 "Duplicate trait '{}' for measure type.",
2840 measure_trait_name(measure_trait)
2841 ));
2842 }
2843 if measure_trait == MeasureTrait::Duration {
2844 validate_duration_trait_requirements(units)?;
2845 }
2846 if measure_trait == MeasureTrait::Calendar {
2847 validate_calendar_trait_requirements(units)?;
2848 }
2849 traits.push(measure_trait);
2850 }
2851 TypeConstraintCommand::Minimum => {
2852 *minimum = Some(parse_measure_declared_bound(
2853 args, "minimum", units, type_name,
2854 )?);
2855 }
2856 TypeConstraintCommand::Maximum => {
2857 *maximum = Some(parse_measure_declared_bound(
2858 args, "maximum", units, type_name,
2859 )?);
2860 }
2861 TypeConstraintCommand::Help => {
2862 apply_type_help_command(help, args)?;
2863 }
2864 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2865 let (target, cmd) = match command {
2866 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2867 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2868 _ => unreachable!("BUG: only Suggest or With in this arm"),
2869 };
2870 let lit = require_literal(args, cmd)?;
2871 if !traits.contains(&MeasureTrait::Calendar) {
2872 reject_calendar_for_suggestion(
2873 lit,
2874 type_name,
2875 SuggestionExpectation::MeasureUnits,
2876 Some(units),
2877 )?;
2878 }
2879 match lit {
2880 crate::literals::Value::NumberWithUnit(n, unit_name) => {
2881 if units
2882 .get(unit_name)
2883 .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
2884 {
2885 if command == TypeConstraintCommand::Suggest {
2886 return Err(format!(
2887 "suggest cannot use unit '{unit_name}' on '{type_name}': its conversion factor is a reference and is not known at planning."
2888 ));
2889 }
2890 *target = Some(RawSuggestion::Measure {
2891 magnitude: lift_parser_decimal(*n)?,
2892 unit_name: unit_name.clone(),
2893 });
2894 } else {
2895 let (magnitude, unit_name) =
2896 parse_measure_declared_bound(args, cmd, units, type_name)?;
2897 *target = Some(RawSuggestion::Measure {
2898 magnitude,
2899 unit_name,
2900 });
2901 }
2902 }
2903 _ => {
2904 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
2905 }
2906 }
2907 }
2908 _ => {
2909 return Err(format!(
2910 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, help, suggest, with",
2911 command
2912 ));
2913 }
2914 },
2915 TypeSpecification::Number {
2916 decimals,
2917 minimum,
2918 maximum,
2919 help,
2920 } => match command {
2921 TypeConstraintCommand::Decimals => {
2922 let d = require_decimal_literal(args, "decimals")?;
2923 *decimals = Some(decimal_to_u8(d, "decimals")?);
2924 }
2925 TypeConstraintCommand::Unit => {
2926 return Err(
2927 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
2928 );
2929 }
2930 TypeConstraintCommand::Minimum => {
2931 *minimum = Some(require_decimal_literal(args, "minimum")?);
2932 }
2933 TypeConstraintCommand::Maximum => {
2934 *maximum = Some(require_decimal_literal(args, "maximum")?);
2935 }
2936 TypeConstraintCommand::Help => {
2937 apply_type_help_command(help, args)?;
2938 }
2939 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2940 let (target, cmd) = match command {
2941 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2942 TypeConstraintCommand::With => (&mut *declared_value, "with"),
2943 _ => unreachable!("BUG: only Suggest or With in this arm"),
2944 };
2945 let lit = require_literal(args, cmd)?;
2946 reject_calendar_for_suggestion(
2947 lit,
2948 type_name,
2949 SuggestionExpectation::Number,
2950 None,
2951 )?;
2952 match lit {
2953 crate::literals::Value::Number(d) => {
2954 *target = Some(RawSuggestion::Value(ValueKind::Number(
2955 lift_parser_decimal(*d)?,
2956 )));
2957 }
2958 _ => {
2959 return Err(
2960 "Please provide a number, for example `-> suggest 42`.".to_string()
2961 );
2962 }
2963 }
2964 }
2965 _ => {
2966 return Err(format!(
2967 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
2968 command
2969 ));
2970 }
2971 },
2972 TypeSpecification::NumberRange {
2973 lower,
2974 upper,
2975 minimum,
2976 maximum,
2977 help,
2978 } => match command {
2979 TypeConstraintCommand::Lower => {
2980 *lower = Some(require_decimal_literal(args, "lower")?);
2981 }
2982 TypeConstraintCommand::Upper => {
2983 *upper = Some(require_decimal_literal(args, "upper")?);
2984 }
2985 TypeConstraintCommand::Minimum => {
2986 let width = require_decimal_literal(args, "minimum")?;
2987 reject_negative_width_magnitude(&width, "minimum")?;
2988 *minimum = Some(width);
2989 }
2990 TypeConstraintCommand::Maximum => {
2991 let width = require_decimal_literal(args, "maximum")?;
2992 reject_negative_width_magnitude(&width, "maximum")?;
2993 *maximum = Some(width);
2994 }
2995 TypeConstraintCommand::Help => {
2996 apply_type_help_command(help, args)?;
2997 }
2998 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2999 let target = match command {
3000 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3001 TypeConstraintCommand::With => &mut *declared_value,
3002 _ => unreachable!("BUG: only Suggest or With in this arm"),
3003 };
3004 let (left, right) = require_suggestion_range_endpoints(
3005 args,
3006 type_name,
3007 SuggestionExpectation::NumberRange,
3008 None,
3009 )?;
3010 let left = literal_value_from_parser_value(left)?;
3011 let right = literal_value_from_parser_value(right)?;
3012 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
3013 return Err(
3014 "Please provide a number range, for example `-> suggest 10...100`."
3015 .to_string(),
3016 );
3017 }
3018 *target = Some(RawSuggestion::Value(ValueKind::Range(
3019 Box::new(left),
3020 Box::new(right),
3021 )));
3022 }
3023 _ => {
3024 return Err(format!(
3025 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3026 command
3027 ));
3028 }
3029 },
3030 TypeSpecification::Ratio {
3031 minimum,
3032 maximum,
3033 units,
3034 unit: _,
3035 help,
3036 } => match command {
3037 TypeConstraintCommand::Decimals => {
3038 return Err(decimals_depend_on_the_unit_error(
3039 "ratio",
3040 names,
3041 args,
3042 units.0.first().map(|unit| unit.name.as_str()),
3043 )?);
3044 }
3045 TypeConstraintCommand::Unit => {
3046 let (unit_name, value_dec) = match args {
3047 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
3048 (name.clone(), *v)
3049 }
3050 _ => {
3051 return Err(
3052 "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."
3053 .to_string(),
3054 );
3055 }
3056 };
3057 let value = crate::computation::rational::decimal_to_rational(value_dec)
3058 .map_err(|failure| {
3059 format!(
3060 "ratio unit value is not exactly representable as a rational: {}",
3061 failure
3062 )
3063 })?;
3064 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
3065 if existing.value != value {
3066 return Err(format!(
3067 "Unit '{unit_name}' is already defined in this type's inherited units; \
3068 cannot change factor. Add a new unit name instead."
3069 ));
3070 }
3071 } else {
3072 units.0.push(RatioUnit {
3073 name: unit_name,
3074 value,
3075 minimum: None,
3076 maximum: None,
3077 suggestion_magnitude: None,
3078 });
3079 }
3080 }
3081 TypeConstraintCommand::Minimum => {
3082 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3083 sync_ratio_units_from_canonical(
3084 units,
3085 &canonical,
3086 UnitConstraintField::Minimum,
3087 )?;
3088 *minimum = Some(canonical);
3089 }
3090 TypeConstraintCommand::Maximum => {
3091 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3092 sync_ratio_units_from_canonical(
3093 units,
3094 &canonical,
3095 UnitConstraintField::Maximum,
3096 )?;
3097 *maximum = Some(canonical);
3098 }
3099 TypeConstraintCommand::Help => {
3100 apply_type_help_command(help, args)?;
3101 }
3102 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3103 let (target, cmd) = match command {
3104 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3105 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3106 _ => unreachable!("BUG: only Suggest or With in this arm"),
3107 };
3108 let lit = require_literal(args, cmd)?;
3109 reject_calendar_for_suggestion(
3110 lit,
3111 type_name,
3112 SuggestionExpectation::Ratio,
3113 None,
3114 )?;
3115 match lit {
3116 crate::literals::Value::NumberWithUnit(_, unit_name) => {
3117 let element_type = LemmaType::primitive(TypeSpecification::Ratio {
3118 minimum: minimum.clone(),
3119 maximum: maximum.clone(),
3120 units: units.clone(),
3121 unit: None,
3122 help: help.clone(),
3123 });
3124 let value = parser_value_to_value_kind(lit, &element_type, None)?;
3125 sync_ratio_suggestion_units(units, &value)?;
3126 *target = Some(RawSuggestion::UnitBound {
3127 value,
3128 unit_name: unit_name.clone(),
3129 });
3130 }
3131 other => {
3132 return Err(format!(
3133 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
3134 value_kind_name(other)
3135 ));
3136 }
3137 }
3138 }
3139 _ => {
3140 return Err(format!(
3141 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, help, suggest, with",
3142 command
3143 ));
3144 }
3145 },
3146 TypeSpecification::RatioRange {
3147 lower,
3148 upper,
3149 minimum,
3150 maximum,
3151 units,
3152 help,
3153 } => match command {
3154 TypeConstraintCommand::Unit => {
3155 return Err(format!(
3156 "'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
3157 ));
3158 }
3159 TypeConstraintCommand::Lower => {
3160 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
3161 }
3162 TypeConstraintCommand::Upper => {
3163 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
3164 }
3165 TypeConstraintCommand::Minimum => {
3166 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3167 reject_negative_width_magnitude(&width, "minimum")?;
3168 *minimum = Some(width);
3169 }
3170 TypeConstraintCommand::Maximum => {
3171 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3172 reject_negative_width_magnitude(&width, "maximum")?;
3173 *maximum = Some(width);
3174 }
3175 TypeConstraintCommand::Help => {
3176 apply_type_help_command(help, args)?;
3177 }
3178 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3179 let target = match command {
3180 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3181 TypeConstraintCommand::With => &mut *declared_value,
3182 _ => unreachable!("BUG: only Suggest or With in this arm"),
3183 };
3184 let (left, right) = require_suggestion_range_endpoints(
3185 args,
3186 type_name,
3187 SuggestionExpectation::RatioRange,
3188 None,
3189 )?;
3190 let element_type = LemmaType::primitive(
3191 TypeSpecification::RatioRange {
3192 lower: lower.clone(),
3193 upper: upper.clone(),
3194 minimum: minimum.clone(),
3195 maximum: maximum.clone(),
3196 units: units.clone(),
3197 help: help.clone(),
3198 }
3199 .element_from_range()
3200 .expect("BUG: RatioRange must define element_from_range"),
3201 );
3202 let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3203 let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3204 if !value_kind_matches_spec(&left.value, &element_type.specifications)
3205 || !value_kind_matches_spec(&right.value, &element_type.specifications)
3206 || !matches!(&left.value, ValueKind::Ratio(_))
3207 || !matches!(&right.value, ValueKind::Ratio(_))
3208 {
3209 return Err(
3210 "Please provide a ratio range, for example `-> suggest 10%...50%`."
3211 .to_string(),
3212 );
3213 }
3214 *target = Some(RawSuggestion::Value(ValueKind::Range(
3215 Box::new(left),
3216 Box::new(right),
3217 )));
3218 }
3219 _ => {
3220 return Err(format!(
3221 "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3222 command
3223 ));
3224 }
3225 },
3226 TypeSpecification::Text { length, help } => match command {
3227 TypeConstraintCommand::Length => {
3228 let d = require_decimal_literal(args, "length")?;
3229 *length = Some(decimal_to_usize(d, "length")?);
3230 }
3231 TypeConstraintCommand::Help => {
3232 apply_type_help_command(help, args)?;
3233 }
3234 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3235 let (target, cmd) = match command {
3236 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3237 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3238 _ => unreachable!("BUG: only Suggest or With in this arm"),
3239 };
3240 let lit = require_literal(args, cmd)?;
3241 reject_calendar_for_suggestion(
3242 lit,
3243 type_name,
3244 SuggestionExpectation::Text,
3245 None,
3246 )?;
3247 match lit {
3248 crate::literals::Value::Text(s) => {
3249 *target =
3250 Some(RawSuggestion::Value(ValueKind::Text(Arc::from(s.as_str()))));
3251 }
3252 _ => {
3253 return Err(
3254 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
3255 .to_string(),
3256 );
3257 }
3258 }
3259 }
3260 _ => {
3261 return Err(format!(
3262 "Invalid command '{}' for text type. Valid commands: length, help, suggest, with",
3263 command
3264 ));
3265 }
3266 },
3267 TypeSpecification::Date {
3268 minimum,
3269 maximum,
3270 help,
3271 } => match command {
3272 TypeConstraintCommand::Minimum => {
3273 let dt = require_date_literal(args, "minimum")?;
3274 *minimum = Some(dt);
3275 }
3276 TypeConstraintCommand::Maximum => {
3277 let dt = require_date_literal(args, "maximum")?;
3278 *maximum = Some(dt);
3279 }
3280 TypeConstraintCommand::Help => {
3281 apply_type_help_command(help, args)?;
3282 }
3283 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3284 let (target, cmd) = match command {
3285 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3286 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3287 _ => unreachable!("BUG: only Suggest or With in this arm"),
3288 };
3289 let lit = require_literal(args, cmd)?;
3290 reject_calendar_for_suggestion(
3291 lit,
3292 type_name,
3293 SuggestionExpectation::Date,
3294 None,
3295 )?;
3296 match lit {
3297 crate::literals::Value::Date(dt) => {
3298 *target = Some(RawSuggestion::Value(ValueKind::Date(
3299 date_time_to_semantic(dt),
3300 )));
3301 }
3302 _ => {
3303 return Err(
3304 "Please provide a date, for example `-> suggest 2024-06-15`."
3305 .to_string(),
3306 );
3307 }
3308 }
3309 }
3310 _ => {
3311 return Err(format!(
3312 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
3313 command
3314 ));
3315 }
3316 },
3317 TypeSpecification::DateRange {
3318 lower,
3319 upper,
3320 minimum,
3321 maximum,
3322 help,
3323 } => match command {
3324 TypeConstraintCommand::Lower => {
3325 *lower = Some(require_date_literal(args, "lower")?);
3326 }
3327 TypeConstraintCommand::Upper => {
3328 *upper = Some(require_date_literal(args, "upper")?);
3329 }
3330 TypeConstraintCommand::Minimum => {
3331 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3332 }
3333 TypeConstraintCommand::Maximum => {
3334 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3335 }
3336 TypeConstraintCommand::Help => {
3337 apply_type_help_command(help, args)?;
3338 }
3339 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3340 let target = match command {
3341 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3342 TypeConstraintCommand::With => &mut *declared_value,
3343 _ => unreachable!("BUG: only Suggest or With in this arm"),
3344 };
3345 let (left, right) = require_suggestion_range_endpoints(
3346 args,
3347 type_name,
3348 SuggestionExpectation::DateRange,
3349 None,
3350 )?;
3351 let left = literal_value_from_parser_value(left)?;
3352 let right = literal_value_from_parser_value(right)?;
3353 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
3354 return Err(
3355 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
3356 .to_string(),
3357 );
3358 }
3359 *target = Some(RawSuggestion::Value(ValueKind::Range(
3360 Box::new(left),
3361 Box::new(right),
3362 )));
3363 }
3364 _ => {
3365 return Err(format!(
3366 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3367 command
3368 ));
3369 }
3370 },
3371 TypeSpecification::Time {
3372 minimum,
3373 maximum,
3374 help,
3375 } => match command {
3376 TypeConstraintCommand::Minimum => {
3377 let t = require_time_literal(args, "minimum")?;
3378 *minimum = Some(t);
3379 }
3380 TypeConstraintCommand::Maximum => {
3381 let t = require_time_literal(args, "maximum")?;
3382 *maximum = Some(t);
3383 }
3384 TypeConstraintCommand::Help => {
3385 apply_type_help_command(help, args)?;
3386 }
3387 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3388 let (target, cmd) = match command {
3389 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3390 TypeConstraintCommand::With => (&mut *declared_value, "with"),
3391 _ => unreachable!("BUG: only Suggest or With in this arm"),
3392 };
3393 let lit = require_literal(args, cmd)?;
3394 reject_calendar_for_suggestion(
3395 lit,
3396 type_name,
3397 SuggestionExpectation::Time,
3398 None,
3399 )?;
3400 match lit {
3401 crate::literals::Value::Time(t) => {
3402 *target =
3403 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
3404 }
3405 _ => {
3406 return Err(
3407 "Please provide a time, for example `-> suggest 09:00:00`."
3408 .to_string(),
3409 );
3410 }
3411 }
3412 }
3413 _ => {
3414 return Err(format!(
3415 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
3416 command
3417 ));
3418 }
3419 },
3420 TypeSpecification::TimeRange {
3421 lower,
3422 upper,
3423 minimum,
3424 maximum,
3425 help,
3426 } => match command {
3427 TypeConstraintCommand::Lower => {
3428 *lower = Some(require_time_literal(args, "lower")?);
3429 }
3430 TypeConstraintCommand::Upper => {
3431 *upper = Some(require_time_literal(args, "upper")?);
3432 }
3433 TypeConstraintCommand::Minimum => {
3434 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3435 }
3436 TypeConstraintCommand::Maximum => {
3437 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3438 }
3439 TypeConstraintCommand::Help => {
3440 apply_type_help_command(help, args)?;
3441 }
3442 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3443 let target = match command {
3444 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3445 TypeConstraintCommand::With => &mut *declared_value,
3446 _ => unreachable!("BUG: only Suggest or With in this arm"),
3447 };
3448 let (left, right) = require_suggestion_range_endpoints(
3449 args,
3450 type_name,
3451 SuggestionExpectation::TimeRange,
3452 None,
3453 )?;
3454 let left = literal_value_from_parser_value(left)?;
3455 let right = literal_value_from_parser_value(right)?;
3456 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
3457 return Err(
3458 "Please provide a time range, for example `-> suggest 09:00...17:00`."
3459 .to_string(),
3460 );
3461 }
3462 *target = Some(RawSuggestion::Value(ValueKind::Range(
3463 Box::new(left),
3464 Box::new(right),
3465 )));
3466 }
3467 _ => {
3468 return Err(format!(
3469 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3470 command
3471 ));
3472 }
3473 },
3474 TypeSpecification::MeasureRange {
3475 lower,
3476 upper,
3477 minimum,
3478 maximum,
3479 units,
3480 decomposition,
3481 help,
3482 } => match command {
3483 TypeConstraintCommand::Unit => {
3484 return Err(format!(
3485 "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
3486 ));
3487 }
3488 TypeConstraintCommand::Lower => {
3489 *lower = Some(parse_measure_declared_bound(
3490 args, "lower", units, type_name,
3491 )?);
3492 }
3493 TypeConstraintCommand::Upper => {
3494 *upper = Some(parse_measure_declared_bound(
3495 args, "upper", units, type_name,
3496 )?);
3497 }
3498 TypeConstraintCommand::Minimum => {
3499 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
3500 reject_negative_width_magnitude(&width.0, "minimum")?;
3501 *minimum = Some(width);
3502 }
3503 TypeConstraintCommand::Maximum => {
3504 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
3505 reject_negative_width_magnitude(&width.0, "maximum")?;
3506 *maximum = Some(width);
3507 }
3508 TypeConstraintCommand::Help => {
3509 apply_type_help_command(help, args)?;
3510 }
3511 TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3512 let target = match command {
3513 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3514 TypeConstraintCommand::With => &mut *declared_value,
3515 _ => unreachable!("BUG: only Suggest or With in this arm"),
3516 };
3517 let (left, right) = require_suggestion_range_endpoints(
3518 args,
3519 type_name,
3520 SuggestionExpectation::MeasureRange,
3521 Some(units),
3522 )?;
3523 let element_type = LemmaType::primitive(
3524 TypeSpecification::MeasureRange {
3525 lower: lower.clone(),
3526 upper: upper.clone(),
3527 minimum: minimum.clone(),
3528 maximum: maximum.clone(),
3529 units: units.clone(),
3530 decomposition: decomposition.clone(),
3531 help: help.clone(),
3532 }
3533 .element_from_range()
3534 .expect("BUG: MeasureRange must define element_from_range"),
3535 );
3536 let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3537 let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3538 if !value_kind_matches_spec(&left.value, &element_type.specifications)
3539 || !value_kind_matches_spec(&right.value, &element_type.specifications)
3540 || !matches!(&left.value, ValueKind::Measure(_))
3541 || !matches!(&right.value, ValueKind::Measure(_))
3542 {
3543 return Err(format!(
3544 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
3545 ));
3546 }
3547 *target = Some(RawSuggestion::Value(ValueKind::Range(
3548 Box::new(left),
3549 Box::new(right),
3550 )));
3551 }
3552 _ => {
3553 return Err(format!(
3554 "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3555 command
3556 ));
3557 }
3558 },
3559 TypeSpecification::Veto { .. } => {
3560 return Err(format!(
3561 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
3562 command
3563 ));
3564 }
3565 TypeSpecification::Undetermined => {
3566 return Err(format!(
3567 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
3568 command
3569 ));
3570 }
3571 TypeSpecification::Spec { .. } => {
3572 return Err(format!(
3573 "Invalid command '{command}' for spec type. A spec instance has no data constraints"
3574 ));
3575 }
3576 TypeSpecification::List { element, help, .. } => match command {
3577 TypeConstraintCommand::Help => {
3578 if matches!(element.specifications, TypeSpecification::Spec { .. }) {
3579 apply_type_help_command(help, args)?;
3580 } else {
3581 let mut unused_suggestion = None;
3582 let mut unused_value = None;
3583 Arc::make_mut(element).specifications.apply_constraint(
3584 names,
3585 command,
3586 args,
3587 &mut unused_suggestion,
3588 &mut unused_value,
3589 resolve_unit,
3590 )?;
3591 }
3592 }
3593 TypeConstraintCommand::With => {
3594 return Err(
3595 "BUG: list -> with is applied before TypeSpecification::apply_constraint"
3596 .to_string(),
3597 );
3598 }
3599 other => {
3600 return Err(format!(
3601 "Invalid command '{other}' for list type. Valid commands: with, help"
3602 ));
3603 }
3604 },
3605 }
3606 Ok(())
3607 }
3608}
3609
3610pub fn parse_number_unit(
3613 value_str: &str,
3614 type_spec: &TypeSpecification,
3615) -> Result<crate::parsing::ast::Value, String> {
3616 use crate::literals::{NumberWithUnit, RatioLiteral};
3617 use crate::parsing::ast::Value;
3618
3619 let trimmed = value_str.trim();
3620 match type_spec {
3621 TypeSpecification::Measure { units, .. } => {
3622 if units.is_empty() {
3623 unreachable!(
3624 "BUG: Measure type has no units; should have been validated during planning"
3625 );
3626 }
3627 match trimmed.parse::<NumberWithUnit>() {
3628 Ok(n) => {
3629 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3630 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3631 }
3632 Err(e) => {
3633 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3634 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3635 let example_unit = units
3636 .iter()
3637 .next()
3638 .expect("BUG: units non-empty after guard")
3639 .name
3640 .as_str();
3641 Err(format!(
3642 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3643 trimmed,
3644 example_unit,
3645 valid.join(", ")
3646 ))
3647 } else {
3648 Err(e)
3649 }
3650 }
3651 }
3652 }
3653 TypeSpecification::Ratio { units, .. } => {
3654 if units.is_empty() {
3655 unreachable!(
3656 "BUG: Ratio type has no units; should have been validated during planning"
3657 );
3658 }
3659 match trimmed.parse::<RatioLiteral>()? {
3660 RatioLiteral::Bare(_) => {
3661 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3662 }
3663 RatioLiteral::Percent(n) => {
3664 let unit = units.get("percent").map_err(|e| e.to_string())?;
3665 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3666 }
3667 RatioLiteral::Permille(n) => {
3668 let unit = units.get("permille").map_err(|e| e.to_string())?;
3669 Ok(Value::NumberWithUnit(n, unit.name.clone()))
3670 }
3671 RatioLiteral::Named { value, unit } => {
3672 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3673 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3674 }
3675 }
3676 }
3677 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3678 }
3679}
3680
3681pub fn parse_value_from_string(
3684 value_str: &str,
3685 type_spec: &TypeSpecification,
3686 source: &Source,
3687) -> Result<crate::parsing::ast::Value, Error> {
3688 match type_spec {
3689 TypeSpecification::NumberRange { .. }
3690 | TypeSpecification::MeasureRange { .. }
3691 | TypeSpecification::DateRange { .. }
3692 | TypeSpecification::TimeRange { .. }
3693 | TypeSpecification::RatioRange { .. } => {
3694 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3695 unreachable!(
3696 "BUG: range_element_type_specification missing arm for known range type"
3697 )
3698 });
3699 parse_range_value_from_string(value_str, &element_spec, source)
3700 }
3701 _ => parse_scalar_value_from_string(value_str, type_spec, source),
3702 }
3703}
3704
3705fn parse_range_value_from_string(
3706 value_str: &str,
3707 element_spec: &TypeSpecification,
3708 source: &Source,
3709) -> Result<crate::parsing::ast::Value, Error> {
3710 use crate::parsing::ast::Value;
3711
3712 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3713
3714 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3715 to_err("Range value must use '...' between the two endpoints".to_string())
3716 })?;
3717 if left_str.trim().is_empty() || right_str.trim().is_empty() {
3718 return Err(to_err(
3719 "Range value must contain a non-empty left and right endpoint".to_string(),
3720 ));
3721 }
3722 let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3723 let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3724 Ok(Value::Range(Box::new(left), Box::new(right)))
3725}
3726
3727fn parse_scalar_value_from_string(
3728 value_str: &str,
3729 type_spec: &TypeSpecification,
3730 source: &Source,
3731) -> Result<crate::parsing::ast::Value, Error> {
3732 use crate::parsing::ast::Value;
3733
3734 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3735
3736 match type_spec {
3737 TypeSpecification::Text { .. } => value_str
3738 .parse::<crate::literals::TextLiteral>()
3739 .map(|t| Value::Text(t.0))
3740 .map_err(to_err),
3741 TypeSpecification::Number { .. } => value_str
3742 .parse::<crate::literals::NumberLiteral>()
3743 .map(|n| Value::Number(n.0))
3744 .map_err(to_err),
3745 TypeSpecification::Measure { .. } => {
3746 parse_number_unit(value_str, type_spec).map_err(to_err)
3747 }
3748 TypeSpecification::Boolean { .. } => value_str
3749 .parse::<BooleanValue>()
3750 .map(Value::Boolean)
3751 .map_err(to_err),
3752 TypeSpecification::Date { .. } => {
3753 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3754 Ok(Value::Date(date))
3755 }
3756 TypeSpecification::Time { .. } => {
3757 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3758 Ok(Value::Time(time))
3759 }
3760 TypeSpecification::Ratio { .. } => {
3761 parse_number_unit(value_str, type_spec).map_err(to_err)
3762 }
3763 TypeSpecification::NumberRange { .. }
3764 | TypeSpecification::MeasureRange { .. }
3765 | TypeSpecification::DateRange { .. }
3766 | TypeSpecification::TimeRange { .. }
3767 | TypeSpecification::RatioRange { .. } => {
3768 unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3769 }
3770 TypeSpecification::Veto { .. } => Err(to_err(
3771 "Veto type cannot be parsed from string".to_string(),
3772 )),
3773 TypeSpecification::Undetermined => unreachable!(
3774 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3775 ),
3776 TypeSpecification::Spec { .. } => Err(to_err(
3777 "spec values are not evaluation-data scalars".to_string(),
3778 )),
3779 TypeSpecification::List { .. } => Err(to_err(
3780 "list values are not evaluation-data scalars".to_string(),
3781 )),
3782 }
3783}
3784
3785#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3790pub enum SemanticCalendarUnit {
3791 Month,
3792 Year,
3793}
3794
3795impl fmt::Display for SemanticCalendarUnit {
3796 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3797 let s = match self {
3798 SemanticCalendarUnit::Month => "month",
3799 SemanticCalendarUnit::Year => "year",
3800 };
3801 write!(f, "{}", s)
3802 }
3803}
3804
3805pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3806 match unit_name {
3807 "month" => SemanticCalendarUnit::Month,
3808 "year" => SemanticCalendarUnit::Year,
3809 other => unreachable!(
3810 "BUG: calendar measure signature unit must be month or year, got '{other}'"
3811 ),
3812 }
3813}
3814
3815pub fn semantic_calendar_unit_from_measure_signature(
3816 signature: &[(String, i32)],
3817) -> SemanticCalendarUnit {
3818 let unit_name = signature
3819 .first()
3820 .map(|(name, _)| name.as_str())
3821 .expect("BUG: calendar measure must carry a unit signature");
3822 semantic_calendar_unit_from_unit_name(unit_name)
3823}
3824
3825pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3826 if !lemma_type.is_calendar_like() {
3827 unreachable!(
3828 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3829 lemma_type.name()
3830 );
3831 }
3832 let signature = lemma_type.measure_runtime_signature();
3833 if signature.is_empty() {
3834 return SemanticCalendarUnit::Month;
3835 }
3836 semantic_calendar_unit_from_measure_signature(&signature)
3837}
3838
3839#[derive(Debug, Clone, PartialEq, Eq)]
3841pub enum SemanticConversionTarget {
3842 Type(ConversionTypeKind),
3843 Unit {
3845 unit_name: String,
3846 owning_type: Arc<LemmaType>,
3848 },
3849}
3850
3851impl std::hash::Hash for SemanticConversionTarget {
3852 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3853 match self {
3854 Self::Type(kind) => {
3855 0u8.hash(state);
3856 kind.hash(state);
3857 }
3858 Self::Unit {
3859 unit_name,
3860 owning_type,
3861 } => {
3862 1u8.hash(state);
3863 unit_name.hash(state);
3864 owning_type.hash(state);
3865 }
3866 }
3867 }
3868}
3869
3870impl SemanticConversionTarget {}
3871
3872impl fmt::Display for SemanticConversionTarget {
3873 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3874 match self {
3875 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
3876 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
3877 }
3878 }
3879}
3880
3881#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3884pub struct SemanticTimezone {
3885 pub offset_minutes: i16,
3886}
3887
3888impl SemanticTimezone {
3889 pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
3891 TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
3892 }
3893
3894 pub fn offset_seconds(&self) -> i32 {
3895 i32::from(self.offset_minutes) * 60
3896 }
3897}
3898
3899impl From<&TimezoneValue> for SemanticTimezone {
3900 fn from(tz: &TimezoneValue) -> Self {
3901 Self {
3902 offset_minutes: tz.offset_minutes,
3903 }
3904 }
3905}
3906
3907impl From<&SemanticTimezone> for TimezoneValue {
3908 fn from(tz: &SemanticTimezone) -> Self {
3909 Self {
3910 offset_minutes: tz.offset_minutes,
3911 }
3912 }
3913}
3914
3915impl fmt::Display for SemanticTimezone {
3916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3917 TimezoneValue::from(self).fmt(f)
3918 }
3919}
3920
3921impl Serialize for SemanticTimezone {
3922 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3923 serializer.serialize_str(&self.to_string())
3924 }
3925}
3926
3927impl<'de> Deserialize<'de> for SemanticTimezone {
3928 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3929 let s = String::deserialize(deserializer)?;
3930 Self::from_str(&s).map_err(serde::de::Error::custom)
3931 }
3932}
3933
3934impl FromStr for SemanticTimezone {
3935 type Err = String;
3936
3937 fn from_str(s: &str) -> Result<Self, Self::Err> {
3938 Ok(Self::from(&TimezoneValue::from_str(s)?))
3939 }
3940}
3941
3942#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3944pub struct SemanticTime {
3945 pub hour: u32,
3946 pub minute: u32,
3947 pub second: u32,
3948 pub microsecond: u32,
3949 pub timezone: Option<SemanticTimezone>,
3950}
3951
3952impl fmt::Display for SemanticTime {
3953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3954 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
3955 if self.microsecond != 0 {
3956 write!(f, ".{:06}", self.microsecond)?;
3957 }
3958 if let Some(timezone) = &self.timezone {
3959 write!(f, "{}", timezone)?;
3960 }
3961 Ok(())
3962 }
3963}
3964
3965impl Serialize for SemanticTime {
3966 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3967 serializer.serialize_str(&self.to_string())
3968 }
3969}
3970
3971impl<'de> Deserialize<'de> for SemanticTime {
3972 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3973 let s = String::deserialize(deserializer)?;
3974 Self::from_str(&s).map_err(serde::de::Error::custom)
3975 }
3976}
3977
3978impl FromStr for SemanticTime {
3979 type Err = String;
3980
3981 fn from_str(s: &str) -> Result<Self, Self::Err> {
3982 Ok(time_to_semantic(&TimeValue::from_str(s)?))
3983 }
3984}
3985
3986#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3988pub struct SemanticDateTime {
3989 pub year: i32,
3990 pub month: u32,
3991 pub day: u32,
3992 pub hour: u32,
3993 pub minute: u32,
3994 pub second: u32,
3995 pub microsecond: u32,
3996 pub timezone: Option<SemanticTimezone>,
3997}
3998
3999impl fmt::Display for SemanticDateTime {
4000 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4001 let has_time = self.hour != 0
4002 || self.minute != 0
4003 || self.second != 0
4004 || self.microsecond != 0
4005 || self.timezone.is_some();
4006 if !has_time {
4007 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
4008 } else {
4009 write!(
4010 f,
4011 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
4012 self.year, self.month, self.day, self.hour, self.minute, self.second
4013 )?;
4014 if self.microsecond != 0 {
4015 write!(f, ".{:06}", self.microsecond)?;
4016 }
4017 if let Some(tz) = &self.timezone {
4018 write!(f, "{}", tz)?;
4019 }
4020 Ok(())
4021 }
4022 }
4023}
4024
4025impl Serialize for SemanticDateTime {
4026 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4027 serializer.serialize_str(&self.to_string())
4028 }
4029}
4030
4031impl<'de> Deserialize<'de> for SemanticDateTime {
4032 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4033 let s = String::deserialize(deserializer)?;
4034 Self::from_str(&s).map_err(serde::de::Error::custom)
4035 }
4036}
4037
4038impl FromStr for SemanticDateTime {
4039 type Err = String;
4040
4041 fn from_str(s: &str) -> Result<Self, Self::Err> {
4042 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
4043 }
4044}
4045
4046#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4050pub enum RawSuggestion {
4051 Value(ValueKind),
4052 Measure {
4054 magnitude: RationalInteger,
4055 unit_name: String,
4056 },
4057 UnitBound {
4060 value: ValueKind,
4061 unit_name: String,
4062 },
4063 List(Vec<RawSuggestion>),
4064}
4065
4066#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4073pub struct BoundValueKind {
4074 pub value: ValueKind,
4075 pub unit: Option<Arc<str>>,
4076}
4077
4078impl BoundValueKind {
4079 pub fn unbound(value: ValueKind) -> Self {
4080 Self { value, unit: None }
4081 }
4082
4083 pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
4084 Self { value, unit }
4085 }
4086
4087 pub fn to_literal(&self) -> LiteralValue {
4088 LiteralValue {
4089 value: self.value.clone(),
4090 }
4091 }
4092
4093 #[must_use]
4096 pub fn agree_or_left_binding(
4097 left: Option<&Arc<str>>,
4098 right: Option<&Arc<str>>,
4099 ) -> Option<Arc<str>> {
4100 match (left, right) {
4101 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
4102 (Some(a), Some(_)) => Some(Arc::clone(a)),
4103 (Some(a), None) => Some(Arc::clone(a)),
4104 (None, Some(b)) => Some(Arc::clone(b)),
4105 (None, None) => None,
4106 }
4107 }
4108}
4109
4110pub fn bound_value_kind_from_raw_suggestion(
4111 raw: RawSuggestion,
4112 specifications: &TypeSpecification,
4113 type_name: &str,
4114) -> Result<BoundValueKind, String> {
4115 enum Step<'a> {
4116 Visit(RawSuggestion, &'a TypeSpecification),
4117 CollectList {
4118 element: &'a Arc<LemmaType>,
4119 rows: usize,
4120 },
4121 }
4122
4123 let mut steps = vec![Step::Visit(raw, specifications)];
4124 let mut bounds: Vec<BoundValueKind> = Vec::new();
4125 while let Some(step) = steps.pop() {
4126 match step {
4127 Step::Visit(raw, specifications) => match raw {
4128 RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
4129 RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
4130 value,
4131 unit: Some(Arc::from(unit_name)),
4132 }),
4133 RawSuggestion::Measure {
4134 magnitude,
4135 unit_name,
4136 } => {
4137 let TypeSpecification::Measure { units, .. } = specifications else {
4138 unreachable!(
4139 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
4140 );
4141 };
4142 if units
4143 .get(&unit_name)
4144 .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
4145 {
4146 bounds.push(BoundValueKind {
4147 value: ValueKind::UnscaledMeasure {
4148 magnitude,
4149 unit: Arc::from(unit_name.as_str()),
4150 },
4151 unit: Some(Arc::from(unit_name)),
4152 });
4153 } else {
4154 let canonical = measure_declared_bound_to_canonical(
4155 &magnitude, &unit_name, units, type_name, "suggest",
4156 )?;
4157 bounds.push(BoundValueKind {
4158 value: ValueKind::Measure(canonical),
4159 unit: Some(Arc::from(unit_name)),
4160 });
4161 }
4162 }
4163 RawSuggestion::List(rows) => {
4164 let TypeSpecification::List { element, .. } = specifications else {
4165 unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
4166 };
4167 steps.push(Step::CollectList {
4168 element,
4169 rows: rows.len(),
4170 });
4171 for row in rows.into_iter().rev() {
4172 steps.push(Step::Visit(row, &element.specifications));
4173 }
4174 }
4175 },
4176 Step::CollectList { element, rows } => {
4177 let start = bounds
4178 .len()
4179 .checked_sub(rows)
4180 .expect("BUG: list rows missing from bound stack");
4181 let converted: Arc<[TypedLiteral]> = bounds
4182 .split_off(start)
4183 .iter()
4184 .map(|bound| typed_literal_from_bound(bound, element))
4185 .collect();
4186 bounds.push(BoundValueKind::unbound(ValueKind::List(converted.into())));
4187 }
4188 }
4189 }
4190 Ok(bounds.pop().expect("BUG: bound walk left no result"))
4191}
4192
4193#[must_use]
4197pub fn display_binding_unit(
4198 schema: &LemmaType,
4199 settled: Option<&BoundValueKind>,
4200 fill: Option<&BoundValueKind>,
4201 suggestion: Option<&BoundValueKind>,
4202) -> Option<String> {
4203 if schema.is_measure_range() || schema.is_ratio_range() {
4204 return None;
4205 }
4206 if let Some(unit) = schema.unit() {
4207 return Some(unit.to_string());
4208 }
4209 if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
4210 return Some(unit.to_string());
4211 }
4212 if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
4213 return Some(unit.to_string());
4214 }
4215 suggestion
4216 .and_then(|bound| bound.unit.as_ref())
4217 .map(|unit| unit.to_string())
4218}
4219
4220#[must_use]
4225pub fn lemma_type_with_display_binding(
4226 schema: &LemmaType,
4227 settled: Option<&BoundValueKind>,
4228 fill: Option<&BoundValueKind>,
4229 suggestion: Option<&BoundValueKind>,
4230) -> LemmaType {
4231 if schema.is_measure_range() || schema.is_ratio_range() {
4232 return schema.clone();
4233 }
4234 match display_binding_unit(schema, settled, fill, suggestion) {
4235 Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
4236 Some(_) | None => schema.clone(),
4237 }
4238}
4239
4240#[must_use]
4242pub fn lemma_type_arc_with_display_binding(
4243 schema: &Arc<LemmaType>,
4244 settled: Option<&BoundValueKind>,
4245 fill: Option<&BoundValueKind>,
4246 suggestion: Option<&BoundValueKind>,
4247) -> Arc<LemmaType> {
4248 if schema.is_measure_range() || schema.is_ratio_range() {
4249 return Arc::clone(schema);
4250 }
4251 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
4252 Some(unit) if schema.unit() != Some(unit.as_str()) => {
4253 Arc::new(schema.as_ref().clone().with_unit(unit))
4254 }
4255 Some(_) | None => Arc::clone(schema),
4256 }
4257}
4258
4259#[must_use]
4264pub fn typed_literal_from_bound(
4265 bound: &BoundValueKind,
4266 lemma_type: &Arc<LemmaType>,
4267) -> TypedLiteral {
4268 let lemma_type = match (&bound.unit, &lemma_type.specifications) {
4269 (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
4270 if lemma_type.unit() != Some(unit.as_ref()) =>
4271 {
4272 Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
4273 }
4274 _ => Arc::clone(lemma_type),
4275 };
4276 TypedLiteral {
4277 value: bound.value.clone(),
4278 lemma_type,
4279 }
4280}
4281
4282#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4289pub enum ValueKind {
4290 Number(RationalInteger),
4291 Measure(RationalInteger),
4297 UnscaledMeasure {
4301 magnitude: RationalInteger,
4302 unit: Arc<str>,
4303 },
4304 Text(Arc<str>),
4305 Date(SemanticDateTime),
4306 Time(SemanticTime),
4307 Boolean(bool),
4308 Ratio(RationalInteger),
4310 Range(Box<TypedLiteral>, Box<TypedLiteral>),
4312 Spec(SpecValue),
4314 List(ListValue),
4316}
4317
4318#[derive(Debug, Clone)]
4321pub struct ListValue {
4322 items: ListItems,
4323 index: Option<Arc<crate::planning::instance_list::KeyIndex>>,
4324}
4325
4326pub(crate) struct StoredRows<'l> {
4328 pub rows: &'l [Arc<RowInstance>],
4329 pub owner: Option<&'l InstanceKey>,
4330}
4331
4332#[derive(Debug, Clone)]
4333enum ListItems {
4334 Values(Arc<[TypedLiteral]>),
4335 Rows {
4338 store: Arc<crate::planning::instance_list::RowStore>,
4339 owner: Option<InstanceKey>,
4340 row_type: Arc<LemmaType>,
4341 },
4342}
4343
4344impl ListValue {
4345 fn held(items: Arc<[TypedLiteral]>) -> Self {
4347 Self {
4348 items: ListItems::Values(items),
4349 index: None,
4350 }
4351 }
4352
4353 pub(crate) fn indexed(
4354 items: Arc<[TypedLiteral]>,
4355 index: Arc<crate::planning::instance_list::KeyIndex>,
4356 ) -> Self {
4357 Self {
4358 items: ListItems::Values(items),
4359 index: Some(index),
4360 }
4361 }
4362
4363 pub(crate) fn stored_rows(
4365 store: &Arc<crate::planning::instance_list::RowStore>,
4366 owner: Option<InstanceKey>,
4367 row_type: Arc<LemmaType>,
4368 ) -> Self {
4369 Self {
4370 index: store.index.clone(),
4371 items: ListItems::Rows {
4372 store: Arc::clone(store),
4373 owner,
4374 row_type,
4375 },
4376 }
4377 }
4378
4379 pub fn len(&self) -> usize {
4380 match &self.items {
4381 ListItems::Values(items) => items.len(),
4382 ListItems::Rows { store, .. } => store.rows.len(),
4383 }
4384 }
4385
4386 pub fn is_empty(&self) -> bool {
4387 self.len() == 0
4388 }
4389
4390 pub fn get(&self, position: usize) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4391 match &self.items {
4392 ListItems::Values(items) => items.get(position).map(std::borrow::Cow::Borrowed),
4393 ListItems::Rows {
4394 store,
4395 owner,
4396 row_type,
4397 } => store.rows.get(position).map(|row| {
4398 std::borrow::Cow::Owned(TypedLiteral {
4399 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
4400 Arc::clone(row),
4401 owner.clone(),
4402 ))),
4403 lemma_type: Arc::clone(row_type),
4404 })
4405 }),
4406 }
4407 }
4408
4409 pub fn first(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4410 self.get(0)
4411 }
4412
4413 pub fn last(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4414 self.len().checked_sub(1).and_then(|last| self.get(last))
4415 }
4416
4417 pub fn iter(
4418 &self,
4419 ) -> impl DoubleEndedIterator<Item = std::borrow::Cow<'_, TypedLiteral>> + ExactSizeIterator
4420 {
4421 (0..self.len()).map(|position| {
4422 self.get(position)
4423 .expect("BUG: list position below the list length")
4424 })
4425 }
4426
4427 pub(crate) fn values(&self) -> &[TypedLiteral] {
4429 match &self.items {
4430 ListItems::Values(items) => items,
4431 ListItems::Rows { .. } => panic!("BUG: held items read from a list of stored rows"),
4432 }
4433 }
4434
4435 pub(crate) fn stored(&self) -> Option<StoredRows<'_>> {
4437 match &self.items {
4438 ListItems::Values(_) => None,
4439 ListItems::Rows { store, owner, .. } => Some(StoredRows {
4440 rows: &store.rows,
4441 owner: owner.as_ref(),
4442 }),
4443 }
4444 }
4445
4446 pub fn to_vec(&self) -> Vec<TypedLiteral> {
4448 self.iter().map(std::borrow::Cow::into_owned).collect()
4449 }
4450
4451 pub(crate) fn index(&self) -> Option<&Arc<crate::planning::instance_list::KeyIndex>> {
4452 self.index.as_ref()
4453 }
4454
4455 pub(crate) fn unindexed(&self) -> Self {
4457 Self {
4458 items: self.items.clone(),
4459 index: None,
4460 }
4461 }
4462
4463 pub(crate) fn with_index(&self, index: Arc<crate::planning::instance_list::KeyIndex>) -> Self {
4465 Self {
4466 items: self.items.clone(),
4467 index: Some(index),
4468 }
4469 }
4470}
4471
4472impl From<Arc<[TypedLiteral]>> for ListValue {
4473 fn from(items: Arc<[TypedLiteral]>) -> Self {
4474 Self::held(items)
4475 }
4476}
4477
4478impl From<Vec<TypedLiteral>> for ListValue {
4479 fn from(items: Vec<TypedLiteral>) -> Self {
4480 Self::held(Arc::from(items))
4481 }
4482}
4483
4484impl FromIterator<TypedLiteral> for ListValue {
4485 fn from_iter<I: IntoIterator<Item = TypedLiteral>>(iter: I) -> Self {
4486 Self::held(iter.into_iter().collect())
4487 }
4488}
4489
4490impl PartialEq for ListValue {
4491 fn eq(&self, other: &Self) -> bool {
4492 self.len() == other.len() && self.iter().eq(other.iter())
4493 }
4494}
4495
4496impl Eq for ListValue {}
4497
4498impl Hash for ListValue {
4499 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4500 self.len().hash(state);
4501 for item in self.iter() {
4502 item.hash(state);
4503 }
4504 }
4505}
4506
4507impl ValueKind {
4508 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
4510 match self {
4511 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
4512 n.try_to_decimal().map_err(|failure| failure.to_string())
4513 }
4514 other => Err(format!("expected numeric value kind, got {other}")),
4515 }
4516 }
4517
4518 pub fn structural_byte_size(&self) -> usize {
4523 fn rational_byte_estimate(r: &RationalInteger) -> usize {
4524 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
4525 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
4526 numer_bytes.max(1) + denom_bytes.max(1)
4527 }
4528 match self {
4529 ValueKind::Number(r) => rational_byte_estimate(r),
4530 ValueKind::Measure(r) => rational_byte_estimate(r),
4531 ValueKind::UnscaledMeasure { magnitude, unit } => {
4532 rational_byte_estimate(magnitude) + unit.len()
4533 }
4534 ValueKind::Ratio(r) => rational_byte_estimate(r),
4535 ValueKind::Text(s) => s.len(),
4536 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
4540 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
4541 }
4542 ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
4543 ValueKind::Spec(SpecValue::Row(InstanceKey { row, .. })) => row.structural_byte_size(),
4544 ValueKind::List(rows) => rows
4545 .iter()
4546 .map(|row| row.value.structural_byte_size())
4547 .sum(),
4548 }
4549 }
4550
4551 #[must_use]
4553 pub fn has_unscaled_measure(&self) -> bool {
4554 match self {
4555 ValueKind::UnscaledMeasure { .. } => true,
4556 ValueKind::Range(left, right) => match (&left.value, &right.value) {
4557 (ValueKind::UnscaledMeasure { .. }, _) | (_, ValueKind::UnscaledMeasure { .. }) => {
4558 true
4559 }
4560 (ValueKind::Range(_, _) | ValueKind::List(_), _)
4561 | (_, ValueKind::Range(_, _) | ValueKind::List(_)) => unscaled_in_value_tree(self),
4562 _ => false,
4563 },
4564 ValueKind::List(_) => unscaled_in_value_tree(self),
4565 _ => false,
4566 }
4567 }
4568}
4569
4570fn unscaled_in_value_tree(root: &ValueKind) -> bool {
4571 let mut pending = vec![root];
4572 while let Some(value) = pending.pop() {
4573 match value {
4574 ValueKind::UnscaledMeasure { .. } => return true,
4575 ValueKind::Range(left, right) => {
4576 pending.push(&right.value);
4577 pending.push(&left.value);
4578 }
4579 ValueKind::List(rows) => {
4580 if rows.stored().is_none() {
4581 for item in rows.values().iter() {
4582 pending.push(&item.value);
4583 }
4584 }
4585 }
4586 _ => {}
4587 }
4588 }
4589 false
4590}
4591
4592fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
4593 rational.display_str()
4594}
4595
4596fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
4597 use crate::parsing::ast::Value;
4598 match rational.try_to_decimal() {
4599 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
4600 Err(_) => format!("{} {}", rational.display_str(), unit),
4601 }
4602}
4603
4604impl fmt::Display for ValueKind {
4605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4606 match self {
4607 ValueKind::Number(rational) => {
4608 write!(f, "{}", format_rational_magnitude_for_display(rational))
4609 }
4610 ValueKind::Measure(rational) => {
4611 write!(f, "{}", format_rational_magnitude_for_display(rational))
4612 }
4613 ValueKind::UnscaledMeasure { magnitude, unit } => {
4614 write!(
4615 f,
4616 "{} {unit}",
4617 format_rational_magnitude_for_display(magnitude)
4618 )
4619 }
4620 ValueKind::Text(s) => {
4621 write!(f, "{}", crate::parsing::ast::Value::Text(s.to_string()))
4622 }
4623 ValueKind::Ratio(rational) => {
4624 write!(f, "{}", format_rational_magnitude_for_display(rational))
4625 }
4626 ValueKind::Date(dt) => write!(f, "{}", dt),
4627 ValueKind::Time(t) => write!(
4628 f,
4629 "{}",
4630 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
4631 hour: t.hour as u8,
4632 minute: t.minute as u8,
4633 second: t.second as u8,
4634 microsecond: t.microsecond,
4635 timezone: t
4636 .timezone
4637 .as_ref()
4638 .map(crate::parsing::ast::TimezoneValue::from),
4639 })
4640 ),
4641 ValueKind::Boolean(b) => write!(f, "{}", b),
4642 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4643 ValueKind::Spec(value) => write!(f, "{value}"),
4644 ValueKind::List(rows) => {
4645 write!(f, "list[")?;
4646 for (i, row) in rows.iter().enumerate() {
4647 if i > 0 {
4648 write!(f, ", ")?;
4649 }
4650 write!(f, "{row}")?;
4651 }
4652 write!(f, "]")
4653 }
4654 }
4655 }
4656}
4657
4658#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4670pub struct PathSegment {
4671 pub uses: Arc<str>,
4673 pub row: Option<usize>,
4675 pub repository: Option<Arc<str>>,
4678 pub spec: Arc<str>,
4680}
4681
4682impl PathSegment {
4683 pub(crate) fn import(alias: &str, identity: &SpecIdentity) -> Self {
4685 Self {
4686 uses: Arc::from(alias),
4687 row: None,
4688 repository: identity.repository.as_deref().map(Arc::from),
4689 spec: Arc::from(identity.spec.as_str()),
4690 }
4691 }
4692
4693 pub(crate) fn list_row_shared(
4697 list: &Arc<str>,
4698 number: usize,
4699 spec: &Arc<str>,
4700 repository: Option<&Arc<str>>,
4701 ) -> Self {
4702 Self {
4703 uses: Arc::clone(list),
4704 row: Some(number),
4705 repository: repository.map(Arc::clone),
4706 spec: Arc::clone(spec),
4707 }
4708 }
4709
4710 pub fn input_key(&self) -> String {
4712 match self.row {
4713 Some(number) => format!("{}.{number}", self.uses),
4714 None => self.uses.to_string(),
4715 }
4716 }
4717}
4718
4719#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4722pub struct SpecInstance {
4723 pub prefix: Vec<PathSegment>,
4724}
4725
4726impl SpecInstance {
4727 pub fn structural_byte_size(&self) -> usize {
4728 self.prefix
4729 .iter()
4730 .map(|segment| {
4731 segment.uses.len()
4732 + segment.spec.len()
4733 + segment.repository.as_deref().map_or(0, str::len)
4734 + segment.row.map_or(0, |_| std::mem::size_of::<usize>())
4735 })
4736 .sum()
4737 }
4738}
4739
4740impl fmt::Display for SpecInstance {
4741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4742 for (index, segment) in self.prefix.iter().enumerate() {
4743 if index > 0 {
4744 write!(f, ".")?;
4745 }
4746 write!(f, "{}", segment.input_key())?;
4747 }
4748 Ok(())
4749 }
4750}
4751
4752#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4762pub enum SpecValue {
4763 Fixed(SpecInstance),
4764 Row(InstanceKey),
4765}
4766
4767impl fmt::Display for SpecValue {
4768 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4769 match self {
4770 SpecValue::Fixed(instance) => write!(f, "{instance}"),
4771 SpecValue::Row(key) => write!(f, "{key}"),
4772 }
4773 }
4774}
4775
4776#[derive(Debug, Clone)]
4780pub struct InstanceKey {
4781 pub row: Arc<RowInstance>,
4782 pub owner: Option<Box<InstanceKey>>,
4783}
4784
4785impl InstanceKey {
4786 pub fn new(row: Arc<RowInstance>, owner: Option<InstanceKey>) -> Self {
4787 Self {
4788 row,
4789 owner: owner.map(Box::new),
4790 }
4791 }
4792
4793 pub fn owner(&self) -> Option<&InstanceKey> {
4794 self.owner.as_deref()
4795 }
4796
4797 pub(crate) fn scope(&self, hops: u32) -> Option<&InstanceKey> {
4800 let mut scope = self.owner();
4801 for _ in 0..hops {
4802 scope = scope
4803 .expect("BUG: a binding hops above the root scope")
4804 .owner();
4805 }
4806 scope
4807 }
4808
4809 pub(crate) fn under(&self, owner: &InstanceKey) -> InstanceKey {
4812 let mut rows: Vec<&Arc<RowInstance>> = Vec::new();
4813 let mut key = Some(self);
4814 while let Some(current) = key {
4815 rows.push(¤t.row);
4816 key = current.owner();
4817 }
4818 let mut rebased = owner.clone();
4819 for row in rows.into_iter().rev() {
4820 rebased = InstanceKey::new(Arc::clone(row), Some(rebased));
4821 }
4822 rebased
4823 }
4824
4825 fn naming_origins(&self) -> Vec<&RowOrigin> {
4827 let mut origins = Vec::new();
4828 let mut key = Some(self);
4829 while let Some(current) = key {
4830 origins.push(¤t.row.origin);
4831 if matches!(current.row.origin, RowOrigin::Numbered { .. }) {
4832 break;
4833 }
4834 key = current.owner();
4835 }
4836 origins
4837 }
4838
4839 pub fn path(&self) -> Option<String> {
4843 let origins = self.naming_origins();
4844 if origins.iter().any(|origin| origin.is_inline()) {
4845 return None;
4846 }
4847 Some(
4848 origins
4849 .iter()
4850 .rev()
4851 .map(ToString::to_string)
4852 .collect::<Vec<_>>()
4853 .join("."),
4854 )
4855 }
4856
4857 pub(crate) fn label_under(&self, row: &InstanceKey) -> Option<String> {
4860 let mut below: Vec<&RowOrigin> = Vec::new();
4861 let mut key = self;
4862 while key != row {
4863 below.push(&key.row.origin);
4864 key = key.owner()?;
4865 }
4866 Some(
4867 below
4868 .iter()
4869 .rev()
4870 .map(ToString::to_string)
4871 .collect::<Vec<_>>()
4872 .join("."),
4873 )
4874 }
4875
4876 pub(crate) fn table_row(&self) -> (&InstanceKey, Option<String>) {
4880 let mut key = self;
4881 loop {
4882 match &key.row.origin {
4883 RowOrigin::Numbered { .. } => return (key, self.path()),
4884 RowOrigin::Inline { .. } if key == self => return (key, None),
4885 RowOrigin::Inline { .. } => return (key, self.label_under(key)),
4886 RowOrigin::Import { .. } | RowOrigin::Written { .. } => {
4887 key = key
4888 .owner()
4889 .expect("BUG: an instance off the rows table published as a table row");
4890 }
4891 }
4892 }
4893 }
4894
4895 pub(crate) fn field_key(&self, key: &str) -> Option<String> {
4898 self.request_path().map(|path| format!("{path}.{key}"))
4899 }
4900
4901 fn request_path(&self) -> Option<String> {
4904 let origins = self.naming_origins();
4905 if origins.iter().any(|origin| origin.is_inline()) {
4906 return None;
4907 }
4908 Some(
4909 origins
4910 .iter()
4911 .rev()
4912 .map(|origin| match origin {
4913 RowOrigin::Numbered {
4914 under: Some(under), ..
4915 } => format!("{under}.{origin}"),
4916 origin => origin.to_string(),
4917 })
4918 .collect::<Vec<_>>()
4919 .join("."),
4920 )
4921 }
4922
4923 pub(crate) fn segments(&self) -> Option<Vec<PathSegment>> {
4926 let mut parts: Vec<Vec<PathSegment>> = Vec::new();
4927 let mut key = Some(self);
4928 while let Some(current) = key {
4929 match ¤t.row.origin {
4930 RowOrigin::Import { alias } => {
4931 parts.push(vec![PathSegment::import(alias, ¤t.row.identity)])
4932 }
4933 RowOrigin::Written { instance, .. } => parts.push(instance.prefix.clone()),
4934 RowOrigin::Numbered { .. } | RowOrigin::Inline { .. } => return None,
4935 }
4936 key = current.owner();
4937 }
4938 Some(parts.into_iter().rev().flatten().collect())
4939 }
4940
4941 pub(crate) fn import_hops(&self) -> Vec<PathSegment> {
4944 let mut hops = Vec::new();
4945 let mut key = Some(self);
4946 while let Some(current) = key {
4947 let RowOrigin::Import { alias } = ¤t.row.origin else {
4948 break;
4949 };
4950 hops.push(PathSegment::import(alias, ¤t.row.identity));
4951 key = current.owner();
4952 }
4953 hops.reverse();
4954 hops
4955 }
4956
4957 pub(crate) fn is_planned(&self) -> bool {
4960 let mut key = Some(self);
4961 while let Some(current) = key {
4962 if !matches!(
4963 current.row.origin,
4964 RowOrigin::Import { .. } | RowOrigin::Written { .. }
4965 ) {
4966 return false;
4967 }
4968 key = current.owner();
4969 }
4970 true
4971 }
4972}
4973
4974impl fmt::Display for InstanceKey {
4975 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4976 for (index, origin) in self.naming_origins().iter().rev().enumerate() {
4977 if index > 0 {
4978 write!(f, ".")?;
4979 }
4980 write!(f, "{origin}")?;
4981 }
4982 Ok(())
4983 }
4984}
4985
4986impl PartialEq for InstanceKey {
4987 fn eq(&self, other: &Self) -> bool {
4988 Arc::ptr_eq(&self.row, &other.row) && self.owner == other.owner
4989 }
4990}
4991
4992impl Eq for InstanceKey {}
4993
4994impl std::hash::Hash for InstanceKey {
4995 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4996 (Arc::as_ptr(&self.row) as usize).hash(state);
4997 self.owner.hash(state);
4998 }
4999}
5000
5001#[derive(Debug, Clone)]
5008pub enum RowBinding {
5009 Bound(OperationResult),
5010 Deferred {
5014 target: ReferenceTarget,
5015 hops: u32,
5016 },
5017 Rows {
5021 rows: Vec<Arc<RowInstance>>,
5022 row_type: Arc<LemmaType>,
5023 hops: u32,
5024 },
5025 Instance(Arc<RowInstance>),
5028}
5029
5030impl RowBinding {
5031 pub(crate) fn is_copy_of(&self, other: &RowBinding) -> bool {
5033 match (self, other) {
5034 (RowBinding::Bound(a), RowBinding::Bound(b)) => a == b,
5035 (
5036 RowBinding::Deferred { target, hops },
5037 RowBinding::Deferred {
5038 target: other_target,
5039 hops: other_hops,
5040 },
5041 ) => target == other_target && hops == other_hops,
5042 (
5043 RowBinding::Rows { rows, hops, .. },
5044 RowBinding::Rows {
5045 rows: other_rows,
5046 hops: other_hops,
5047 ..
5048 },
5049 ) => {
5050 hops == other_hops
5051 && rows.len() == other_rows.len()
5052 && rows
5053 .iter()
5054 .zip(other_rows)
5055 .all(|(row, other)| Arc::ptr_eq(row, other))
5056 }
5057 (RowBinding::Instance(row), RowBinding::Instance(other)) => Arc::ptr_eq(row, other),
5058 _ => false,
5059 }
5060 }
5061
5062 pub(crate) fn rows_value(
5064 rows: &[Arc<RowInstance>],
5065 row_type: &Arc<LemmaType>,
5066 owner: Option<&InstanceKey>,
5067 ) -> OperationResult {
5068 OperationResult::from_literal(LiteralValue {
5069 value: ValueKind::List(
5070 rows.iter()
5071 .map(|row| TypedLiteral {
5072 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5073 Arc::clone(row),
5074 owner.cloned(),
5075 ))),
5076 lemma_type: Arc::clone(row_type),
5077 })
5078 .collect(),
5079 ),
5080 })
5081 }
5082}
5083
5084#[derive(Debug, Clone)]
5089pub struct RowInstance {
5090 pub identity: Arc<SpecIdentity>,
5091 pub origin: RowOrigin,
5092 pub bindings: Box<[Option<RowBinding>]>,
5093}
5094
5095impl RowInstance {
5096 pub fn get_binding(
5097 &self,
5098 template: &crate::planning::execution_plan::ExecutionPlan,
5099 path: &DataPath,
5100 ) -> Option<&RowBinding> {
5101 let index = template.data.get_index_of(path)?;
5102 self.bindings.get(index)?.as_ref()
5103 }
5104
5105 pub fn structural_byte_size(&self) -> usize {
5107 let mut size = 0;
5108 let mut pending: Vec<&RowInstance> = vec![self];
5109 while let Some(row) = pending.pop() {
5110 for binding in row.bindings.iter().flatten() {
5111 match binding {
5112 RowBinding::Bound(OperationResult::Value(bound)) => {
5113 size += bound.value.structural_byte_size();
5114 }
5115 RowBinding::Rows { rows, .. } => pending.extend(rows.iter().map(Arc::as_ref)),
5116 RowBinding::Instance(row) => pending.push(row),
5117 RowBinding::Bound(OperationResult::Veto(_)) | RowBinding::Deferred { .. } => {}
5118 }
5119 }
5120 }
5121 size
5122 }
5123}
5124
5125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5127pub enum RowOrigin {
5128 Numbered {
5132 list: String,
5133 number: usize,
5134 under: Option<Arc<str>>,
5135 },
5136 Written {
5139 instance: SpecInstance,
5140 source: Source,
5141 },
5142 Import { alias: Arc<str> },
5144 Inline { node: NormalFormId, position: usize },
5150}
5151
5152#[must_use]
5154pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
5155 format!("{list_key}.{number}")
5156}
5157
5158#[must_use]
5160pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
5161 format!("{}.{field_key}", numbered_row_origin(list_key, number))
5162}
5163
5164impl RowOrigin {
5165 pub(crate) fn list_position(&self) -> (usize, bool) {
5167 match self {
5168 RowOrigin::Numbered { number, .. } => (*number, false),
5169 RowOrigin::Written { instance, .. } => (
5170 instance
5171 .prefix
5172 .last()
5173 .and_then(|last| last.row)
5174 .expect("BUG: written row origin without a row hop"),
5175 false,
5176 ),
5177 RowOrigin::Inline { position, .. } => (*position, true),
5178 RowOrigin::Import { alias } => {
5179 unreachable!("BUG: the row of `uses {alias}` is not a list row")
5180 }
5181 }
5182 }
5183
5184 pub(crate) fn is_inline(&self) -> bool {
5185 matches!(self, RowOrigin::Inline { .. })
5186 }
5187}
5188
5189impl fmt::Display for RowOrigin {
5190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5191 match self {
5192 RowOrigin::Numbered { list, number, .. } => {
5193 write!(f, "{}", numbered_row_origin(list, *number))
5194 }
5195 RowOrigin::Written { instance, .. } => write!(f, "{instance}"),
5196 RowOrigin::Import { alias } => write!(f, "{alias}"),
5197 RowOrigin::Inline { position, .. } => write!(f, "appended row {position}"),
5198 }
5199 }
5200}
5201
5202#[must_use]
5204pub fn row_literal(
5205 identity: Arc<SpecIdentity>,
5206 origin: RowOrigin,
5207 bindings: Vec<Option<RowBinding>>,
5208 lemma_type: Arc<LemmaType>,
5209) -> TypedLiteral {
5210 TypedLiteral {
5211 value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5212 Arc::new(RowInstance {
5213 identity,
5214 origin,
5215 bindings: bindings.into(),
5216 }),
5217 None,
5218 ))),
5219 lemma_type,
5220 }
5221}
5222
5223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5225pub enum RowEnd {
5226 Data(DataPath),
5227 Rule(RulePath),
5228}
5229
5230impl fmt::Display for RowEnd {
5231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5232 match self {
5233 RowEnd::Data(path) => write!(f, "{}", path.input_key()),
5234 RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
5235 }
5236 }
5237}
5238
5239#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5243pub struct DataPath {
5244 pub segments: Vec<PathSegment>,
5246 pub data: String,
5248}
5249
5250impl DataPath {
5251 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
5253 Self { segments, data }
5254 }
5255
5256 pub fn local(data: String) -> Self {
5258 Self {
5259 segments: vec![],
5260 data,
5261 }
5262 }
5263
5264 pub fn input_key(&self) -> String {
5267 let mut s = String::new();
5268 for segment in &self.segments {
5269 s.push_str(&segment.input_key());
5270 s.push('.');
5271 }
5272 s.push_str(&self.data);
5273 s
5274 }
5275}
5276
5277#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5281pub struct RulePath {
5282 pub segments: Vec<PathSegment>,
5284 pub rule: String,
5286}
5287
5288impl RulePath {
5289 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
5291 Self { segments, rule }
5292 }
5293
5294 pub fn input_key(&self) -> String {
5297 if self.segments.is_empty() {
5298 return self.rule.clone();
5299 }
5300 let mut s = String::new();
5301 for segment in &self.segments {
5302 s.push_str(&segment.input_key());
5303 s.push('.');
5304 }
5305 s.push_str(&self.rule);
5306 s
5307 }
5308}
5309
5310pub type Expression = ExprTree<ExpressionKind>;
5323
5324#[derive(Debug, Clone, PartialEq)]
5326pub enum ExpressionKind {
5327 Literal(Box<TypedLiteral>),
5329 DataPath(DataPath),
5331 RulePath(RulePath),
5333 Logical {
5334 connective: LogicalConnective,
5335 first: ExprId,
5336 steps: Vec<ChainStep<()>>,
5337 },
5338 Arithmetic {
5340 first: ExprId,
5341 steps: Vec<ChainStep<ArithmeticComputation>>,
5342 },
5343 Comparison(ExprId, ComparisonComputation, ExprId),
5344 UnitConversion(ExprId, SemanticConversionTarget),
5345 LogicalNegation(ExprId, NegationType),
5346 MathematicalComputation(MathematicalComputation, ExprId),
5347 StepRounding {
5349 operand: ExprId,
5350 mode: RoundingMode,
5351 step: Option<ExprId>,
5352 },
5353 Veto(VetoExpression),
5354 Now,
5356 DateRelative(DateRelativeKind, ExprId),
5358 DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
5360 RangeLiteral(ExprId, ExprId),
5361 PastFutureRange(DateRelativeKind, ExprId),
5362 RangeContainment(ExprId, ExprId),
5363 RangeBound(RangeBound, ExprId),
5365 ResultIsVeto(ExprId),
5367 SpecMember {
5373 base: ExprId,
5374 path: Vec<String>,
5375 name: String,
5376 },
5377 RowField {
5382 level: u32,
5383 identity: SpecIdentity,
5384 end: RowEnd,
5385 qualifier: Option<Vec<String>>,
5388 },
5389 RowValue {
5391 level: u32,
5392 list: ExprId,
5393 },
5394 Piecewise(Vec<(ExprId, ExprId)>),
5397 ListCommand {
5400 list: ExprId,
5401 op: ListOp,
5402 row_level: Option<u32>,
5403 },
5404}
5405
5406#[derive(Debug, Clone, PartialEq)]
5408pub enum ListOp {
5409 Find {
5410 keys: Vec<ExprId>,
5411 },
5412 First,
5413 Last,
5414 Count,
5415 Reduce {
5416 kind: ListReduce,
5417 argument: Option<ExprId>,
5418 },
5419 Predicate {
5420 kind: ListPredKind,
5421 form: crate::parsing::ast::ListPredicateForm,
5422 expr: ExprId,
5423 },
5424 Sort {
5425 descending: bool,
5426 argument: Option<ExprId>,
5427 },
5428 Top {
5429 n: ExprId,
5430 },
5431 With {
5432 rows: Vec<ExprId>,
5433 },
5434 WithRow {
5436 identity: SpecIdentity,
5437 fields: Vec<(DataPath, ExprId)>,
5438 },
5439 Key {
5441 fields: Vec<DataPath>,
5442 },
5443}
5444
5445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5446pub enum ListReduce {
5447 Sum,
5448 Avg,
5449 Product,
5450 Min,
5451 Max,
5452}
5453
5454impl ListReduce {
5455 pub fn keyword(self) -> &'static str {
5456 match self {
5457 ListReduce::Sum => "sum",
5458 ListReduce::Avg => "avg",
5459 ListReduce::Product => "product",
5460 ListReduce::Min => "min",
5461 ListReduce::Max => "max",
5462 }
5463 }
5464}
5465
5466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5467pub enum ListPredKind {
5468 Any,
5469 All,
5470 Where,
5471}
5472
5473impl ListPredKind {
5474 pub fn keyword(self) -> &'static str {
5475 match self {
5476 ListPredKind::Any => "any",
5477 ListPredKind::All => "all",
5478 ListPredKind::Where => "where",
5479 }
5480 }
5481}
5482
5483impl ExprKind for ExpressionKind {
5484 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5485 match self {
5486 ExpressionKind::Literal(_)
5487 | ExpressionKind::DataPath(_)
5488 | ExpressionKind::RulePath(_)
5489 | ExpressionKind::Veto(_)
5490 | ExpressionKind::Now
5491 | ExpressionKind::RowField { .. }
5492 | ExpressionKind::RowValue { .. } => {}
5493 ExpressionKind::Logical { first, steps, .. } => {
5494 f(*first);
5495 steps.iter().for_each(|step| f(step.operand));
5496 }
5497 ExpressionKind::Arithmetic { first, steps } => {
5498 f(*first);
5499 steps.iter().for_each(|step| f(step.operand));
5500 }
5501 ExpressionKind::Comparison(left, _, right)
5502 | ExpressionKind::RangeLiteral(left, right)
5503 | ExpressionKind::RangeContainment(left, right) => {
5504 f(*left);
5505 f(*right);
5506 }
5507 ExpressionKind::StepRounding { operand, step, .. } => {
5508 f(*operand);
5509 if let Some(step) = step {
5510 f(*step);
5511 }
5512 }
5513 ExpressionKind::UnitConversion(inner, _)
5514 | ExpressionKind::LogicalNegation(inner, _)
5515 | ExpressionKind::MathematicalComputation(_, inner)
5516 | ExpressionKind::DateRelative(_, inner)
5517 | ExpressionKind::DateCalendar(_, _, inner)
5518 | ExpressionKind::PastFutureRange(_, inner)
5519 | ExpressionKind::RangeBound(_, inner)
5520 | ExpressionKind::ResultIsVeto(inner)
5521 | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
5522 ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
5523 f(*condition);
5524 f(*result);
5525 }),
5526 ExpressionKind::ListCommand { list, op, .. } => {
5527 f(*list);
5528 op.for_each_child(f);
5529 }
5530 }
5531 }
5532
5533 fn children_mut(&mut self) -> Vec<&mut ExprId> {
5534 match self {
5535 ExpressionKind::Literal(_)
5536 | ExpressionKind::DataPath(_)
5537 | ExpressionKind::RulePath(_)
5538 | ExpressionKind::Veto(_)
5539 | ExpressionKind::Now
5540 | ExpressionKind::RowField { .. }
5541 | ExpressionKind::RowValue { .. } => Vec::new(),
5542 ExpressionKind::Logical { first, steps, .. } => std::iter::once(first)
5543 .chain(steps.iter_mut().map(|step| &mut step.operand))
5544 .collect(),
5545 ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
5546 .chain(steps.iter_mut().map(|step| &mut step.operand))
5547 .collect(),
5548 ExpressionKind::Comparison(left, _, right)
5549 | ExpressionKind::RangeLiteral(left, right)
5550 | ExpressionKind::RangeContainment(left, right) => vec![left, right],
5551 ExpressionKind::StepRounding { operand, step, .. } => {
5552 let mut children = vec![operand];
5553 if let Some(step) = step {
5554 children.push(step);
5555 }
5556 children
5557 }
5558 ExpressionKind::UnitConversion(inner, _)
5559 | ExpressionKind::LogicalNegation(inner, _)
5560 | ExpressionKind::MathematicalComputation(_, inner)
5561 | ExpressionKind::DateRelative(_, inner)
5562 | ExpressionKind::DateCalendar(_, _, inner)
5563 | ExpressionKind::PastFutureRange(_, inner)
5564 | ExpressionKind::RangeBound(_, inner)
5565 | ExpressionKind::ResultIsVeto(inner) => vec![inner],
5566 ExpressionKind::SpecMember { base, .. } => vec![base],
5567 ExpressionKind::Piecewise(arms) => arms
5568 .iter_mut()
5569 .flat_map(|(condition, result)| [condition, result])
5570 .collect(),
5571 ExpressionKind::ListCommand { list, op, .. } => {
5572 let mut children = vec![list];
5573 children.extend(op.children_mut());
5574 children
5575 }
5576 }
5577 }
5578}
5579
5580impl ListOp {
5581 fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5582 match self {
5583 ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => {}
5584 ListOp::Find { keys } => keys.iter().copied().for_each(f),
5585 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5586 argument.iter().copied().for_each(f)
5587 }
5588 ListOp::Predicate { expr, .. } => f(*expr),
5589 ListOp::Top { n } => f(*n),
5590 ListOp::With { rows } => rows.iter().copied().for_each(f),
5591 ListOp::WithRow { fields, .. } => fields.iter().for_each(|(_, value)| f(*value)),
5592 }
5593 }
5594
5595 fn children_mut(&mut self) -> Vec<&mut ExprId> {
5596 match self {
5597 ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => Vec::new(),
5598 ListOp::Find { keys } => keys.iter_mut().collect(),
5599 ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5600 argument.iter_mut().collect()
5601 }
5602 ListOp::Predicate { expr, .. } => vec![expr],
5603 ListOp::Top { n } => vec![n],
5604 ListOp::With { rows } => rows.iter_mut().collect(),
5605 ListOp::WithRow { fields, .. } => fields.iter_mut().map(|(_, value)| value).collect(),
5606 }
5607 }
5608}
5609
5610#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5616pub enum TypeDefiningSpec {
5617 Local,
5619 Import,
5621}
5622
5623#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
5630pub struct TypeFamily {
5631 pub repository: Option<String>,
5632 pub spec: String,
5633 pub effective: crate::parsing::ast::EffectiveDate,
5634 pub name: String,
5635}
5636
5637impl TypeFamily {
5638 #[must_use]
5639 pub(crate) fn declared_in(spec: &SpecVersionKey, name: &str) -> Self {
5640 Self {
5641 repository: spec.repository.clone(),
5642 spec: spec.spec.clone(),
5643 effective: spec.effective.clone(),
5644 name: name.to_string(),
5645 }
5646 }
5647
5648 #[must_use]
5650 pub(crate) fn is_declared_in(&self, spec: &SpecVersionKey) -> bool {
5651 self.repository == spec.repository
5652 && self.spec == spec.spec
5653 && self.effective == spec.effective
5654 }
5655
5656 #[must_use]
5658 pub fn without_effective(&self) -> Self {
5659 Self {
5660 repository: self.repository.clone(),
5661 spec: self.spec.clone(),
5662 effective: crate::parsing::ast::EffectiveDate::Origin,
5663 name: self.name.clone(),
5664 }
5665 }
5666}
5667
5668impl fmt::Display for TypeFamily {
5670 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5671 if let Some(repository) = &self.repository {
5672 write!(f, "{repository} ")?;
5673 }
5674 write!(f, "{}", self.spec)?;
5675 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
5676 write!(f, " {at}")?;
5677 }
5678 write!(f, ".{}", self.name)
5679 }
5680}
5681
5682#[derive(Clone, Debug)]
5684pub enum TypeExtends {
5685 Primitive,
5687 Custom {
5690 parent: String,
5691 family: TypeFamily,
5692 defining_spec: TypeDefiningSpec,
5693 },
5694}
5695
5696impl PartialEq for TypeExtends {
5697 fn eq(&self, other: &Self) -> bool {
5698 match (self, other) {
5699 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
5700 (
5701 TypeExtends::Custom {
5702 parent: lp,
5703 family: lf,
5704 defining_spec: ld,
5705 },
5706 TypeExtends::Custom {
5707 parent: rp,
5708 family: rf,
5709 defining_spec: rd,
5710 },
5711 ) => lp == rp && lf == rf && ld == rd,
5712 _ => false,
5713 }
5714 }
5715}
5716
5717impl Eq for TypeExtends {}
5718
5719impl std::hash::Hash for TypeExtends {
5720 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
5721 match self {
5722 TypeExtends::Primitive => {
5723 0u8.hash(state);
5724 }
5725 TypeExtends::Custom {
5726 parent,
5727 family,
5728 defining_spec,
5729 } => {
5730 1u8.hash(state);
5731 parent.hash(state);
5732 family.hash(state);
5733 defining_spec.hash(state);
5734 }
5735 }
5736 }
5737}
5738
5739impl TypeExtends {
5740 #[must_use]
5742 pub fn custom_local(parent: String, family: TypeFamily) -> Self {
5743 TypeExtends::Custom {
5744 parent,
5745 family,
5746 defining_spec: TypeDefiningSpec::Local,
5747 }
5748 }
5749
5750 #[must_use]
5752 pub fn parent_name(&self) -> Option<&str> {
5753 match self {
5754 TypeExtends::Primitive => None,
5755 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
5756 }
5757 }
5758
5759 #[must_use]
5760 fn with_families_without_effective(&self) -> Self {
5761 match self {
5762 TypeExtends::Primitive => TypeExtends::Primitive,
5763 TypeExtends::Custom {
5764 parent,
5765 family,
5766 defining_spec,
5767 } => TypeExtends::Custom {
5768 parent: parent.clone(),
5769 family: family.without_effective(),
5770 defining_spec: defining_spec.clone(),
5771 },
5772 }
5773 }
5774}
5775
5776#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5781pub struct LemmaType {
5782 pub name: Option<String>,
5784 pub specifications: TypeSpecification,
5786 pub extends: TypeExtends,
5788 pub(crate) options: TypeOptions,
5789}
5790
5791impl LemmaType {
5792 pub fn map_measure<F>(self, f: F) -> Self
5796 where
5797 F: FnOnce(
5798 MeasureUnits,
5799 Option<BaseMeasureVector>,
5800 ) -> (MeasureUnits, Option<BaseMeasureVector>),
5801 {
5802 let LemmaType {
5803 name,
5804 specifications,
5805 extends,
5806 options,
5807 } = self;
5808 let specifications = match specifications {
5809 TypeSpecification::Measure {
5810 minimum,
5811 maximum,
5812 units,
5813 traits,
5814 decomposition,
5815 unit,
5816 help,
5817 } => {
5818 let (units, decomposition) = f(units, decomposition);
5819 TypeSpecification::Measure {
5820 minimum,
5821 maximum,
5822 units,
5823 traits,
5824 decomposition,
5825 unit,
5826 help,
5827 }
5828 }
5829 other => other,
5830 };
5831 LemmaType {
5832 name,
5833 specifications,
5834 extends,
5835 options,
5836 }
5837 }
5838
5839 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
5841 Self {
5842 name: Some(name),
5843 specifications,
5844 extends,
5845 options: TypeOptions::default(),
5846 }
5847 }
5848
5849 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
5851 Self {
5852 name: None,
5853 specifications,
5854 extends,
5855 options: TypeOptions::default(),
5856 }
5857 }
5858
5859 pub fn primitive(specifications: TypeSpecification) -> Self {
5861 Self {
5862 name: None,
5863 specifications,
5864 extends: TypeExtends::Primitive,
5865 options: TypeOptions::default(),
5866 }
5867 }
5868
5869 pub fn apply_constraint(
5873 &mut self,
5874 names: ConstraintNames<'_>,
5875 command: TypeConstraintCommand,
5876 args: &[CommandArg],
5877 declared_suggestion: &mut Option<RawSuggestion>,
5878 declared_value: &mut Option<RawSuggestion>,
5879 resolve_unit: Option<&UnitTokenResolver<'_>>,
5880 ) -> Result<(), String> {
5881 self.specifications.apply_constraint(
5882 names,
5883 command,
5884 args,
5885 declared_suggestion,
5886 declared_value,
5887 resolve_unit,
5888 )
5889 }
5890
5891 pub(crate) fn reject_disallowed(&self, value: &ValueKind) -> Result<(), String> {
5893 let options = self.options_for_check();
5894 if options.rule.is_some() || options.values.is_empty() {
5895 return Ok(());
5896 }
5897 if static_option_match(&options, value) {
5898 return Ok(());
5899 }
5900 Err(disallowed_message(self, &options, value, None))
5901 }
5902
5903 #[must_use]
5905 pub fn options_rule(&self) -> Option<&str> {
5906 if let Some(name) = &self.options.rule {
5907 return Some(name.as_str());
5908 }
5909 if let TypeSpecification::List { element, .. } = &self.specifications {
5910 if let Some(name) = &element.options.rule {
5911 return Some(name.as_str());
5912 }
5913 }
5914 None
5915 }
5916
5917 #[must_use]
5920 pub(crate) fn needs_runtime_option_check(&self) -> bool {
5921 self.options_rule().is_some()
5922 }
5923
5924 #[must_use]
5926 pub fn static_options(&self) -> Vec<String> {
5927 let options = self.options_for_check();
5928 options
5929 .values
5930 .iter()
5931 .map(|option| self.display_bound(&option.value))
5932 .collect()
5933 }
5934
5935 #[must_use]
5938 pub fn inline_option_values(&self) -> Vec<BoundValueKind> {
5939 self.options_for_check()
5940 .values
5941 .iter()
5942 .map(|option| option.value.clone())
5943 .collect()
5944 }
5945
5946 pub(crate) fn display_bound(&self, bound: &BoundValueKind) -> String {
5947 typed_literal_from_bound(bound, &Arc::new(self.clone())).display_value()
5948 }
5949
5950 pub(crate) fn options_for_check(&self) -> std::borrow::Cow<'_, TypeOptions> {
5952 match &self.specifications {
5953 TypeSpecification::List { element, .. }
5954 if !self.options.is_empty() && !element.options.is_empty() =>
5955 {
5956 let mut options = self.options.clone();
5957 options
5958 .values
5959 .extend(element.options.values.iter().cloned());
5960 if options.rule.is_none() {
5961 options.rule.clone_from(&element.options.rule);
5962 }
5963 std::borrow::Cow::Owned(options)
5964 }
5965 TypeSpecification::List { element, .. } if !element.options.is_empty() => {
5966 std::borrow::Cow::Borrowed(&element.options)
5967 }
5968 _ => std::borrow::Cow::Borrowed(&self.options),
5969 }
5970 }
5971
5972 pub(crate) fn explain_disallowed(
5974 &self,
5975 value: &ValueKind,
5976 rule_rows: Option<&ListValue>,
5977 ) -> Option<String> {
5978 let options = self.options_for_check();
5979 let allowed = |candidate: &ValueKind| {
5980 if static_option_match(&options, candidate) {
5981 return true;
5982 }
5983 if let Some(rows) = rule_rows {
5984 return rows
5985 .iter()
5986 .any(|option| option_values_match(&option.value, candidate));
5987 }
5988 options.is_empty()
5989 };
5990 match (&self.specifications, value) {
5991 (TypeSpecification::List { .. }, ValueKind::List(rows)) => {
5992 for row in rows.iter() {
5993 if !allowed(&row.value) {
5994 return Some(disallowed_message(self, &options, &row.value, rule_rows));
5995 }
5996 }
5997 None
5998 }
5999 _ => {
6000 if allowed(value) {
6001 None
6002 } else {
6003 Some(disallowed_message(self, &options, value, rule_rows))
6004 }
6005 }
6006 }
6007 }
6008
6009 #[must_use]
6013 pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
6014 let unit_name = unit_name.into();
6015 match &mut self.specifications {
6016 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6017 *unit = Some(unit_name);
6018 }
6019 other => panic!(
6020 "BUG: with_unit called on non-measure/non-ratio type {}",
6021 other
6022 ),
6023 }
6024 self
6025 }
6026
6027 #[must_use]
6029 pub fn unit(&self) -> Option<&str> {
6030 match &self.specifications {
6031 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6032 unit.as_deref()
6033 }
6034 _ => None,
6035 }
6036 }
6037
6038 pub fn name(&self) -> String {
6040 self.name
6041 .clone()
6042 .unwrap_or_else(|| self.specifications.to_string())
6043 }
6044
6045 pub fn is_boolean(&self) -> bool {
6047 matches!(&self.specifications, TypeSpecification::Boolean { .. })
6048 }
6049
6050 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
6051 matches!(
6052 (kind, &self.specifications),
6053 (PrimitiveKind::Number, TypeSpecification::Number { .. })
6054 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
6055 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
6056 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
6057 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
6058 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
6059 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
6060 )
6061 }
6062
6063 pub fn is_measure(&self) -> bool {
6065 matches!(&self.specifications, TypeSpecification::Measure { .. })
6066 }
6067
6068 pub fn is_measure_range(&self) -> bool {
6069 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
6070 }
6071
6072 pub fn is_number(&self) -> bool {
6074 matches!(&self.specifications, TypeSpecification::Number { .. })
6075 }
6076
6077 pub fn is_number_range(&self) -> bool {
6078 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
6079 }
6080
6081 pub fn is_numeric(&self) -> bool {
6083 matches!(
6084 &self.specifications,
6085 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
6086 )
6087 }
6088
6089 pub fn is_text(&self) -> bool {
6091 matches!(&self.specifications, TypeSpecification::Text { .. })
6092 }
6093
6094 pub fn is_date(&self) -> bool {
6096 matches!(&self.specifications, TypeSpecification::Date { .. })
6097 }
6098
6099 pub fn is_date_range(&self) -> bool {
6100 matches!(&self.specifications, TypeSpecification::DateRange { .. })
6101 }
6102
6103 pub fn is_time_range(&self) -> bool {
6104 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
6105 }
6106
6107 pub fn is_time(&self) -> bool {
6109 matches!(&self.specifications, TypeSpecification::Time { .. })
6110 }
6111
6112 pub fn has_trait_duration(&self) -> bool {
6113 match &self.specifications {
6114 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
6115 _ => false,
6116 }
6117 }
6118
6119 pub fn is_duration_like_measure(&self) -> bool {
6120 if !self.is_measure() {
6121 return false;
6122 }
6123 if self.has_trait_duration() {
6124 return true;
6125 }
6126 self.is_anonymous_measure()
6127 && self
6128 .measure_type_decomposition()
6129 .is_some_and(|d| *d == duration_decomposition())
6130 }
6131
6132 pub fn is_duration_like(&self) -> bool {
6133 self.is_duration_like_measure()
6134 }
6135
6136 pub fn has_trait_calendar(&self) -> bool {
6137 match &self.specifications {
6138 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
6139 _ => false,
6140 }
6141 }
6142
6143 pub fn is_calendar_like_measure(&self) -> bool {
6144 if !self.is_measure() {
6145 return false;
6146 }
6147 if self.has_trait_calendar() {
6148 return true;
6149 }
6150 self.is_anonymous_measure()
6151 && self
6152 .measure_type_decomposition()
6153 .is_some_and(|d| *d == calendar_decomposition())
6154 }
6155
6156 pub fn is_calendar_like(&self) -> bool {
6157 self.is_calendar_like_measure()
6158 }
6159
6160 pub fn is_ratio(&self) -> bool {
6162 matches!(&self.specifications, TypeSpecification::Ratio { .. })
6163 }
6164
6165 pub fn is_ratio_range(&self) -> bool {
6166 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
6167 }
6168
6169 pub fn is_calendar_measure_range(&self) -> bool {
6170 matches!(
6171 &self.specifications,
6172 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
6173 if *decomposition == calendar_decomposition()
6174 )
6175 }
6176
6177 pub fn is_calendar_like_range(&self) -> bool {
6178 self.is_calendar_measure_range()
6179 }
6180
6181 pub fn is_range(&self) -> bool {
6182 matches!(
6183 &self.specifications,
6184 TypeSpecification::DateRange { .. }
6185 | TypeSpecification::TimeRange { .. }
6186 | TypeSpecification::NumberRange { .. }
6187 | TypeSpecification::MeasureRange { .. }
6188 | TypeSpecification::RatioRange { .. }
6189 )
6190 }
6191
6192 pub fn is_spec(&self) -> bool {
6194 matches!(&self.specifications, TypeSpecification::Spec { .. })
6195 }
6196
6197 pub fn is_list(&self) -> bool {
6198 matches!(&self.specifications, TypeSpecification::List { .. })
6199 }
6200
6201 pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
6202 match &self.specifications {
6203 TypeSpecification::List { element, .. } => Some(&element.specifications),
6204 _ => None,
6205 }
6206 }
6207
6208 pub fn list_of(element: Arc<LemmaType>) -> Self {
6209 Self {
6210 name: Some(format!("{} list", element.name())),
6211 specifications: TypeSpecification::List {
6212 element,
6213 key: ListKey::Numbered,
6214 help: String::new(),
6215 },
6216 extends: TypeExtends::Primitive,
6217 options: TypeOptions::default(),
6218 }
6219 }
6220
6221 pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
6223 let TypeSpecification::List { element, .. } = &self.specifications else {
6224 panic!(
6225 "BUG: list_element_lemma_type on non-list type {}",
6226 self.name()
6227 );
6228 };
6229 element
6230 }
6231
6232 #[must_use]
6234 pub fn spec_identity(&self) -> Option<&SpecIdentity> {
6235 match &self.specifications {
6236 TypeSpecification::Spec { spec } => Some(spec),
6237 _ => None,
6238 }
6239 }
6240
6241 pub fn vetoed(&self) -> bool {
6243 matches!(&self.specifications, TypeSpecification::Veto { .. })
6244 }
6245
6246 pub fn is_undetermined(&self) -> bool {
6248 matches!(&self.specifications, TypeSpecification::Undetermined)
6249 }
6250
6251 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
6253 use TypeSpecification::*;
6254 matches!(
6255 (&self.specifications, &other.specifications),
6256 (Boolean { .. }, Boolean { .. })
6257 | (Number { .. }, Number { .. })
6258 | (NumberRange { .. }, NumberRange { .. })
6259 | (Measure { .. }, Measure { .. })
6260 | (MeasureRange { .. }, MeasureRange { .. })
6261 | (Text { .. }, Text { .. })
6262 | (Date { .. }, Date { .. })
6263 | (DateRange { .. }, DateRange { .. })
6264 | (Time { .. }, Time { .. })
6265 | (TimeRange { .. }, TimeRange { .. })
6266 | (Ratio { .. }, Ratio { .. })
6267 | (RatioRange { .. }, RatioRange { .. })
6268 | (Veto { .. }, Veto { .. })
6269 | (Undetermined, Undetermined)
6270 | (Spec { .. }, Spec { .. })
6271 | (List { .. }, List { .. })
6272 )
6273 }
6274
6275 #[must_use]
6277 pub fn family(&self) -> Option<&TypeFamily> {
6278 match &self.extends {
6279 TypeExtends::Custom { family, .. } => Some(family),
6280 TypeExtends::Primitive => None,
6281 }
6282 }
6283
6284 #[must_use]
6288 pub fn range_element_type(&self) -> Arc<LemmaType> {
6289 let element_spec = self
6290 .specifications
6291 .element_from_range()
6292 .expect("BUG: range_element_type requires a range specification");
6293 let name = match &self.extends {
6294 TypeExtends::Custom { family, .. } => Some(family.name.clone()),
6295 TypeExtends::Primitive => self.name.clone(),
6296 };
6297 Arc::new(LemmaType {
6298 name,
6299 specifications: element_spec,
6300 extends: self.extends.clone(),
6301 options: TypeOptions::default(),
6302 })
6303 }
6304
6305 #[must_use]
6307 pub fn measure_family(&self) -> Option<&TypeFamily> {
6308 (self.is_measure() || self.is_measure_range())
6309 .then(|| self.family())
6310 .flatten()
6311 }
6312
6313 #[must_use]
6315 pub(crate) fn held_value_type(&self) -> &LemmaType {
6316 let mut current = self;
6317 while current.is_list() {
6318 current = current.list_element_lemma_type().as_ref();
6319 }
6320 current
6321 }
6322
6323 #[must_use]
6325 pub(crate) fn held_measure_family(&self) -> Option<&TypeFamily> {
6326 self.held_value_type().measure_family()
6327 }
6328
6329 #[must_use]
6331 pub fn ratio_family(&self) -> Option<&TypeFamily> {
6332 (self.is_ratio() || self.is_ratio_range())
6333 .then(|| self.family())
6334 .flatten()
6335 }
6336
6337 #[must_use]
6339 pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
6340 self.measure_family().or_else(|| self.ratio_family())
6341 }
6342
6343 #[must_use]
6345 pub fn measure_family_name(&self) -> Option<&str> {
6346 self.measure_family().map(|family| family.name.as_str())
6347 }
6348
6349 #[must_use]
6351 pub fn ratio_family_name(&self) -> Option<&str> {
6352 self.ratio_family().map(|family| family.name.as_str())
6353 }
6354
6355 #[must_use]
6359 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
6360 if !self.has_same_base_type(other) {
6361 return false;
6362 }
6363 if self.is_measure() {
6364 return match (self.measure_family(), other.measure_family()) {
6365 (Some(_), Some(_)) => self.same_measure_family(other),
6366 (None, None) => {
6367 self.measure_type_decomposition() == other.measure_type_decomposition()
6368 }
6369 _ => false,
6370 };
6371 }
6372 if let (
6373 TypeSpecification::Spec { spec: self_spec },
6374 TypeSpecification::Spec { spec: other_spec },
6375 ) = (&self.specifications, &other.specifications)
6376 {
6377 return self_spec == other_spec;
6378 }
6379 if let (
6380 TypeSpecification::List {
6381 element: self_element,
6382 key: self_key,
6383 ..
6384 },
6385 TypeSpecification::List {
6386 element: other_element,
6387 key: other_key,
6388 ..
6389 },
6390 ) = (&self.specifications, &other.specifications)
6391 {
6392 return self_key == other_key && self_element.same_value_type(other_element);
6393 }
6394 if self.is_measure_range() {
6395 match (self.family(), other.family()) {
6396 (Some(self_family), Some(other_family)) => self_family == other_family,
6397 (None, None) => {
6398 let self_element = self
6399 .specifications
6400 .element_from_range()
6401 .expect("BUG: MeasureRange always defines element_from_range");
6402 let other_element = other
6403 .specifications
6404 .element_from_range()
6405 .expect("BUG: MeasureRange always defines element_from_range");
6406 match (&self_element, &other_element) {
6407 (
6408 TypeSpecification::Measure {
6409 decomposition: self_decomp,
6410 ..
6411 },
6412 TypeSpecification::Measure {
6413 decomposition: other_decomp,
6414 ..
6415 },
6416 ) => self_decomp == other_decomp,
6417 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
6418 }
6419 }
6420 _ => false,
6421 }
6422 } else {
6423 true
6424 }
6425 }
6426
6427 #[must_use]
6430 pub(crate) fn without_slot_bounds(&self) -> Self {
6431 let mut cleared = self.clone();
6432 match &mut cleared.specifications {
6433 TypeSpecification::Measure {
6434 minimum,
6435 maximum,
6436 unit,
6437 help,
6438 ..
6439 } => {
6440 *minimum = None;
6441 *maximum = None;
6442 *unit = None;
6443 *help = String::new();
6444 }
6445 TypeSpecification::Ratio {
6446 minimum,
6447 maximum,
6448 unit,
6449 help,
6450 ..
6451 } => {
6452 *minimum = None;
6453 *maximum = None;
6454 *unit = None;
6455 *help = String::new();
6456 }
6457 TypeSpecification::Number {
6458 minimum,
6459 maximum,
6460 help,
6461 ..
6462 } => {
6463 *minimum = None;
6464 *maximum = None;
6465 *help = String::new();
6466 }
6467 TypeSpecification::Date {
6468 minimum,
6469 maximum,
6470 help,
6471 ..
6472 } => {
6473 *minimum = None;
6474 *maximum = None;
6475 *help = String::new();
6476 }
6477 TypeSpecification::Time {
6478 minimum,
6479 maximum,
6480 help,
6481 ..
6482 } => {
6483 *minimum = None;
6484 *maximum = None;
6485 *help = String::new();
6486 }
6487 TypeSpecification::MeasureRange {
6488 minimum,
6489 maximum,
6490 help,
6491 ..
6492 } => {
6493 *minimum = None;
6494 *maximum = None;
6495 *help = String::new();
6496 }
6497 TypeSpecification::DateRange {
6498 minimum,
6499 maximum,
6500 help,
6501 ..
6502 } => {
6503 *minimum = None;
6504 *maximum = None;
6505 *help = String::new();
6506 }
6507 TypeSpecification::TimeRange {
6508 minimum,
6509 maximum,
6510 help,
6511 ..
6512 } => {
6513 *minimum = None;
6514 *maximum = None;
6515 *help = String::new();
6516 }
6517 TypeSpecification::NumberRange {
6518 minimum,
6519 maximum,
6520 help,
6521 ..
6522 } => {
6523 *minimum = None;
6524 *maximum = None;
6525 *help = String::new();
6526 }
6527 TypeSpecification::RatioRange {
6528 minimum,
6529 maximum,
6530 help,
6531 ..
6532 } => {
6533 *minimum = None;
6534 *maximum = None;
6535 *help = String::new();
6536 }
6537 TypeSpecification::Boolean { help, .. } | TypeSpecification::Text { help, .. } => {
6538 *help = String::new();
6539 }
6540 TypeSpecification::Veto { .. }
6541 | TypeSpecification::Undetermined
6542 | TypeSpecification::Spec { .. }
6543 | TypeSpecification::List { .. } => {}
6544 }
6545 cleared
6546 }
6547
6548 #[must_use]
6550 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
6551 if !self.is_measure() || !other.is_measure() {
6552 return false;
6553 }
6554 match (self.measure_family(), other.measure_family()) {
6555 (Some(self_family), Some(other_family)) => self_family == other_family,
6556 _ => false,
6557 }
6558 }
6559
6560 #[must_use]
6564 pub fn same_interface(&self, other: &Self) -> bool {
6565 self.with_families_without_effective() == other.with_families_without_effective()
6566 }
6567
6568 #[must_use]
6571 pub fn same_interface_ignoring_options(&self, other: &Self) -> bool {
6572 let mut left = self.with_families_without_effective();
6573 let mut right = other.with_families_without_effective();
6574 left.options = TypeOptions::default();
6575 right.options = TypeOptions::default();
6576 left == right
6577 }
6578
6579 #[must_use]
6581 pub fn with_families_without_effective(&self) -> Self {
6582 Self {
6583 name: self.name.clone(),
6584 specifications: self.specifications.with_families_without_effective(),
6585 extends: self.extends.with_families_without_effective(),
6586 options: self.options.clone(),
6587 }
6588 }
6589
6590 #[must_use]
6591 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
6592 if !self.is_measure() || !other.is_measure() {
6593 return false;
6594 }
6595 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
6596 return false;
6597 }
6598 match (
6599 self.measure_type_decomposition(),
6600 other.measure_type_decomposition(),
6601 ) {
6602 (Some(a), Some(b)) => a == b,
6603 _ => false,
6604 }
6605 }
6606
6607 pub fn veto_type() -> Self {
6609 Self::primitive(TypeSpecification::veto())
6610 }
6611
6612 pub fn undetermined_type() -> Self {
6615 Self::primitive(TypeSpecification::Undetermined)
6616 }
6617
6618 pub fn decimal_places(&self) -> Option<u8> {
6621 match &self.specifications {
6622 TypeSpecification::Number { decimals, .. } => *decimals,
6623 _ => None,
6624 }
6625 }
6626
6627 pub fn try_rational_as_decimal(
6632 &self,
6633 magnitude: &crate::computation::rational::RationalInteger,
6634 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
6635 let decimal = magnitude.try_to_decimal()?;
6636 Ok(format_decimal_for_api(decimal))
6637 }
6638
6639 pub fn try_measure_canonical_as_decimal_in_unit(
6641 &self,
6642 canonical_magnitude: &crate::computation::rational::RationalInteger,
6643 unit_name: &str,
6644 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
6645 use crate::computation::rational::checked_div;
6646 let unit_factor = self.measure_unit_factor(unit_name);
6647 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
6648 self.try_rational_as_decimal(&magnitude_in_unit)
6649 }
6650
6651 pub fn try_ratio_canonical_as_decimal_in_unit(
6653 &self,
6654 canonical_magnitude: &crate::computation::rational::RationalInteger,
6655 unit_name: &str,
6656 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
6657 use crate::computation::rational::checked_mul;
6658 let units = match &self.specifications {
6659 TypeSpecification::Ratio { units, .. } => units,
6660 _ => unreachable!(
6661 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
6662 self.name()
6663 ),
6664 };
6665 let ratio_unit = units
6666 .iter()
6667 .find(|unit| unit.name == unit_name)
6668 .unwrap_or_else(|| {
6669 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
6670 unreachable!(
6671 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
6672 unit_name,
6673 self.name(),
6674 valid.join(", ")
6675 )
6676 });
6677 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
6678 self.try_rational_as_decimal(&magnitude_in_unit)
6679 }
6680
6681 pub fn example_value(&self) -> &'static str {
6683 match &self.specifications {
6684 TypeSpecification::Text { .. } => "\"hello world\"",
6685 TypeSpecification::Measure { .. } => "12.50 eur",
6686 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
6687 TypeSpecification::Number { .. } => "3.14",
6688 TypeSpecification::NumberRange { .. } => "0...100",
6689 TypeSpecification::Boolean { .. } => "true",
6690 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
6691 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
6692 TypeSpecification::TimeRange { .. } => "09:00...17:00",
6693 TypeSpecification::Veto { .. } => "veto",
6694 TypeSpecification::Time { .. } => "14:30:00",
6695 TypeSpecification::Ratio { .. } => "50%",
6696 TypeSpecification::RatioRange { .. } => "10%...50%",
6697 TypeSpecification::Undetermined => unreachable!(
6698 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
6699 ),
6700 TypeSpecification::Spec { .. } => unreachable!(
6701 "BUG: example_value called on a spec type; spec instances are not prompted data"
6702 ),
6703 TypeSpecification::List { element, .. } => element.example_value(),
6704 }
6705 }
6706
6707 #[must_use]
6711 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
6715 match &self.specifications {
6716 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
6717 _ => unreachable!(
6718 "BUG: measure_type_decomposition called on non-measure type {}",
6719 self.name()
6720 ),
6721 }
6722 }
6723
6724 #[must_use]
6726 pub fn measure_units_table(&self) -> Option<&MeasureUnits> {
6727 match &self.specifications {
6728 TypeSpecification::Measure { units, .. }
6729 | TypeSpecification::MeasureRange { units, .. } => Some(units),
6730 _ => None,
6731 }
6732 }
6733
6734 #[must_use]
6737 pub fn has_dynamic_units(&self) -> bool {
6738 if self
6739 .measure_units_table()
6740 .is_some_and(|units| units.iter().any(MeasureUnit::is_dynamic))
6741 {
6742 return true;
6743 }
6744 match &self.specifications {
6745 TypeSpecification::List { element, .. } => element.has_dynamic_units(),
6746 _ => false,
6747 }
6748 }
6749
6750 #[must_use]
6752 pub fn unit_is_dynamic(&self, unit_name: &str) -> bool {
6753 self.measure_units_table()
6754 .and_then(|units| units.get(unit_name).ok())
6755 .is_some_and(MeasureUnit::is_dynamic)
6756 }
6757
6758 #[must_use]
6761 pub fn measure_fallback_unit(&self) -> Option<&MeasureUnit> {
6762 let units = self.measure_units_table()?;
6763 units
6764 .iter()
6765 .find(|unit| unit.is_canonical_factor())
6766 .or_else(|| units.iter().find(|unit| unit.literal_factor().is_some()))
6767 .or_else(|| units.iter().next())
6768 }
6769
6770 pub(crate) fn measure_display_unit_name(
6773 &self,
6774 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
6775 ) -> Option<&str> {
6776 match &self.specifications {
6777 TypeSpecification::Measure { units, .. } if !units.is_empty() => Some(
6778 measure_display_unit(units, self, runtime_scales)
6779 .name
6780 .as_str(),
6781 ),
6782 _ => None,
6783 }
6784 }
6785
6786 #[must_use]
6795 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
6796 if let Some(binding) = self.unit() {
6797 return vec![(binding.to_string(), 1)];
6798 }
6799 match &self.specifications {
6800 TypeSpecification::Measure {
6801 units,
6802 decomposition,
6803 ..
6804 } => {
6805 if let Some(unit) = self.measure_fallback_unit() {
6806 return vec![(unit.name.clone(), 1)];
6807 }
6808 if units.is_empty() {
6809 return base_measure_vector_as_signature(
6810 decomposition
6811 .as_ref()
6812 .expect("BUG: measure type without units must carry a decomposition"),
6813 );
6814 }
6815 unreachable!("BUG: non-empty measure unit table has no fallback unit");
6816 }
6817 TypeSpecification::MeasureRange {
6818 units,
6819 decomposition,
6820 ..
6821 } => {
6822 if let Some(unit) = self.measure_fallback_unit() {
6823 return vec![(unit.name.clone(), 1)];
6824 }
6825 if units.is_empty() {
6826 return base_measure_vector_as_signature(
6827 decomposition
6828 .as_ref()
6829 .expect("BUG: measure type without units must carry a decomposition"),
6830 );
6831 }
6832 unreachable!("BUG: non-empty measure-range unit table has no fallback unit");
6833 }
6834 _ => unreachable!(
6835 "BUG: measure_runtime_signature called on non-measure type {}",
6836 self.name()
6837 ),
6838 }
6839 }
6840
6841 #[must_use]
6843 pub fn ratio_primary_unit(&self) -> Option<&str> {
6844 match &self.specifications {
6845 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
6846 units.iter().next().map(|unit| unit.name.as_str())
6847 }
6848 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
6849 units.iter().next().map(|unit| unit.name.as_str())
6850 }
6851 _ => None,
6852 }
6853 }
6854
6855 pub fn is_anonymous_measure(&self) -> bool {
6858 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
6859 }
6860
6861 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
6866 Self {
6867 name: None,
6868 specifications: TypeSpecification::Measure {
6869 minimum: None,
6870 maximum: None,
6871 units: crate::literals::MeasureUnits::new(),
6872 traits: Vec::new(),
6873 decomposition: Some(decomposition),
6874 unit: None,
6875 help: String::new(),
6876 },
6877 extends: TypeExtends::Primitive,
6878 options: TypeOptions::default(),
6879 }
6880 }
6881
6882 #[must_use]
6884 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
6885 match &self.specifications {
6886 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
6887 Some(units.iter().map(|unit| unit.name.as_str()).collect())
6888 }
6889 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
6890 Some(units.iter().map(|unit| unit.name.as_str()).collect())
6891 }
6892 _ => None,
6893 }
6894 }
6895
6896 #[must_use]
6898 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
6899 match &self.specifications {
6900 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
6901 Some(units.iter().map(|unit| unit.name.as_str()).collect())
6902 }
6903 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
6904 Some(units.iter().map(|unit| unit.name.as_str()).collect())
6905 }
6906 _ => None,
6907 }
6908 }
6909
6910 #[must_use]
6912 pub fn label_for_data_input(&self, input_key: &str) -> String {
6913 let type_label = if let Some(parent) = self.extends.parent_name() {
6914 parent.to_string()
6915 } else {
6916 let type_name = self.name();
6917 if type_name == input_key {
6918 self.specifications.to_string()
6919 } else {
6920 type_name
6921 }
6922 };
6923 if type_label == input_key {
6924 input_key.to_string()
6925 } else {
6926 format!("{input_key} [{type_label}]")
6927 }
6928 }
6929
6930 #[must_use]
6932 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
6933 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
6934 }
6935
6936 #[must_use]
6938 pub fn empty_runtime_input_vetoes(&self) -> bool {
6939 !matches!(self.specifications, TypeSpecification::Text { .. })
6940 }
6941
6942 pub fn measure_unit_factor(
6944 &self,
6945 unit_name: &str,
6946 ) -> &crate::computation::rational::RationalInteger {
6947 let units = match &self.specifications {
6948 TypeSpecification::Measure { units, .. } => units,
6949 TypeSpecification::MeasureRange { units, .. } => units,
6950 _ => unreachable!(
6951 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
6952 self.name()
6953 ),
6954 };
6955 match units.get(unit_name) {
6956 Ok(unit) => unit.require_literal_factor(),
6957 Err(_) => {
6958 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
6959 unreachable!(
6960 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
6961 unit_name,
6962 self.name(),
6963 valid.join(", ")
6964 );
6965 }
6966 }
6967 }
6968
6969 pub fn ratio_unit_factor(
6970 &self,
6971 unit_name: &str,
6972 ) -> &crate::computation::rational::RationalInteger {
6973 let units = match &self.specifications {
6974 TypeSpecification::Ratio { units, .. } => units,
6975 _ => unreachable!(
6976 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
6977 self.name()
6978 ),
6979 };
6980 match units.get(unit_name) {
6981 Ok(RatioUnit { value, .. }) => value,
6982 Err(_) => {
6983 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
6984 unreachable!(
6985 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
6986 unit_name,
6987 self.name(),
6988 valid.join(", ")
6989 );
6990 }
6991 }
6992 }
6993
6994 pub(crate) fn measure_literal_unit_map(
6996 &self,
6997 literal: &LiteralValue,
6998 unit_names: &[&str],
6999 factor_source: UnitFactorSource<'_>,
7000 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7001 use crate::computation::rational::checked_div;
7002
7003 let ValueKind::Measure(magnitude) = &literal.value else {
7004 panic!("BUG: measure_literal_unit_map called with non-measure value");
7005 };
7006 let mut map = BTreeMap::new();
7007 for &unit_name in unit_names {
7008 let Some(unit_factor) = factor_source.try_measure_unit_factor(unit_name) else {
7009 continue;
7010 };
7011 let magnitude_in_unit = checked_div(magnitude, &unit_factor)
7012 .map_err(LiteralUnitMapFailure::UnitConversion)?;
7013 let decimal = self
7014 .try_rational_as_decimal(&magnitude_in_unit)
7015 .map_err(LiteralUnitMapFailure::Commit)?;
7016 map.insert(unit_name.to_string(), decimal);
7017 }
7018 Ok(map)
7019 }
7020
7021 pub(crate) fn ratio_literal_unit_map(
7023 &self,
7024 literal: &LiteralValue,
7025 unit_names: &[&str],
7026 factor_source: UnitFactorSource<'_>,
7027 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7028 use crate::computation::rational::checked_mul;
7029
7030 let ratio_api_type = match &self.specifications {
7031 TypeSpecification::Ratio { .. } => self,
7032 TypeSpecification::RatioRange { .. } => {
7033 return ratio_element_type_for_api(self).ratio_literal_unit_map(
7034 literal,
7035 unit_names,
7036 factor_source,
7037 );
7038 }
7039 _ => {
7040 panic!(
7041 "BUG: ratio_literal_unit_map called with non-ratio type {}",
7042 self.name()
7043 );
7044 }
7045 };
7046 let ValueKind::Ratio(canonical) = &literal.value else {
7047 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
7048 };
7049 if unit_names.is_empty() {
7050 panic!(
7051 "BUG: ratio literal type '{}' must have at least one unit name",
7052 self.name()
7053 );
7054 }
7055 let mut map = BTreeMap::new();
7056 for &unit_name in unit_names {
7057 let unit_factor = factor_source.ratio_unit_factor(unit_name);
7058 let magnitude_in_unit = checked_mul(canonical, unit_factor)
7059 .map_err(LiteralUnitMapFailure::UnitConversion)?;
7060 let decimal = ratio_api_type
7061 .try_rational_as_decimal(&magnitude_in_unit)
7062 .map_err(LiteralUnitMapFailure::Commit)?;
7063 map.insert(unit_name.to_string(), decimal);
7064 }
7065 Ok(map)
7066 }
7067}
7068
7069pub(crate) enum UnitFactorSource<'a> {
7071 DeclaredOn(&'a LemmaType),
7072 Merged {
7073 measure: Option<&'a MeasureUnits>,
7074 ratio: Option<&'a RatioUnits>,
7075 },
7076 Runtime {
7079 scales:
7080 &'a std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
7081 fallback: &'a UnitFactorSource<'a>,
7082 },
7083}
7084
7085impl UnitFactorSource<'_> {
7086 fn try_measure_unit_factor(
7089 &self,
7090 unit_name: &str,
7091 ) -> Option<crate::computation::rational::RationalInteger> {
7092 match self {
7093 UnitFactorSource::DeclaredOn(lemma_type) => {
7094 let units = lemma_type.measure_units_table().unwrap_or_else(|| {
7095 panic!(
7096 "BUG: measure_unit_factor on non-measure type {}",
7097 lemma_type.name()
7098 )
7099 });
7100 let unit = units.get(unit_name).unwrap_or_else(|_| {
7101 panic!(
7102 "BUG: unknown unit '{unit_name}' for measure type {}",
7103 lemma_type.name()
7104 )
7105 });
7106 unit.literal_factor().cloned()
7107 }
7108 UnitFactorSource::Merged { measure, .. } => {
7109 let units = measure.unwrap_or_else(|| {
7110 panic!(
7111 "BUG: family measure expansion missing merged table for unit '{unit_name}'"
7112 )
7113 });
7114 let unit = units.get(unit_name).unwrap_or_else(|_| {
7115 panic!("BUG: family unit '{unit_name}' missing from merged measure table")
7116 });
7117 unit.literal_factor().cloned()
7118 }
7119 UnitFactorSource::Runtime { scales, fallback } => scales
7120 .get(unit_name)
7121 .cloned()
7122 .or_else(|| fallback.try_measure_unit_factor(unit_name)),
7123 }
7124 }
7125
7126 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
7127 match self {
7128 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
7129 UnitFactorSource::Merged { ratio, .. } => {
7130 let units = ratio.unwrap_or_else(|| {
7131 panic!(
7132 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
7133 )
7134 });
7135 &units
7136 .get(unit_name)
7137 .unwrap_or_else(|_| {
7138 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
7139 })
7140 .value
7141 }
7142 UnitFactorSource::Runtime { fallback, .. } => fallback.ratio_unit_factor(unit_name),
7143 }
7144 }
7145}
7146
7147pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
7149 match &lemma_type.specifications {
7150 TypeSpecification::Ratio { .. } => lemma_type.clone(),
7151 TypeSpecification::RatioRange { .. } => {
7152 let element = range_element_type_specification(&lemma_type.specifications)
7153 .expect("BUG: ratio range type must have ratio element specification");
7154 let TypeSpecification::Ratio { units, .. } = element else {
7155 panic!("BUG: ratio range element spec must be Ratio");
7156 };
7157 LemmaType::primitive(TypeSpecification::Ratio {
7158 minimum: None,
7159 maximum: None,
7160 units,
7161 unit: None,
7162 help: String::new(),
7163 })
7164 }
7165 _ => panic!(
7166 "BUG: ratio_element_type_for_api called with non-ratio type {}",
7167 lemma_type.name()
7168 ),
7169 }
7170}
7171
7172#[derive(Debug, Clone, PartialEq, Eq)]
7174pub(crate) enum LiteralUnitMapFailure {
7175 Commit(crate::computation::rational::NumericFailure),
7176 UnitConversion(crate::computation::rational::NumericFailure),
7177}
7178
7179#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7182pub struct LiteralValue {
7183 pub value: ValueKind,
7184}
7185
7186impl LiteralValue {
7187 #[inline]
7188 pub fn new(value: ValueKind) -> Self {
7189 Self { value }
7190 }
7191
7192 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
7193 match signature {
7194 [(unit_name, 1)] => Some(unit_name.as_str()),
7195 _ => None,
7196 }
7197 }
7198}
7199
7200#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7206pub struct TypedLiteral {
7207 pub value: ValueKind,
7208 pub lemma_type: Arc<LemmaType>,
7209}
7210
7211impl TypedLiteral {
7212 #[inline]
7213 pub fn to_literal(&self) -> LiteralValue {
7214 LiteralValue {
7215 value: self.value.clone(),
7216 }
7217 }
7218
7219 #[inline]
7220 pub fn into_literal(self) -> LiteralValue {
7221 LiteralValue { value: self.value }
7222 }
7223}
7224
7225impl From<TypedLiteral> for LiteralValue {
7226 fn from(typed: TypedLiteral) -> Self {
7227 typed.into_literal()
7228 }
7229}
7230
7231impl From<&TypedLiteral> for LiteralValue {
7232 fn from(typed: &TypedLiteral) -> Self {
7233 typed.to_literal()
7234 }
7235}
7236
7237impl LiteralValue {
7238 pub fn text(s: impl Into<Arc<str>>) -> Self {
7239 Self {
7240 value: ValueKind::Text(s.into()),
7241 }
7242 }
7243
7244 pub fn number(n: RationalInteger) -> Self {
7245 Self {
7246 value: ValueKind::Number(n),
7247 }
7248 }
7249
7250 pub fn number_from_decimal(decimal: Decimal) -> Self {
7251 Self::number(
7252 crate::literals::rational_from_parsed_decimal(decimal)
7253 .expect("BUG: literal number from decimal must lift at boundary"),
7254 )
7255 }
7256
7257 pub fn measure(n: RationalInteger) -> Self {
7258 Self {
7259 value: ValueKind::Measure(n),
7260 }
7261 }
7262
7263 pub fn from_bool(b: bool) -> Self {
7264 Self {
7265 value: ValueKind::Boolean(b),
7266 }
7267 }
7268
7269 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7270 Self::date(date_time_to_semantic(dt))
7271 }
7272
7273 pub fn date(dt: SemanticDateTime) -> Self {
7274 Self {
7275 value: ValueKind::Date(dt),
7276 }
7277 }
7278
7279 pub fn time(t: SemanticTime) -> Self {
7280 Self {
7281 value: ValueKind::Time(t),
7282 }
7283 }
7284
7285 pub fn ratio(r: RationalInteger) -> Self {
7286 Self {
7287 value: ValueKind::Ratio(r),
7288 }
7289 }
7290
7291 pub fn ratio_from_decimal(r: Decimal) -> Self {
7292 Self::ratio(
7293 crate::literals::rational_from_parsed_decimal(r)
7294 .expect("BUG: ratio literal from decimal must lift at boundary"),
7295 )
7296 }
7297
7298 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
7299 Self {
7300 value: ValueKind::Range(Box::new(left), Box::new(right)),
7301 }
7302 }
7303
7304 pub fn display_value(&self) -> String {
7306 match &self.value {
7307 ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
7308 format!("{}", self.value)
7309 }
7310 ValueKind::Range(left, right) => {
7311 format!("{}...{}", left.display_value(), right.display_value())
7312 }
7313 ValueKind::List(rows) if rows.is_empty() => String::new(),
7314 ValueKind::List(rows) => rows
7315 .iter()
7316 .map(|row| row.display_value())
7317 .collect::<Vec<_>>()
7318 .join(", "),
7319 _ => format!("{}", self.value),
7320 }
7321 }
7322
7323 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
7325 self.display_value_with_scales(lemma_type, None)
7326 }
7327
7328 pub fn display_value_with_scales(
7330 &self,
7331 lemma_type: &LemmaType,
7332 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7333 ) -> String {
7334 match &self.value {
7335 ValueKind::Measure(n) => {
7336 let signature = lemma_type.measure_runtime_signature();
7337 format_measure_canonical_for_display(n, lemma_type, &signature, runtime_scales)
7338 }
7339 ValueKind::UnscaledMeasure { magnitude, unit } => {
7340 format!(
7341 "{} {unit}",
7342 format_rational_magnitude_for_display(magnitude)
7343 )
7344 }
7345 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
7346 ValueKind::Range(left, right) => {
7347 format!(
7348 "{}...{}",
7349 left.to_literal()
7350 .display_value_with_scales(&left.lemma_type, runtime_scales),
7351 right
7352 .to_literal()
7353 .display_value_with_scales(&right.lemma_type, runtime_scales)
7354 )
7355 }
7356 ValueKind::List(rows) if rows.is_empty() => lemma_type.specifications.to_string(),
7357 ValueKind::List(rows) => rows
7358 .iter()
7359 .map(|row| {
7360 row.to_literal()
7361 .display_value_with_scales(&row.lemma_type, runtime_scales)
7362 })
7363 .collect::<Vec<_>>()
7364 .join(", "),
7365 _ => format!("{}", self.value),
7366 }
7367 }
7368
7369 pub fn byte_size(&self) -> usize {
7371 self.value.structural_byte_size()
7372 }
7373
7374 #[must_use]
7376 pub fn magnitude_suggestion_for_decimal_prompt(
7377 &self,
7378 lemma_type: &LemmaType,
7379 ) -> Option<rust_decimal::Decimal> {
7380 match &self.value {
7381 ValueKind::Number(n) => Some(
7382 lemma_type
7383 .try_rational_as_decimal(n)
7384 .expect("BUG: stored number literal must convert to decimal for prompt"),
7385 ),
7386 ValueKind::Measure(n) => {
7387 let signature = lemma_type.measure_runtime_signature();
7388 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
7389 "BUG: measure prompt requires exactly one signature unit with exponent 1",
7390 );
7391 Some(
7392 lemma_type
7393 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
7394 .expect("BUG: stored measure literal must convert to decimal for prompt"),
7395 )
7396 }
7397 ValueKind::Ratio(n) => {
7398 if let Some(unit_name) = lemma_type.unit() {
7399 Some(
7400 lemma_type
7401 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
7402 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
7403 )
7404 } else {
7405 Some(lemma_type.try_rational_as_decimal(n).expect(
7406 "BUG: stored bare ratio literal must convert to decimal for prompt",
7407 ))
7408 }
7409 }
7410 _ => None,
7411 }
7412 }
7413
7414 #[must_use]
7416 pub fn measure_units(
7417 &self,
7418 lemma_type: &LemmaType,
7419 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7420 if !matches!(self.value, ValueKind::Measure(_)) {
7421 return None;
7422 }
7423 lemma_type.measure_unit_names()?;
7424 let declared: Vec<&str> = lemma_type
7425 .measure_unit_names()
7426 .expect("BUG: measure_unit_names checked above");
7427 lemma_type
7428 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7429 .ok()
7430 }
7431
7432 #[must_use]
7434 pub fn ratio_units(
7435 &self,
7436 lemma_type: &LemmaType,
7437 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7438 if !matches!(self.value, ValueKind::Ratio(_)) {
7439 return None;
7440 }
7441 let has_declared_units = match &lemma_type.specifications {
7442 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
7443 TypeSpecification::RatioRange { .. } => true,
7444 _ => return None,
7445 };
7446 if !has_declared_units {
7447 return None;
7448 }
7449 let declared: Vec<&str> = lemma_type
7450 .ratio_unit_names()
7451 .expect("BUG: ratio units checked above");
7452 lemma_type
7453 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7454 .ok()
7455 }
7456
7457 #[must_use]
7459 pub fn magnitude_in_unit(
7460 &self,
7461 lemma_type: &LemmaType,
7462 unit: &str,
7463 ) -> Option<rust_decimal::Decimal> {
7464 self.measure_units(lemma_type)
7465 .and_then(|map| map.get(unit).copied())
7466 .or_else(|| {
7467 self.ratio_units(lemma_type)
7468 .and_then(|map| map.get(unit).copied())
7469 })
7470 }
7471
7472 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
7474 let ValueKind::Measure(magnitude) = &self.value else {
7475 unreachable!(
7476 "BUG: duration_canonical_seconds called with {:?}",
7477 self.value
7478 );
7479 };
7480 if !lemma_type.is_duration_like_measure() {
7481 unreachable!(
7482 "BUG: duration_canonical_seconds called with type {}",
7483 lemma_type.name()
7484 );
7485 }
7486 let factor = lemma_type.measure_unit_factor("second");
7487 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
7488 }
7489
7490 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
7492 let ValueKind::Measure(magnitude) = &self.value else {
7493 unreachable!(
7494 "BUG: calendar_canonical_months called with {:?}",
7495 self.value
7496 );
7497 };
7498 if !lemma_type.is_calendar_like() {
7499 unreachable!(
7500 "BUG: calendar_canonical_months called with type {}",
7501 lemma_type.name()
7502 );
7503 }
7504 let factor = lemma_type.measure_unit_factor("month");
7505 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
7506 }
7507}
7508
7509impl TypedLiteral {
7510 pub fn text(s: impl Into<Arc<str>>) -> Self {
7511 Self {
7512 value: ValueKind::Text(s.into()),
7513 lemma_type: primitive_text_arc().clone(),
7514 }
7515 }
7516
7517 pub fn text_with_type(s: impl Into<Arc<str>>, lemma_type: Arc<LemmaType>) -> Self {
7518 Self {
7519 value: ValueKind::Text(s.into()),
7520 lemma_type,
7521 }
7522 }
7523
7524 pub fn number(n: RationalInteger) -> Self {
7525 Self {
7526 value: ValueKind::Number(n),
7527 lemma_type: primitive_number_arc().clone(),
7528 }
7529 }
7530
7531 pub fn number_from_decimal(decimal: Decimal) -> Self {
7532 Self::number(
7533 crate::literals::rational_from_parsed_decimal(decimal)
7534 .expect("BUG: literal number from decimal must lift at boundary"),
7535 )
7536 }
7537
7538 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7539 Self {
7540 value: ValueKind::Number(n),
7541 lemma_type,
7542 }
7543 }
7544
7545 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
7546 Self::number_with_type(
7547 crate::literals::rational_from_parsed_decimal(decimal)
7548 .expect("BUG: literal number from decimal must lift at boundary"),
7549 lemma_type,
7550 )
7551 }
7552
7553 pub fn measure_with_bound_unit(
7555 n: RationalInteger,
7556 unit_name: impl Into<String>,
7557 lemma_type: Arc<LemmaType>,
7558 ) -> Self {
7559 Self::measure_with_type(
7560 n,
7561 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
7562 )
7563 }
7564
7565 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7566 Self {
7567 value: ValueKind::Measure(n),
7568 lemma_type,
7569 }
7570 }
7571
7572 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7573 Self {
7574 value: ValueKind::Measure(n),
7575 lemma_type,
7576 }
7577 }
7578
7579 pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
7581 Self {
7582 value: ValueKind::Measure(value),
7583 lemma_type: Arc::new(anonymous_measure_type()),
7584 }
7585 }
7586
7587 pub fn from_bool(b: bool) -> Self {
7588 Self {
7589 value: ValueKind::Boolean(b),
7590 lemma_type: primitive_boolean_arc().clone(),
7591 }
7592 }
7593
7594 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7595 Self::date(date_time_to_semantic(dt))
7596 }
7597
7598 pub fn date(dt: SemanticDateTime) -> Self {
7599 Self {
7600 value: ValueKind::Date(dt),
7601 lemma_type: primitive_date_arc().clone(),
7602 }
7603 }
7604
7605 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
7606 Self {
7607 value: ValueKind::Date(dt),
7608 lemma_type,
7609 }
7610 }
7611
7612 pub fn time(t: SemanticTime) -> Self {
7613 Self {
7614 value: ValueKind::Time(t),
7615 lemma_type: primitive_time_arc().clone(),
7616 }
7617 }
7618
7619 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
7620 Self {
7621 value: ValueKind::Time(t),
7622 lemma_type,
7623 }
7624 }
7625
7626 pub fn calendar(
7627 value: RationalInteger,
7628 unit: SemanticCalendarUnit,
7629 lemma_type: Arc<LemmaType>,
7630 ) -> Self {
7631 let unit_name = unit.to_string();
7632 debug_assert_eq!(
7633 semantic_calendar_unit_from_unit_name(&unit_name),
7634 unit,
7635 "BUG: calendar unit name must round-trip"
7636 );
7637 Self::measure_with_bound_unit(value, unit_name, lemma_type)
7638 }
7639
7640 pub fn calendar_from_decimal(
7641 value: Decimal,
7642 unit: SemanticCalendarUnit,
7643 lemma_type: Arc<LemmaType>,
7644 ) -> Self {
7645 Self::calendar(
7646 crate::literals::rational_from_parsed_decimal(value)
7647 .expect("BUG: calendar literal from decimal must lift at boundary"),
7648 unit,
7649 lemma_type,
7650 )
7651 }
7652
7653 pub fn calendar_with_type(
7654 value: RationalInteger,
7655 unit: SemanticCalendarUnit,
7656 lemma_type: Arc<LemmaType>,
7657 ) -> Self {
7658 Self::calendar(value, unit, lemma_type)
7659 }
7660
7661 pub fn ratio(r: RationalInteger) -> Self {
7662 Self {
7663 value: ValueKind::Ratio(r),
7664 lemma_type: primitive_ratio_arc().clone(),
7665 }
7666 }
7667
7668 pub fn ratio_from_decimal(r: Decimal) -> Self {
7669 Self::ratio(
7670 crate::literals::rational_from_parsed_decimal(r)
7671 .expect("BUG: ratio literal from decimal must lift at boundary"),
7672 )
7673 }
7674
7675 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7676 Self {
7677 value: ValueKind::Ratio(r),
7678 lemma_type,
7679 }
7680 }
7681
7682 pub fn ratio_with_bound_unit(
7683 r: RationalInteger,
7684 unit_name: impl Into<String>,
7685 lemma_type: Arc<LemmaType>,
7686 ) -> Self {
7687 Self::ratio_with_type(
7688 r,
7689 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
7690 )
7691 }
7692
7693 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
7694 let specifications =
7695 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
7696 .unwrap_or_else(|| {
7697 unreachable!(
7698 "BUG: attempted to construct a range literal from incompatible endpoint types"
7699 )
7700 });
7701
7702 Self {
7703 value: ValueKind::Range(Box::new(left), Box::new(right)),
7704 lemma_type: Arc::new(LemmaType::primitive(specifications)),
7705 }
7706 }
7707
7708 pub fn display_value(&self) -> String {
7709 self.to_literal().display_value_with_type(&self.lemma_type)
7710 }
7711
7712 pub fn get_type(&self) -> &LemmaType {
7713 &self.lemma_type
7714 }
7715
7716 pub fn byte_size(&self) -> usize {
7717 self.value.structural_byte_size()
7718 }
7719}
7720
7721impl fmt::Display for TypedLiteral {
7722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7723 write!(f, "{}", self.display_value())
7724 }
7725}
7726
7727#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7730pub enum ReferenceTarget {
7731 Data(DataPath),
7732 Rule(RulePath),
7733}
7734
7735impl fmt::Display for ReferenceTarget {
7736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7737 match self {
7738 ReferenceTarget::Data(path) => write!(f, "{path}"),
7739 ReferenceTarget::Rule(path) => write!(f, "{path}"),
7740 }
7741 }
7742}
7743
7744#[derive(Clone, Debug, PartialEq, Eq)]
7748pub(crate) enum ReferenceEnd {
7749 Promptable(DataPath),
7750 Rule(RulePath),
7751}
7752
7753#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7760pub enum DataOrigin {
7761 Declared,
7762 Bound,
7763}
7764
7765#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7767pub enum ImportEffectivePin {
7768 Data(DataPath),
7769 Rule(RulePath),
7770}
7771
7772#[derive(Clone, Debug)]
7774pub enum DataDefinition {
7775 Value {
7777 value: LiteralValue,
7778 resolved_type: Arc<LemmaType>,
7779 origin: DataOrigin,
7780 source: Source,
7781 },
7782 TypeDeclaration {
7788 resolved_type: Arc<LemmaType>,
7789 declared_suggestion: Option<BoundValueKind>,
7790 declared_value: Option<BoundValueKind>,
7791 source: Source,
7792 },
7793 Import {
7802 row: Arc<RowInstance>,
7803 pin: Option<ImportEffectivePin>,
7805 candidates: std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>>,
7807 source: Source,
7808 },
7809 InstanceList {
7814 resolved_type: Arc<LemmaType>,
7815 origin: DataOrigin,
7816 source: Source,
7817 },
7818 Reference {
7843 target: ReferenceTarget,
7844 resolved_type: Arc<LemmaType>,
7845 local_constraints: Option<Vec<Constraint>>,
7846 local_suggestion: Option<BoundValueKind>,
7847 local_value: Option<BoundValueKind>,
7848 origin: DataOrigin,
7849 source: Source,
7850 },
7851}
7852
7853impl DataDefinition {
7854 pub fn is_bound(&self) -> bool {
7857 match self {
7858 DataDefinition::Value { origin, .. }
7859 | DataDefinition::InstanceList { origin, .. }
7860 | DataDefinition::Reference { origin, .. } => *origin == DataOrigin::Bound,
7861 DataDefinition::TypeDeclaration { .. } | DataDefinition::Import { .. } => false,
7862 }
7863 }
7864
7865 pub fn lemma_type(&self) -> Option<&LemmaType> {
7867 self.resolved_type_arc().map(|arc| arc.as_ref())
7868 }
7869
7870 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
7872 match self {
7873 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
7874 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
7875 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
7876 DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
7877 DataDefinition::Import { .. } => None,
7878 }
7879 }
7880
7881 #[inline]
7883 pub fn schema_type(&self) -> Option<&LemmaType> {
7884 self.lemma_type()
7885 }
7886
7887 pub fn value(&self) -> Option<LiteralValue> {
7891 match self {
7892 DataDefinition::Value { value, .. } => Some(value.clone()),
7893 DataDefinition::TypeDeclaration {
7894 declared_value: Some(dv),
7895 ..
7896 } => Some(dv.to_literal()),
7897 DataDefinition::Reference {
7898 local_value: Some(dv),
7899 resolved_type: _,
7900 ..
7901 } => Some(dv.to_literal()),
7902 DataDefinition::TypeDeclaration { .. }
7903 | DataDefinition::Import { .. }
7904 | DataDefinition::InstanceList { .. }
7905 | DataDefinition::Reference { .. } => None,
7906 }
7907 }
7908
7909 pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
7911 match self {
7912 DataDefinition::TypeDeclaration {
7913 declared_value: Some(dv),
7914 ..
7915 }
7916 | DataDefinition::Reference {
7917 local_value: Some(dv),
7918 ..
7919 } => Some(dv),
7920 DataDefinition::Value { .. }
7921 | DataDefinition::TypeDeclaration { .. }
7922 | DataDefinition::Reference { .. }
7923 | DataDefinition::InstanceList { .. }
7924 | DataDefinition::Import { .. } => None,
7925 }
7926 }
7927
7928 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
7932 match self {
7933 DataDefinition::TypeDeclaration {
7934 declared_suggestion: Some(dv),
7935 ..
7936 }
7937 | DataDefinition::Reference {
7938 local_suggestion: Some(dv),
7939 ..
7940 } => Some(dv),
7941 DataDefinition::Value { .. }
7942 | DataDefinition::TypeDeclaration { .. }
7943 | DataDefinition::Reference { .. }
7944 | DataDefinition::InstanceList { .. }
7945 | DataDefinition::Import { .. } => None,
7946 }
7947 }
7948
7949 pub fn source(&self) -> &Source {
7951 match self {
7952 DataDefinition::Value { source, .. } => source,
7953 DataDefinition::TypeDeclaration { source, .. } => source,
7954 DataDefinition::Import { source, .. } => source,
7955 DataDefinition::InstanceList { source, .. } => source,
7956 DataDefinition::Reference { source, .. } => source,
7957 }
7958 }
7959
7960 pub fn import_sole_row(&self) -> &Arc<RowInstance> {
7962 let DataDefinition::Import { pin, row, .. } = self else {
7963 panic!("BUG: import_sole_row on non-import data");
7964 };
7965 assert!(
7966 pin.is_none(),
7967 "BUG: dynamic import needs pin selection before reading a row"
7968 );
7969 row
7970 }
7971
7972 pub fn import_candidates(&self) -> &std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>> {
7974 let DataDefinition::Import {
7975 pin, candidates, ..
7976 } = self
7977 else {
7978 panic!("BUG: import_candidates on non-import data");
7979 };
7980 assert!(pin.is_some(), "BUG: static import has no candidate map");
7981 candidates
7982 }
7983
7984 pub fn import_pin(&self) -> Option<&ImportEffectivePin> {
7986 match self {
7987 DataDefinition::Import { pin, .. } => pin.as_ref(),
7988 _ => None,
7989 }
7990 }
7991
7992 pub fn import_row_for(&self, identity: &SpecIdentity) -> &Arc<RowInstance> {
7994 let DataDefinition::Import {
7995 pin,
7996 row,
7997 candidates,
7998 ..
7999 } = self
8000 else {
8001 panic!("BUG: import_row_for on non-import data");
8002 };
8003 if pin.is_none() {
8004 return row;
8005 }
8006 candidates
8007 .get(identity)
8008 .unwrap_or_else(|| panic!("BUG: dynamic import has no row for identity '{identity}'"))
8009 }
8010}
8011
8012pub fn number_with_unit_to_value_kind(
8014 magnitude: rust_decimal::Decimal,
8015 unit_name: &str,
8016 lemma_type: &LemmaType,
8017) -> Result<ValueKind, String> {
8018 match &lemma_type.specifications {
8019 TypeSpecification::Ratio { units, .. } => {
8020 use crate::computation::rational::{checked_div, decimal_to_rational};
8021 let unit = units.get(unit_name)?;
8022 let magnitude_rational = decimal_to_rational(magnitude)
8023 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8024 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8025 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8026 Ok(ValueKind::Ratio(canonical_rational))
8027 }
8028 TypeSpecification::Measure { units, .. } => {
8029 use crate::computation::rational::checked_mul;
8030 let rational = lift_parser_decimal(magnitude)?;
8031 let unit = units.get(unit_name)?;
8032 match unit.literal_factor() {
8033 Some(factor) => {
8034 let canonical = checked_mul(&rational, factor).map_err(|failure| {
8035 format!("measure canonicalization overflow: {failure}")
8036 })?;
8037 Ok(ValueKind::Measure(canonical))
8038 }
8039 None => Ok(ValueKind::UnscaledMeasure {
8040 magnitude: rational,
8041 unit: std::sync::Arc::from(unit_name),
8042 }),
8043 }
8044 }
8045 _ => Err(format!(
8046 "Unit '{}' is defined on type '{}' which is not measure or ratio",
8047 unit_name,
8048 lemma_type.name()
8049 )),
8050 }
8051}
8052
8053pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
8056 matches!(
8057 (type_spec, value),
8058 (TypeSpecification::Number { .. }, ValueKind::Number(_))
8059 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
8060 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
8061 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
8062 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
8063 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
8064 | (
8065 TypeSpecification::Measure { .. },
8066 ValueKind::UnscaledMeasure { .. }
8067 )
8068 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
8069 | (
8070 TypeSpecification::NumberRange { .. },
8071 ValueKind::Range(_, _)
8072 )
8073 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
8074 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
8075 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
8076 | (
8077 TypeSpecification::MeasureRange { .. },
8078 ValueKind::Range(_, _)
8079 )
8080 | (TypeSpecification::Veto { .. }, _)
8081 | (TypeSpecification::Undetermined, _)
8082 | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
8083 | (TypeSpecification::List { .. }, ValueKind::List(_))
8084 )
8085}
8086
8087fn parser_value_type_mismatch(
8088 value: &crate::literals::Value,
8089 type_spec: &TypeSpecification,
8090) -> String {
8091 use crate::parsing::ast::AsLemmaSource;
8092 let value_str = format!("{}", AsLemmaSource(value));
8093 match type_spec {
8094 TypeSpecification::Measure { units, .. } => {
8095 let unit_hint = units
8096 .iter()
8097 .find(|u| u.is_canonical_factor())
8098 .map(|u| u.name.as_str())
8099 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
8100 .unwrap_or("unit");
8101 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8102 }
8103 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
8104 let unit_hint = units
8105 .iter()
8106 .next()
8107 .map(|u| u.name.as_str())
8108 .unwrap_or("unit");
8109 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8110 }
8111 _ => format!("cannot use {value_str} as {type_spec}"),
8112 }
8113}
8114
8115pub fn refresh_measure_literal_canonical_magnitude(
8120 lit: &mut LiteralValue,
8121 previous_type: &LemmaType,
8122 resolved_type: &LemmaType,
8123) -> Result<(), crate::computation::rational::NumericFailure> {
8124 use crate::computation::rational::{checked_div, checked_mul};
8125 let ValueKind::Measure(magnitude) = &mut lit.value else {
8126 if matches!(lit.value, ValueKind::UnscaledMeasure { .. }) {
8127 return Ok(());
8128 }
8129 return Ok(());
8130 };
8131 let signature = previous_type.measure_runtime_signature();
8135 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
8136 return Ok(());
8137 };
8138 if signature.len() != 1 {
8139 return Ok(());
8140 }
8141 let stored_factor = previous_type.measure_unit_factor(unit_name);
8142 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
8143 if stored_factor == resolved_factor {
8144 return Ok(());
8145 }
8146 let scaled = checked_mul(magnitude, resolved_factor)?;
8147 *magnitude = checked_div(&scaled, stored_factor)?;
8148 Ok(())
8149}
8150
8151fn number_with_unit_value_kind(
8152 magnitude: rust_decimal::Decimal,
8153 unit_name: &str,
8154 type_spec: &TypeSpecification,
8155) -> Result<ValueKind, String> {
8156 use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
8157 match type_spec {
8158 TypeSpecification::Ratio { units, .. } => {
8159 let unit = units.get(unit_name)?;
8160 let magnitude_rational = decimal_to_rational(magnitude)
8161 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8162 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8163 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8164 Ok(ValueKind::Ratio(canonical_rational))
8165 }
8166 TypeSpecification::Measure { units, .. } => {
8167 let rational = lift_parser_decimal(magnitude)?;
8168 let unit = units.get(unit_name)?;
8169 match unit.literal_factor() {
8170 Some(factor) => {
8171 let canonical = checked_mul(&rational, factor).map_err(|failure| {
8172 format!("measure canonicalization overflow: {failure}")
8173 })?;
8174 Ok(ValueKind::Measure(canonical))
8175 }
8176 None => Ok(ValueKind::UnscaledMeasure {
8177 magnitude: rational,
8178 unit: Arc::from(unit_name),
8179 }),
8180 }
8181 }
8182 _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
8183 }
8184}
8185
8186fn parser_scalar_to_value_kind(
8191 value: &crate::literals::Value,
8192 type_spec: &TypeSpecification,
8193) -> Result<ValueKind, String> {
8194 use crate::literals::Value;
8195 match (value, type_spec) {
8196 (Value::NumberWithUnit(magnitude, unit_name), _) => {
8197 number_with_unit_value_kind(*magnitude, unit_name, type_spec)
8198 }
8199 (Value::Number(n), TypeSpecification::Number { .. }) => {
8200 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
8201 }
8202 (Value::Text(s), TypeSpecification::Text { .. }) => {
8203 Ok(ValueKind::Text(Arc::from(s.as_str())))
8204 }
8205 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
8206 (Value::Date(dt), TypeSpecification::Date { .. }) => {
8207 Ok(ValueKind::Date(date_time_to_semantic(dt)))
8208 }
8209 (Value::Time(t), TypeSpecification::Time { .. }) => {
8210 Ok(ValueKind::Time(time_to_semantic(t)))
8211 }
8212 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
8213 }
8214}
8215
8216pub fn parser_value_to_value_kind(
8222 value: &crate::literals::Value,
8223 lemma_type: &LemmaType,
8224 resolve_unit: Option<&UnitTokenResolver<'_>>,
8225) -> Result<ValueKind, String> {
8226 use crate::literals::Value;
8227 match (value, &lemma_type.specifications) {
8228 (
8229 Value::Range(left, right),
8230 TypeSpecification::NumberRange { .. }
8231 | TypeSpecification::DateRange { .. }
8232 | TypeSpecification::TimeRange { .. }
8233 | TypeSpecification::RatioRange { .. }
8234 | TypeSpecification::MeasureRange { .. },
8235 ) => {
8236 let endpoint = match &lemma_type.specifications {
8237 TypeSpecification::MeasureRange { .. } | TypeSpecification::RatioRange { .. } => {
8238 lemma_type.range_element_type()
8239 }
8240 other => Arc::new(LemmaType::primitive(
8241 range_element_type_specification(other).expect(
8242 "BUG: range_element_type_specification missing arm for known range type",
8243 ),
8244 )),
8245 };
8246 let left_lit = lift_range_endpoint(left, endpoint.as_ref(), resolve_unit)?;
8247 let right_lit = lift_range_endpoint(right, endpoint.as_ref(), resolve_unit)?;
8248 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
8249 }
8250 (value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
8251 }
8252}
8253
8254pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
8258 use crate::parsing::ast::Value;
8259 Ok(match value {
8260 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
8261 Value::Text(s) => ValueKind::Text(Arc::from(s.as_str())),
8262 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
8263 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
8264 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
8265 Value::NumberWithUnit(_, _) => {
8266 return Err(
8267 "number_with_unit literal requires type context (measure or ratio)".to_string(),
8268 );
8269 }
8270 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
8271 })
8272}
8273
8274pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
8276 SemanticDateTime {
8277 year: dt.year,
8278 month: dt.month,
8279 day: dt.day,
8280 hour: dt.hour,
8281 minute: dt.minute,
8282 second: dt.second,
8283 microsecond: dt.microsecond,
8284 timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
8285 }
8286}
8287
8288pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
8290 SemanticTime {
8291 hour: date.hour,
8292 minute: date.minute,
8293 second: date.second,
8294 microsecond: date.microsecond,
8295 timezone: date.timezone.clone(),
8296 }
8297}
8298
8299pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
8301 SemanticTime {
8302 hour: t.hour.into(),
8303 minute: t.minute.into(),
8304 second: t.second.into(),
8305 microsecond: t.microsecond,
8306 timezone: t.timezone.as_ref().map(SemanticTimezone::from),
8307 }
8308}
8309
8310pub(crate) fn compare_semantic_dates(
8314 left: &SemanticDateTime,
8315 right: &SemanticDateTime,
8316) -> std::cmp::Ordering {
8317 left.year
8318 .cmp(&right.year)
8319 .then_with(|| left.month.cmp(&right.month))
8320 .then_with(|| left.day.cmp(&right.day))
8321 .then_with(|| left.hour.cmp(&right.hour))
8322 .then_with(|| left.minute.cmp(&right.minute))
8323 .then_with(|| left.second.cmp(&right.second))
8324 .then_with(|| left.microsecond.cmp(&right.microsecond))
8325}
8326
8327pub(crate) fn compare_semantic_times(
8330 left: &SemanticTime,
8331 right: &SemanticTime,
8332) -> std::cmp::Ordering {
8333 left.hour
8334 .cmp(&right.hour)
8335 .then_with(|| left.minute.cmp(&right.minute))
8336 .then_with(|| left.second.cmp(&right.second))
8337 .then_with(|| left.microsecond.cmp(&right.microsecond))
8338}
8339
8340pub fn conversion_target_to_semantic(
8342 ct: &ConversionTarget,
8343 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
8344 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
8345) -> Result<SemanticConversionTarget, String> {
8346 match ct {
8347 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
8348 ConversionTarget::Unit { unit_name } => {
8349 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
8350 let (bare, owning_type) = match resolved_types {
8351 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
8352 None => index.resolve(unit_name)?,
8353 };
8354 Ok(SemanticConversionTarget::Unit {
8355 unit_name: bare,
8356 owning_type,
8357 })
8358 }
8359 }
8360}
8361
8362static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
8368static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
8369static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
8370static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8371static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8372static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
8373static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
8374static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8375static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8376static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8377static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8378static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8379
8380#[must_use]
8381pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
8382 lemma_type_for_primitive(PrimitiveKind::Boolean)
8383}
8384
8385#[must_use]
8386pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
8387 lemma_type_for_primitive(PrimitiveKind::Number)
8388}
8389
8390#[must_use]
8391pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
8392 lemma_type_for_primitive(PrimitiveKind::Text)
8393}
8394
8395#[must_use]
8396pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
8397 lemma_type_for_primitive(PrimitiveKind::Date)
8398}
8399
8400#[must_use]
8401pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
8402 lemma_type_for_primitive(PrimitiveKind::DateRange)
8403}
8404
8405#[must_use]
8406pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
8407 lemma_type_for_primitive(PrimitiveKind::Time)
8408}
8409
8410#[must_use]
8411pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
8412 lemma_type_for_primitive(PrimitiveKind::Ratio)
8413}
8414
8415#[must_use]
8417pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
8418 let cell = match kind {
8419 PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
8420 PrimitiveKind::Number => &PRIMITIVE_NUMBER,
8421 PrimitiveKind::Text => &PRIMITIVE_TEXT,
8422 PrimitiveKind::Date => &PRIMITIVE_DATE,
8423 PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
8424 PrimitiveKind::Time => &PRIMITIVE_TIME,
8425 PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
8426 PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
8427 PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
8428 PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
8429 PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
8430 PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
8431 };
8432 cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
8433}
8434
8435#[must_use]
8437pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
8438 match kind {
8439 PrimitiveKind::Boolean => TypeSpecification::boolean(),
8440 PrimitiveKind::Measure => TypeSpecification::measure(),
8441 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
8442 PrimitiveKind::Number => TypeSpecification::number(),
8443 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
8444 PrimitiveKind::Ratio => TypeSpecification::ratio(),
8445 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
8446 PrimitiveKind::Text => TypeSpecification::text(),
8447 PrimitiveKind::Date => TypeSpecification::date(),
8448 PrimitiveKind::DateRange => TypeSpecification::date_range(),
8449 PrimitiveKind::Time => TypeSpecification::time(),
8450 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
8451 }
8452}
8453
8454impl fmt::Display for PathSegment {
8459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8460 let hop = self.input_key();
8461 match &self.repository {
8462 Some(repository) => write!(f, "{hop} → {} {}", repository, self.spec),
8463 None => write!(f, "{hop} → {}", self.spec),
8464 }
8465 }
8466}
8467
8468impl fmt::Display for DataPath {
8469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8470 write!(f, "{}", self.input_key())
8471 }
8472}
8473
8474impl fmt::Display for RulePath {
8475 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8476 write!(f, "{}", self.input_key())
8477 }
8478}
8479
8480impl fmt::Display for LemmaType {
8481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8482 write!(f, "{}", self.name())
8483 }
8484}
8485
8486pub(crate) fn format_decimal_for_api(decimal: rust_decimal::Decimal) -> rust_decimal::Decimal {
8487 let normalized = decimal.normalize();
8488 if normalized.fract().is_zero() {
8489 normalized.trunc()
8490 } else {
8491 normalized
8492 }
8493}
8494
8495fn format_decimal_for_human_display(decimal: rust_decimal::Decimal) -> String {
8496 decimal.normalize().to_string()
8497}
8498
8499fn format_rational_for_human_display(
8500 magnitude: &crate::computation::rational::RationalInteger,
8501) -> String {
8502 match magnitude.try_to_decimal() {
8503 Ok(decimal) => format_decimal_for_human_display(decimal),
8504 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
8505 Err(_) => magnitude.display_str(),
8506 }
8507}
8508
8509fn measure_display_unit<'a>(
8510 units: &'a crate::literals::MeasureUnits,
8511 lemma_type: &LemmaType,
8512 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8513) -> &'a crate::literals::MeasureUnit {
8514 let factor_known = |unit: &crate::literals::MeasureUnit| {
8515 unit.literal_factor().is_some()
8516 || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8517 };
8518 if let Some(binding) = lemma_type.unit() {
8519 let unit = units
8520 .iter()
8521 .find(|unit| unit.name == binding)
8522 .unwrap_or_else(|| {
8523 panic!(
8524 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
8525 lemma_type.name()
8526 )
8527 });
8528 if factor_known(unit) {
8529 return unit;
8530 }
8531 }
8532 units
8533 .iter()
8534 .find(|unit| unit.is_canonical_factor() && factor_known(unit))
8535 .or_else(|| {
8536 units
8537 .iter()
8538 .find(|unit| unit.literal_factor().is_some() && factor_known(unit))
8539 })
8540 .or_else(|| units.iter().find(|unit| factor_known(unit)))
8541 .or_else(|| units.iter().next())
8542 .expect("BUG: measure type with non-empty units must have a first unit")
8543}
8544
8545fn format_measure_canonical_for_display(
8546 canonical: &crate::computation::rational::RationalInteger,
8547 lemma_type: &LemmaType,
8548 signature: &[(String, i32)],
8549 runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8550) -> String {
8551 use crate::computation::rational::checked_div;
8552
8553 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
8554 if !units.is_empty() {
8555 let unit = measure_display_unit(units, lemma_type, runtime_scales);
8556 let factor = unit
8557 .literal_factor()
8558 .cloned()
8559 .or_else(|| {
8560 runtime_scales.and_then(|scales| scales.get(&unit.name).cloned())
8561 })
8562 .unwrap_or_else(|| {
8563 panic!(
8564 "BUG: display of unit '{}' on {} requires a literal factor or a settled runtime scale",
8565 unit.name,
8566 lemma_type.name()
8567 )
8568 });
8569 let in_unit = checked_div(canonical, &factor)
8570 .expect("BUG: de-canonicalization for measure display must not fail");
8571 return format_number_with_unit_for_display(&in_unit, &unit.name);
8572 }
8573 }
8574
8575 let unit_label = match signature {
8576 [] => String::new(),
8577 [(name, 1)] => name.clone(),
8578 _ => format_signature_operator_style(signature),
8579 };
8580 let formatted = format_rational_for_human_display(canonical);
8581 if unit_label.is_empty() {
8582 formatted
8583 } else {
8584 format!("{formatted} {unit_label}")
8585 }
8586}
8587
8588fn format_ratio_canonical_for_display(
8589 canonical: &crate::computation::rational::RationalInteger,
8590 lemma_type: &LemmaType,
8591) -> String {
8592 use crate::computation::rational::checked_mul;
8593
8594 let units = match &lemma_type.specifications {
8595 TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
8596 units
8597 }
8598 _ => unreachable!(
8599 "BUG: ratio display with non-ratio type {}",
8600 lemma_type.name()
8601 ),
8602 };
8603 let display_unit = lemma_type
8604 .unit()
8605 .or_else(|| lemma_type.ratio_primary_unit());
8606 let Some(unit_name) = display_unit else {
8607 return format_rational_magnitude_for_display(canonical);
8608 };
8609 let unit = units.get(unit_name).unwrap_or_else(|_| {
8610 panic!(
8611 "BUG: ratio display unit '{unit_name}' is not declared on type {}",
8612 lemma_type.name()
8613 )
8614 });
8615 let in_unit = checked_mul(canonical, &unit.value)
8616 .expect("BUG: de-canonicalization for ratio display must not fail");
8617 format_number_with_unit_for_display(&in_unit, unit_name)
8618}
8619
8620impl fmt::Display for LiteralValue {
8621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8622 match &self.value {
8626 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
8627 write!(f, "{}", self.value)
8628 }
8629 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
8630 _ => write!(f, "{}", self.value),
8631 }
8632 }
8633}
8634
8635#[cfg(test)]
8640pub(crate) mod tests {
8641 use super::*;
8642 use crate::computation::rational::decimal_to_rational;
8643 use crate::literals::DateGranularity;
8644 use crate::literals::Value;
8645 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
8646 use rust_decimal::Decimal;
8647 use std::str::FromStr;
8648
8649 fn meter_axis(exponent: i32) -> BaseMeasureVector {
8650 [(Dimension::Family(test_type_family("meter")), exponent)]
8651 .into_iter()
8652 .collect()
8653 }
8654
8655 #[test]
8656 fn scale_decomposition_exponent_one_is_identity() {
8657 let base = meter_axis(1);
8658 assert_eq!(scale_decomposition(&base, 1), Some(base));
8659 }
8660
8661 #[test]
8662 fn scale_decomposition_squares_meter() {
8663 assert_eq!(scale_decomposition(&meter_axis(1), 2), Some(meter_axis(2)));
8664 }
8665
8666 #[test]
8667 fn scale_decomposition_zero_is_empty() {
8668 assert_eq!(
8669 scale_decomposition(&meter_axis(1), 0),
8670 Some(BaseMeasureVector::new())
8671 );
8672 }
8673
8674 #[test]
8675 fn scale_decomposition_negative_inverts_meter() {
8676 assert_eq!(
8677 scale_decomposition(&meter_axis(1), -1),
8678 Some(meter_axis(-1))
8679 );
8680 }
8681
8682 #[test]
8683 fn scale_decomposition_at_exponent_cap() {
8684 assert_eq!(
8685 scale_decomposition(&meter_axis(32), 2),
8686 Some(meter_axis(64))
8687 );
8688 }
8689
8690 #[test]
8691 fn scale_decomposition_past_exponent_cap_is_none() {
8692 assert_eq!(scale_decomposition(&meter_axis(1), 65), None);
8693 assert_eq!(scale_decomposition(&meter_axis(32), 3), None);
8694 }
8695
8696 #[must_use]
8697 pub(crate) fn test_type_family(name: &str) -> TypeFamily {
8698 TypeFamily {
8699 repository: None,
8700 spec: "test".to_string(),
8701 effective: crate::parsing::ast::EffectiveDate::Origin,
8702 name: name.to_string(),
8703 }
8704 }
8705
8706 #[must_use]
8707 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
8708 crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
8709 }
8710
8711 #[must_use]
8712 pub(crate) fn primitive_measure() -> &'static LemmaType {
8713 primitive_measure_arc().as_ref()
8714 }
8715
8716 #[test]
8717 fn default_primitive_help_is_goal_oriented() {
8718 let kinds = [
8719 PrimitiveKind::Boolean,
8720 PrimitiveKind::Measure,
8721 PrimitiveKind::MeasureRange,
8722 PrimitiveKind::Number,
8723 PrimitiveKind::NumberRange,
8724 PrimitiveKind::Ratio,
8725 PrimitiveKind::RatioRange,
8726 PrimitiveKind::Text,
8727 PrimitiveKind::Date,
8728 PrimitiveKind::DateRange,
8729 PrimitiveKind::Time,
8730 PrimitiveKind::TimeRange,
8731 ];
8732 for kind in kinds {
8733 let spec = type_spec_for_primitive(kind);
8734 let help = match &spec {
8735 TypeSpecification::Boolean { help, .. }
8736 | TypeSpecification::Number { help, .. }
8737 | TypeSpecification::NumberRange { help, .. }
8738 | TypeSpecification::Text { help, .. }
8739 | TypeSpecification::Measure { help, .. }
8740 | TypeSpecification::MeasureRange { help, .. }
8741 | TypeSpecification::Ratio { help, .. }
8742 | TypeSpecification::RatioRange { help, .. }
8743 | TypeSpecification::Date { help, .. }
8744 | TypeSpecification::DateRange { help, .. }
8745 | TypeSpecification::TimeRange { help, .. }
8746 | TypeSpecification::Time { help, .. } => help,
8747 TypeSpecification::Veto { .. }
8748 | TypeSpecification::Undetermined
8749 | TypeSpecification::Spec { .. }
8750 | TypeSpecification::List { .. } => {
8751 unreachable!(
8752 "BUG: primitive kind {:?} mapped to non-primitive spec",
8753 kind
8754 )
8755 }
8756 };
8757 assert!(!help.is_empty(), "help for {:?}", kind);
8758 assert!(
8759 !help.to_ascii_lowercase().contains("format:"),
8760 "help for {:?} must not describe syntax: {:?}",
8761 kind,
8762 help
8763 );
8764 assert_eq!(help, default_help_for_primitive(kind));
8765 }
8766 }
8767
8768 #[test]
8769 fn data_path_and_rule_path_display_are_input_key() {
8770 let implicit = PathSegment {
8771 uses: Arc::from("bag"),
8772 row: None,
8773 repository: None,
8774 spec: Arc::from("bag"),
8775 };
8776 let explicit = PathSegment {
8777 uses: Arc::from("bag"),
8778 row: None,
8779 repository: None,
8780 spec: Arc::from("item_bag"),
8781 };
8782 let named_repo = PathSegment {
8783 uses: Arc::from("calc"),
8784 row: None,
8785 repository: Some(Arc::from("alpha")),
8786 spec: Arc::from("pricing"),
8787 };
8788 let row = PathSegment {
8789 uses: Arc::from("staff"),
8790 row: Some(2),
8791 repository: None,
8792 spec: Arc::from("employee"),
8793 };
8794
8795 assert_eq!(
8796 DataPath::new(vec![row.clone()], "salary".to_string()).to_string(),
8797 "staff.2.salary"
8798 );
8799 assert_eq!(
8800 RulePath::new(vec![row.clone()], "double".to_string()).to_string(),
8801 "staff.2.double"
8802 );
8803 assert_eq!(row.to_string(), "staff.2 → employee");
8804 assert_eq!(
8805 SpecInstance {
8806 prefix: vec![implicit.clone(), row]
8807 }
8808 .to_string(),
8809 "bag.staff.2"
8810 );
8811 assert_eq!(
8812 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
8813 "bag.weight"
8814 );
8815 assert_eq!(
8816 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
8817 "bag.weight"
8818 );
8819 assert_eq!(
8820 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
8821 "calc.price"
8822 );
8823 assert_eq!(
8824 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
8825 "bag.cost"
8826 );
8827 assert_eq!(
8828 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
8829 "bag.cost"
8830 );
8831 assert_eq!(
8832 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
8833 "calc.price"
8834 );
8835
8836 assert_eq!(
8837 PathSegment {
8838 uses: Arc::from("bag"),
8839 row: None,
8840 repository: None,
8841 spec: Arc::from("item_bag"),
8842 }
8843 .to_string(),
8844 "bag → item_bag"
8845 );
8846 assert_eq!(
8847 PathSegment {
8848 uses: Arc::from("calc"),
8849 row: None,
8850 repository: Some(Arc::from("alpha")),
8851 spec: Arc::from("pricing"),
8852 }
8853 .to_string(),
8854 "calc → alpha pricing"
8855 );
8856 }
8857
8858 #[test]
8859 fn test_negated_comparison() {
8860 assert_eq!(
8861 negated_comparison(ComparisonComputation::LessThan),
8862 ComparisonComputation::GreaterThanOrEqual
8863 );
8864 assert_eq!(
8865 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
8866 ComparisonComputation::LessThan
8867 );
8868 assert_eq!(
8869 negated_comparison(ComparisonComputation::Is),
8870 ComparisonComputation::IsNot
8871 );
8872 assert_eq!(
8873 negated_comparison(ComparisonComputation::IsNot),
8874 ComparisonComputation::Is
8875 );
8876 }
8877
8878 #[test]
8879 fn value_to_semantic_number_is_decimal() {
8880 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
8881 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
8882 }
8883
8884 #[test]
8885 fn value_kind_measure_serializes_magnitude_only() {
8886 let kind =
8887 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
8888 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
8889 assert_eq!(json["measure"]["value"], "99.5");
8890 assert!(json["measure"].get("signature").is_none());
8891 }
8892
8893 #[test]
8894 fn value_kind_measure_roundtrips() {
8895 let original =
8896 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
8897 let api = crate::api::ValueKind::from(&original);
8898 let json = serde_json::to_string(&api).unwrap();
8899 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
8900 assert_eq!(api, parsed);
8901 }
8902
8903 #[test]
8904 fn value_kind_measure_empty_roundtrips() {
8905 let original =
8906 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
8907 let api = crate::api::ValueKind::from(&original);
8908 let json = serde_json::to_string(&api).unwrap();
8909 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
8910 assert_eq!(api, parsed);
8911 }
8912
8913 #[test]
8914 fn literal_value_number_serde_not_rational_array() {
8915 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
8916 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
8917 let number = json
8918 .get("value")
8919 .and_then(|v| v.get("number"))
8920 .expect("number field");
8921 assert!(number.is_string());
8922 assert_eq!(number.as_str(), Some("20"));
8923 assert!(
8924 !number.is_array(),
8925 "stored number must not serialize as [n,d]"
8926 );
8927 }
8928
8929 #[test]
8930 fn test_literal_value_to_primitive_type() {
8931 let one = rational_new(1, 1);
8932
8933 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
8934 assert_eq!(
8935 TypedLiteral::number(one.clone()).lemma_type.name(),
8936 "number"
8937 );
8938 assert_eq!(
8939 TypedLiteral::from_bool(bool::from(BooleanValue::True))
8940 .lemma_type
8941 .name(),
8942 "boolean"
8943 );
8944
8945 let dt = DateTimeValue {
8946 year: 2024,
8947 month: 1,
8948 day: 1,
8949 hour: 0,
8950 minute: 0,
8951 second: 0,
8952 microsecond: 0,
8953 timezone: None,
8954
8955 granularity: DateGranularity::Full,
8956 };
8957 assert_eq!(
8958 TypedLiteral::date(date_time_to_semantic(&dt))
8959 .lemma_type
8960 .name(),
8961 "date"
8962 );
8963 assert_eq!(
8964 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
8965 .lemma_type
8966 .name(),
8967 "ratio"
8968 );
8969 let dur_type = LemmaType::new(
8970 "duration".to_string(),
8971 TypeSpecification::Measure {
8972 minimum: None,
8973 maximum: None,
8974 units: MeasureUnits::from(vec![MeasureUnit {
8975 name: "second".to_string(),
8976 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
8977 derived_measure_factors: Vec::new(),
8978 decomposition: BaseMeasureVector::new(),
8979 minimum: None,
8980 maximum: None,
8981 suggestion_magnitude: None,
8982 }]),
8983 traits: vec![MeasureTrait::Duration],
8984 decomposition: None,
8985 unit: None,
8986 help: String::new(),
8987 },
8988 TypeExtends::Primitive,
8989 );
8990 assert_eq!(
8991 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
8992 .lemma_type
8993 .name(),
8994 "duration"
8995 );
8996 }
8997
8998 #[test]
8999 fn test_type_display() {
9000 let specs = TypeSpecification::text();
9001 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
9002 assert_eq!(format!("{}", lemma_type), "name");
9003 }
9004
9005 #[test]
9006 fn test_type_serialization() {
9007 let specs = TypeSpecification::number();
9008 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
9009 let api = crate::api::LemmaType::from(&lemma_type);
9010 let serialized = serde_json::to_string(&api).unwrap();
9011 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
9012 assert_eq!(api, deserialized);
9013 }
9014
9015 #[test]
9016 fn empty_list_display_value_with_type_is_type_spec() {
9017 let element = Arc::new(LemmaType::new(
9018 "item".to_string(),
9019 TypeSpecification::Spec {
9020 spec: SpecIdentity {
9021 repository: None,
9022 spec: "item".to_string(),
9023 effective: crate::parsing::ast::EffectiveDate::Origin,
9024 slice: crate::parsing::ast::EffectiveDate::Origin,
9025 },
9026 },
9027 TypeExtends::Primitive,
9028 ));
9029 let list_ty = LemmaType::list_of(element);
9030 let empty = LiteralValue::new(ValueKind::List(Vec::new().into()));
9031 assert_eq!(empty.display_value_with_type(&list_ty), "item list");
9032 assert_eq!(empty.display_value(), "");
9033 }
9034
9035 #[test]
9036 fn test_literal_value_display_value() {
9037 let ten = rational_new(10, 1);
9038
9039 assert_eq!(
9040 LiteralValue::text("hello".to_string()).display_value(),
9041 "hello"
9042 );
9043 assert_eq!(LiteralValue::number(ten).display_value(), "10");
9044 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
9045 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
9046
9047 let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
9049 let ten_percent_ratio = LiteralValue::ratio(
9050 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
9051 .expect("ratio decimal"),
9052 );
9053 assert_eq!(
9054 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
9055 "10%"
9056 );
9057
9058 let time = TimeValue {
9059 hour: 14,
9060 minute: 30,
9061 second: 0,
9062 microsecond: 0,
9063 timezone: None,
9064 };
9065 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
9066 assert!(time_display.contains("14"));
9067 assert!(time_display.contains("30"));
9068 }
9069
9070 #[test]
9071 fn test_measure_display_trims_trailing_zeros() {
9072 let money_type = LemmaType {
9073 name: Some("money".to_string()),
9074 specifications: TypeSpecification::Measure {
9075 minimum: None,
9076 maximum: None,
9077 units: MeasureUnits::from(vec![MeasureUnit {
9078 name: "eur".to_string(),
9079 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9080 derived_measure_factors: Vec::new(),
9081 decomposition: BaseMeasureVector::new(),
9082 minimum: None,
9083 maximum: None,
9084 suggestion_magnitude: None,
9085 }]),
9086 traits: Vec::new(),
9087 decomposition: None,
9088 unit: None,
9089 help: String::new(),
9090 },
9091 extends: TypeExtends::Primitive,
9092 options: TypeOptions::default(),
9093 };
9094 let money_type = Arc::new(money_type);
9095 let val =
9096 LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
9097 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.8 eur");
9098 let more_precision = LiteralValue::measure(
9099 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
9100 );
9101 assert_eq!(
9102 more_precision.display_value_with_type(money_type.as_ref()),
9103 "1.8 eur"
9104 );
9105 let count_type = Arc::new(LemmaType {
9106 name: Some("count".to_string()),
9107 specifications: TypeSpecification::Measure {
9108 minimum: None,
9109 maximum: None,
9110 units: MeasureUnits::from(vec![MeasureUnit {
9111 name: "items".to_string(),
9112 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9113 derived_measure_factors: Vec::new(),
9114 decomposition: BaseMeasureVector::new(),
9115 minimum: None,
9116 maximum: None,
9117 suggestion_magnitude: None,
9118 }]),
9119 traits: Vec::new(),
9120 decomposition: None,
9121 unit: None,
9122 help: String::new(),
9123 },
9124 extends: TypeExtends::Primitive,
9125 options: TypeOptions::default(),
9126 });
9127 let val_any = LiteralValue::measure(
9128 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
9129 );
9130 assert_eq!(
9131 val_any.display_value_with_type(count_type.as_ref()),
9132 "42.5 items"
9133 );
9134 }
9135
9136 #[test]
9137 fn test_literal_value_time_type() {
9138 let time = TimeValue {
9139 hour: 14,
9140 minute: 30,
9141 second: 0,
9142 microsecond: 0,
9143 timezone: None,
9144 };
9145 let lit = TypedLiteral::time(time_to_semantic(&time));
9146 assert_eq!(lit.lemma_type.name(), "time");
9147 }
9148
9149 #[test]
9150 fn test_measure_family_name_none_without_declaring_spec() {
9151 let money_primitive = LemmaType::new(
9152 "money".to_string(),
9153 TypeSpecification::measure(),
9154 TypeExtends::Primitive,
9155 );
9156 assert_eq!(money_primitive.measure_family_name(), None);
9157 }
9158
9159 #[test]
9160 fn test_measure_family_name_custom() {
9161 let measure_spec = TypeSpecification::measure();
9162 let money_custom = LemmaType::new(
9163 "money".to_string(),
9164 measure_spec,
9165 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9166 );
9167 assert_eq!(money_custom.measure_family_name(), Some("money"));
9168 }
9169
9170 #[test]
9171 fn test_same_named_roots_in_different_specs_are_different_families() {
9172 let declared_in = |spec: &str| TypeFamily {
9173 repository: None,
9174 spec: spec.to_string(),
9175 effective: crate::parsing::ast::EffectiveDate::Origin,
9176 name: "money".to_string(),
9177 };
9178 let pricing_money = LemmaType::new(
9179 "money".to_string(),
9180 TypeSpecification::measure(),
9181 TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
9182 );
9183 let shop_money = LemmaType::new(
9184 "money".to_string(),
9185 TypeSpecification::measure(),
9186 TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
9187 );
9188 assert!(!pricing_money.same_measure_family(&shop_money));
9189 assert!(!shop_money.same_measure_family(&pricing_money));
9190 }
9191
9192 #[test]
9193 fn test_same_measure_family_parent_and_child() {
9194 let measure_spec = TypeSpecification::measure();
9195 let type_x = LemmaType::new(
9196 "x".to_string(),
9197 measure_spec.clone(),
9198 TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
9199 );
9200 let type_x2 = LemmaType::new(
9201 "x2".to_string(),
9202 measure_spec,
9203 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9204 );
9205 assert_eq!(type_x.measure_family_name(), Some("x"));
9206 assert_eq!(type_x2.measure_family_name(), Some("x"));
9207 assert!(type_x.same_measure_family(&type_x2));
9208 assert!(type_x2.same_measure_family(&type_x));
9209 }
9210
9211 #[test]
9212 fn test_same_measure_family_siblings() {
9213 let measure_spec = TypeSpecification::measure();
9214 let type_x2_a = LemmaType::new(
9215 "x2a".to_string(),
9216 measure_spec.clone(),
9217 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9218 );
9219 let type_x2_b = LemmaType::new(
9220 "x2b".to_string(),
9221 measure_spec,
9222 TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9223 );
9224 assert!(type_x2_a.same_measure_family(&type_x2_b));
9225 }
9226
9227 #[test]
9228 fn test_same_measure_family_different_families() {
9229 let measure_spec = TypeSpecification::measure();
9230 let money = LemmaType::new(
9231 "money".to_string(),
9232 measure_spec.clone(),
9233 TypeExtends::Primitive,
9234 );
9235 let temperature = LemmaType::new(
9236 "temperature".to_string(),
9237 measure_spec,
9238 TypeExtends::Primitive,
9239 );
9240 assert!(!money.same_measure_family(&temperature));
9241 assert!(!temperature.same_measure_family(&money));
9242 }
9243
9244 #[test]
9245 fn test_same_measure_family_measure_vs_non_measure() {
9246 let measure_spec = TypeSpecification::measure();
9247 let number_spec = TypeSpecification::number();
9248 let measure_type =
9249 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
9250 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9251 assert!(!measure_type.same_measure_family(&number_type));
9252 assert!(!number_type.same_measure_family(&measure_type));
9253 }
9254
9255 #[test]
9256 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
9257 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
9258 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
9259
9260 assert!(!left.same_measure_family(&right));
9261 assert!(left.compatible_with_anonymous_measure(&right));
9262 }
9263
9264 #[test]
9265 fn same_value_type_distinct_families_same_decomposition_are_not_same() {
9266 let decomp: BaseMeasureVector = [
9267 (Dimension::Family(test_type_family("length")), 1i32),
9268 (Dimension::Family(test_type_family("elapsed")), -1i32),
9269 ]
9270 .into_iter()
9271 .collect();
9272 let speed_spec = |name: &str, family: &str| {
9273 let mut spec = TypeSpecification::measure();
9274 if let TypeSpecification::Measure {
9275 decomposition,
9276 help,
9277 ..
9278 } = &mut spec
9279 {
9280 *decomposition = Some(decomp.clone());
9281 *help = String::new();
9282 }
9283 LemmaType::new(
9284 name.to_string(),
9285 spec,
9286 TypeExtends::custom_local("measure".to_string(), test_type_family(family)),
9287 )
9288 };
9289 let speed_a = speed_spec("speed_a", "speed_a");
9290 let speed_b = speed_spec("speed_b", "speed_b");
9291 assert_eq!(
9292 speed_a.measure_type_decomposition(),
9293 speed_b.measure_type_decomposition()
9294 );
9295 assert!(!speed_a.same_measure_family(&speed_b));
9296 assert!(
9297 !speed_a.same_value_type(&speed_b),
9298 "same decomposition alone must not make distinct measure families same_value_type"
9299 );
9300 }
9301
9302 #[test]
9303 fn test_measure_family_name_non_measure_returns_none() {
9304 let number_spec = TypeSpecification::number();
9305 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9306 assert_eq!(number_type.measure_family_name(), None);
9307 }
9308
9309 #[test]
9310 fn test_lemma_type_inequality_local_vs_import_same_shape() {
9311 let measure_spec = TypeSpecification::measure();
9312 let local = LemmaType::new(
9313 "t".to_string(),
9314 measure_spec.clone(),
9315 TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9316 );
9317 let imported = LemmaType::new(
9318 "t".to_string(),
9319 measure_spec,
9320 TypeExtends::Custom {
9321 parent: "money".to_string(),
9322 family: test_type_family("money"),
9323 defining_spec: TypeDefiningSpec::Import,
9324 },
9325 );
9326 assert_ne!(local, imported);
9327 }
9328
9329 #[test]
9330 fn test_lemma_type_equality_import_unit_variant() {
9331 let measure_spec = TypeSpecification::measure();
9332 let left = LemmaType::new(
9333 "t".to_string(),
9334 measure_spec.clone(),
9335 TypeExtends::Custom {
9336 parent: "money".to_string(),
9337 family: test_type_family("money"),
9338 defining_spec: TypeDefiningSpec::Import,
9339 },
9340 );
9341 let right = LemmaType::new(
9342 "t".to_string(),
9343 measure_spec,
9344 TypeExtends::Custom {
9345 parent: "money".to_string(),
9346 family: test_type_family("money"),
9347 defining_spec: TypeDefiningSpec::Import,
9348 },
9349 );
9350 assert_eq!(left, right);
9351 }
9352
9353 fn month_suggestion_arg() -> CommandArg {
9354 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9355 Decimal::ONE,
9356 "month".to_string(),
9357 ))
9358 }
9359
9360 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
9361 [
9362 CommandArg::Label(name.to_string()),
9363 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
9364 ]
9365 }
9366
9367 #[test]
9368 fn default_calendar_on_text_reports_hint() {
9369 let mut specs = TypeSpecification::text();
9370 let mut default = None;
9371 let err = specs
9372 .apply_constraint(
9373 ConstraintNames {
9374 type_name: "notes",
9375 data_name: "notes",
9376 },
9377 TypeConstraintCommand::Suggest,
9378 &[month_suggestion_arg()],
9379 &mut default,
9380 &mut None,
9381 None,
9382 )
9383 .unwrap_err();
9384 assert!(err.contains("Unit 'month' is for calendar data"));
9385 assert!(err.contains("double quotes"));
9386 }
9387
9388 #[test]
9389 fn default_calendar_on_duration_reports_valid_units() {
9390 let mut specs = TypeSpecification::measure();
9391 specs
9392 .apply_constraint(
9393 ConstraintNames {
9394 type_name: "duration",
9395 data_name: "duration",
9396 },
9397 TypeConstraintCommand::Unit,
9398 &unit_factor_arg("second", 1),
9399 &mut None,
9400 &mut None,
9401 None,
9402 )
9403 .unwrap();
9404 specs
9405 .apply_constraint(
9406 ConstraintNames {
9407 type_name: "duration",
9408 data_name: "duration",
9409 },
9410 TypeConstraintCommand::Unit,
9411 &unit_factor_arg("week", 604_800),
9412 &mut None,
9413 &mut None,
9414 None,
9415 )
9416 .unwrap();
9417 specs
9418 .apply_constraint(
9419 ConstraintNames {
9420 type_name: "duration",
9421 data_name: "duration",
9422 },
9423 TypeConstraintCommand::Trait,
9424 &[CommandArg::Label("duration".to_string())],
9425 &mut None,
9426 &mut None,
9427 None,
9428 )
9429 .unwrap();
9430 let mut default = None;
9431 let err = specs
9432 .apply_constraint(
9433 ConstraintNames {
9434 type_name: "duration",
9435 data_name: "duration",
9436 },
9437 TypeConstraintCommand::Suggest,
9438 &[month_suggestion_arg()],
9439 &mut default,
9440 &mut None,
9441 None,
9442 )
9443 .unwrap_err();
9444 assert!(err.contains("Unit 'month' is for calendar data"));
9445 assert!(err.contains("Valid 'duration' units are"));
9446 assert!(err.contains("week"));
9447 }
9448
9449 #[test]
9450 fn default_valid_duration_weeks_accepted() {
9451 let mut specs = TypeSpecification::measure();
9452 specs
9453 .apply_constraint(
9454 ConstraintNames {
9455 type_name: "duration",
9456 data_name: "duration",
9457 },
9458 TypeConstraintCommand::Unit,
9459 &unit_factor_arg("second", 1),
9460 &mut None,
9461 &mut None,
9462 None,
9463 )
9464 .unwrap();
9465 specs
9466 .apply_constraint(
9467 ConstraintNames {
9468 type_name: "duration",
9469 data_name: "duration",
9470 },
9471 TypeConstraintCommand::Unit,
9472 &unit_factor_arg("week", 604_800),
9473 &mut None,
9474 &mut None,
9475 None,
9476 )
9477 .unwrap();
9478 specs
9479 .apply_constraint(
9480 ConstraintNames {
9481 type_name: "duration",
9482 data_name: "duration",
9483 },
9484 TypeConstraintCommand::Trait,
9485 &[CommandArg::Label("duration".to_string())],
9486 &mut None,
9487 &mut None,
9488 None,
9489 )
9490 .unwrap();
9491 let mut default = None;
9492 specs
9493 .apply_constraint(
9494 ConstraintNames {
9495 type_name: "duration",
9496 data_name: "duration",
9497 },
9498 TypeConstraintCommand::Suggest,
9499 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9500 Decimal::from(4),
9501 "week".to_string(),
9502 ))],
9503 &mut default,
9504 &mut None,
9505 None,
9506 )
9507 .unwrap();
9508 assert!(matches!(
9509 default,
9510 Some(RawSuggestion::Measure {
9511 unit_name,
9512 ..
9513 }) if unit_name == "week"
9514 ));
9515 }
9516
9517 #[test]
9518 fn default_unknown_unit_on_duration_lists_valid_units() {
9519 let mut specs = TypeSpecification::measure();
9520 specs
9521 .apply_constraint(
9522 ConstraintNames {
9523 type_name: "duration",
9524 data_name: "duration",
9525 },
9526 TypeConstraintCommand::Unit,
9527 &unit_factor_arg("second", 1),
9528 &mut None,
9529 &mut None,
9530 None,
9531 )
9532 .unwrap();
9533 specs
9534 .apply_constraint(
9535 ConstraintNames {
9536 type_name: "duration",
9537 data_name: "duration",
9538 },
9539 TypeConstraintCommand::Trait,
9540 &[CommandArg::Label("duration".to_string())],
9541 &mut None,
9542 &mut None,
9543 None,
9544 )
9545 .unwrap();
9546 let mut default = None;
9547 let err = specs
9548 .apply_constraint(
9549 ConstraintNames {
9550 type_name: "duration",
9551 data_name: "duration",
9552 },
9553 TypeConstraintCommand::Suggest,
9554 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9555 Decimal::ONE,
9556 "fortnight".to_string(),
9557 ))],
9558 &mut default,
9559 &mut None,
9560 None,
9561 )
9562 .unwrap_err();
9563 assert!(err.contains("fortnight"));
9564 assert!(err.contains("not defined on 'duration'"));
9565 assert!(err.contains("Valid units are"));
9566 }
9567
9568 fn money_measure_type() -> LemmaType {
9569 LemmaType::new(
9570 "Money".to_string(),
9571 TypeSpecification::Measure {
9572 minimum: None,
9573 maximum: None,
9574 units: MeasureUnits::from(vec![
9575 MeasureUnit {
9576 name: "eur".to_string(),
9577 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9578 derived_measure_factors: Vec::new(),
9579 decomposition: BaseMeasureVector::new(),
9580 minimum: None,
9581 maximum: None,
9582 suggestion_magnitude: None,
9583 },
9584 MeasureUnit {
9585 name: "usd".to_string(),
9586 scale: UnitScale::Literal(
9587 crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
9588 .expect("factor"),
9589 ),
9590 derived_measure_factors: Vec::new(),
9591 decomposition: BaseMeasureVector::new(),
9592 minimum: None,
9593 maximum: None,
9594 suggestion_magnitude: None,
9595 },
9596 ]),
9597 traits: Vec::new(),
9598 decomposition: None,
9599 unit: None,
9600 help: String::new(),
9601 },
9602 TypeExtends::Primitive,
9603 )
9604 }
9605
9606 #[test]
9607 fn measure_unit_names_for_named_measure() {
9608 let money = money_measure_type();
9609 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
9610 }
9611
9612 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
9617 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
9618 }
9619
9620 #[test]
9621 fn combine_signatures_multiply_adds_exponents() {
9622 let left = sig(&[("eur", 1)]);
9623 let right = sig(&[("hour", -1)]);
9624 let result = combine_signatures(&left, &right, true);
9625 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
9626 }
9627
9628 #[test]
9629 fn combine_signatures_divide_subtracts_exponents() {
9630 let left = sig(&[("eur", 1)]);
9631 let right = sig(&[("hour", 1)]);
9632 let result = combine_signatures(&left, &right, false);
9633 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
9634 }
9635
9636 #[test]
9637 fn combine_signatures_cancels_to_empty() {
9638 let left = sig(&[("ce", 1), ("minute", -1)]);
9639 let right = sig(&[("minute", 1)]);
9640 let result = combine_signatures(&left, &right, true);
9641 assert_eq!(result, sig(&[("ce", 1)]));
9643 }
9644
9645 #[test]
9646 fn combine_signatures_output_is_canonical_form() {
9647 let left = sig(&[("eur", 1), ("hour", 1)]);
9648 let right = sig(&[("minute", 1)]);
9649 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
9652 assert_eq!(result, expected);
9653 }
9654
9655 #[test]
9656 fn canonicalize_signature_drops_zero_exponents() {
9657 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
9658 let result = canonicalize_signature(&sig_with_zero);
9659 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
9660 }
9661
9662 #[test]
9663 fn canonicalize_signature_sorts_by_name() {
9664 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
9665 let result = canonicalize_signature(&unsorted);
9666 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
9667 }
9668
9669 #[test]
9675 fn format_signature_operator_style_numerator_only() {
9676 let signature = sig(&[("eur", 1)]);
9677 let result = format_signature_operator_style(&signature);
9678 assert_eq!(result, "eur");
9679 }
9680
9681 #[test]
9682 fn format_signature_operator_style_with_denominator() {
9683 let signature = sig(&[("eur", 1), ("hour", -1)]);
9684 let result = format_signature_operator_style(&signature);
9685 assert_eq!(result, "eur/hour");
9686 }
9687
9688 #[test]
9689 fn format_signature_operator_style_denominator_only() {
9690 let signature = sig(&[("meter", -1)]);
9691 let result = format_signature_operator_style(&signature);
9692 assert_eq!(result, "1/meter");
9693 }
9694
9695 #[test]
9696 fn format_signature_operator_style_with_exponents() {
9697 let signature = sig(&[("meter", 2), ("second", -2)]);
9698 let result = format_signature_operator_style(&signature);
9699 assert_eq!(result, "meter^2/second^2");
9700 }
9701
9702 #[test]
9707 fn calendar_unit_factor_table_completeness() {
9708 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
9711 let name = unit.to_string();
9712 assert!(
9713 calendar_unit_factor(&name).is_some(),
9714 "calendar_unit_factor('{}') must return Some",
9715 name
9716 );
9717 }
9718 }
9719
9720 #[test]
9721 fn semantic_calendar_unit_display_returns_singular() {
9722 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
9725 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
9726 }
9727
9728 #[test]
9733 fn signature_factor_with_calendar_units() {
9734 let calendar = test_calendar_type_for_signature_factor();
9735 let unit_index = crate::planning::unit_index::UnitIndex::new();
9736 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
9739 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
9740 .expect("must not overflow");
9741 let expected = rational_new(1, 12);
9742 assert_eq!(factor, expected, "month/year factor must be 1/12");
9743 }
9744
9745 fn test_calendar_type_for_signature_factor() -> LemmaType {
9746 use crate::computation::rational::{decimal_to_rational, rational_one};
9747 use crate::literals::{MeasureUnit, MeasureUnits};
9748 use rust_decimal::Decimal;
9749 LemmaType::new(
9750 "calendar".to_string(),
9751 TypeSpecification::Measure {
9752 minimum: None,
9753 maximum: None,
9754 units: MeasureUnits::from(vec![
9755 MeasureUnit {
9756 name: "month".to_string(),
9757 scale: UnitScale::Literal(rational_one()),
9758 minimum: None,
9759 maximum: None,
9760 suggestion_magnitude: None,
9761 decomposition: calendar_decomposition(),
9762 derived_measure_factors: Vec::new(),
9763 },
9764 MeasureUnit {
9765 name: "year".to_string(),
9766 scale: UnitScale::Literal(
9767 decimal_to_rational(Decimal::from(12)).expect("year factor"),
9768 ),
9769 minimum: None,
9770 maximum: None,
9771 suggestion_magnitude: None,
9772 decomposition: calendar_decomposition(),
9773 derived_measure_factors: Vec::new(),
9774 },
9775 ]),
9776 traits: vec![MeasureTrait::Calendar],
9777 decomposition: Some(calendar_decomposition()),
9778 unit: None,
9779 help: String::new(),
9780 },
9781 TypeExtends::Primitive,
9782 )
9783 }
9784
9785 #[test]
9786 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
9787 fn signature_factor_panics_on_unresolved_name() {
9788 let unit_index = crate::planning::unit_index::UnitIndex::new();
9789 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
9790 signature_factor(&bad_sig, &unit_index, None)
9791 .expect("BUG: signature_factor must panic before returning a failure");
9792 }
9793
9794 #[test]
9795 fn signature_factor_uses_owner_when_expression_index_empty() {
9796 let money = test_money_type_for_signature_factor();
9797 let expression_units = crate::planning::unit_index::UnitIndex::new();
9798 let sig_usd = sig(&[("usd", 1)]);
9799 let factor =
9800 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
9801 assert_eq!(factor, rational_new(91, 100));
9802 }
9803
9804 fn test_money_type_for_signature_factor() -> LemmaType {
9805 use crate::computation::rational::decimal_to_rational;
9806 use crate::literals::{MeasureUnit, MeasureUnits};
9807 use rust_decimal::Decimal;
9808 LemmaType::new(
9809 "money".to_string(),
9810 TypeSpecification::Measure {
9811 minimum: None,
9812 maximum: None,
9813 units: MeasureUnits::from(vec![
9814 MeasureUnit {
9815 name: "eur".to_string(),
9816 scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9817 minimum: None,
9818 maximum: None,
9819 suggestion_magnitude: None,
9820 decomposition: BaseMeasureVector::new(),
9821 derived_measure_factors: Vec::new(),
9822 },
9823 MeasureUnit {
9824 name: "usd".to_string(),
9825 scale: UnitScale::Literal(
9826 decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
9827 ),
9828 minimum: None,
9829 maximum: None,
9830 suggestion_magnitude: None,
9831 decomposition: BaseMeasureVector::new(),
9832 derived_measure_factors: Vec::new(),
9833 },
9834 ]),
9835 traits: Vec::new(),
9836 decomposition: None,
9837 unit: None,
9838 help: String::new(),
9839 },
9840 TypeExtends::Primitive,
9841 )
9842 }
9843
9844 fn measure_type_with_kilogram() -> TypeSpecification {
9845 use crate::computation::rational::rational_one;
9846 use crate::literals::{MeasureUnit, MeasureUnits};
9847 let mut units = MeasureUnits::new();
9848 units.push(MeasureUnit {
9849 name: "kilogram".to_string(),
9850 scale: UnitScale::Literal(rational_one()),
9851 minimum: None,
9852 maximum: None,
9853 suggestion_magnitude: None,
9854 decomposition: BaseMeasureVector::new(),
9855 derived_measure_factors: Vec::new(),
9856 });
9857 TypeSpecification::Measure {
9858 minimum: None,
9859 maximum: None,
9860 units,
9861 traits: Vec::new(),
9862 decomposition: None,
9863 unit: None,
9864 help: String::new(),
9865 }
9866 }
9867
9868 #[test]
9869 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
9870 let ten = Value::Number(Decimal::from(10));
9871 let measure = LemmaType::primitive(measure_type_with_kilogram());
9872 let err = parser_value_to_value_kind(&ten, &measure, None)
9873 .expect_err("bare number must not bind to measure");
9874 assert!(
9875 err.contains("kilogram"),
9876 "error must hint expected unit, got: {err}"
9877 );
9878 }
9879
9880 #[test]
9881 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
9882 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
9883 let measure = LemmaType::primitive(measure_type_with_kilogram());
9884 let kind = parser_value_to_value_kind(&ten_kg, &measure, None)
9885 .expect("10 kilogram must bind to measure");
9886 assert!(matches!(kind, ValueKind::Measure(_)));
9887 }
9888
9889 #[test]
9890 fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
9891 let ten = Value::Number(Decimal::from(10));
9892 let ratio = LemmaType::primitive(TypeSpecification::ratio());
9893 let err = parser_value_to_value_kind(&ten, &ratio, None)
9894 .expect_err("bare number must not bind to ratio");
9895 assert!(
9896 err.contains("percent"),
9897 "error must hint expected unit, got: {err}"
9898 );
9899 }
9900
9901 #[test]
9902 fn value_kind_matches_spec_rejects_number_for_measure() {
9903 let n = ValueKind::Number(rational_new(10, 1));
9904 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
9905 }
9906
9907 #[test]
9908 fn value_kind_matches_spec_rejects_number_for_ratio() {
9909 let n = ValueKind::Number(rational_new(1, 2));
9910 assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
9911 }
9912
9913 #[test]
9914 fn apply_constraint_rejects_inherited_unit_factor_change() {
9915 let mut specs = TypeSpecification::measure();
9916 specs
9917 .apply_constraint(
9918 ConstraintNames {
9919 type_name: "money",
9920 data_name: "money",
9921 },
9922 TypeConstraintCommand::Unit,
9923 &unit_factor_arg("eur", 1),
9924 &mut None,
9925 &mut None,
9926 None,
9927 )
9928 .expect("seed eur");
9929 let err = specs
9930 .apply_constraint(
9931 ConstraintNames {
9932 type_name: "money",
9933 data_name: "money",
9934 },
9935 TypeConstraintCommand::Unit,
9936 &[
9937 CommandArg::Label("eur".to_string()),
9938 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
9939 ],
9940 &mut None,
9941 &mut None,
9942 None,
9943 )
9944 .expect_err("must not change inherited unit factor");
9945 assert!(err.contains("eur"), "error must name unit, got: {err}");
9946 assert!(
9947 err.contains("inherited") || err.contains("cannot change"),
9948 "error must reject factor change, got: {err}"
9949 );
9950 }
9951
9952 #[test]
9953 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
9954 let mut specs = TypeSpecification::measure();
9955 specs
9956 .apply_constraint(
9957 ConstraintNames {
9958 type_name: "money",
9959 data_name: "money",
9960 },
9961 TypeConstraintCommand::Unit,
9962 &unit_factor_arg("eur", 1),
9963 &mut None,
9964 &mut None,
9965 None,
9966 )
9967 .expect("seed eur");
9968 specs
9969 .apply_constraint(
9970 ConstraintNames {
9971 type_name: "money",
9972 data_name: "money",
9973 },
9974 TypeConstraintCommand::Unit,
9975 &unit_factor_arg("usd", 1),
9976 &mut None,
9977 &mut None,
9978 None,
9979 )
9980 .expect("add usd");
9981 match &specs {
9982 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
9983 other => panic!("expected Measure, got {other:?}"),
9984 }
9985 }
9986
9987 #[test]
9988 fn apply_constraint_idempotent_inherited_unit_redeclare() {
9989 let mut specs = TypeSpecification::measure();
9990 specs
9991 .apply_constraint(
9992 ConstraintNames {
9993 type_name: "money",
9994 data_name: "money",
9995 },
9996 TypeConstraintCommand::Unit,
9997 &unit_factor_arg("eur", 1),
9998 &mut None,
9999 &mut None,
10000 None,
10001 )
10002 .expect("seed eur");
10003 specs
10004 .apply_constraint(
10005 ConstraintNames {
10006 type_name: "money",
10007 data_name: "money",
10008 },
10009 TypeConstraintCommand::Unit,
10010 &unit_factor_arg("eur", 1),
10011 &mut None,
10012 &mut None,
10013 None,
10014 )
10015 .expect("idempotent eur");
10016 match &specs {
10017 TypeSpecification::Measure { units, .. } => {
10018 assert_eq!(units.len(), 1);
10019 assert_eq!(
10020 *units
10021 .iter()
10022 .find(|u| u.name == "eur")
10023 .expect("eur")
10024 .require_literal_factor(),
10025 crate::computation::rational::rational_one()
10026 );
10027 }
10028 other => panic!("expected Measure, got {other:?}"),
10029 }
10030 }
10031
10032 #[test]
10033 fn element_from_range_returns_element_for_every_range_primitive() {
10034 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
10035 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
10036 (PrimitiveKind::NumberRange, |element| {
10037 matches!(element, TypeSpecification::Number { .. })
10038 }),
10039 (PrimitiveKind::MeasureRange, |element| {
10040 matches!(element, TypeSpecification::Measure { .. })
10041 }),
10042 (PrimitiveKind::RatioRange, |element| {
10043 matches!(element, TypeSpecification::Ratio { .. })
10044 }),
10045 (PrimitiveKind::DateRange, |element| {
10046 matches!(element, TypeSpecification::Date { .. })
10047 }),
10048 (PrimitiveKind::TimeRange, |element| {
10049 matches!(element, TypeSpecification::Time { .. })
10050 }),
10051 ];
10052 for (kind, matches_element) in cases {
10053 let range_spec = type_spec_for_primitive(kind);
10054 let element = range_spec
10055 .element_from_range()
10056 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
10057 assert!(
10058 matches_element(&element),
10059 "{kind:?} element must match documented mapping, got {element:?}"
10060 );
10061 }
10062 }
10063
10064 #[test]
10065 fn element_from_range_returns_none_for_non_range_primitives() {
10066 let non_range = [
10067 type_spec_for_primitive(PrimitiveKind::Boolean),
10068 type_spec_for_primitive(PrimitiveKind::Measure),
10069 TypeSpecification::Undetermined,
10070 TypeSpecification::veto(),
10071 ];
10072 for spec in non_range {
10073 assert!(
10074 spec.element_from_range().is_none(),
10075 "{spec:?} must not define element_from_range"
10076 );
10077 }
10078 }
10079
10080 fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
10081 LemmaType::primitive(TypeSpecification::Spec {
10082 spec: SpecIdentity {
10083 repository: None,
10084 spec: name.to_string(),
10085 effective: effective.clone(),
10086 slice: effective,
10087 },
10088 })
10089 }
10090
10091 #[test]
10092 fn same_value_type_spec_identity() {
10093 use crate::parsing::ast::EffectiveDate;
10094 let origin = EffectiveDate::Origin;
10095 let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
10096 year: 2026,
10097 month: 1,
10098 day: 1,
10099 hour: 0,
10100 minute: 0,
10101 second: 0,
10102 microsecond: 0,
10103 timezone: None,
10104 granularity: crate::literals::DateGranularity::Full,
10105 });
10106 let tax = spec_type("tax_bracket", origin.clone());
10107 let tax_again = spec_type("tax_bracket", origin.clone());
10108 let other = spec_type("menu_item", origin);
10109 let tax_dated = spec_type("tax_bracket", dated.clone());
10110 assert!(tax.same_value_type(&tax_again));
10111 assert!(!tax.same_value_type(&other));
10112 assert!(!tax.same_value_type(&tax_dated));
10113 let mut tax_later_slice = tax.clone();
10114 match &mut tax_later_slice.specifications {
10115 TypeSpecification::Spec { spec } => spec.slice = dated,
10116 other => panic!("expected Spec, got {other:?}"),
10117 }
10118 assert!(!tax.same_value_type(&tax_later_slice));
10119 let mut other_repo = tax.clone();
10120 match &mut other_repo.specifications {
10121 TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
10122 other => panic!("expected Spec, got {other:?}"),
10123 }
10124 assert!(!tax.same_value_type(&other_repo));
10125 }
10126
10127 #[test]
10128 fn spec_identity_displays_repository_space_spec_space_effective() {
10129 use crate::parsing::ast::EffectiveDate;
10130 let workspace = SpecIdentity {
10131 repository: None,
10132 spec: "tax_bracket".to_string(),
10133 effective: EffectiveDate::Origin,
10134 slice: EffectiveDate::Origin,
10135 };
10136 assert_eq!(workspace.to_string(), "tax_bracket");
10137 let start =
10138 EffectiveDate::DateTimeValue(DateTimeValue::from_str("2026-01-01").expect("date"));
10139 let dated = SpecIdentity {
10140 repository: Some("@jack/finance".to_string()),
10141 spec: "tax_bracket".to_string(),
10142 effective: start.clone(),
10143 slice: start,
10144 };
10145 assert_eq!(
10146 dated.to_string(),
10147 format!(
10148 "@jack/finance tax_bracket {}",
10149 DateTimeValue::from_str("2026-01-01").expect("date")
10150 )
10151 );
10152 let later_slice = SpecIdentity {
10153 slice: EffectiveDate::DateTimeValue(
10154 DateTimeValue::from_str("2026-07-01").expect("date"),
10155 ),
10156 ..workspace
10157 };
10158 assert_eq!(
10159 later_slice.to_string(),
10160 format!(
10161 "tax_bracket slice {}",
10162 DateTimeValue::from_str("2026-07-01").expect("date")
10163 )
10164 );
10165 }
10166
10167 #[test]
10168 fn type_family_displays_declaring_spec_then_type() {
10169 let workspace = TypeFamily {
10170 repository: None,
10171 spec: "pricing".to_string(),
10172 effective: crate::parsing::ast::EffectiveDate::Origin,
10173 name: "money".to_string(),
10174 };
10175 assert_eq!(workspace.to_string(), "pricing.money");
10176 let stdlib = TypeFamily {
10177 repository: Some("lemma".to_string()),
10178 spec: "units".to_string(),
10179 effective: crate::parsing::ast::EffectiveDate::Origin,
10180 name: "mass".to_string(),
10181 };
10182 assert_eq!(stdlib.to_string(), "lemma units.mass");
10183 let dated = TypeFamily {
10184 repository: None,
10185 spec: "pricing".to_string(),
10186 effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
10187 DateTimeValue::from_str("2024-01-01").expect("date"),
10188 ),
10189 name: "money".to_string(),
10190 };
10191 assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
10192 }
10193
10194 #[test]
10195 fn piecewise_type_keeps_spec_identity() {
10196 use crate::parsing::ast::EffectiveDate;
10197 use crate::planning::typing::{piecewise_type, MeasureScope};
10198 use crate::planning::unit_index::UnitIndex;
10199 let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
10200 let unit_index = UnitIndex::new();
10201 let signature_index = crate::computation::arithmetic::SignatureIndex::new();
10202 let named_types = IndexMap::new();
10203 let scope = MeasureScope {
10204 unit_index: &unit_index,
10205 signature_index: &signature_index,
10206 named_types: &named_types,
10207 };
10208 let picked = piecewise_type([Arc::new(LemmaType::veto_type()), Arc::clone(&tax)], &scope);
10209 assert!(picked.same_value_type(&tax));
10210 }
10211
10212 #[test]
10213 #[should_panic(expected = "BUG: literal factor required")]
10214 fn measure_unit_factor_panics_on_reference_scale() {
10215 let units = crate::literals::MeasureUnits::from(vec![
10216 crate::literals::MeasureUnit::from_reference(
10217 "gbp".to_string(),
10218 "rate".to_string(),
10219 Vec::new(),
10220 "rate".to_string(),
10221 ),
10222 ]);
10223 let lemma_type = LemmaType::new(
10224 "money".to_string(),
10225 TypeSpecification::Measure {
10226 units,
10227 traits: vec![],
10228 decomposition: None,
10229 minimum: None,
10230 maximum: None,
10231 unit: None,
10232 help: String::new(),
10233 },
10234 TypeExtends::Primitive,
10235 );
10236 let _ = lemma_type.measure_unit_factor("gbp");
10237 }
10238}