use crate::parsing::arena::{ExprId, ExprKind, ExprTree};
pub use crate::parsing::ast::{
ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
RangeBound, RoundingMode, VetoExpression,
};
pub use crate::parsing::source::Source;
#[must_use]
pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
match op {
ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
ComparisonComputation::Is => ComparisonComputation::IsNot,
ComparisonComputation::IsNot => ComparisonComputation::Is,
}
}
#[must_use]
pub fn mirrored_comparison(op: ComparisonComputation) -> ComparisonComputation {
match op {
ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
ComparisonComputation::Is => ComparisonComputation::Is,
ComparisonComputation::IsNot => ComparisonComputation::IsNot,
}
}
use crate::computation::rational::{checked_div, rational_new, RationalInteger};
use crate::computation::OperationResult;
use crate::parsing::ast::Constraint;
use crate::parsing::ast::{
BooleanValue, CalendarPeriodUnit, ChainStep, CommandArg, ConversionTarget, ConversionTypeKind,
DateCalendarKind, DateRelativeKind, DateTimeValue, LogicalConnective, PrimitiveKind, TimeValue,
TimezoneValue, TypeConstraintCommand,
};
use crate::planning::normalize::NormalFormId;
use crate::planning::unit_index::UnitIndex;
use crate::Error;
use indexmap::IndexMap;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
pub use crate::literals::{
BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, UnitScale,
};
pub fn combine_decompositions(
left: &BaseMeasureVector,
right: &BaseMeasureVector,
is_multiply: bool,
) -> Option<BaseMeasureVector> {
let mut result = left.clone();
for (dim, &exp) in right {
let delta = if is_multiply { exp } else { exp.checked_neg()? };
let entry = result.entry(dim.clone()).or_insert(0);
*entry = entry.checked_add(delta)?;
if entry.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
return None;
}
if *entry == 0 {
result.remove(dim);
}
}
Some(result)
}
pub fn scale_decomposition(base: &BaseMeasureVector, exponent: i32) -> Option<BaseMeasureVector> {
if exponent == 0 {
return Some(BaseMeasureVector::new());
}
let mut result = BaseMeasureVector::new();
for (dimension, &power) in base {
let scaled = power.checked_mul(exponent)?;
if scaled.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
return None;
}
if scaled != 0 {
result.insert(dimension.clone(), scaled);
}
}
Some(result)
}
#[cfg(test)]
pub fn combine_signatures(
left: &[(String, i32)],
right: &[(String, i32)],
is_multiply: bool,
) -> Vec<(String, i32)> {
use std::collections::BTreeMap;
let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
for (name, exponent) in left {
*accumulator.entry(name.clone()).or_insert(0) += exponent;
}
for (name, exponent) in right {
let delta = if is_multiply { *exponent } else { -*exponent };
*accumulator.entry(name.clone()).or_insert(0) += delta;
}
accumulator
.into_iter()
.filter(|(_, exponent)| *exponent != 0)
.collect()
}
pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
let canonical = canonicalize_signature(signature);
let mut numerator: Vec<(String, i32)> = Vec::new();
let mut denominator: Vec<(String, i32)> = Vec::new();
for (name, exponent) in canonical {
if exponent > 0 {
numerator.push((name, exponent));
} else if exponent < 0 {
denominator.push((name, -exponent));
}
}
let render = |terms: &[(String, i32)]| -> String {
terms
.iter()
.map(|(name, exp)| {
if *exp == 1 {
name.clone()
} else {
format!("{name}^{exp}")
}
})
.collect::<Vec<_>>()
.join("*")
};
match (numerator.is_empty(), denominator.is_empty()) {
(true, true) => String::new(),
(false, true) => render(&numerator),
(true, false) => format!("1/{}", render(&denominator)),
(false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
}
}
pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
use crate::computation::rational::rational_one;
match name {
"month" => Some(rational_one()),
"year" => Some(rational_new(12, 1)),
_ => None,
}
}
fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
use crate::computation::rational::rational_zero;
use std::cmp::Ordering;
match magnitude.try_cmp(&rational_zero()) {
Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
}
}
fn parse_unresolved_width_bound(
args: &[CommandArg],
cmd: &str,
) -> Result<(RationalInteger, String), String> {
use crate::computation::rational::decimal_to_rational;
let lit = require_literal(args, cmd)?;
let (magnitude, unit_name) = match lit {
crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
other => {
return Err(format!(
"{cmd} requires a measure literal with a unit, got {}",
value_kind_name(other)
));
}
};
let magnitude_rational = decimal_to_rational(magnitude)
.map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
reject_negative_width_magnitude(&magnitude_rational, cmd)?;
Ok((magnitude_rational, unit_name))
}
pub(crate) fn check_range_bound_consistency(
spec: &TypeSpecification,
unit_index: &crate::planning::unit_index::UnitIndex,
) -> Result<(), String> {
use std::cmp::Ordering;
fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
!= Ordering::Greater
}
fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
}
match spec {
TypeSpecification::NumberRange {
lower,
upper,
minimum,
maximum,
..
}
| TypeSpecification::RatioRange {
lower,
upper,
minimum,
maximum,
..
} => {
if let (Some(lo), Some(hi)) = (lower, upper) {
if lo
.try_cmp(hi)
.map_err(|failure| format!("range endpoint compare failed: {failure}"))?
== Ordering::Greater
{
return Err(format!(
"invalid range: lower {} is greater than upper {}",
lo.display_str(),
hi.display_str()
));
}
}
if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
if min_w
.try_cmp(max_w)
.map_err(|failure| format!("range width compare failed: {failure}"))?
== Ordering::Greater
{
return Err(format!(
"invalid range: minimum width {} is greater than maximum width {}",
min_w.display_str(),
max_w.display_str()
));
}
}
Ok(())
}
TypeSpecification::MeasureRange {
lower,
upper,
minimum,
maximum,
units,
..
} => {
if let (Some(lo), Some(hi)) = (lower, upper) {
let lo_c =
measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
let hi_c =
measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
if lo_c
.try_cmp(&hi_c)
.map_err(|failure| format!("range endpoint compare failed: {failure}"))?
== Ordering::Greater
{
return Err(format!(
"invalid range: lower {} {} is greater than upper {} {}",
lo.0.display_str(),
lo.1,
hi.0.display_str(),
hi.1
));
}
}
if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
let min_c = measure_declared_bound_to_canonical(
&min_w.0, &min_w.1, units, "range", "minimum",
)?;
let max_c = measure_declared_bound_to_canonical(
&max_w.0, &max_w.1, units, "range", "maximum",
)?;
if min_c
.try_cmp(&max_c)
.map_err(|failure| format!("range width compare failed: {failure}"))?
== Ordering::Greater
{
return Err(format!(
"invalid range: minimum width {} {} is greater than maximum width {} {}",
min_w.0.display_str(),
min_w.1,
max_w.0.display_str(),
max_w.1
));
}
}
Ok(())
}
TypeSpecification::DateRange {
lower,
upper,
minimum,
maximum,
..
} => {
if let (Some(lo), Some(hi)) = (lower, upper) {
if !endpoint_order_ok_dates(lo, hi) {
return Err(format!(
"invalid range: lower {lo} is greater than upper {hi}"
));
}
}
check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
}
TypeSpecification::TimeRange {
lower,
upper,
minimum,
maximum,
..
} => {
if let (Some(lo), Some(hi)) = (lower, upper) {
if !endpoint_order_ok_times(lo, hi) {
return Err(format!(
"invalid range: lower {lo} is greater than upper {hi}"
));
}
}
check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
}
_ => Ok(()),
}
}
fn check_temporal_width_pair_consistency(
minimum: &Option<(RationalInteger, String)>,
maximum: &Option<(RationalInteger, String)>,
unit_index: &crate::planning::unit_index::UnitIndex,
allow_calendar: bool,
) -> Result<(), String> {
use std::cmp::Ordering;
let resolve = |bound: &(RationalInteger, String),
command: &str|
-> Result<(RationalInteger, Arc<LemmaType>), String> {
let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
format!(
"{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
bound.1
)
})?;
if allow_calendar {
if !owner.is_duration_like() && !owner.is_calendar_like() {
return Err(format!(
"{command} width unit '{bare}' must be a duration or calendar unit",
));
}
} else if !owner.is_duration_like() {
return Err(format!(
"{command} width unit '{bare}' must be a duration unit",
));
}
let TypeSpecification::Measure { units, .. } = &owner.specifications else {
return Err(format!(
"{command} width unit '{bare}' must resolve to a measure type",
));
};
let canonical = measure_declared_bound_to_canonical(
&bound.0,
&bare,
units,
owner.name().as_str(),
command,
)?;
Ok((canonical, Arc::clone(&owner)))
};
match (minimum, maximum) {
(None, None) => Ok(()),
(Some(min_w), None) => {
resolve(min_w, "minimum")?;
Ok(())
}
(None, Some(max_w)) => {
resolve(max_w, "maximum")?;
Ok(())
}
(Some(min_w), Some(max_w)) => {
let (min_c, min_owner) = resolve(min_w, "minimum")?;
let (max_c, max_owner) = resolve(max_w, "maximum")?;
if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
return Err(
"invalid range: minimum and maximum width must not mix calendar and duration units"
.to_string(),
);
}
if min_c
.try_cmp(&max_c)
.map_err(|failure| format!("range width compare failed: {failure}"))?
== Ordering::Greater
{
return Err(format!(
"invalid range: minimum width {} {} is greater than maximum width {} {}",
min_w.0.display_str(),
min_w.1,
max_w.0.display_str(),
max_w.1
));
}
Ok(())
}
}
}
fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
owner
.measure_unit_names()
.is_some_and(|names| names.contains(&unit_name))
}
pub fn signature_factor(
signature: &[(String, i32)],
expression_units: &crate::planning::unit_index::UnitIndex,
owner: Option<&LemmaType>,
) -> Result<
crate::computation::rational::RationalInteger,
crate::computation::rational::NumericFailure,
> {
use crate::computation::rational::{checked_div, checked_mul, rational_one};
let mut acc = rational_one();
for (name, exponent) in signature {
let factor =
if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
owner.measure_unit_factor(name).clone()
} else if let Some(lemma_type) = expression_units.unique_owner(name) {
lemma_type.measure_unit_factor(name).clone()
} else if !expression_units.owners_for(name).is_empty() {
panic!(
"BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
name
);
} else {
panic!(
"BUG: signature_factor called with unresolved unit name '{}'",
name
);
};
let mut term = rational_one();
let abs_exp = exponent.unsigned_abs();
for _ in 0..abs_exp {
term = checked_mul(&term, &factor)?;
}
if *exponent >= 0 {
acc = checked_mul(&acc, &term)?;
} else {
acc = checked_div(&acc, &term)?;
}
}
Ok(acc)
}
pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
use std::collections::BTreeMap;
let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
for (name, exponent) in signature {
let entry = accumulator.entry(name.clone()).or_insert(0);
*entry = entry
.checked_add(*exponent)
.expect("BUG: planning bounds signature unit exponents");
}
accumulator
.into_iter()
.filter(|(_, exponent)| *exponent != 0)
.collect()
}
pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
decomposition
.iter()
.filter(|(_, exponent)| **exponent != 0)
.map(|(dimension, exponent)| (dimension.name().to_string(), *exponent))
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Dimension {
Duration,
Calendar,
Family(TypeFamily),
}
impl Dimension {
#[must_use]
pub fn name(&self) -> &str {
match self {
Dimension::Duration => "duration",
Dimension::Calendar => "calendar",
Dimension::Family(family) => &family.name,
}
}
#[must_use]
fn with_families_without_effective(&self) -> Self {
match self {
Dimension::Duration => Dimension::Duration,
Dimension::Calendar => Dimension::Calendar,
Dimension::Family(family) => Dimension::Family(family.without_effective()),
}
}
}
fn base_measure_vector_without_effective(domain: &BaseMeasureVector) -> BaseMeasureVector {
domain
.iter()
.map(|(dimension, exponent)| (dimension.with_families_without_effective(), *exponent))
.collect()
}
fn measure_units_without_effective(units: &MeasureUnits) -> MeasureUnits {
MeasureUnits(
units
.iter()
.map(|unit| MeasureUnit {
name: unit.name.clone(),
scale: unit.scale.clone(),
derived_measure_factors: unit.derived_measure_factors.clone(),
decomposition: base_measure_vector_without_effective(&unit.decomposition),
minimum: unit.minimum.clone(),
maximum: unit.maximum.clone(),
suggestion_magnitude: unit.suggestion_magnitude.clone(),
})
.collect(),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MeasureTrait {
Duration,
Calendar,
}
pub fn duration_decomposition() -> BaseMeasureVector {
[(Dimension::Duration, 1i32)].into_iter().collect()
}
pub fn calendar_decomposition() -> BaseMeasureVector {
[(Dimension::Calendar, 1i32)].into_iter().collect()
}
pub fn anonymous_measure_type() -> LemmaType {
LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
}
pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub(crate) struct TypeOptions {
pub values: Vec<OptionValue>,
pub rule: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct OptionValue {
pub value: BoundValueKind,
}
pub(crate) const SPEC_LIST_HAS_NO_OPTIONS: &str =
"`-> option` and `-> options` do not apply to a list of spec instances";
pub(crate) fn spec_member_input_key(path: &[String], name: &str) -> String {
let mut key = String::new();
for segment in path {
key.push_str(segment);
key.push('.');
}
key.push_str(name);
key
}
impl TypeOptions {
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty() && self.rule.is_none()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum TypeSpecification {
Boolean {
help: String,
},
Measure {
minimum: Option<(RationalInteger, String)>,
maximum: Option<(RationalInteger, String)>,
units: MeasureUnits,
traits: Vec<MeasureTrait>,
decomposition: Option<BaseMeasureVector>,
unit: Option<String>,
help: String,
},
Number {
minimum: Option<RationalInteger>,
maximum: Option<RationalInteger>,
decimals: Option<u8>,
help: String,
},
NumberRange {
lower: Option<RationalInteger>,
upper: Option<RationalInteger>,
minimum: Option<RationalInteger>,
maximum: Option<RationalInteger>,
help: String,
},
Ratio {
minimum: Option<RationalInteger>,
maximum: Option<RationalInteger>,
units: RatioUnits,
unit: Option<String>,
help: String,
},
RatioRange {
lower: Option<RationalInteger>,
upper: Option<RationalInteger>,
minimum: Option<RationalInteger>,
maximum: Option<RationalInteger>,
units: RatioUnits,
help: String,
},
Text {
length: Option<usize>,
help: String,
},
Date {
minimum: Option<DateTimeValue>,
maximum: Option<DateTimeValue>,
help: String,
},
DateRange {
lower: Option<DateTimeValue>,
upper: Option<DateTimeValue>,
minimum: Option<(RationalInteger, String)>,
maximum: Option<(RationalInteger, String)>,
help: String,
},
Time {
minimum: Option<TimeValue>,
maximum: Option<TimeValue>,
help: String,
},
TimeRange {
lower: Option<TimeValue>,
upper: Option<TimeValue>,
minimum: Option<(RationalInteger, String)>,
maximum: Option<(RationalInteger, String)>,
help: String,
},
MeasureRange {
lower: Option<(RationalInteger, String)>,
upper: Option<(RationalInteger, String)>,
minimum: Option<(RationalInteger, String)>,
maximum: Option<(RationalInteger, String)>,
units: MeasureUnits,
decomposition: Option<BaseMeasureVector>,
help: String,
},
Veto {
message: Option<String>,
},
Undetermined,
Spec {
spec: SpecIdentity,
},
List {
element: Arc<LemmaType>,
key: ListKey,
help: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct KeyField {
pub name: String,
pub path: DataPath,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ListKey {
Numbered,
Fields(Vec<KeyField>),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct SpecVersionKey {
pub repository: Option<String>,
pub spec: String,
pub effective: crate::parsing::ast::EffectiveDate,
}
impl SpecVersionKey {
#[must_use]
pub(crate) fn of(
repository: &crate::parsing::ast::LemmaRepository,
spec: &crate::parsing::ast::LemmaSpec,
) -> Self {
Self {
repository: repository.name.clone(),
spec: spec.name.clone(),
effective: spec.effective_from.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SpecIdentity {
pub repository: Option<String>,
pub spec: String,
pub effective: crate::parsing::ast::EffectiveDate,
pub slice: crate::parsing::ast::EffectiveDate,
}
impl SpecIdentity {
#[must_use]
pub(crate) fn of(
repository: &crate::parsing::ast::LemmaRepository,
spec: &crate::parsing::ast::LemmaSpec,
slice: crate::parsing::ast::EffectiveDate,
) -> Self {
assert!(
slice >= spec.effective_from,
"BUG: slice {slice:?} of spec '{}' starts before its version",
spec.name
);
Self {
repository: repository.name.clone(),
spec: spec.name.clone(),
effective: spec.effective_from.clone(),
slice,
}
}
}
impl fmt::Display for SpecIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(repository) = &self.repository {
write!(f, "{repository} ")?;
}
write!(f, "{}", self.spec)?;
if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
write!(f, " {at}")?;
}
if self.slice != self.effective {
let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.slice else {
unreachable!("BUG: a slice after its version's start is a date");
};
write!(f, " slice {at}")?;
}
Ok(())
}
}
impl std::fmt::Display for TypeSpecification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Boolean { .. } => f.write_str("boolean"),
Self::Measure { .. } => f.write_str("measure"),
Self::MeasureRange { .. } => f.write_str("measure range"),
Self::Number { .. } => f.write_str("number"),
Self::NumberRange { .. } => f.write_str("number range"),
Self::Text { .. } => f.write_str("text"),
Self::Date { .. } => f.write_str("date"),
Self::DateRange { .. } => f.write_str("date range"),
Self::Time { .. } => f.write_str("time"),
Self::TimeRange { .. } => f.write_str("time range"),
Self::Ratio { .. } => f.write_str("ratio"),
Self::RatioRange { .. } => f.write_str("ratio range"),
Self::Veto { .. } => f.write_str("veto"),
Self::Undetermined => f.write_str("undetermined"),
Self::Spec { spec } => write!(f, "{spec}"),
Self::List {
element,
key: ListKey::Numbered,
..
} => write!(f, "{} list", element.specifications),
Self::List {
element,
key: ListKey::Fields(fields),
..
} => write!(
f,
"{} list key {}",
element.specifications,
fields
.iter()
.map(|field| field.name.as_str())
.collect::<Vec<_>>()
.join(", ")
),
}
}
}
impl TypeSpecification {
pub fn help(&self) -> &str {
match self {
Self::Boolean { help, .. }
| Self::Measure { help, .. }
| Self::Number { help, .. }
| Self::NumberRange { help, .. }
| Self::Text { help, .. }
| Self::Date { help, .. }
| Self::DateRange { help, .. }
| Self::Time { help, .. }
| Self::TimeRange { help, .. }
| Self::Ratio { help, .. }
| Self::RatioRange { help, .. }
| Self::MeasureRange { help, .. } => help.as_str(),
Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
Self::List { element, help, .. } => {
if help.is_empty() {
element.specifications.help()
} else {
help.as_str()
}
}
}
}
#[must_use]
pub fn is_unresolved_measure_range(&self) -> bool {
matches!(
self,
Self::MeasureRange {
units,
decomposition: None,
..
} if units.0.is_empty()
)
}
#[must_use]
fn with_families_without_effective(&self) -> Self {
match self {
Self::Measure {
minimum,
maximum,
units,
traits,
decomposition,
unit,
help,
} => Self::Measure {
minimum: minimum.clone(),
maximum: maximum.clone(),
units: measure_units_without_effective(units),
traits: traits.clone(),
decomposition: decomposition
.as_ref()
.map(base_measure_vector_without_effective),
unit: unit.clone(),
help: help.clone(),
},
Self::MeasureRange {
lower,
upper,
minimum,
maximum,
units,
decomposition,
help,
} => Self::MeasureRange {
lower: lower.clone(),
upper: upper.clone(),
minimum: minimum.clone(),
maximum: maximum.clone(),
units: measure_units_without_effective(units),
decomposition: decomposition
.as_ref()
.map(base_measure_vector_without_effective),
help: help.clone(),
},
Self::List { element, key, help } => Self::List {
element: Arc::new(element.with_families_without_effective()),
key: key.clone(),
help: help.clone(),
},
other => other.clone(),
}
}
#[must_use]
pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Measure {
minimum: a_min,
maximum: a_max,
units: a_units,
traits: a_traits,
decomposition: a_decomp,
help: a_help,
unit: _,
},
Self::Measure {
minimum: b_min,
maximum: b_max,
units: b_units,
traits: b_traits,
decomposition: b_decomp,
help: b_help,
unit: _,
},
) => {
a_min == b_min
&& a_max == b_max
&& a_units == b_units
&& a_traits == b_traits
&& a_decomp == b_decomp
&& a_help == b_help
}
(
Self::Ratio {
minimum: a_min,
maximum: a_max,
units: a_units,
help: a_help,
unit: _,
},
Self::Ratio {
minimum: b_min,
maximum: b_max,
units: b_units,
help: b_help,
unit: _,
},
) => a_min == b_min && a_max == b_max && a_units == b_units && a_help == b_help,
_ => self == other,
}
}
}
fn require_literal<'a>(
args: &'a [CommandArg],
cmd: &str,
) -> Result<&'a crate::literals::Value, String> {
if args.len() > 1 {
return Err(format!("{cmd} takes exactly one value"));
}
let arg = args
.first()
.ok_or_else(|| format!("{} requires an argument", cmd))?;
match arg {
CommandArg::Literal(v) => Ok(v),
CommandArg::Label(name) => Err(format!(
"{} requires a literal value, got identifier '{}'",
cmd, name
)),
CommandArg::UnitExpr(_) => Err(format!(
"{} requires a literal value, got a unit expression (only valid for 'unit' command)",
cmd
)),
}
}
fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
match require_literal(args, "help")? {
crate::literals::Value::Text(s) => {
*help = s.clone();
Ok(())
}
other => Err(format!(
"help requires a text literal (quoted string), got {}",
value_kind_name(other)
)),
}
}
fn format_measure_units_list(units: &MeasureUnits) -> String {
units
.iter()
.map(|u| u.name.as_str())
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SuggestionExpectation {
MeasureUnits,
Text,
Number,
Boolean,
Date,
Time,
Ratio,
NumberRange,
DateRange,
TimeRange,
MeasureRange,
RatioRange,
}
pub(crate) fn suggestion_value_mismatch_error(
calendar_unit: &str,
type_name: &str,
expectation: SuggestionExpectation,
measure_units: Option<&MeasureUnits>,
) -> String {
let unit_label = calendar_unit;
let first = format!("Unit '{unit_label}' is for calendar data.");
match expectation {
SuggestionExpectation::MeasureUnits => {
let list = measure_units
.map(format_measure_units_list)
.unwrap_or_default();
format!("{first} Valid '{type_name}' units are: {list}.")
}
SuggestionExpectation::Text => format!(
"{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
),
SuggestionExpectation::Number => format!(
"{first} Please provide a number, for example `-> suggest 42`."
),
SuggestionExpectation::Boolean => format!(
"{first} Please provide true or false, for example `-> suggest true`."
),
SuggestionExpectation::Date => format!(
"{first} Please provide a date, for example `-> suggest 2024-06-15`."
),
SuggestionExpectation::Time => format!(
"{first} Please provide a time, for example `-> suggest 09:00:00`."
),
SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
"{first} Please provide a ratio, for example `-> suggest 25%`."
),
SuggestionExpectation::NumberRange => format!(
"{first} Please provide a number range, for example `-> suggest 10...100`."
),
SuggestionExpectation::DateRange => format!(
"{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
),
SuggestionExpectation::TimeRange => format!(
"{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
),
SuggestionExpectation::MeasureRange => format!(
"{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
),
}
}
fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
let example = if traits.contains(&MeasureTrait::Duration) {
"4 week"
} else if traits.contains(&MeasureTrait::Calendar) {
"3 month"
} else {
"30 kilogram"
};
format!(
"Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
)
}
#[derive(Clone, Copy, Debug)]
pub struct ConstraintNames<'a> {
pub type_name: &'a str,
pub data_name: &'a str,
}
fn decimals_depend_on_the_unit_error(
kind: &str,
names: ConstraintNames<'_>,
args: &[CommandArg],
first_unit: Option<&str>,
) -> Result<String, String> {
let ConstraintNames {
type_name,
data_name,
} = names;
let places = decimal_to_u8(require_decimal_literal(args, "decimals")?, "decimals")?;
let step = match places {
0 => "1".to_string(),
_ => format!("0.{}1", "0".repeat(usize::from(places) - 1)),
};
let advice = match first_unit {
Some(unit) => {
format!("Round in a rule instead, for example '{data_name} -> round {step} {unit}'.")
}
None => format!(
"Round in a rule instead, for example '{data_name} -> round' followed by a step of {step} in one of the type's units."
),
};
Ok(format!("'decimals' is not valid on {kind} type '{type_name}': precision depends on the unit. {advice}"))
}
fn reject_calendar_for_suggestion(
value: &crate::literals::Value,
type_name: &str,
expectation: SuggestionExpectation,
measure_units: Option<&MeasureUnits>,
) -> Result<(), String> {
if let crate::literals::Value::NumberWithUnit(_, unit) = value {
if calendar_unit_factor(unit).is_some() {
return Err(suggestion_value_mismatch_error(
unit,
type_name,
expectation,
measure_units,
));
}
}
Ok(())
}
fn value_kind_name(v: &crate::literals::Value) -> &'static str {
use crate::literals::Value;
match v {
Value::Number(_) => "number",
Value::NumberWithUnit(_, _) => "number_with_unit",
Value::Text(_) => "text",
Value::Date(_) => "date",
Value::Time(_) => "time",
Value::Boolean(_) => "boolean",
Value::Range(_, _) => "range",
}
}
fn require_suggestion_range_endpoints<'a>(
args: &'a [CommandArg],
type_name: &str,
expectation: SuggestionExpectation,
measure_units: Option<&MeasureUnits>,
) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
match require_literal(args, "suggest")? {
crate::literals::Value::NumberWithUnit(_, unit)
if calendar_unit_factor(unit).is_some() =>
{
Err(suggestion_value_mismatch_error(
unit,
type_name,
expectation,
measure_units,
))
}
crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
_ => Err(match expectation {
SuggestionExpectation::NumberRange => {
"Please provide a number range, for example `-> suggest 10...100`.".to_string()
}
SuggestionExpectation::DateRange => {
"Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
.to_string()
}
SuggestionExpectation::RatioRange => {
"Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
}
SuggestionExpectation::MeasureRange => format!(
"Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
),
_ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
}),
}
}
fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
crate::computation::rational::decimal_to_rational(decimal)
.map_err(|failure| format!("literal failed rational lift: {failure}"))
}
pub fn range_element_type_specification(
range_spec: &TypeSpecification,
) -> Option<TypeSpecification> {
range_spec.element_from_range()
}
fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
match (&left.specifications, &right.specifications) {
(TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
(TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
(TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
(TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
left.same_measure_family(right)
}
(TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
_ => false,
}
}
pub fn range_type_specification_from_endpoints(
left: &LemmaType,
right: &LemmaType,
) -> Option<TypeSpecification> {
if !range_endpoints_compatible(left, right) {
return None;
}
left.specifications.range_from_element()
}
pub enum UnitTokenResolver<'a> {
Declared {
unit_index: &'a UnitIndex,
named_types: &'a IndexMap<String, Arc<LemmaType>>,
},
Undeclared,
Planning {
unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
named_types: &'a IndexMap<String, Arc<LemmaType>>,
},
Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
Typed {
unit_index: &'a UnitIndex,
named_types: &'a IndexMap<String, Arc<LemmaType>>,
lemma_type: &'a LemmaType,
},
}
impl UnitTokenResolver<'_> {
pub fn resolve(&self, unit: &str) -> Result<String, String> {
match self {
UnitTokenResolver::Declared {
unit_index,
named_types,
} => unit_index
.resolve_with_named_types(unit, named_types)
.map(|(bare, _)| bare),
UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
UnitTokenResolver::Planning {
unit_index,
named_types,
} => unit_index
.resolve_with_named_types(unit, named_types)
.map(|(bare, _)| bare),
UnitTokenResolver::Search(scopes) => scopes
.iter()
.find_map(|(unit_index, named_types)| {
unit_index
.resolve_with_named_types(unit, named_types)
.ok()
.map(|(bare, _)| bare)
})
.ok_or_else(|| unknown_unit_message(unit)),
UnitTokenResolver::Typed {
unit_index,
named_types,
lemma_type,
} => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
}
}
}
fn unknown_unit_message(unit: &str) -> String {
format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
}
fn resolve_declared_on_type(
written: &str,
lemma_type: &LemmaType,
unit_index: &UnitIndex,
named_types: &IndexMap<String, Arc<LemmaType>>,
) -> Result<String, String> {
match unit_index.resolve_with_named_types(written, named_types) {
Ok((bare, _)) => {
if unit_name_declared_on_type(lemma_type, &bare)
|| range_element_declares_unit(lemma_type, &bare)
{
Ok(bare)
} else {
Err(format!(
"Unit '{written}' resolves to '{bare}', which is not declared on type {}",
lemma_type.name()
))
}
}
Err(index_err) => {
if unit_name_declared_on_type(lemma_type, written)
|| range_element_declares_unit(lemma_type, written)
{
Ok(written.to_string())
} else {
Err(index_err)
}
}
}
}
fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
let Some(element) = lemma_type.specifications.element_from_range() else {
return false;
};
unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
}
fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
match &lemma_type.specifications {
TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
_ => false,
}
}
fn bare_declared_unit_on_element(
element_spec: &TypeSpecification,
token: &str,
) -> Result<String, String> {
match element_spec {
TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
}
}
pub fn resolve_endpoint_unit_token(
token: &str,
element_spec: &TypeSpecification,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<String, String> {
match bare_declared_unit_on_element(element_spec, token) {
Ok(bare) => Ok(bare),
Err(declared_err) => {
let Some(resolve) = resolve_unit else {
return Err(declared_err);
};
let bare = resolve.resolve(token)?;
bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
})
}
}
}
pub(crate) fn lift_range_endpoint(
value: &crate::parsing::ast::Value,
element_type: &LemmaType,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<TypedLiteral, String> {
use crate::parsing::ast::Value;
let element_spec = &element_type.specifications;
match value {
Value::NumberWithUnit(magnitude, unit_token) => {
let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
let lemma_type = match &kind {
ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
Arc::new(element_type.clone().with_unit(bare))
}
_ => Arc::new(element_type.clone()),
};
Ok(TypedLiteral {
value: kind,
lemma_type,
})
}
_ => {
let kind = parser_scalar_to_value_kind(value, element_spec)?;
Ok(TypedLiteral {
value: kind,
lemma_type: Arc::new(element_type.clone()),
})
}
}
}
fn literal_value_from_parser_value(
value: &crate::parsing::ast::Value,
) -> Result<TypedLiteral, String> {
use crate::parsing::ast::Value;
enum Step<'a> {
Visit(&'a Value),
CombineRange,
}
let mut steps = vec![Step::Visit(value)];
let mut literals: Vec<TypedLiteral> = Vec::new();
while let Some(step) = steps.pop() {
match step {
Step::Visit(Value::Range(left, right)) => {
steps.push(Step::CombineRange);
steps.push(Step::Visit(right));
steps.push(Step::Visit(left));
}
Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
Step::CombineRange => {
let right = literals
.pop()
.expect("BUG: range right endpoint missing from literal stack");
let left = literals
.pop()
.expect("BUG: range left endpoint missing from literal stack");
let compatible = match (
&left.lemma_type.specifications,
&right.lemma_type.specifications,
) {
(TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
(TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
(TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
(TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
left.lemma_type.same_measure_family(&right.lemma_type)
|| left
.lemma_type
.compatible_with_anonymous_measure(&right.lemma_type)
|| right
.lemma_type
.compatible_with_anonymous_measure(&left.lemma_type)
}
(TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
_ => false,
};
if !compatible {
return Err(format!(
"range endpoints must have the same supported base type, got {} and {}",
left.lemma_type.name(),
right.lemma_type.name()
));
}
literals.push(TypedLiteral::range(left, right));
}
}
}
Ok(literals.pop().expect("BUG: literal walk left no result"))
}
fn scalar_literal_from_parser_value(
value: &crate::parsing::ast::Value,
) -> Result<TypedLiteral, String> {
use crate::parsing::ast::Value;
match value {
Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
lift_parser_decimal(*n)?,
)),
Value::Range(_, _) => {
unreachable!("BUG: range values are walked by literal_value_from_parser_value")
}
}
}
fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
if !d.is_integer() {
return Err(format!(
"{} requires a whole number, got fractional value",
ctx
));
}
d.numer_to_u8()
.ok_or_else(|| format!("{} value out of range for u8", ctx))
}
fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
if !d.is_integer() {
return Err(format!(
"{} requires a whole number, got fractional value",
ctx
));
}
d.numer_to_usize()
.ok_or_else(|| format!("{} value out of range for usize", ctx))
}
fn ratio_bound_to_canonical_rational(
args: &[CommandArg],
cmd: &str,
units: &RatioUnits,
) -> Result<RationalInteger, String> {
use crate::computation::rational::{checked_div, decimal_to_rational};
let lit = require_literal(args, cmd)?;
match lit {
crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
let unit = units.get(unit_name.as_str())?;
let magnitude_rational = decimal_to_rational(*magnitude)
.map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
checked_div(&magnitude_rational, &unit.value)
.map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
}
other => Err(format!(
"{cmd} requires a ratio literal with a unit, got {}",
value_kind_name(other)
)),
}
}
fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
use crate::computation::rational::decimal_to_rational;
match require_literal(args, cmd)? {
crate::literals::Value::Number(d) => decimal_to_rational(*d)
.map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
other => Err(format!(
"{} requires a number literal, got {}",
cmd,
value_kind_name(other)
)),
}
}
enum UnitConstraintField {
Minimum,
Maximum,
SuggestionMagnitude,
}
fn apply_measure_unit_factor(
units: &mut MeasureUnits,
unit_name: String,
value: rust_decimal::Decimal,
derived_measure_factors: Vec<(String, i32)>,
) -> Result<(), String> {
if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
let new_factor = crate::computation::rational::decimal_to_rational(value)
.map_err(|failure| failure.to_string())?;
if existing.literal_factor() != Some(&new_factor)
|| existing.derived_measure_factors != derived_measure_factors
{
return Err(format!(
"Unit '{unit_name}' is already defined in this type's inherited units; \
cannot change factor or decomposition. Add a new unit name instead."
));
}
return Ok(());
}
units.0.push(MeasureUnit::from_decimal_factor(
unit_name,
value,
derived_measure_factors,
)?);
Ok(())
}
pub(crate) fn measure_declared_bound_to_canonical(
magnitude: &RationalInteger,
unit_name: &str,
units: &MeasureUnits,
type_name: &str,
command: &str,
) -> Result<RationalInteger, String> {
use crate::computation::rational::checked_mul;
let unit = units.get(unit_name).map_err(|_| {
format!(
"Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
format_measure_units_list(units)
)
})?;
let factor = unit.literal_factor().ok_or_else(|| {
format!(
"{command} cannot use unit '{unit_name}' on '{type_name}': its conversion factor is a reference and is not known at planning."
)
})?;
checked_mul(magnitude, factor)
.map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
}
fn parse_measure_declared_bound(
args: &[CommandArg],
cmd: &str,
units: &MeasureUnits,
type_name: &str,
) -> Result<(RationalInteger, String), String> {
use crate::computation::rational::decimal_to_rational;
let lit = require_literal(args, cmd)?;
let (magnitude, unit_name) = match lit {
crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
other => {
return Err(format!(
"{cmd} requires a measure literal with a unit, got {}",
value_kind_name(other)
));
}
};
units.get(unit_name.as_str()).map_err(|_| {
format!(
"Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
format_measure_units_list(units)
)
})?;
let magnitude_rational = decimal_to_rational(magnitude)
.map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
Ok((magnitude_rational, unit_name))
}
fn sync_measure_units_from_canonical(
units: &mut MeasureUnits,
canonical: &RationalInteger,
field: UnitConstraintField,
) -> Result<(), String> {
use crate::computation::rational::checked_div;
for unit in &mut units.0 {
let Some(factor) = unit.literal_factor() else {
continue;
};
let magnitude = checked_div(canonical, factor).map_err(|failure| {
format!(
"cannot derive per-unit constraint for unit '{}': {failure}",
unit.name
)
})?;
match field {
UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
}
}
Ok(())
}
fn sync_ratio_units_from_canonical(
units: &mut RatioUnits,
canonical: &RationalInteger,
field: UnitConstraintField,
) -> Result<(), String> {
use crate::computation::rational::checked_mul;
for unit in &mut units.0 {
let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
format!(
"cannot derive per-unit constraint for ratio unit '{}': {failure}",
unit.name
)
})?;
match field {
UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
}
}
Ok(())
}
fn sync_measure_suggestion_units(
units: &mut MeasureUnits,
default: &ValueKind,
type_name: &str,
) -> Result<(), String> {
let ValueKind::Measure(magnitude) = default else {
return Ok(());
};
let unit_name = units
.iter()
.find(|unit| unit.is_canonical_factor())
.or_else(|| units.iter().next())
.map(|unit| unit.name.as_str())
.expect(
"BUG: Measure suggestion value requires at least one declared unit on the measure type",
);
units.get(unit_name).map_err(|_| {
format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
})?;
sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
}
pub(crate) fn finalize_measure_unit_constraint_magnitudes(
specification: &mut TypeSpecification,
declared_suggestion: Option<&ValueKind>,
type_name: &str,
) -> Result<(), String> {
let TypeSpecification::Measure {
minimum,
maximum,
units,
..
} = specification
else {
return Ok(());
};
let mut synced_minimum = false;
if let Some(bound) = minimum.as_ref() {
let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
if bound_unit.literal_factor().is_some() {
let canonical = measure_declared_bound_to_canonical(
&bound.0, &bound.1, units, type_name, "minimum",
)?;
sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
synced_minimum = true;
}
}
let mut synced_maximum = false;
if let Some(bound) = maximum.as_ref() {
let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
if bound_unit.literal_factor().is_some() {
let canonical = measure_declared_bound_to_canonical(
&bound.0, &bound.1, units, type_name, "maximum",
)?;
sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
synced_maximum = true;
}
}
let mut synced_suggestion = false;
if let Some(default) = declared_suggestion {
if matches!(default, ValueKind::Measure(_)) {
sync_measure_suggestion_units(units, default, type_name)?;
synced_suggestion = true;
}
}
if synced_minimum {
for unit in units.iter() {
if unit.literal_factor().is_none() {
continue;
}
assert!(
unit.minimum.is_some(),
"BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
unit.name
);
}
}
if synced_maximum {
for unit in units.iter() {
if unit.literal_factor().is_none() {
continue;
}
assert!(
unit.maximum.is_some(),
"BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
unit.name
);
}
}
if synced_suggestion {
for unit in units.iter() {
if unit.literal_factor().is_none() {
continue;
}
assert!(
unit.suggestion_magnitude.is_some(),
"BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
unit.name
);
}
}
Ok(())
}
fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
let ValueKind::Ratio(canonical) = default else {
return Ok(());
};
sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
}
pub(crate) fn apply_option_constraint(
lemma_type: &mut LemmaType,
command: TypeConstraintCommand,
args: &[CommandArg],
resolve_unit: Option<&UnitTokenResolver<'_>>,
spec: &crate::parsing::ast::LemmaSpec,
) -> Result<(), String> {
let mut hops = 0;
{
let mut probe = &*lemma_type;
while let TypeSpecification::List { element, .. } = &probe.specifications {
hops += 1;
probe = element.as_ref();
}
}
let mut current = lemma_type;
for _ in 0..hops {
let TypeSpecification::List { element, .. } = &mut current.specifications else {
unreachable!("BUG: option forwarding counted a list");
};
current = Arc::make_mut(element);
}
apply_options_command(current, command, args, resolve_unit, spec)
}
pub(crate) fn apply_spec_member_command(
lemma_type: &LemmaType,
command: TypeConstraintCommand,
args: &[CommandArg],
spec: &crate::parsing::ast::LemmaSpec,
target: &mut Option<RawSuggestion>,
) -> Result<(), String> {
let label = match args {
[CommandArg::Label(name)] => name,
_ => return Err(format!("{command} on a spec takes one instance name")),
};
let option = spec_alias_option(
lemma_type,
uses_alias(spec, label)
.ok_or_else(|| format!("'{label}' is not an instance of this spec"))?,
label,
)?;
*target = Some(RawSuggestion::Value(option.value.value));
Ok(())
}
pub(crate) fn apply_list_with_rows(
lemma_type: &mut LemmaType,
names: ConstraintNames<'_>,
args: &[CommandArg],
declared_value: &mut Option<RawSuggestion>,
resolve_unit: Option<&UnitTokenResolver<'_>>,
spec: &crate::parsing::ast::LemmaSpec,
) -> Result<(), String> {
let mut rows = match declared_value.take() {
Some(RawSuggestion::List(existing)) => existing,
None => Vec::new(),
Some(other) => {
unreachable!("BUG: list declared value must be RawSuggestion::List, got {other:?}")
}
};
let TypeSpecification::List { element, .. } = &mut lemma_type.specifications else {
unreachable!("BUG: apply_list_with_rows on a non-list type");
};
for arg in args {
rows.push(list_with_row(
Arc::make_mut(element),
names,
arg,
resolve_unit,
spec,
)?);
}
*declared_value = Some(RawSuggestion::List(rows));
Ok(())
}
fn list_with_row(
element: &mut LemmaType,
names: ConstraintNames<'_>,
arg: &CommandArg,
resolve_unit: Option<&UnitTokenResolver<'_>>,
spec: &crate::parsing::ast::LemmaSpec,
) -> Result<RawSuggestion, String> {
let mut hops = 0;
{
let mut probe = &*element;
while let TypeSpecification::List { element, .. } = &probe.specifications {
hops += 1;
probe = element.as_ref();
}
}
let mut current = element;
for _ in 0..hops {
let TypeSpecification::List { element, .. } = &mut current.specifications else {
unreachable!("BUG: list -> with counted a list");
};
current = Arc::make_mut(element);
}
let mut row_value = None;
if matches!(current.specifications, TypeSpecification::Spec { .. }) {
apply_spec_member_command(
current,
TypeConstraintCommand::With,
std::slice::from_ref(arg),
spec,
&mut row_value,
)?;
} else {
let mut unused_suggestion = None;
current.apply_constraint(
names,
TypeConstraintCommand::With,
std::slice::from_ref(arg),
&mut unused_suggestion,
&mut row_value,
resolve_unit,
)?;
}
let mut value = row_value.expect("BUG: -> with on a list row must produce a value");
for _ in 0..hops {
value = RawSuggestion::List(vec![value]);
}
Ok(value)
}
fn apply_options_command(
lemma_type: &mut LemmaType,
command: TypeConstraintCommand,
args: &[CommandArg],
resolve_unit: Option<&UnitTokenResolver<'_>>,
spec: &crate::parsing::ast::LemmaSpec,
) -> Result<(), String> {
if command == TypeConstraintCommand::Option && args.len() != 1 {
return Err("option takes exactly one argument".to_string());
}
if args.is_empty() {
return Err("options takes at least one value".to_string());
}
let mut literals: Vec<OptionValue> = Vec::new();
let mut names: Vec<String> = Vec::new();
for arg in args {
match arg {
CommandArg::Literal(value) => {
literals.push(OptionValue {
value: option_bound_from_literal(value, lemma_type, resolve_unit)?,
});
}
CommandArg::Label(name) => names.push(name.clone()),
CommandArg::UnitExpr(_) => {
return Err(format!(
"{command} requires a value of this type, got a unit expression"
));
}
}
}
if !literals.is_empty() && !names.is_empty() {
return Err(format!(
"{command} mixes values and a rule. Write the values, or name one rule."
));
}
if matches!(lemma_type.specifications, TypeSpecification::Spec { .. }) {
return apply_spec_list_options(lemma_type, command, args, spec);
}
if !names.is_empty() {
return apply_option_names(lemma_type, &names, spec);
}
append_option_literals(lemma_type, literals)?;
Ok(())
}
fn option_bound_from_literal(
value: &crate::literals::Value,
lemma_type: &LemmaType,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<BoundValueKind, String> {
let kind = parser_value_to_value_kind(value, lemma_type, resolve_unit)?;
let token = match value {
crate::literals::Value::NumberWithUnit(_, unit) => Some(unit.as_str()),
crate::literals::Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
(
crate::literals::Value::NumberWithUnit(_, left_unit),
crate::literals::Value::NumberWithUnit(_, right_unit),
) if left_unit == right_unit => Some(left_unit.as_str()),
_ => None,
},
_ => None,
};
let unit = match token {
Some(token) => Some(Arc::<str>::from(resolve_endpoint_unit_token(
token,
&lemma_type.specifications,
resolve_unit,
)?)),
None => None,
};
Ok(BoundValueKind {
value: kind,
unit,
instance_factors: None,
})
}
fn apply_spec_list_options(
_lemma_type: &mut LemmaType,
_command: TypeConstraintCommand,
_args: &[CommandArg],
_spec: &crate::parsing::ast::LemmaSpec,
) -> Result<(), String> {
Err(SPEC_LIST_HAS_NO_OPTIONS.to_string())
}
enum OptionName {
Text(OptionValue),
Instance(OptionValue),
Rule,
}
fn apply_option_names(
lemma_type: &mut LemmaType,
names: &[String],
spec: &crate::parsing::ast::LemmaSpec,
) -> Result<(), String> {
let mut meanings = Vec::with_capacity(names.len());
for name in names {
meanings.push(option_name_meaning(lemma_type, spec, name)?);
}
let rule_count = meanings
.iter()
.filter(|meaning| matches!(meaning, OptionName::Rule))
.count();
if rule_count > 0 {
if names.len() != 1 || rule_count != 1 {
return Err(
"options names one rule, or lists values. A rule is not mixed with values."
.to_string(),
);
}
if let Some(existing) = &lemma_type.options.rule {
return Err(format!(
"`-> options` already names rule '{existing}', not '{}'",
names[0]
));
}
lemma_type.options.rule = Some(names[0].clone());
return Ok(());
}
let mut literals = Vec::with_capacity(meanings.len());
for meaning in meanings {
match meaning {
OptionName::Text(value) | OptionName::Instance(value) => literals.push(value),
OptionName::Rule => unreachable!("BUG: rule meanings are returned above"),
}
}
reject_duplicate_options(lemma_type, &literals)?;
append_option_literals(lemma_type, literals)?;
Ok(())
}
fn option_name_meaning(
lemma_type: &LemmaType,
spec: &crate::parsing::ast::LemmaSpec,
name: &str,
) -> Result<OptionName, String> {
let mut found: Vec<OptionName> = Vec::new();
if matches!(lemma_type.specifications, TypeSpecification::Text { .. }) {
found.push(OptionName::Text(OptionValue {
value: BoundValueKind::unbound(ValueKind::Text(Arc::from(name))),
}));
}
if let Some(data) = uses_alias(spec, name) {
if let Ok(instance) = spec_alias_option(lemma_type, data, name) {
found.push(OptionName::Instance(instance));
}
}
if spec.rules.iter().any(|rule| rule.name == name) {
found.push(OptionName::Rule);
}
match found.len() {
1 => Ok(found.pop().expect("BUG: one option meaning")),
0 => Err(format!("'{name}' is not a value of this type or a rule")),
_ => Err(format!(
"'{name}' is more than one of a text option, an instance, and a rule"
)),
}
}
fn uses_alias<'a>(
spec: &'a crate::parsing::ast::LemmaSpec,
name: &str,
) -> Option<&'a crate::parsing::ast::LemmaData> {
spec.data.iter().find(|data| {
data.reference.name == name
&& matches!(data.value, crate::parsing::ast::DataValue::Import { .. })
})
}
fn spec_alias_option(
lemma_type: &LemmaType,
data: &crate::parsing::ast::LemmaData,
name: &str,
) -> Result<OptionValue, String> {
let TypeSpecification::Spec { spec } = &lemma_type.specifications else {
return Err(format!("'{name}' is not a value of this type or a rule"));
};
let crate::parsing::ast::DataValue::Import { spec_ref, .. } = &data.value else {
unreachable!("BUG: uses_alias returns an import");
};
if spec_ref.name != spec.spec {
return Err(format!(
"'{name}' is an instance of '{}', not '{}'",
spec_ref.name, spec.spec
));
}
unreachable!(
"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"
)
}
fn append_option_literals(
lemma_type: &mut LemmaType,
incoming: Vec<OptionValue>,
) -> Result<(), String> {
for option in incoming {
if lemma_type
.options
.values
.iter()
.any(|existing| existing.value == option.value)
{
return Err(format!(
"duplicate option '{}'",
lemma_type.display_bound(&option.value)
));
}
lemma_type.options.values.push(option);
}
Ok(())
}
fn static_option_match(options: &TypeOptions, value: &ValueKind) -> bool {
options
.values
.iter()
.any(|option| option_values_match(&option.value.value, value))
}
fn options_have_unscaled_measure(options: &TypeOptions) -> bool {
options
.values
.iter()
.any(|option| option.value.value.has_unscaled_measure())
}
pub(crate) fn settle_value_kind_with_scales(
value: &ValueKind,
scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
) -> Result<ValueKind, String> {
use crate::computation::rational::checked_mul;
enum Task {
Visit(ValueKind),
AssembleRange {
left_type: Arc<LemmaType>,
right_type: Arc<LemmaType>,
},
AssembleList {
row_types: Vec<Arc<LemmaType>>,
},
}
let mut tasks = vec![Task::Visit(value.clone())];
let mut done: Vec<ValueKind> = Vec::new();
while let Some(task) = tasks.pop() {
match task {
Task::Visit(ValueKind::UnscaledMeasure { magnitude, unit }) => {
let factor = scales.get(unit.as_ref()).ok_or_else(|| {
format!("unit '{unit}' scale is not settled for option membership")
})?;
let canonical =
checked_mul(&magnitude, factor).map_err(|failure| failure.to_string())?;
done.push(ValueKind::Measure(canonical));
}
Task::Visit(ValueKind::Range(left, right)) => {
tasks.push(Task::AssembleRange {
left_type: Arc::clone(&left.lemma_type),
right_type: Arc::clone(&right.lemma_type),
});
let right = *right;
let left = *left;
tasks.push(Task::Visit(right.value));
tasks.push(Task::Visit(left.value));
}
Task::Visit(ValueKind::List(rows)) => {
let owned = rows.to_vec();
let row_types: Vec<Arc<LemmaType>> = owned
.iter()
.map(|row| Arc::clone(&row.lemma_type))
.collect();
tasks.push(Task::AssembleList { row_types });
for row in owned.into_iter().rev() {
tasks.push(Task::Visit(row.value));
}
}
Task::Visit(other) => done.push(other),
Task::AssembleRange {
left_type,
right_type,
} => {
let right_value = done
.pop()
.expect("BUG: settled range missing right endpoint");
let left_value = done
.pop()
.expect("BUG: settled range missing left endpoint");
done.push(ValueKind::Range(
Box::new(TypedLiteral {
value: left_value,
lemma_type: left_type,
}),
Box::new(TypedLiteral {
value: right_value,
lemma_type: right_type,
}),
));
}
Task::AssembleList { row_types } => {
let mut settled = Vec::with_capacity(row_types.len());
for lemma_type in row_types.into_iter().rev() {
let value = done.pop().expect("BUG: settled list missing a row");
settled.push(TypedLiteral { value, lemma_type });
}
settled.reverse();
done.push(ValueKind::List(settled.into()));
}
}
}
Ok(done
.pop()
.expect("BUG: settle_value_kind_with_scales leaves one result"))
}
fn static_option_match_with_scales(
options: &TypeOptions,
value: &ValueKind,
scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
) -> Result<bool, String> {
let settled_value = settle_value_kind_with_scales(value, scales)?;
for option in &options.values {
let settled_option = settle_value_kind_with_scales(&option.value.value, scales)?;
if option_values_match(&settled_option, &settled_value) {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn disallowed_message(
lemma_type: &LemmaType,
options: &TypeOptions,
value: &ValueKind,
rule_rows: Option<&ListValue>,
) -> String {
let mut shown = options
.values
.iter()
.map(|option| lemma_type.display_bound(&option.value))
.collect::<Vec<_>>()
.join(", ");
if let Some(rows) = rule_rows {
let rule_shown = rows
.iter()
.map(|row| row.value.to_string())
.collect::<Vec<_>>()
.join(", ");
if !shown.is_empty() && !rule_shown.is_empty() {
shown.push_str(", ");
}
shown.push_str(&rule_shown);
}
format!("'{value}' is not one of the allowed options: {shown}.")
}
fn option_values_match(option: &ValueKind, value: &ValueKind) -> bool {
let mut values: Vec<(ValueKind, ValueKind)> = vec![(option.clone(), value.clone())];
while let Some((left, right)) = values.pop() {
match (left, right) {
(ValueKind::Spec(_), _) | (_, ValueKind::Spec(_)) => {
unreachable!(
"BUG: option matching saw a spec value; {}",
SPEC_LIST_HAS_NO_OPTIONS
);
}
(ValueKind::List(left), ValueKind::List(right)) => {
if left.len() != right.len() {
return false;
}
for (a, b) in left.iter().zip(right.iter()) {
if a.lemma_type != b.lemma_type {
return false;
}
values.push((a.into_owned().value, b.into_owned().value));
}
}
(left, right) => {
if left != right {
return false;
}
}
}
}
true
}
pub(crate) fn option_detail_line(lemma_type: &LemmaType) -> Option<String> {
let options = &lemma_type.options;
let mut parts = Vec::new();
if !options.values.is_empty() {
parts.push(
options
.values
.iter()
.map(|option| lemma_type.display_bound(&option.value))
.collect::<Vec<_>>()
.join(", "),
);
}
if let Some(name) = &options.rule {
parts.push(name.clone());
}
if parts.is_empty() {
None
} else {
Some(format!("options: {}", parts.join(", ")))
}
}
fn reject_duplicate_options(lemma_type: &LemmaType, values: &[OptionValue]) -> Result<(), String> {
for (index, option) in values.iter().enumerate() {
if values[..index]
.iter()
.any(|earlier| earlier.value == option.value)
{
return Err(format!(
"duplicate option '{}'",
lemma_type.display_bound(&option.value)
));
}
}
Ok(())
}
fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
match arg {
CommandArg::Label(name) => Ok(name.clone()),
CommandArg::Literal(other) => Err(format!(
"{} requires an identifier, got {}",
cmd,
value_kind_name(other)
)),
CommandArg::UnitExpr(_) => Err(format!(
"{} requires an identifier, got a unit expression",
cmd
)),
}
}
fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
match measure_trait {
MeasureTrait::Duration => "duration",
MeasureTrait::Calendar => "calendar",
}
}
fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
if args.len() != 1 {
return Err("trait requires exactly one identifier argument".to_string());
}
match label_name(&args[0], "trait")?
.trim()
.to_lowercase()
.as_str()
{
"duration" => Ok(MeasureTrait::Duration),
"calendar" => Ok(MeasureTrait::Calendar),
other => Err(format!("Unknown measure trait '{}'", other)),
}
}
fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
let month_unit = units
.iter()
.find(|unit| unit.name == "month")
.ok_or_else(|| {
"trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
.to_string()
})?;
if !month_unit.is_canonical_factor() {
return Err("trait calendar requires unit month 1".to_string());
}
Ok(())
}
fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
let second_unit = units
.iter()
.find(|unit| unit.name == "second")
.ok_or_else(|| {
"trait duration requires a canonical 'second' unit declared before 'trait duration'"
.to_string()
})?;
if !second_unit.is_canonical_factor() {
return Err("trait duration requires unit second 1".to_string());
}
Ok(())
}
fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
match require_literal(args, cmd)? {
crate::literals::Value::Date(dt) => Ok(dt.clone()),
other => Err(format!(
"{} requires a date literal (e.g. 2024-01-01), got {}",
cmd,
value_kind_name(other)
)),
}
}
fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
match require_literal(args, cmd)? {
crate::literals::Value::Time(t) => Ok(t.clone()),
other => Err(format!(
"{} requires a time literal (e.g. 12:30:00), got {}",
cmd,
value_kind_name(other)
)),
}
}
#[must_use]
pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
use PrimitiveKind::*;
match kind {
Boolean => "Whether this holds (true or false).",
Number => "A dimensionless number.",
NumberRange => "The lower and upper bound of the number range.",
Text => "A text value.",
Measure => "A numeric amount in one of this type's units.",
MeasureRange => "The lower and upper bound of the measure range.",
Ratio => "A ratio in one of this type's units (e.g. percent).",
RatioRange => "The lower and upper bound of the ratio range.",
Date => "A date, or a date and time with optional timezone.",
DateRange => "The start date and end date of the date range.",
Time => "A time of day, with optional timezone.",
TimeRange => "The start time and end time of the time range.",
}
}
impl TypeSpecification {
pub fn boolean() -> Self {
TypeSpecification::Boolean {
help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
}
}
pub fn measure() -> Self {
TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::new(),
traits: Vec::new(),
decomposition: None,
unit: None,
help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
}
}
pub fn number() -> Self {
TypeSpecification::Number {
minimum: None,
maximum: None,
decimals: None,
help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
}
}
pub fn number_range() -> Self {
TypeSpecification::NumberRange {
lower: None,
upper: None,
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
}
}
pub fn ratio() -> Self {
TypeSpecification::Ratio {
minimum: None,
maximum: None,
units: RatioUnits(vec![
RatioUnit {
name: "percent".to_string(),
value: crate::computation::rational::rational_new(100, 1),
minimum: None,
maximum: None,
suggestion_magnitude: None,
},
RatioUnit {
name: "permille".to_string(),
value: crate::computation::rational::rational_new(1000, 1),
minimum: None,
maximum: None,
suggestion_magnitude: None,
},
]),
unit: None,
help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
}
}
pub fn ratio_range() -> Self {
TypeSpecification::RatioRange {
lower: None,
upper: None,
minimum: None,
maximum: None,
units: match TypeSpecification::ratio() {
TypeSpecification::Ratio { units, .. } => units,
_ => unreachable!("BUG: ratio constructor must return a ratio type"),
},
help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
}
}
pub fn text() -> Self {
TypeSpecification::Text {
length: None,
help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
}
}
pub fn date() -> Self {
TypeSpecification::Date {
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
}
}
pub fn date_range() -> Self {
TypeSpecification::DateRange {
lower: None,
upper: None,
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
}
}
pub fn time() -> Self {
TypeSpecification::Time {
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
}
}
pub fn time_range() -> Self {
TypeSpecification::TimeRange {
lower: None,
upper: None,
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
}
}
pub fn measure_range() -> Self {
TypeSpecification::MeasureRange {
lower: None,
upper: None,
minimum: None,
maximum: None,
units: MeasureUnits::new(),
decomposition: None,
help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
}
}
#[must_use]
pub fn element_from_range(&self) -> Option<Self> {
match self {
TypeSpecification::NumberRange { lower, upper, .. } => {
Some(TypeSpecification::Number {
minimum: lower.clone(),
maximum: upper.clone(),
decimals: None,
help: String::new(),
})
}
TypeSpecification::MeasureRange {
lower,
upper,
units,
decomposition,
..
} => Some(TypeSpecification::Measure {
minimum: lower.clone(),
maximum: upper.clone(),
units: units.clone(),
traits: Vec::new(),
decomposition: decomposition.clone(),
unit: None,
help: String::new(),
}),
TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
minimum: lower.clone(),
maximum: upper.clone(),
help: String::new(),
}),
TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
minimum: lower.clone(),
maximum: upper.clone(),
help: String::new(),
}),
TypeSpecification::RatioRange {
lower,
upper,
units,
..
} => Some(TypeSpecification::Ratio {
minimum: lower.clone(),
maximum: upper.clone(),
units: units.clone(),
unit: None,
help: String::new(),
}),
_ => None,
}
}
#[must_use]
pub fn range_from_element(&self) -> Option<Self> {
match self {
TypeSpecification::Number {
minimum, maximum, ..
} => Some(TypeSpecification::NumberRange {
lower: minimum.clone(),
upper: maximum.clone(),
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
}),
TypeSpecification::Measure {
minimum,
maximum,
units,
decomposition,
..
} => Some(TypeSpecification::MeasureRange {
lower: minimum.clone(),
upper: maximum.clone(),
minimum: None,
maximum: None,
units: units.clone(),
decomposition: decomposition.clone(),
help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
}),
TypeSpecification::Date {
minimum, maximum, ..
} => Some(TypeSpecification::DateRange {
lower: minimum.clone(),
upper: maximum.clone(),
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
}),
TypeSpecification::Time {
minimum, maximum, ..
} => Some(TypeSpecification::TimeRange {
lower: minimum.clone(),
upper: maximum.clone(),
minimum: None,
maximum: None,
help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
}),
TypeSpecification::Ratio {
minimum,
maximum,
units,
..
} => Some(TypeSpecification::RatioRange {
lower: minimum.clone(),
upper: maximum.clone(),
minimum: None,
maximum: None,
units: units.clone(),
help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
}),
_ => None,
}
}
#[must_use]
pub fn minimum_decimal(&self) -> Option<Decimal> {
match self {
TypeSpecification::Number { minimum, .. }
| TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
bound
.try_to_decimal()
.expect("BUG: planned minimum must convert to decimal")
}),
TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
bound
.try_to_decimal()
.expect("BUG: planned minimum must convert to decimal")
}),
_ => None,
}
}
#[must_use]
pub fn maximum_decimal(&self) -> Option<Decimal> {
match self {
TypeSpecification::Number { maximum, .. }
| TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
bound
.try_to_decimal()
.expect("BUG: planned maximum must convert to decimal")
}),
TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
bound
.try_to_decimal()
.expect("BUG: planned maximum must convert to decimal")
}),
_ => None,
}
}
pub fn veto() -> Self {
TypeSpecification::Veto { message: None }
}
pub fn apply_constraint(
&mut self,
names: ConstraintNames<'_>,
command: TypeConstraintCommand,
args: &[CommandArg],
declared_suggestion: &mut Option<RawSuggestion>,
declared_value: &mut Option<RawSuggestion>,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<(), String> {
let type_name = names.type_name;
if command == TypeConstraintCommand::With
&& args.len() > 1
&& !matches!(self, TypeSpecification::List { .. })
{
return Err(format!("`-> with` on {self} takes exactly one value"));
}
if command == TypeConstraintCommand::Trait
&& !matches!(&self, TypeSpecification::Measure { .. })
{
return Err("trait command is only valid on measure types".to_string());
}
match self {
TypeSpecification::Boolean { help } => match command {
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Boolean,
None,
)?;
match lit {
crate::literals::Value::Boolean(bv) => {
*target =
Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
}
_ => {
return Err(
"Please provide true or false, for example `-> suggest true`."
.to_string(),
);
}
}
}
other => {
return Err(format!(
"Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
other
));
}
},
TypeSpecification::Measure {
minimum,
maximum,
units,
traits,
help,
..
} => match command {
TypeConstraintCommand::Decimals => {
return Err(decimals_depend_on_the_unit_error(
"measure",
names,
args,
units.0.first().map(|unit| unit.name.as_str()),
)?);
}
TypeConstraintCommand::Unit => match args {
[CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Reference(reference))] =>
{
let unit_name = name.clone();
let written = reference.to_string();
if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
let same = match &existing.scale {
UnitScale::Unresolved {
segments,
name: target_name,
..
} => {
segments == &reference.segments
&& target_name == &reference.name
}
UnitScale::Literal(_) | UnitScale::Reference { .. } => false,
};
if !same {
return Err(format!(
"Unit '{unit_name}' is already defined in this type's inherited units; \
cannot change factor or decomposition. Add a new unit name instead."
));
}
} else {
units.0.push(MeasureUnit::from_reference(
unit_name,
written,
reference.segments.clone(),
reference.name.clone(),
));
}
}
[CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(value))] =>
{
apply_measure_unit_factor(units, name.clone(), *value, Vec::new())?;
}
[CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(prefix, factors))] =>
{
let derived_measure_factors: Vec<(String, i32)> = factors
.iter()
.map(|f| (f.measure_ref.clone(), f.exp))
.collect();
apply_measure_unit_factor(
units,
name.clone(),
*prefix,
derived_measure_factors,
)?;
}
_ => {
return Err(
"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')"
.to_string(),
);
}
},
TypeConstraintCommand::Trait => {
let measure_trait = parse_measure_trait(args)?;
if traits.contains(&measure_trait) {
return Err(format!(
"Duplicate trait '{}' for measure type.",
measure_trait_name(measure_trait)
));
}
if measure_trait == MeasureTrait::Duration {
validate_duration_trait_requirements(units)?;
}
if measure_trait == MeasureTrait::Calendar {
validate_calendar_trait_requirements(units)?;
}
traits.push(measure_trait);
}
TypeConstraintCommand::Minimum => {
*minimum = Some(parse_measure_declared_bound(
args, "minimum", units, type_name,
)?);
}
TypeConstraintCommand::Maximum => {
*maximum = Some(parse_measure_declared_bound(
args, "maximum", units, type_name,
)?);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
if !traits.contains(&MeasureTrait::Calendar) {
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::MeasureUnits,
Some(units),
)?;
}
match lit {
crate::literals::Value::NumberWithUnit(n, unit_name) => {
if units
.get(unit_name)
.is_ok_and(crate::literals::MeasureUnit::is_dynamic)
{
*target = Some(RawSuggestion::Measure {
magnitude: lift_parser_decimal(*n)?,
unit_name: unit_name.clone(),
});
} else {
let (magnitude, unit_name) =
parse_measure_declared_bound(args, cmd, units, type_name)?;
*target = Some(RawSuggestion::Measure {
magnitude,
unit_name,
});
}
}
_ => {
return Err(measure_suggestion_wrong_shape_error(type_name, traits));
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::Number {
decimals,
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Decimals => {
let d = require_decimal_literal(args, "decimals")?;
*decimals = Some(decimal_to_u8(d, "decimals")?);
}
TypeConstraintCommand::Unit => {
return Err(
"Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
);
}
TypeConstraintCommand::Minimum => {
*minimum = Some(require_decimal_literal(args, "minimum")?);
}
TypeConstraintCommand::Maximum => {
*maximum = Some(require_decimal_literal(args, "maximum")?);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Number,
None,
)?;
match lit {
crate::literals::Value::Number(d) => {
*target = Some(RawSuggestion::Value(ValueKind::Number(
lift_parser_decimal(*d)?,
)));
}
_ => {
return Err(
"Please provide a number, for example `-> suggest 42`.".to_string()
);
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
command
));
}
},
TypeSpecification::NumberRange {
lower,
upper,
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Lower => {
*lower = Some(require_decimal_literal(args, "lower")?);
}
TypeConstraintCommand::Upper => {
*upper = Some(require_decimal_literal(args, "upper")?);
}
TypeConstraintCommand::Minimum => {
let width = require_decimal_literal(args, "minimum")?;
reject_negative_width_magnitude(&width, "minimum")?;
*minimum = Some(width);
}
TypeConstraintCommand::Maximum => {
let width = require_decimal_literal(args, "maximum")?;
reject_negative_width_magnitude(&width, "maximum")?;
*maximum = Some(width);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let target = match command {
TypeConstraintCommand::Suggest => &mut *declared_suggestion,
TypeConstraintCommand::With => &mut *declared_value,
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let (left, right) = require_suggestion_range_endpoints(
args,
type_name,
SuggestionExpectation::NumberRange,
None,
)?;
let left = literal_value_from_parser_value(left)?;
let right = literal_value_from_parser_value(right)?;
if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
return Err(
"Please provide a number range, for example `-> suggest 10...100`."
.to_string(),
);
}
*target = Some(RawSuggestion::Value(ValueKind::Range(
Box::new(left),
Box::new(right),
)));
}
_ => {
return Err(format!(
"Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::Ratio {
minimum,
maximum,
units,
unit: _,
help,
} => match command {
TypeConstraintCommand::Decimals => {
return Err(decimals_depend_on_the_unit_error(
"ratio",
names,
args,
units.0.first().map(|unit| unit.name.as_str()),
)?);
}
TypeConstraintCommand::Unit => {
let (unit_name, value_dec) = match args {
[CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
(name.clone(), *v)
}
_ => {
return Err(
"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."
.to_string(),
);
}
};
let value = crate::computation::rational::decimal_to_rational(value_dec)
.map_err(|failure| {
format!(
"ratio unit value is not exactly representable as a rational: {}",
failure
)
})?;
if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
if existing.value != value {
return Err(format!(
"Unit '{unit_name}' is already defined in this type's inherited units; \
cannot change factor. Add a new unit name instead."
));
}
} else {
units.0.push(RatioUnit {
name: unit_name,
value,
minimum: None,
maximum: None,
suggestion_magnitude: None,
});
}
}
TypeConstraintCommand::Minimum => {
let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
sync_ratio_units_from_canonical(
units,
&canonical,
UnitConstraintField::Minimum,
)?;
*minimum = Some(canonical);
}
TypeConstraintCommand::Maximum => {
let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
sync_ratio_units_from_canonical(
units,
&canonical,
UnitConstraintField::Maximum,
)?;
*maximum = Some(canonical);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Ratio,
None,
)?;
match lit {
crate::literals::Value::NumberWithUnit(_, unit_name) => {
let element_type = LemmaType::primitive(TypeSpecification::Ratio {
minimum: minimum.clone(),
maximum: maximum.clone(),
units: units.clone(),
unit: None,
help: help.clone(),
});
let value = parser_value_to_value_kind(lit, &element_type, None)?;
sync_ratio_suggestion_units(units, &value)?;
*target = Some(RawSuggestion::UnitBound {
value,
unit_name: unit_name.clone(),
});
}
other => {
return Err(format!(
"suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
value_kind_name(other)
));
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::RatioRange {
lower,
upper,
minimum,
maximum,
units,
help,
} => match command {
TypeConstraintCommand::Unit => {
return Err(format!(
"'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
));
}
TypeConstraintCommand::Lower => {
*lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
}
TypeConstraintCommand::Upper => {
*upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
}
TypeConstraintCommand::Minimum => {
let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
reject_negative_width_magnitude(&width, "minimum")?;
*minimum = Some(width);
}
TypeConstraintCommand::Maximum => {
let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
reject_negative_width_magnitude(&width, "maximum")?;
*maximum = Some(width);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let target = match command {
TypeConstraintCommand::Suggest => &mut *declared_suggestion,
TypeConstraintCommand::With => &mut *declared_value,
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let (left, right) = require_suggestion_range_endpoints(
args,
type_name,
SuggestionExpectation::RatioRange,
None,
)?;
let element_type = LemmaType::primitive(
TypeSpecification::RatioRange {
lower: lower.clone(),
upper: upper.clone(),
minimum: minimum.clone(),
maximum: maximum.clone(),
units: units.clone(),
help: help.clone(),
}
.element_from_range()
.expect("BUG: RatioRange must define element_from_range"),
);
let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
if !value_kind_matches_spec(&left.value, &element_type.specifications)
|| !value_kind_matches_spec(&right.value, &element_type.specifications)
|| !matches!(&left.value, ValueKind::Ratio(_))
|| !matches!(&right.value, ValueKind::Ratio(_))
{
return Err(
"Please provide a ratio range, for example `-> suggest 10%...50%`."
.to_string(),
);
}
*target = Some(RawSuggestion::Value(ValueKind::Range(
Box::new(left),
Box::new(right),
)));
}
_ => {
return Err(format!(
"Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::Text { length, help } => match command {
TypeConstraintCommand::Length => {
let d = require_decimal_literal(args, "length")?;
*length = Some(decimal_to_usize(d, "length")?);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Text,
None,
)?;
match lit {
crate::literals::Value::Text(s) => {
*target =
Some(RawSuggestion::Value(ValueKind::Text(Arc::from(s.as_str()))));
}
_ => {
return Err(
"Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
.to_string(),
);
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for text type. Valid commands: length, help, suggest, with",
command
));
}
},
TypeSpecification::Date {
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Minimum => {
let dt = require_date_literal(args, "minimum")?;
*minimum = Some(dt);
}
TypeConstraintCommand::Maximum => {
let dt = require_date_literal(args, "maximum")?;
*maximum = Some(dt);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Date,
None,
)?;
match lit {
crate::literals::Value::Date(dt) => {
*target = Some(RawSuggestion::Value(ValueKind::Date(
date_time_to_semantic(dt),
)));
}
_ => {
return Err(
"Please provide a date, for example `-> suggest 2024-06-15`."
.to_string(),
);
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::DateRange {
lower,
upper,
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Lower => {
*lower = Some(require_date_literal(args, "lower")?);
}
TypeConstraintCommand::Upper => {
*upper = Some(require_date_literal(args, "upper")?);
}
TypeConstraintCommand::Minimum => {
*minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
}
TypeConstraintCommand::Maximum => {
*maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let target = match command {
TypeConstraintCommand::Suggest => &mut *declared_suggestion,
TypeConstraintCommand::With => &mut *declared_value,
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let (left, right) = require_suggestion_range_endpoints(
args,
type_name,
SuggestionExpectation::DateRange,
None,
)?;
let left = literal_value_from_parser_value(left)?;
let right = literal_value_from_parser_value(right)?;
if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
return Err(
"Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
.to_string(),
);
}
*target = Some(RawSuggestion::Value(ValueKind::Range(
Box::new(left),
Box::new(right),
)));
}
_ => {
return Err(format!(
"Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::Time {
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Minimum => {
let t = require_time_literal(args, "minimum")?;
*minimum = Some(t);
}
TypeConstraintCommand::Maximum => {
let t = require_time_literal(args, "maximum")?;
*maximum = Some(t);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let (target, cmd) = match command {
TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
TypeConstraintCommand::With => (&mut *declared_value, "with"),
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let lit = require_literal(args, cmd)?;
reject_calendar_for_suggestion(
lit,
type_name,
SuggestionExpectation::Time,
None,
)?;
match lit {
crate::literals::Value::Time(t) => {
*target =
Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
}
_ => {
return Err(
"Please provide a time, for example `-> suggest 09:00:00`."
.to_string(),
);
}
}
}
_ => {
return Err(format!(
"Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::TimeRange {
lower,
upper,
minimum,
maximum,
help,
} => match command {
TypeConstraintCommand::Lower => {
*lower = Some(require_time_literal(args, "lower")?);
}
TypeConstraintCommand::Upper => {
*upper = Some(require_time_literal(args, "upper")?);
}
TypeConstraintCommand::Minimum => {
*minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
}
TypeConstraintCommand::Maximum => {
*maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let target = match command {
TypeConstraintCommand::Suggest => &mut *declared_suggestion,
TypeConstraintCommand::With => &mut *declared_value,
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let (left, right) = require_suggestion_range_endpoints(
args,
type_name,
SuggestionExpectation::TimeRange,
None,
)?;
let left = literal_value_from_parser_value(left)?;
let right = literal_value_from_parser_value(right)?;
if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
return Err(
"Please provide a time range, for example `-> suggest 09:00...17:00`."
.to_string(),
);
}
*target = Some(RawSuggestion::Value(ValueKind::Range(
Box::new(left),
Box::new(right),
)));
}
_ => {
return Err(format!(
"Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::MeasureRange {
lower,
upper,
minimum,
maximum,
units,
decomposition,
help,
} => match command {
TypeConstraintCommand::Unit => {
return Err(format!(
"'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
));
}
TypeConstraintCommand::Lower => {
*lower = Some(parse_measure_declared_bound(
args, "lower", units, type_name,
)?);
}
TypeConstraintCommand::Upper => {
*upper = Some(parse_measure_declared_bound(
args, "upper", units, type_name,
)?);
}
TypeConstraintCommand::Minimum => {
let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
reject_negative_width_magnitude(&width.0, "minimum")?;
*minimum = Some(width);
}
TypeConstraintCommand::Maximum => {
let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
reject_negative_width_magnitude(&width.0, "maximum")?;
*maximum = Some(width);
}
TypeConstraintCommand::Help => {
apply_type_help_command(help, args)?;
}
TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
let target = match command {
TypeConstraintCommand::Suggest => &mut *declared_suggestion,
TypeConstraintCommand::With => &mut *declared_value,
_ => unreachable!("BUG: only Suggest or With in this arm"),
};
let (left, right) = require_suggestion_range_endpoints(
args,
type_name,
SuggestionExpectation::MeasureRange,
Some(units),
)?;
let element_type = LemmaType::primitive(
TypeSpecification::MeasureRange {
lower: lower.clone(),
upper: upper.clone(),
minimum: minimum.clone(),
maximum: maximum.clone(),
units: units.clone(),
decomposition: decomposition.clone(),
help: help.clone(),
}
.element_from_range()
.expect("BUG: MeasureRange must define element_from_range"),
);
let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
if !value_kind_matches_spec(&left.value, &element_type.specifications)
|| !value_kind_matches_spec(&right.value, &element_type.specifications)
|| !matches!(&left.value, ValueKind::Measure(_))
|| !matches!(&right.value, ValueKind::Measure(_))
{
return Err(format!(
"Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
));
}
*target = Some(RawSuggestion::Value(ValueKind::Range(
Box::new(left),
Box::new(right),
)));
}
_ => {
return Err(format!(
"Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
command
));
}
},
TypeSpecification::Veto { .. } => {
return Err(format!(
"Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
command
));
}
TypeSpecification::Undetermined => {
return Err(format!(
"Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
command
));
}
TypeSpecification::Spec { .. } => {
return Err(format!(
"Invalid command '{command}' for spec type. A spec instance has no data constraints"
));
}
TypeSpecification::List { element, help, .. } => match command {
TypeConstraintCommand::Help => {
if matches!(element.specifications, TypeSpecification::Spec { .. }) {
apply_type_help_command(help, args)?;
} else {
let mut unused_suggestion = None;
let mut unused_value = None;
Arc::make_mut(element).specifications.apply_constraint(
names,
command,
args,
&mut unused_suggestion,
&mut unused_value,
resolve_unit,
)?;
}
}
TypeConstraintCommand::With => {
return Err(
"BUG: list -> with is applied before TypeSpecification::apply_constraint"
.to_string(),
);
}
other => {
return Err(format!(
"Invalid command '{other}' for list type. Valid commands: with, help"
));
}
},
}
Ok(())
}
}
pub fn parse_number_unit(
value_str: &str,
type_spec: &TypeSpecification,
) -> Result<crate::parsing::ast::Value, String> {
use crate::literals::{NumberWithUnit, RatioLiteral};
use crate::parsing::ast::Value;
let trimmed = value_str.trim();
match type_spec {
TypeSpecification::Measure { units, .. } => {
if units.is_empty() {
unreachable!(
"BUG: Measure type has no units; should have been validated during planning"
);
}
match trimmed.parse::<NumberWithUnit>() {
Ok(n) => {
let unit = units.get(&n.1).map_err(|e| e.to_string())?;
Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
}
Err(e) => {
if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
let example_unit = units
.iter()
.next()
.expect("BUG: units non-empty after guard")
.name
.as_str();
Err(format!(
"Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
trimmed,
example_unit,
valid.join(", ")
))
} else {
Err(e)
}
}
}
}
TypeSpecification::Ratio { units, .. } => {
if units.is_empty() {
unreachable!(
"BUG: Ratio type has no units; should have been validated during planning"
);
}
match trimmed.parse::<RatioLiteral>()? {
RatioLiteral::Bare(_) => {
Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
}
RatioLiteral::Percent(n) => {
let unit = units.get("percent").map_err(|e| e.to_string())?;
Ok(Value::NumberWithUnit(n, unit.name.clone()))
}
RatioLiteral::Permille(n) => {
let unit = units.get("permille").map_err(|e| e.to_string())?;
Ok(Value::NumberWithUnit(n, unit.name.clone()))
}
RatioLiteral::Named { value, unit } => {
let resolved = units.get(&unit).map_err(|e| e.to_string())?;
Ok(Value::NumberWithUnit(value, resolved.name.clone()))
}
}
}
_ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
}
}
pub fn parse_value_from_string(
value_str: &str,
type_spec: &TypeSpecification,
source: &Source,
) -> Result<crate::parsing::ast::Value, Error> {
match type_spec {
TypeSpecification::NumberRange { .. }
| TypeSpecification::MeasureRange { .. }
| TypeSpecification::DateRange { .. }
| TypeSpecification::TimeRange { .. }
| TypeSpecification::RatioRange { .. } => {
let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
unreachable!(
"BUG: range_element_type_specification missing arm for known range type"
)
});
parse_range_value_from_string(value_str, &element_spec, source)
}
_ => parse_scalar_value_from_string(value_str, type_spec, source),
}
}
fn parse_range_value_from_string(
value_str: &str,
element_spec: &TypeSpecification,
source: &Source,
) -> Result<crate::parsing::ast::Value, Error> {
use crate::parsing::ast::Value;
let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
to_err("Range value must use '...' between the two endpoints".to_string())
})?;
if left_str.trim().is_empty() || right_str.trim().is_empty() {
return Err(to_err(
"Range value must contain a non-empty left and right endpoint".to_string(),
));
}
let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
Ok(Value::Range(Box::new(left), Box::new(right)))
}
fn parse_scalar_value_from_string(
value_str: &str,
type_spec: &TypeSpecification,
source: &Source,
) -> Result<crate::parsing::ast::Value, Error> {
use crate::parsing::ast::Value;
let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
match type_spec {
TypeSpecification::Text { .. } => value_str
.parse::<crate::literals::TextLiteral>()
.map(|t| Value::Text(t.0))
.map_err(to_err),
TypeSpecification::Number { .. } => value_str
.parse::<crate::literals::NumberLiteral>()
.map(|n| Value::Number(n.0))
.map_err(to_err),
TypeSpecification::Measure { .. } => {
parse_number_unit(value_str, type_spec).map_err(to_err)
}
TypeSpecification::Boolean { .. } => value_str
.parse::<BooleanValue>()
.map(Value::Boolean)
.map_err(to_err),
TypeSpecification::Date { .. } => {
let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
Ok(Value::Date(date))
}
TypeSpecification::Time { .. } => {
let time = value_str.parse::<TimeValue>().map_err(to_err)?;
Ok(Value::Time(time))
}
TypeSpecification::Ratio { .. } => {
parse_number_unit(value_str, type_spec).map_err(to_err)
}
TypeSpecification::NumberRange { .. }
| TypeSpecification::MeasureRange { .. }
| TypeSpecification::DateRange { .. }
| TypeSpecification::TimeRange { .. }
| TypeSpecification::RatioRange { .. } => {
unreachable!("BUG: range types are parsed by parse_range_value_from_string")
}
TypeSpecification::Veto { .. } => Err(to_err(
"Veto type cannot be parsed from string".to_string(),
)),
TypeSpecification::Undetermined => unreachable!(
"BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
),
TypeSpecification::Spec { .. } => Err(to_err(
"spec values are not evaluation-data scalars".to_string(),
)),
TypeSpecification::List { .. } => Err(to_err(
"list values are not evaluation-data scalars".to_string(),
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SemanticCalendarUnit {
Month,
Year,
}
impl fmt::Display for SemanticCalendarUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
SemanticCalendarUnit::Month => "month",
SemanticCalendarUnit::Year => "year",
};
write!(f, "{}", s)
}
}
pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
match unit_name {
"month" => SemanticCalendarUnit::Month,
"year" => SemanticCalendarUnit::Year,
other => unreachable!(
"BUG: calendar measure signature unit must be month or year, got '{other}'"
),
}
}
pub fn semantic_calendar_unit_from_measure_signature(
signature: &[(String, i32)],
) -> SemanticCalendarUnit {
let unit_name = signature
.first()
.map(|(name, _)| name.as_str())
.expect("BUG: calendar measure must carry a unit signature");
semantic_calendar_unit_from_unit_name(unit_name)
}
pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
if !lemma_type.is_calendar_like() {
unreachable!(
"BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
lemma_type.name()
);
}
let signature = lemma_type.measure_runtime_signature();
if signature.is_empty() {
return SemanticCalendarUnit::Month;
}
semantic_calendar_unit_from_measure_signature(&signature)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SemanticConversionTarget {
Type(ConversionTypeKind),
Unit {
unit_name: String,
owning_type: Arc<LemmaType>,
},
}
impl std::hash::Hash for SemanticConversionTarget {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Self::Type(kind) => {
0u8.hash(state);
kind.hash(state);
}
Self::Unit {
unit_name,
owning_type,
} => {
1u8.hash(state);
unit_name.hash(state);
owning_type.hash(state);
}
}
}
}
impl SemanticConversionTarget {}
impl fmt::Display for SemanticConversionTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SemanticTimezone {
pub offset_minutes: i16,
}
impl SemanticTimezone {
pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
}
pub fn offset_seconds(&self) -> i32 {
i32::from(self.offset_minutes) * 60
}
}
impl From<&TimezoneValue> for SemanticTimezone {
fn from(tz: &TimezoneValue) -> Self {
Self {
offset_minutes: tz.offset_minutes,
}
}
}
impl From<&SemanticTimezone> for TimezoneValue {
fn from(tz: &SemanticTimezone) -> Self {
Self {
offset_minutes: tz.offset_minutes,
}
}
}
impl fmt::Display for SemanticTimezone {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
TimezoneValue::from(self).fmt(f)
}
}
impl Serialize for SemanticTimezone {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SemanticTimezone {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl FromStr for SemanticTimezone {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(&TimezoneValue::from_str(s)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SemanticTime {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub microsecond: u32,
pub timezone: Option<SemanticTimezone>,
}
impl fmt::Display for SemanticTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
if self.microsecond != 0 {
write!(f, ".{:06}", self.microsecond)?;
}
if let Some(timezone) = &self.timezone {
write!(f, "{}", timezone)?;
}
Ok(())
}
}
impl Serialize for SemanticTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SemanticTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl FromStr for SemanticTime {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(time_to_semantic(&TimeValue::from_str(s)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SemanticDateTime {
pub year: i32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: u32,
pub microsecond: u32,
pub timezone: Option<SemanticTimezone>,
}
impl fmt::Display for SemanticDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let has_time = self.hour != 0
|| self.minute != 0
|| self.second != 0
|| self.microsecond != 0
|| self.timezone.is_some();
if !has_time {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
} else {
write!(
f,
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
self.year, self.month, self.day, self.hour, self.minute, self.second
)?;
if self.microsecond != 0 {
write!(f, ".{:06}", self.microsecond)?;
}
if let Some(tz) = &self.timezone {
write!(f, "{}", tz)?;
}
Ok(())
}
}
}
impl Serialize for SemanticDateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SemanticDateTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl FromStr for SemanticDateTime {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RawSuggestion {
Value(ValueKind),
Measure {
magnitude: RationalInteger,
unit_name: String,
},
UnitBound {
value: ValueKind,
unit_name: String,
},
List(Vec<RawSuggestion>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BoundValueKind {
pub value: ValueKind,
pub unit: Option<Arc<str>>,
pub instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
}
impl BoundValueKind {
pub fn unbound(value: ValueKind) -> Self {
Self {
value,
unit: None,
instance_factors: None,
}
}
pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
Self {
value,
unit,
instance_factors: None,
}
}
pub fn with_instance_factors(
value: ValueKind,
unit: Option<Arc<str>>,
instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
) -> Self {
Self {
value,
unit,
instance_factors,
}
}
pub fn to_literal(&self) -> LiteralValue {
LiteralValue {
value: self.value.clone(),
}
}
#[must_use]
pub fn agree_or_left_binding(
left: Option<&Arc<str>>,
right: Option<&Arc<str>>,
) -> Option<Arc<str>> {
match (left, right) {
(Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
(Some(a), Some(_)) => Some(Arc::clone(a)),
(Some(a), None) => Some(Arc::clone(a)),
(None, Some(b)) => Some(Arc::clone(b)),
(None, None) => None,
}
}
#[must_use]
pub fn agree_instance_factors(
left: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
right: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
) -> Option<Arc<std::collections::BTreeMap<String, RationalInteger>>> {
match (left, right) {
(None, None) | (Some(_), None) | (None, Some(_)) => None,
(Some(left), Some(right)) => {
let mut agreed = std::collections::BTreeMap::new();
for (unit, factor) in left.iter() {
if right.get(unit) == Some(factor) {
agreed.insert(unit.clone(), factor.clone());
}
}
if agreed.is_empty() {
None
} else {
Some(Arc::new(agreed))
}
}
}
}
#[must_use]
pub fn binding_with_agreed_factors(
unit: Option<Arc<str>>,
factors: &Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
) -> Option<Arc<str>> {
match (unit, factors) {
(Some(unit), Some(factors)) if factors.contains_key(unit.as_ref()) => Some(unit),
(Some(_), Some(_)) => None,
(unit, None) => unit,
(None, _) => None,
}
}
}
pub fn bound_value_kind_from_raw_suggestion(
raw: RawSuggestion,
specifications: &TypeSpecification,
type_name: &str,
) -> Result<BoundValueKind, String> {
enum Step<'a> {
Visit(RawSuggestion, &'a TypeSpecification),
CollectList {
element: &'a Arc<LemmaType>,
rows: usize,
},
}
let mut steps = vec![Step::Visit(raw, specifications)];
let mut bounds: Vec<BoundValueKind> = Vec::new();
while let Some(step) = steps.pop() {
match step {
Step::Visit(raw, specifications) => match raw {
RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
value,
unit: Some(Arc::from(unit_name)),
instance_factors: None,
}),
RawSuggestion::Measure {
magnitude,
unit_name,
} => {
let TypeSpecification::Measure { units, .. } = specifications else {
unreachable!(
"BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
);
};
if units
.get(&unit_name)
.is_ok_and(crate::literals::MeasureUnit::is_dynamic)
{
bounds.push(BoundValueKind {
value: ValueKind::UnscaledMeasure {
magnitude,
unit: Arc::from(unit_name.as_str()),
},
unit: Some(Arc::from(unit_name)),
instance_factors: None,
});
} else {
let canonical = measure_declared_bound_to_canonical(
&magnitude, &unit_name, units, type_name, "suggest",
)?;
bounds.push(BoundValueKind {
value: ValueKind::Measure(canonical),
unit: Some(Arc::from(unit_name)),
instance_factors: None,
});
}
}
RawSuggestion::List(rows) => {
let TypeSpecification::List { element, .. } = specifications else {
unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
};
steps.push(Step::CollectList {
element,
rows: rows.len(),
});
for row in rows.into_iter().rev() {
steps.push(Step::Visit(row, &element.specifications));
}
}
},
Step::CollectList { element, rows } => {
let start = bounds
.len()
.checked_sub(rows)
.expect("BUG: list rows missing from bound stack");
let converted: Arc<[TypedLiteral]> = bounds
.split_off(start)
.iter()
.map(|bound| typed_literal_from_bound(bound, element))
.collect();
bounds.push(BoundValueKind::unbound(ValueKind::List(converted.into())));
}
}
}
Ok(bounds.pop().expect("BUG: bound walk left no result"))
}
#[must_use]
pub fn display_binding_unit(
schema: &LemmaType,
settled: Option<&BoundValueKind>,
fill: Option<&BoundValueKind>,
suggestion: Option<&BoundValueKind>,
) -> Option<String> {
if schema.is_measure_range() || schema.is_ratio_range() {
return None;
}
if let Some(unit) = schema.unit() {
return Some(unit.to_string());
}
if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
return Some(unit.to_string());
}
if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
return Some(unit.to_string());
}
suggestion
.and_then(|bound| bound.unit.as_ref())
.map(|unit| unit.to_string())
}
#[must_use]
pub fn lemma_type_with_display_binding(
schema: &LemmaType,
settled: Option<&BoundValueKind>,
fill: Option<&BoundValueKind>,
suggestion: Option<&BoundValueKind>,
) -> LemmaType {
if schema.is_measure_range() || schema.is_ratio_range() {
return schema.clone();
}
match display_binding_unit(schema, settled, fill, suggestion) {
Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
Some(_) | None => schema.clone(),
}
}
#[must_use]
pub fn lemma_type_arc_with_display_binding(
schema: &Arc<LemmaType>,
settled: Option<&BoundValueKind>,
fill: Option<&BoundValueKind>,
suggestion: Option<&BoundValueKind>,
) -> Arc<LemmaType> {
if schema.is_measure_range() || schema.is_ratio_range() {
return Arc::clone(schema);
}
match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
Some(unit) if schema.unit() != Some(unit.as_str()) => {
Arc::new(schema.as_ref().clone().with_unit(unit))
}
Some(_) | None => Arc::clone(schema),
}
}
#[must_use]
pub fn typed_literal_from_bound(
bound: &BoundValueKind,
lemma_type: &Arc<LemmaType>,
) -> TypedLiteral {
let lemma_type = match (&bound.unit, &lemma_type.specifications) {
(Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
if lemma_type.unit() != Some(unit.as_ref()) =>
{
Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
}
_ => Arc::clone(lemma_type),
};
TypedLiteral {
value: bound.value.clone(),
lemma_type,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ValueKind {
Number(RationalInteger),
Measure(RationalInteger),
UnscaledMeasure {
magnitude: RationalInteger,
unit: Arc<str>,
},
Text(Arc<str>),
Date(SemanticDateTime),
Time(SemanticTime),
Boolean(bool),
Ratio(RationalInteger),
Range(Box<TypedLiteral>, Box<TypedLiteral>),
Spec(SpecValue),
List(ListValue),
}
#[derive(Debug, Clone)]
pub struct ListValue {
items: ListItems,
index: Option<Arc<crate::planning::instance_list::KeyIndex>>,
}
pub(crate) struct StoredRows<'l> {
pub rows: &'l [Arc<RowInstance>],
pub owner: Option<&'l InstanceKey>,
}
#[derive(Debug, Clone)]
enum ListItems {
Values(Arc<[TypedLiteral]>),
Rows {
store: Arc<crate::planning::instance_list::RowStore>,
owner: Option<InstanceKey>,
row_type: Arc<LemmaType>,
},
}
impl ListValue {
fn held(items: Arc<[TypedLiteral]>) -> Self {
Self {
items: ListItems::Values(items),
index: None,
}
}
pub(crate) fn indexed(
items: Arc<[TypedLiteral]>,
index: Arc<crate::planning::instance_list::KeyIndex>,
) -> Self {
Self {
items: ListItems::Values(items),
index: Some(index),
}
}
pub(crate) fn stored_rows(
store: &Arc<crate::planning::instance_list::RowStore>,
owner: Option<InstanceKey>,
row_type: Arc<LemmaType>,
) -> Self {
Self {
index: store.index.clone(),
items: ListItems::Rows {
store: Arc::clone(store),
owner,
row_type,
},
}
}
pub fn len(&self) -> usize {
match &self.items {
ListItems::Values(items) => items.len(),
ListItems::Rows { store, .. } => store.rows.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, position: usize) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
match &self.items {
ListItems::Values(items) => items.get(position).map(std::borrow::Cow::Borrowed),
ListItems::Rows {
store,
owner,
row_type,
} => store.rows.get(position).map(|row| {
std::borrow::Cow::Owned(TypedLiteral {
value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
Arc::clone(row),
owner.clone(),
))),
lemma_type: Arc::clone(row_type),
})
}),
}
}
pub fn first(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
self.get(0)
}
pub fn last(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
self.len().checked_sub(1).and_then(|last| self.get(last))
}
pub fn iter(
&self,
) -> impl DoubleEndedIterator<Item = std::borrow::Cow<'_, TypedLiteral>> + ExactSizeIterator
{
(0..self.len()).map(|position| {
self.get(position)
.expect("BUG: list position below the list length")
})
}
pub(crate) fn values(&self) -> &[TypedLiteral] {
match &self.items {
ListItems::Values(items) => items,
ListItems::Rows { .. } => panic!("BUG: held items read from a list of stored rows"),
}
}
pub(crate) fn stored(&self) -> Option<StoredRows<'_>> {
match &self.items {
ListItems::Values(_) => None,
ListItems::Rows { store, owner, .. } => Some(StoredRows {
rows: &store.rows,
owner: owner.as_ref(),
}),
}
}
pub fn to_vec(&self) -> Vec<TypedLiteral> {
self.iter().map(std::borrow::Cow::into_owned).collect()
}
pub(crate) fn index(&self) -> Option<&Arc<crate::planning::instance_list::KeyIndex>> {
self.index.as_ref()
}
pub(crate) fn unindexed(&self) -> Self {
Self {
items: self.items.clone(),
index: None,
}
}
pub(crate) fn with_index(&self, index: Arc<crate::planning::instance_list::KeyIndex>) -> Self {
Self {
items: self.items.clone(),
index: Some(index),
}
}
}
impl From<Arc<[TypedLiteral]>> for ListValue {
fn from(items: Arc<[TypedLiteral]>) -> Self {
Self::held(items)
}
}
impl From<Vec<TypedLiteral>> for ListValue {
fn from(items: Vec<TypedLiteral>) -> Self {
Self::held(Arc::from(items))
}
}
impl FromIterator<TypedLiteral> for ListValue {
fn from_iter<I: IntoIterator<Item = TypedLiteral>>(iter: I) -> Self {
Self::held(iter.into_iter().collect())
}
}
impl PartialEq for ListValue {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.iter().eq(other.iter())
}
}
impl Eq for ListValue {}
impl Hash for ListValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
for item in self.iter() {
item.hash(state);
}
}
}
impl ValueKind {
pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
match self {
ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
n.try_to_decimal().map_err(|failure| failure.to_string())
}
other => Err(format!("expected numeric value kind, got {other}")),
}
}
pub fn structural_byte_size(&self) -> usize {
fn rational_byte_estimate(r: &RationalInteger) -> usize {
let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
numer_bytes.max(1) + denom_bytes.max(1)
}
match self {
ValueKind::Number(r) => rational_byte_estimate(r),
ValueKind::Measure(r) => rational_byte_estimate(r),
ValueKind::UnscaledMeasure { magnitude, unit } => {
rational_byte_estimate(magnitude) + unit.len()
}
ValueKind::Ratio(r) => rational_byte_estimate(r),
ValueKind::Text(s) => s.len(),
ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
}
ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
ValueKind::Spec(SpecValue::Row(InstanceKey { row, .. })) => row.structural_byte_size(),
ValueKind::List(rows) => rows
.iter()
.map(|row| row.value.structural_byte_size())
.sum(),
}
}
#[must_use]
pub fn has_unscaled_measure(&self) -> bool {
match self {
ValueKind::UnscaledMeasure { .. } => true,
ValueKind::Range(left, right) => match (&left.value, &right.value) {
(ValueKind::UnscaledMeasure { .. }, _) | (_, ValueKind::UnscaledMeasure { .. }) => {
true
}
(ValueKind::Range(_, _) | ValueKind::List(_), _)
| (_, ValueKind::Range(_, _) | ValueKind::List(_)) => unscaled_in_value_tree(self),
_ => false,
},
ValueKind::List(_) => unscaled_in_value_tree(self),
_ => false,
}
}
}
fn unscaled_in_value_tree(root: &ValueKind) -> bool {
let mut pending = vec![root];
while let Some(value) = pending.pop() {
match value {
ValueKind::UnscaledMeasure { .. } => return true,
ValueKind::Range(left, right) => {
pending.push(&right.value);
pending.push(&left.value);
}
ValueKind::List(rows) => {
if rows.stored().is_none() {
for item in rows.values().iter() {
pending.push(&item.value);
}
}
}
_ => {}
}
}
false
}
fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
rational.display_str()
}
fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
use crate::parsing::ast::Value;
match rational.try_to_decimal() {
Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
Err(_) => format!("{} {}", rational.display_str(), unit),
}
}
impl fmt::Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValueKind::Number(rational) => {
write!(f, "{}", format_rational_magnitude_for_display(rational))
}
ValueKind::Measure(rational) => {
write!(f, "{}", format_rational_magnitude_for_display(rational))
}
ValueKind::UnscaledMeasure { magnitude, unit } => {
write!(
f,
"{} {unit}",
format_rational_magnitude_for_display(magnitude)
)
}
ValueKind::Text(s) => {
write!(f, "{}", crate::parsing::ast::Value::Text(s.to_string()))
}
ValueKind::Ratio(rational) => {
write!(f, "{}", format_rational_magnitude_for_display(rational))
}
ValueKind::Date(dt) => write!(f, "{}", dt),
ValueKind::Time(t) => write!(
f,
"{}",
crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
hour: t.hour as u8,
minute: t.minute as u8,
second: t.second as u8,
microsecond: t.microsecond,
timezone: t
.timezone
.as_ref()
.map(crate::parsing::ast::TimezoneValue::from),
})
),
ValueKind::Boolean(b) => write!(f, "{}", b),
ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
ValueKind::Spec(value) => write!(f, "{value}"),
ValueKind::List(rows) => {
write!(f, "list[")?;
for (i, row) in rows.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{row}")?;
}
write!(f, "]")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PathSegment {
pub uses: Arc<str>,
pub row: Option<usize>,
pub repository: Option<Arc<str>>,
pub spec: Arc<str>,
}
impl PathSegment {
pub(crate) fn import(alias: &str, identity: &SpecIdentity) -> Self {
Self {
uses: Arc::from(alias),
row: None,
repository: identity.repository.as_deref().map(Arc::from),
spec: Arc::from(identity.spec.as_str()),
}
}
pub(crate) fn list_row_shared(
list: &Arc<str>,
number: usize,
spec: &Arc<str>,
repository: Option<&Arc<str>>,
) -> Self {
Self {
uses: Arc::clone(list),
row: Some(number),
repository: repository.map(Arc::clone),
spec: Arc::clone(spec),
}
}
pub fn input_key(&self) -> String {
match self.row {
Some(number) => format!("{}.{number}", self.uses),
None => self.uses.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SpecInstance {
pub prefix: Vec<PathSegment>,
}
impl SpecInstance {
pub fn structural_byte_size(&self) -> usize {
self.prefix
.iter()
.map(|segment| {
segment.uses.len()
+ segment.spec.len()
+ segment.repository.as_deref().map_or(0, str::len)
+ segment.row.map_or(0, |_| std::mem::size_of::<usize>())
})
.sum()
}
}
impl fmt::Display for SpecInstance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, segment) in self.prefix.iter().enumerate() {
if index > 0 {
write!(f, ".")?;
}
write!(f, "{}", segment.input_key())?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SpecValue {
Fixed(SpecInstance),
Row(InstanceKey),
}
impl fmt::Display for SpecValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpecValue::Fixed(instance) => write!(f, "{instance}"),
SpecValue::Row(key) => write!(f, "{key}"),
}
}
}
#[derive(Debug, Clone)]
pub struct InstanceKey {
pub row: Arc<RowInstance>,
pub owner: Option<Box<InstanceKey>>,
}
impl InstanceKey {
pub fn new(row: Arc<RowInstance>, owner: Option<InstanceKey>) -> Self {
Self {
row,
owner: owner.map(Box::new),
}
}
pub fn owner(&self) -> Option<&InstanceKey> {
self.owner.as_deref()
}
pub(crate) fn scope(&self, hops: u32) -> Option<&InstanceKey> {
let mut scope = self.owner();
for _ in 0..hops {
scope = scope
.expect("BUG: a binding hops above the root scope")
.owner();
}
scope
}
pub(crate) fn under(&self, owner: &InstanceKey) -> InstanceKey {
let mut rows: Vec<&Arc<RowInstance>> = Vec::new();
let mut key = Some(self);
while let Some(current) = key {
rows.push(¤t.row);
key = current.owner();
}
let mut rebased = owner.clone();
for row in rows.into_iter().rev() {
rebased = InstanceKey::new(Arc::clone(row), Some(rebased));
}
rebased
}
fn naming_origins(&self) -> Vec<&RowOrigin> {
let mut origins = Vec::new();
let mut key = Some(self);
while let Some(current) = key {
origins.push(¤t.row.origin);
if matches!(current.row.origin, RowOrigin::Numbered { .. }) {
break;
}
key = current.owner();
}
origins
}
pub fn path(&self) -> Option<String> {
let origins = self.naming_origins();
if origins.iter().any(|origin| origin.is_inline()) {
return None;
}
Some(
origins
.iter()
.rev()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("."),
)
}
pub(crate) fn label_under(&self, row: &InstanceKey) -> Option<String> {
let mut below: Vec<&RowOrigin> = Vec::new();
let mut key = self;
while key != row {
below.push(&key.row.origin);
key = key.owner()?;
}
Some(
below
.iter()
.rev()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("."),
)
}
pub(crate) fn table_row(&self) -> (&InstanceKey, Option<String>) {
let mut key = self;
loop {
match &key.row.origin {
RowOrigin::Numbered { .. } => return (key, self.path()),
RowOrigin::Inline { .. } if key == self => return (key, None),
RowOrigin::Inline { .. } => return (key, self.label_under(key)),
RowOrigin::Import { .. } | RowOrigin::Written { .. } => {
key = key
.owner()
.expect("BUG: an instance off the rows table published as a table row");
}
}
}
}
pub(crate) fn field_key(&self, key: &str) -> Option<String> {
self.request_path().map(|path| format!("{path}.{key}"))
}
fn request_path(&self) -> Option<String> {
let origins = self.naming_origins();
if origins.iter().any(|origin| origin.is_inline()) {
return None;
}
Some(
origins
.iter()
.rev()
.map(|origin| match origin {
RowOrigin::Numbered {
under: Some(under), ..
} => format!("{under}.{origin}"),
origin => origin.to_string(),
})
.collect::<Vec<_>>()
.join("."),
)
}
pub(crate) fn segments(&self) -> Option<Vec<PathSegment>> {
let mut parts: Vec<Vec<PathSegment>> = Vec::new();
let mut key = Some(self);
while let Some(current) = key {
match ¤t.row.origin {
RowOrigin::Import { alias } => {
parts.push(vec![PathSegment::import(alias, ¤t.row.identity)])
}
RowOrigin::Written { instance, .. } => parts.push(instance.prefix.clone()),
RowOrigin::Numbered { .. } | RowOrigin::Inline { .. } => return None,
}
key = current.owner();
}
Some(parts.into_iter().rev().flatten().collect())
}
pub(crate) fn import_hops(&self) -> Vec<PathSegment> {
let mut hops = Vec::new();
let mut key = Some(self);
while let Some(current) = key {
let RowOrigin::Import { alias } = ¤t.row.origin else {
break;
};
hops.push(PathSegment::import(alias, ¤t.row.identity));
key = current.owner();
}
hops.reverse();
hops
}
pub(crate) fn is_planned(&self) -> bool {
let mut key = Some(self);
while let Some(current) = key {
if !matches!(
current.row.origin,
RowOrigin::Import { .. } | RowOrigin::Written { .. }
) {
return false;
}
key = current.owner();
}
true
}
}
impl fmt::Display for InstanceKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, origin) in self.naming_origins().iter().rev().enumerate() {
if index > 0 {
write!(f, ".")?;
}
write!(f, "{origin}")?;
}
Ok(())
}
}
impl PartialEq for InstanceKey {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.row, &other.row) && self.owner == other.owner
}
}
impl Eq for InstanceKey {}
impl std::hash::Hash for InstanceKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(Arc::as_ptr(&self.row) as usize).hash(state);
self.owner.hash(state);
}
}
#[derive(Debug, Clone)]
pub enum RowBinding {
Bound(OperationResult),
Deferred {
target: ReferenceTarget,
hops: u32,
},
Rows {
rows: Vec<Arc<RowInstance>>,
row_type: Arc<LemmaType>,
hops: u32,
},
Instance(Arc<RowInstance>),
}
impl RowBinding {
pub(crate) fn is_copy_of(&self, other: &RowBinding) -> bool {
match (self, other) {
(RowBinding::Bound(a), RowBinding::Bound(b)) => a == b,
(
RowBinding::Deferred { target, hops },
RowBinding::Deferred {
target: other_target,
hops: other_hops,
},
) => target == other_target && hops == other_hops,
(
RowBinding::Rows { rows, hops, .. },
RowBinding::Rows {
rows: other_rows,
hops: other_hops,
..
},
) => {
hops == other_hops
&& rows.len() == other_rows.len()
&& rows
.iter()
.zip(other_rows)
.all(|(row, other)| Arc::ptr_eq(row, other))
}
(RowBinding::Instance(row), RowBinding::Instance(other)) => Arc::ptr_eq(row, other),
_ => false,
}
}
pub(crate) fn rows_value(
rows: &[Arc<RowInstance>],
row_type: &Arc<LemmaType>,
owner: Option<&InstanceKey>,
) -> OperationResult {
OperationResult::from_literal(LiteralValue {
value: ValueKind::List(
rows.iter()
.map(|row| TypedLiteral {
value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
Arc::clone(row),
owner.cloned(),
))),
lemma_type: Arc::clone(row_type),
})
.collect(),
),
})
}
}
#[derive(Debug, Clone)]
pub struct RowInstance {
pub identity: Arc<SpecIdentity>,
pub origin: RowOrigin,
pub bindings: Box<[Option<RowBinding>]>,
}
impl RowInstance {
pub fn get_binding(
&self,
template: &crate::planning::execution_plan::ExecutionPlan,
path: &DataPath,
) -> Option<&RowBinding> {
let index = template.data.get_index_of(path)?;
self.bindings.get(index)?.as_ref()
}
pub fn structural_byte_size(&self) -> usize {
let mut size = 0;
let mut pending: Vec<&RowInstance> = vec![self];
while let Some(row) = pending.pop() {
for binding in row.bindings.iter().flatten() {
match binding {
RowBinding::Bound(OperationResult::Value(bound)) => {
size += bound.value.structural_byte_size();
}
RowBinding::Rows { rows, .. } => pending.extend(rows.iter().map(Arc::as_ref)),
RowBinding::Instance(row) => pending.push(row),
RowBinding::Bound(OperationResult::Veto(_)) | RowBinding::Deferred { .. } => {}
}
}
}
size
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RowOrigin {
Numbered {
list: String,
number: usize,
under: Option<Arc<str>>,
},
Written {
instance: SpecInstance,
source: Source,
},
Import { alias: Arc<str> },
Inline { node: NormalFormId, position: usize },
}
#[must_use]
pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
format!("{list_key}.{number}")
}
#[must_use]
pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
format!("{}.{field_key}", numbered_row_origin(list_key, number))
}
impl RowOrigin {
pub(crate) fn list_position(&self) -> (usize, bool) {
match self {
RowOrigin::Numbered { number, .. } => (*number, false),
RowOrigin::Written { instance, .. } => (
instance
.prefix
.last()
.and_then(|last| last.row)
.expect("BUG: written row origin without a row hop"),
false,
),
RowOrigin::Inline { position, .. } => (*position, true),
RowOrigin::Import { alias } => {
unreachable!("BUG: the row of `uses {alias}` is not a list row")
}
}
}
pub(crate) fn is_inline(&self) -> bool {
matches!(self, RowOrigin::Inline { .. })
}
}
impl fmt::Display for RowOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RowOrigin::Numbered { list, number, .. } => {
write!(f, "{}", numbered_row_origin(list, *number))
}
RowOrigin::Written { instance, .. } => write!(f, "{instance}"),
RowOrigin::Import { alias } => write!(f, "{alias}"),
RowOrigin::Inline { position, .. } => write!(f, "appended row {position}"),
}
}
}
#[must_use]
pub fn row_literal(
identity: Arc<SpecIdentity>,
origin: RowOrigin,
bindings: Vec<Option<RowBinding>>,
lemma_type: Arc<LemmaType>,
) -> TypedLiteral {
TypedLiteral {
value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
Arc::new(RowInstance {
identity,
origin,
bindings: bindings.into(),
}),
None,
))),
lemma_type,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RowEnd {
Data(DataPath),
Rule(RulePath),
}
impl fmt::Display for RowEnd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RowEnd::Data(path) => write!(f, "{}", path.input_key()),
RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DataPath {
pub segments: Vec<PathSegment>,
pub data: String,
}
impl DataPath {
pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
Self { segments, data }
}
pub fn local(data: String) -> Self {
Self {
segments: vec![],
data,
}
}
pub fn input_key(&self) -> String {
let mut s = String::new();
for segment in &self.segments {
s.push_str(&segment.input_key());
s.push('.');
}
s.push_str(&self.data);
s
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RulePath {
pub segments: Vec<PathSegment>,
pub rule: String,
}
impl RulePath {
pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
Self { segments, rule }
}
pub fn input_key(&self) -> String {
if self.segments.is_empty() {
return self.rule.clone();
}
let mut s = String::new();
for segment in &self.segments {
s.push_str(&segment.input_key());
s.push('.');
}
s.push_str(&self.rule);
s
}
}
pub type Expression = ExprTree<ExpressionKind>;
#[derive(Debug, Clone, PartialEq)]
pub enum ExpressionKind {
Literal(Box<TypedLiteral>),
DataPath(DataPath),
RulePath(RulePath),
Logical {
connective: LogicalConnective,
first: ExprId,
steps: Vec<ChainStep<()>>,
},
Arithmetic {
first: ExprId,
steps: Vec<ChainStep<ArithmeticComputation>>,
},
Comparison(ExprId, ComparisonComputation, ExprId),
UnitConversion(ExprId, SemanticConversionTarget),
LogicalNegation(ExprId, NegationType),
MathematicalComputation(MathematicalComputation, ExprId),
StepRounding {
operand: ExprId,
mode: RoundingMode,
step: Option<ExprId>,
},
Veto(VetoExpression),
Now,
DateRelative(DateRelativeKind, ExprId),
DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
RangeLiteral(ExprId, ExprId),
PastFutureRange(DateRelativeKind, ExprId),
RangeContainment(ExprId, ExprId),
RangeBound(RangeBound, ExprId),
ResultIsVeto(ExprId),
SpecMember {
base: ExprId,
path: Vec<String>,
name: String,
},
RowField {
level: u32,
identity: SpecIdentity,
end: RowEnd,
qualifier: Option<Vec<String>>,
},
RowValue {
level: u32,
list: ExprId,
},
Piecewise(Vec<(ExprId, ExprId)>),
ListCommand {
list: ExprId,
op: ListOp,
row_level: Option<u32>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum ListOp {
Find {
keys: Vec<ExprId>,
},
First,
Last,
Count,
Reduce {
kind: ListReduce,
argument: Option<ExprId>,
},
Predicate {
kind: ListPredKind,
form: crate::parsing::ast::ListPredicateForm,
expr: ExprId,
},
Sort {
descending: bool,
argument: Option<ExprId>,
},
Top {
n: ExprId,
},
With {
rows: Vec<ExprId>,
},
WithRow {
identity: SpecIdentity,
fields: Vec<(DataPath, ExprId)>,
},
Key {
fields: Vec<DataPath>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ListReduce {
Sum,
Avg,
Product,
Min,
Max,
}
impl ListReduce {
pub fn keyword(self) -> &'static str {
match self {
ListReduce::Sum => "sum",
ListReduce::Avg => "avg",
ListReduce::Product => "product",
ListReduce::Min => "min",
ListReduce::Max => "max",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ListPredKind {
Any,
All,
Where,
}
impl ListPredKind {
pub fn keyword(self) -> &'static str {
match self {
ListPredKind::Any => "any",
ListPredKind::All => "all",
ListPredKind::Where => "where",
}
}
}
impl ExprKind for ExpressionKind {
fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
match self {
ExpressionKind::Literal(_)
| ExpressionKind::DataPath(_)
| ExpressionKind::RulePath(_)
| ExpressionKind::Veto(_)
| ExpressionKind::Now
| ExpressionKind::RowField { .. }
| ExpressionKind::RowValue { .. } => {}
ExpressionKind::Logical { first, steps, .. } => {
f(*first);
steps.iter().for_each(|step| f(step.operand));
}
ExpressionKind::Arithmetic { first, steps } => {
f(*first);
steps.iter().for_each(|step| f(step.operand));
}
ExpressionKind::Comparison(left, _, right)
| ExpressionKind::RangeLiteral(left, right)
| ExpressionKind::RangeContainment(left, right) => {
f(*left);
f(*right);
}
ExpressionKind::StepRounding { operand, step, .. } => {
f(*operand);
if let Some(step) = step {
f(*step);
}
}
ExpressionKind::UnitConversion(inner, _)
| ExpressionKind::LogicalNegation(inner, _)
| ExpressionKind::MathematicalComputation(_, inner)
| ExpressionKind::DateRelative(_, inner)
| ExpressionKind::DateCalendar(_, _, inner)
| ExpressionKind::PastFutureRange(_, inner)
| ExpressionKind::RangeBound(_, inner)
| ExpressionKind::ResultIsVeto(inner)
| ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
f(*condition);
f(*result);
}),
ExpressionKind::ListCommand { list, op, .. } => {
f(*list);
op.for_each_child(f);
}
}
}
fn children_mut(&mut self) -> Vec<&mut ExprId> {
match self {
ExpressionKind::Literal(_)
| ExpressionKind::DataPath(_)
| ExpressionKind::RulePath(_)
| ExpressionKind::Veto(_)
| ExpressionKind::Now
| ExpressionKind::RowField { .. }
| ExpressionKind::RowValue { .. } => Vec::new(),
ExpressionKind::Logical { first, steps, .. } => std::iter::once(first)
.chain(steps.iter_mut().map(|step| &mut step.operand))
.collect(),
ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
.chain(steps.iter_mut().map(|step| &mut step.operand))
.collect(),
ExpressionKind::Comparison(left, _, right)
| ExpressionKind::RangeLiteral(left, right)
| ExpressionKind::RangeContainment(left, right) => vec![left, right],
ExpressionKind::StepRounding { operand, step, .. } => {
let mut children = vec![operand];
if let Some(step) = step {
children.push(step);
}
children
}
ExpressionKind::UnitConversion(inner, _)
| ExpressionKind::LogicalNegation(inner, _)
| ExpressionKind::MathematicalComputation(_, inner)
| ExpressionKind::DateRelative(_, inner)
| ExpressionKind::DateCalendar(_, _, inner)
| ExpressionKind::PastFutureRange(_, inner)
| ExpressionKind::RangeBound(_, inner)
| ExpressionKind::ResultIsVeto(inner) => vec![inner],
ExpressionKind::SpecMember { base, .. } => vec![base],
ExpressionKind::Piecewise(arms) => arms
.iter_mut()
.flat_map(|(condition, result)| [condition, result])
.collect(),
ExpressionKind::ListCommand { list, op, .. } => {
let mut children = vec![list];
children.extend(op.children_mut());
children
}
}
}
}
impl ListOp {
fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
match self {
ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => {}
ListOp::Find { keys } => keys.iter().copied().for_each(f),
ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
argument.iter().copied().for_each(f)
}
ListOp::Predicate { expr, .. } => f(*expr),
ListOp::Top { n } => f(*n),
ListOp::With { rows } => rows.iter().copied().for_each(f),
ListOp::WithRow { fields, .. } => fields.iter().for_each(|(_, value)| f(*value)),
}
}
fn children_mut(&mut self) -> Vec<&mut ExprId> {
match self {
ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => Vec::new(),
ListOp::Find { keys } => keys.iter_mut().collect(),
ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
argument.iter_mut().collect()
}
ListOp::Predicate { expr, .. } => vec![expr],
ListOp::Top { n } => vec![n],
ListOp::With { rows } => rows.iter_mut().collect(),
ListOp::WithRow { fields, .. } => fields.iter_mut().map(|(_, value)| value).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum TypeDefiningSpec {
Local,
Import,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct TypeFamily {
pub repository: Option<String>,
pub spec: String,
pub effective: crate::parsing::ast::EffectiveDate,
pub name: String,
}
impl TypeFamily {
#[must_use]
pub(crate) fn declared_in(spec: &SpecVersionKey, name: &str) -> Self {
Self {
repository: spec.repository.clone(),
spec: spec.spec.clone(),
effective: spec.effective.clone(),
name: name.to_string(),
}
}
#[must_use]
pub(crate) fn is_declared_in(&self, spec: &SpecVersionKey) -> bool {
self.repository == spec.repository
&& self.spec == spec.spec
&& self.effective == spec.effective
}
#[must_use]
pub fn without_effective(&self) -> Self {
Self {
repository: self.repository.clone(),
spec: self.spec.clone(),
effective: crate::parsing::ast::EffectiveDate::Origin,
name: self.name.clone(),
}
}
}
impl fmt::Display for TypeFamily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(repository) = &self.repository {
write!(f, "{repository} ")?;
}
write!(f, "{}", self.spec)?;
if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
write!(f, " {at}")?;
}
write!(f, ".{}", self.name)
}
}
#[derive(Clone, Debug)]
pub enum TypeExtends {
Primitive,
Custom {
parent: String,
family: TypeFamily,
defining_spec: TypeDefiningSpec,
},
}
impl PartialEq for TypeExtends {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(TypeExtends::Primitive, TypeExtends::Primitive) => true,
(
TypeExtends::Custom {
parent: lp,
family: lf,
defining_spec: ld,
},
TypeExtends::Custom {
parent: rp,
family: rf,
defining_spec: rd,
},
) => lp == rp && lf == rf && ld == rd,
_ => false,
}
}
}
impl Eq for TypeExtends {}
impl std::hash::Hash for TypeExtends {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
TypeExtends::Primitive => {
0u8.hash(state);
}
TypeExtends::Custom {
parent,
family,
defining_spec,
} => {
1u8.hash(state);
parent.hash(state);
family.hash(state);
defining_spec.hash(state);
}
}
}
}
impl TypeExtends {
#[must_use]
pub fn custom_local(parent: String, family: TypeFamily) -> Self {
TypeExtends::Custom {
parent,
family,
defining_spec: TypeDefiningSpec::Local,
}
}
#[must_use]
pub fn parent_name(&self) -> Option<&str> {
match self {
TypeExtends::Primitive => None,
TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
}
}
#[must_use]
fn with_families_without_effective(&self) -> Self {
match self {
TypeExtends::Primitive => TypeExtends::Primitive,
TypeExtends::Custom {
parent,
family,
defining_spec,
} => TypeExtends::Custom {
parent: parent.clone(),
family: family.without_effective(),
defining_spec: defining_spec.clone(),
},
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LemmaType {
pub name: Option<String>,
pub specifications: TypeSpecification,
pub extends: TypeExtends,
pub(crate) options: TypeOptions,
}
impl LemmaType {
pub fn map_measure<F>(self, f: F) -> Self
where
F: FnOnce(
MeasureUnits,
Option<BaseMeasureVector>,
) -> (MeasureUnits, Option<BaseMeasureVector>),
{
let LemmaType {
name,
specifications,
extends,
options,
} = self;
let specifications = match specifications {
TypeSpecification::Measure {
minimum,
maximum,
units,
traits,
decomposition,
unit,
help,
} => {
let (units, decomposition) = f(units, decomposition);
TypeSpecification::Measure {
minimum,
maximum,
units,
traits,
decomposition,
unit,
help,
}
}
other => other,
};
LemmaType {
name,
specifications,
extends,
options,
}
}
pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
Self {
name: Some(name),
specifications,
extends,
options: TypeOptions::default(),
}
}
pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
Self {
name: None,
specifications,
extends,
options: TypeOptions::default(),
}
}
pub fn primitive(specifications: TypeSpecification) -> Self {
Self {
name: None,
specifications,
extends: TypeExtends::Primitive,
options: TypeOptions::default(),
}
}
pub fn apply_constraint(
&mut self,
names: ConstraintNames<'_>,
command: TypeConstraintCommand,
args: &[CommandArg],
declared_suggestion: &mut Option<RawSuggestion>,
declared_value: &mut Option<RawSuggestion>,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<(), String> {
self.specifications.apply_constraint(
names,
command,
args,
declared_suggestion,
declared_value,
resolve_unit,
)
}
pub(crate) fn reject_disallowed(
&self,
value: &ValueKind,
runtime_scales: Option<
&std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
>,
) -> Result<(), String> {
let options = self.options_for_check();
if options.rule.is_some() || options.values.is_empty() {
return Ok(());
}
if options_have_unscaled_measure(&options) {
let Some(scales) = runtime_scales else {
return Ok(());
};
return if static_option_match_with_scales(&options, value, scales)? {
Ok(())
} else {
Err(disallowed_message(self, &options, value, None))
};
}
if static_option_match(&options, value) {
return Ok(());
}
Err(disallowed_message(self, &options, value, None))
}
#[must_use]
pub fn options_rule(&self) -> Option<&str> {
if let Some(name) = &self.options.rule {
return Some(name.as_str());
}
if let TypeSpecification::List { element, .. } = &self.specifications {
if let Some(name) = &element.options.rule {
return Some(name.as_str());
}
}
None
}
#[must_use]
pub(crate) fn has_unscaled_options(&self) -> bool {
options_have_unscaled_measure(&self.options_for_check())
}
#[must_use]
pub(crate) fn needs_runtime_option_check(&self) -> bool {
self.options_rule().is_some() || self.has_unscaled_options()
}
#[must_use]
pub fn static_options(&self) -> Vec<String> {
let options = self.options_for_check();
options
.values
.iter()
.map(|option| self.display_bound(&option.value))
.collect()
}
#[must_use]
pub fn inline_option_values(&self) -> Vec<BoundValueKind> {
self.options_for_check()
.values
.iter()
.map(|option| option.value.clone())
.collect()
}
pub(crate) fn display_bound(&self, bound: &BoundValueKind) -> String {
typed_literal_from_bound(bound, &Arc::new(self.clone())).display_value()
}
pub(crate) fn options_for_check(&self) -> std::borrow::Cow<'_, TypeOptions> {
match &self.specifications {
TypeSpecification::List { element, .. }
if !self.options.is_empty() && !element.options.is_empty() =>
{
let mut options = self.options.clone();
options
.values
.extend(element.options.values.iter().cloned());
if options.rule.is_none() {
options.rule.clone_from(&element.options.rule);
}
std::borrow::Cow::Owned(options)
}
TypeSpecification::List { element, .. } if !element.options.is_empty() => {
std::borrow::Cow::Borrowed(&element.options)
}
_ => std::borrow::Cow::Borrowed(&self.options),
}
}
pub(crate) fn explain_disallowed(
&self,
value: &ValueKind,
rule_rows: Option<&ListValue>,
runtime_scales: Option<
&std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
>,
) -> Option<String> {
let options = self.options_for_check();
let needs_scales = options_have_unscaled_measure(&options)
|| rule_rows
.is_some_and(|rows| rows.iter().any(|row| row.value.has_unscaled_measure()))
|| value.has_unscaled_measure();
if needs_scales && runtime_scales.is_none() {
return None;
}
let allowed = |candidate: &ValueKind| -> Result<bool, String> {
if let Some(scales) = runtime_scales {
if options_have_unscaled_measure(&options) {
if static_option_match_with_scales(&options, candidate, scales)? {
return Ok(true);
}
} else if static_option_match(&options, candidate) {
return Ok(true);
}
if let Some(rows) = rule_rows {
let settled_candidate = settle_value_kind_with_scales(candidate, scales)?;
for option in rows.iter() {
let settled_option = settle_value_kind_with_scales(&option.value, scales)?;
if option_values_match(&settled_option, &settled_candidate) {
return Ok(true);
}
}
return Ok(false);
}
return Ok(options.is_empty());
}
if static_option_match(&options, candidate) {
return Ok(true);
}
if let Some(rows) = rule_rows {
return Ok(rows
.iter()
.any(|option| option_values_match(&option.value, candidate)));
}
Ok(options.is_empty())
};
match (&self.specifications, value) {
(TypeSpecification::List { .. }, ValueKind::List(rows)) => {
for row in rows.iter() {
match allowed(&row.value) {
Ok(true) => {}
Ok(false) => {
return Some(disallowed_message(self, &options, &row.value, rule_rows));
}
Err(message) => return Some(message),
}
}
None
}
_ => match allowed(value) {
Ok(true) => None,
Ok(false) => Some(disallowed_message(self, &options, value, rule_rows)),
Err(message) => Some(message),
},
}
}
#[must_use]
pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
let unit_name = unit_name.into();
match &mut self.specifications {
TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
*unit = Some(unit_name);
}
other => panic!(
"BUG: with_unit called on non-measure/non-ratio type {}",
other
),
}
self
}
#[must_use]
pub(crate) fn reanchor_through_import_path(
lemma_type: Arc<LemmaType>,
base_alias: &str,
base_identity: &SpecIdentity,
member_path: &[String],
mut nested_import: impl FnMut(&SpecIdentity, &str) -> Option<SpecIdentity>,
) -> Arc<LemmaType> {
let mut hops = vec![PathSegment::import(base_alias, base_identity)];
let mut identity = base_identity.clone();
for segment in member_path {
let Some(next) = nested_import(&identity, segment) else {
break;
};
hops.push(PathSegment::import(segment, &next));
identity = next;
}
let mut out = lemma_type.as_ref().clone();
for hop in hops.into_iter().rev() {
out = out.with_scale_import_hop(&hop);
}
Arc::new(out)
}
#[must_use]
pub(crate) fn with_scale_import_hop(&self, segment: &PathSegment) -> Self {
fn prefix_units(units: &mut MeasureUnits, segment: &PathSegment) {
for unit in units.0.iter_mut() {
if let crate::literals::UnitScale::Reference { target, .. } = &mut unit.scale {
match target {
ReferenceTarget::Data(path) => path.segments.insert(0, segment.clone()),
ReferenceTarget::Rule(rule) => rule.segments.insert(0, segment.clone()),
}
}
}
}
fn prefix_specifications(specifications: &mut TypeSpecification, segment: &PathSegment) {
match specifications {
TypeSpecification::Measure { units, .. }
| TypeSpecification::MeasureRange { units, .. } => {
prefix_units(units, segment);
}
TypeSpecification::List { element, .. } => {
let prefixed = element.with_scale_import_hop(segment);
*element = Arc::new(prefixed);
}
_ => {}
}
}
let mut out = self.clone();
prefix_specifications(&mut out.specifications, segment);
out
}
#[must_use]
pub(crate) fn unit_scale_target(&self, unit_name: &str) -> Option<&ReferenceTarget> {
self.measure_units_table()
.and_then(|units| units.get(unit_name).ok())
.and_then(MeasureUnit::resolved_scale_target)
}
#[must_use]
pub(crate) fn dynamic_scale_targets(&self) -> Vec<ReferenceTarget> {
let Some(units) = self.measure_units_table() else {
return Vec::new();
};
units
.iter()
.filter_map(|unit| unit.resolved_scale_target().cloned())
.collect()
}
#[must_use]
pub fn unit(&self) -> Option<&str> {
match &self.specifications {
TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
unit.as_deref()
}
_ => None,
}
}
pub fn name(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| self.specifications.to_string())
}
pub fn is_boolean(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Boolean { .. })
}
pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
matches!(
(kind, &self.specifications),
(PrimitiveKind::Number, TypeSpecification::Number { .. })
| (PrimitiveKind::Text, TypeSpecification::Text { .. })
| (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
| (PrimitiveKind::Date, TypeSpecification::Date { .. })
| (PrimitiveKind::Time, TypeSpecification::Time { .. })
| (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
| (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
)
}
pub fn is_measure(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Measure { .. })
}
pub fn is_measure_range(&self) -> bool {
matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
}
pub fn is_number(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Number { .. })
}
pub fn is_number_range(&self) -> bool {
matches!(&self.specifications, TypeSpecification::NumberRange { .. })
}
pub fn is_numeric(&self) -> bool {
matches!(
&self.specifications,
TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
)
}
pub fn is_text(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Text { .. })
}
pub fn is_date(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Date { .. })
}
pub fn is_date_range(&self) -> bool {
matches!(&self.specifications, TypeSpecification::DateRange { .. })
}
pub fn is_time_range(&self) -> bool {
matches!(&self.specifications, TypeSpecification::TimeRange { .. })
}
pub fn is_time(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Time { .. })
}
pub fn has_trait_duration(&self) -> bool {
match &self.specifications {
TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
_ => false,
}
}
pub fn is_duration_like_measure(&self) -> bool {
if !self.is_measure() {
return false;
}
if self.has_trait_duration() {
return true;
}
self.is_anonymous_measure()
&& self
.measure_type_decomposition()
.is_some_and(|d| *d == duration_decomposition())
}
pub fn is_duration_like(&self) -> bool {
self.is_duration_like_measure()
}
pub fn has_trait_calendar(&self) -> bool {
match &self.specifications {
TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
_ => false,
}
}
pub fn is_calendar_like_measure(&self) -> bool {
if !self.is_measure() {
return false;
}
if self.has_trait_calendar() {
return true;
}
self.is_anonymous_measure()
&& self
.measure_type_decomposition()
.is_some_and(|d| *d == calendar_decomposition())
}
pub fn is_calendar_like(&self) -> bool {
self.is_calendar_like_measure()
}
pub fn is_ratio(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Ratio { .. })
}
pub fn is_ratio_range(&self) -> bool {
matches!(&self.specifications, TypeSpecification::RatioRange { .. })
}
pub fn is_calendar_measure_range(&self) -> bool {
matches!(
&self.specifications,
TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
if *decomposition == calendar_decomposition()
)
}
pub fn is_calendar_like_range(&self) -> bool {
self.is_calendar_measure_range()
}
pub fn is_range(&self) -> bool {
matches!(
&self.specifications,
TypeSpecification::DateRange { .. }
| TypeSpecification::TimeRange { .. }
| TypeSpecification::NumberRange { .. }
| TypeSpecification::MeasureRange { .. }
| TypeSpecification::RatioRange { .. }
)
}
pub fn is_spec(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Spec { .. })
}
pub fn is_list(&self) -> bool {
matches!(&self.specifications, TypeSpecification::List { .. })
}
pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
match &self.specifications {
TypeSpecification::List { element, .. } => Some(&element.specifications),
_ => None,
}
}
pub fn list_of(element: Arc<LemmaType>) -> Self {
Self {
name: Some(format!("{} list", element.name())),
specifications: TypeSpecification::List {
element,
key: ListKey::Numbered,
help: String::new(),
},
extends: TypeExtends::Primitive,
options: TypeOptions::default(),
}
}
pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
let TypeSpecification::List { element, .. } = &self.specifications else {
panic!(
"BUG: list_element_lemma_type on non-list type {}",
self.name()
);
};
element
}
#[must_use]
pub fn spec_identity(&self) -> Option<&SpecIdentity> {
match &self.specifications {
TypeSpecification::Spec { spec } => Some(spec),
_ => None,
}
}
pub fn vetoed(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Veto { .. })
}
pub fn is_undetermined(&self) -> bool {
matches!(&self.specifications, TypeSpecification::Undetermined)
}
pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
use TypeSpecification::*;
matches!(
(&self.specifications, &other.specifications),
(Boolean { .. }, Boolean { .. })
| (Number { .. }, Number { .. })
| (NumberRange { .. }, NumberRange { .. })
| (Measure { .. }, Measure { .. })
| (MeasureRange { .. }, MeasureRange { .. })
| (Text { .. }, Text { .. })
| (Date { .. }, Date { .. })
| (DateRange { .. }, DateRange { .. })
| (Time { .. }, Time { .. })
| (TimeRange { .. }, TimeRange { .. })
| (Ratio { .. }, Ratio { .. })
| (RatioRange { .. }, RatioRange { .. })
| (Veto { .. }, Veto { .. })
| (Undetermined, Undetermined)
| (Spec { .. }, Spec { .. })
| (List { .. }, List { .. })
)
}
#[must_use]
pub fn family(&self) -> Option<&TypeFamily> {
match &self.extends {
TypeExtends::Custom { family, .. } => Some(family),
TypeExtends::Primitive => None,
}
}
#[must_use]
pub fn range_element_type(&self) -> Arc<LemmaType> {
let element_spec = self
.specifications
.element_from_range()
.expect("BUG: range_element_type requires a range specification");
let name = match &self.extends {
TypeExtends::Custom { family, .. } => Some(family.name.clone()),
TypeExtends::Primitive => self.name.clone(),
};
Arc::new(LemmaType {
name,
specifications: element_spec,
extends: self.extends.clone(),
options: TypeOptions::default(),
})
}
#[must_use]
pub fn measure_family(&self) -> Option<&TypeFamily> {
(self.is_measure() || self.is_measure_range())
.then(|| self.family())
.flatten()
}
#[must_use]
pub(crate) fn held_value_type(&self) -> &LemmaType {
let mut current = self;
while current.is_list() {
current = current.list_element_lemma_type().as_ref();
}
current
}
#[must_use]
pub(crate) fn held_measure_family(&self) -> Option<&TypeFamily> {
self.held_value_type().measure_family()
}
#[must_use]
pub fn ratio_family(&self) -> Option<&TypeFamily> {
(self.is_ratio() || self.is_ratio_range())
.then(|| self.family())
.flatten()
}
#[must_use]
pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
self.measure_family().or_else(|| self.ratio_family())
}
#[must_use]
pub fn measure_family_name(&self) -> Option<&str> {
self.measure_family().map(|family| family.name.as_str())
}
#[must_use]
pub fn ratio_family_name(&self) -> Option<&str> {
self.ratio_family().map(|family| family.name.as_str())
}
#[must_use]
pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
if !self.has_same_base_type(other) {
return false;
}
if self.is_measure() {
return match (self.measure_family(), other.measure_family()) {
(Some(_), Some(_)) => self.same_measure_family(other),
(None, None) => {
self.measure_type_decomposition() == other.measure_type_decomposition()
}
_ => false,
};
}
if let (
TypeSpecification::Spec { spec: self_spec },
TypeSpecification::Spec { spec: other_spec },
) = (&self.specifications, &other.specifications)
{
return self_spec == other_spec;
}
if let (
TypeSpecification::List {
element: self_element,
key: self_key,
..
},
TypeSpecification::List {
element: other_element,
key: other_key,
..
},
) = (&self.specifications, &other.specifications)
{
return self_key == other_key && self_element.same_value_type(other_element);
}
if self.is_measure_range() {
match (self.family(), other.family()) {
(Some(self_family), Some(other_family)) => self_family == other_family,
(None, None) => {
let self_element = self
.specifications
.element_from_range()
.expect("BUG: MeasureRange always defines element_from_range");
let other_element = other
.specifications
.element_from_range()
.expect("BUG: MeasureRange always defines element_from_range");
match (&self_element, &other_element) {
(
TypeSpecification::Measure {
decomposition: self_decomp,
..
},
TypeSpecification::Measure {
decomposition: other_decomp,
..
},
) => self_decomp == other_decomp,
_ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
}
}
_ => false,
}
} else {
true
}
}
#[must_use]
pub(crate) fn without_written_unit(&self) -> Self {
let mut cleared = self.clone();
match &mut cleared.specifications {
TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
*unit = None;
}
_ => {}
}
cleared
}
#[must_use]
pub(crate) fn without_slot_bounds(&self) -> Self {
let mut cleared = self.without_written_unit();
match &mut cleared.specifications {
TypeSpecification::Measure {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::Ratio {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::Number {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::Date {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::Time {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::MeasureRange {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::DateRange {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::TimeRange {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::NumberRange {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::RatioRange {
minimum,
maximum,
help,
..
} => {
*minimum = None;
*maximum = None;
*help = String::new();
}
TypeSpecification::Boolean { help, .. } | TypeSpecification::Text { help, .. } => {
*help = String::new();
}
TypeSpecification::Veto { .. }
| TypeSpecification::Undetermined
| TypeSpecification::Spec { .. }
| TypeSpecification::List { .. } => {}
}
cleared
}
#[must_use]
pub fn same_measure_family(&self, other: &LemmaType) -> bool {
if !self.is_measure() || !other.is_measure() {
return false;
}
match (self.measure_family(), other.measure_family()) {
(Some(self_family), Some(other_family)) => self_family == other_family,
_ => false,
}
}
#[must_use]
pub fn same_interface(&self, other: &Self) -> bool {
self.with_families_without_effective() == other.with_families_without_effective()
}
#[must_use]
pub fn same_interface_ignoring_options(&self, other: &Self) -> bool {
let mut left = self.with_families_without_effective();
let mut right = other.with_families_without_effective();
left.options = TypeOptions::default();
right.options = TypeOptions::default();
left == right
}
#[must_use]
pub fn with_families_without_effective(&self) -> Self {
Self {
name: self.name.clone(),
specifications: self.specifications.with_families_without_effective(),
extends: self.extends.with_families_without_effective(),
options: self.options.clone(),
}
}
#[must_use]
pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
if !self.is_measure() || !other.is_measure() {
return false;
}
if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
return false;
}
match (
self.measure_type_decomposition(),
other.measure_type_decomposition(),
) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
pub fn veto_type() -> Self {
Self::primitive(TypeSpecification::veto())
}
pub fn undetermined_type() -> Self {
Self::primitive(TypeSpecification::Undetermined)
}
pub fn decimal_places(&self) -> Option<u8> {
match &self.specifications {
TypeSpecification::Number { decimals, .. } => *decimals,
_ => None,
}
}
pub fn try_rational_as_decimal(
&self,
magnitude: &crate::computation::rational::RationalInteger,
) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
let decimal = magnitude.try_to_decimal()?;
Ok(format_decimal_for_api(decimal))
}
pub fn try_measure_canonical_as_decimal_in_unit(
&self,
canonical_magnitude: &crate::computation::rational::RationalInteger,
unit_name: &str,
) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
use crate::computation::rational::checked_div;
let unit_factor = self.measure_unit_factor(unit_name);
let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
self.try_rational_as_decimal(&magnitude_in_unit)
}
pub fn try_ratio_canonical_as_decimal_in_unit(
&self,
canonical_magnitude: &crate::computation::rational::RationalInteger,
unit_name: &str,
) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
use crate::computation::rational::checked_mul;
let units = match &self.specifications {
TypeSpecification::Ratio { units, .. } => units,
_ => unreachable!(
"BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
self.name()
),
};
let ratio_unit = units
.iter()
.find(|unit| unit.name == unit_name)
.unwrap_or_else(|| {
let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
unreachable!(
"BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
unit_name,
self.name(),
valid.join(", ")
)
});
let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
self.try_rational_as_decimal(&magnitude_in_unit)
}
pub fn example_value(&self) -> &'static str {
match &self.specifications {
TypeSpecification::Text { .. } => "\"hello world\"",
TypeSpecification::Measure { .. } => "12.50 eur",
TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
TypeSpecification::Number { .. } => "3.14",
TypeSpecification::NumberRange { .. } => "0...100",
TypeSpecification::Boolean { .. } => "true",
TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
TypeSpecification::TimeRange { .. } => "09:00...17:00",
TypeSpecification::Veto { .. } => "veto",
TypeSpecification::Time { .. } => "14:30:00",
TypeSpecification::Ratio { .. } => "50%",
TypeSpecification::RatioRange { .. } => "10%...50%",
TypeSpecification::Undetermined => unreachable!(
"BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
),
TypeSpecification::Spec { .. } => unreachable!(
"BUG: example_value called on a spec type; spec instances are not prompted data"
),
TypeSpecification::List { element, .. } => element.example_value(),
}
}
#[must_use]
pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
match &self.specifications {
TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
_ => unreachable!(
"BUG: measure_type_decomposition called on non-measure type {}",
self.name()
),
}
}
#[must_use]
pub(crate) fn is_simple_base_measure(&self) -> bool {
if !self.is_measure() {
return false;
}
match self.measure_type_decomposition() {
Some(decomposition) if decomposition.len() == 1 => {
decomposition.values().next() == Some(&1)
}
_ => false,
}
}
#[must_use]
pub fn measure_units_table(&self) -> Option<&MeasureUnits> {
match &self.specifications {
TypeSpecification::Measure { units, .. }
| TypeSpecification::MeasureRange { units, .. } => Some(units),
_ => None,
}
}
#[must_use]
pub fn has_dynamic_units(&self) -> bool {
if self
.measure_units_table()
.is_some_and(|units| units.iter().any(MeasureUnit::is_dynamic))
{
return true;
}
match &self.specifications {
TypeSpecification::List { element, .. } => element.has_dynamic_units(),
_ => false,
}
}
#[must_use]
pub fn unit_is_dynamic(&self, unit_name: &str) -> bool {
self.measure_units_table()
.and_then(|units| units.get(unit_name).ok())
.is_some_and(MeasureUnit::is_dynamic)
}
#[must_use]
pub(crate) fn measure_display_unit_name(
&self,
runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
) -> Option<&str> {
match &self.specifications {
TypeSpecification::Measure { units, .. } if !units.is_empty() => Some(
measure_display_unit(units, self, runtime_scales)
.name
.as_str(),
),
_ => None,
}
}
#[must_use]
pub fn measure_fallback_unit(&self) -> Option<&MeasureUnit> {
let units = self.measure_units_table()?;
units
.iter()
.find(|unit| unit.is_canonical_factor())
.or_else(|| units.iter().find(|unit| unit.literal_factor().is_some()))
.or_else(|| units.iter().next())
}
#[must_use]
pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
if let Some(binding) = self.unit() {
return vec![(binding.to_string(), 1)];
}
match &self.specifications {
TypeSpecification::Measure {
units,
decomposition,
..
} => {
if let Some(unit) = self.measure_fallback_unit() {
return vec![(unit.name.clone(), 1)];
}
if units.is_empty() {
return base_measure_vector_as_signature(
decomposition
.as_ref()
.expect("BUG: measure type without units must carry a decomposition"),
);
}
unreachable!("BUG: non-empty measure unit table has no fallback unit");
}
TypeSpecification::MeasureRange {
units,
decomposition,
..
} => {
if let Some(unit) = self.measure_fallback_unit() {
return vec![(unit.name.clone(), 1)];
}
if units.is_empty() {
return base_measure_vector_as_signature(
decomposition
.as_ref()
.expect("BUG: measure type without units must carry a decomposition"),
);
}
unreachable!("BUG: non-empty measure-range unit table has no fallback unit");
}
_ => unreachable!(
"BUG: measure_runtime_signature called on non-measure type {}",
self.name()
),
}
}
#[must_use]
pub fn ratio_primary_unit(&self) -> Option<&str> {
match &self.specifications {
TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
units.iter().next().map(|unit| unit.name.as_str())
}
TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
units.iter().next().map(|unit| unit.name.as_str())
}
_ => None,
}
}
pub fn is_anonymous_measure(&self) -> bool {
self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
}
pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
Self {
name: None,
specifications: TypeSpecification::Measure {
minimum: None,
maximum: None,
units: crate::literals::MeasureUnits::new(),
traits: Vec::new(),
decomposition: Some(decomposition),
unit: None,
help: String::new(),
},
extends: TypeExtends::Primitive,
options: TypeOptions::default(),
}
}
#[must_use]
pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
match &self.specifications {
TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
Some(units.iter().map(|unit| unit.name.as_str()).collect())
}
TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
Some(units.iter().map(|unit| unit.name.as_str()).collect())
}
_ => None,
}
}
#[must_use]
pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
match &self.specifications {
TypeSpecification::Measure { units, .. } if !units.is_empty() => {
Some(units.iter().map(|unit| unit.name.as_str()).collect())
}
TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
Some(units.iter().map(|unit| unit.name.as_str()).collect())
}
_ => None,
}
}
#[must_use]
pub fn label_for_data_input(&self, input_key: &str) -> String {
let type_label = if let Some(parent) = self.extends.parent_name() {
parent.to_string()
} else {
let type_name = self.name();
if type_name == input_key {
self.specifications.to_string()
} else {
type_name
}
};
if type_label == input_key {
input_key.to_string()
} else {
format!("{input_key} [{type_label}]")
}
}
#[must_use]
pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
format!("Data {}: {}", self.label_for_data_input(input_key), detail)
}
#[must_use]
pub fn empty_runtime_input_vetoes(&self) -> bool {
!matches!(self.specifications, TypeSpecification::Text { .. })
}
pub fn measure_unit_factor(
&self,
unit_name: &str,
) -> &crate::computation::rational::RationalInteger {
let units = match &self.specifications {
TypeSpecification::Measure { units, .. } => units,
TypeSpecification::MeasureRange { units, .. } => units,
_ => unreachable!(
"BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
self.name()
),
};
match units.get(unit_name) {
Ok(unit) => unit.require_literal_factor(),
Err(_) => {
let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
unreachable!(
"BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
unit_name,
self.name(),
valid.join(", ")
);
}
}
}
pub fn ratio_unit_factor(
&self,
unit_name: &str,
) -> &crate::computation::rational::RationalInteger {
let units = match &self.specifications {
TypeSpecification::Ratio { units, .. } => units,
_ => unreachable!(
"BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
self.name()
),
};
match units.get(unit_name) {
Ok(RatioUnit { value, .. }) => value,
Err(_) => {
let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
unreachable!(
"BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
unit_name,
self.name(),
valid.join(", ")
);
}
}
}
pub(crate) fn measure_literal_unit_map(
&self,
literal: &LiteralValue,
unit_names: &[&str],
factor_source: UnitFactorSource<'_>,
) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
use crate::computation::rational::checked_div;
let ValueKind::Measure(magnitude) = &literal.value else {
panic!("BUG: measure_literal_unit_map called with non-measure value");
};
let mut map = BTreeMap::new();
for &unit_name in unit_names {
let Some(unit_factor) = factor_source.try_measure_unit_factor(unit_name) else {
continue;
};
let magnitude_in_unit = checked_div(magnitude, &unit_factor)
.map_err(LiteralUnitMapFailure::UnitConversion)?;
let decimal = self
.try_rational_as_decimal(&magnitude_in_unit)
.map_err(LiteralUnitMapFailure::Commit)?;
map.insert(unit_name.to_string(), decimal);
}
Ok(map)
}
pub(crate) fn ratio_literal_unit_map(
&self,
literal: &LiteralValue,
unit_names: &[&str],
factor_source: UnitFactorSource<'_>,
) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
use crate::computation::rational::checked_mul;
let ratio_api_type = match &self.specifications {
TypeSpecification::Ratio { .. } => self,
TypeSpecification::RatioRange { .. } => {
return ratio_element_type_for_api(self).ratio_literal_unit_map(
literal,
unit_names,
factor_source,
);
}
_ => {
panic!(
"BUG: ratio_literal_unit_map called with non-ratio type {}",
self.name()
);
}
};
let ValueKind::Ratio(canonical) = &literal.value else {
panic!("BUG: ratio_literal_unit_map called with non-ratio value");
};
if unit_names.is_empty() {
panic!(
"BUG: ratio literal type '{}' must have at least one unit name",
self.name()
);
}
let mut map = BTreeMap::new();
for &unit_name in unit_names {
let unit_factor = factor_source.ratio_unit_factor(unit_name);
let magnitude_in_unit = checked_mul(canonical, unit_factor)
.map_err(LiteralUnitMapFailure::UnitConversion)?;
let decimal = ratio_api_type
.try_rational_as_decimal(&magnitude_in_unit)
.map_err(LiteralUnitMapFailure::Commit)?;
map.insert(unit_name.to_string(), decimal);
}
Ok(map)
}
}
pub(crate) enum UnitFactorSource<'a> {
DeclaredOn(&'a LemmaType),
Merged {
measure: Option<&'a MeasureUnits>,
ratio: Option<&'a RatioUnits>,
},
Runtime {
scales:
&'a std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
fallback: &'a UnitFactorSource<'a>,
},
}
impl UnitFactorSource<'_> {
fn try_measure_unit_factor(
&self,
unit_name: &str,
) -> Option<crate::computation::rational::RationalInteger> {
match self {
UnitFactorSource::DeclaredOn(lemma_type) => {
let units = lemma_type.measure_units_table().unwrap_or_else(|| {
panic!(
"BUG: measure_unit_factor on non-measure type {}",
lemma_type.name()
)
});
let unit = units.get(unit_name).unwrap_or_else(|_| {
panic!(
"BUG: unknown unit '{unit_name}' for measure type {}",
lemma_type.name()
)
});
unit.literal_factor().cloned()
}
UnitFactorSource::Merged { measure, .. } => {
let units = (*measure)?;
let unit = units.get(unit_name).ok()?;
unit.literal_factor().cloned()
}
UnitFactorSource::Runtime { scales, fallback } => scales
.get(unit_name)
.cloned()
.or_else(|| fallback.try_measure_unit_factor(unit_name)),
}
}
fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
match self {
UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
UnitFactorSource::Merged { ratio, .. } => {
let units = ratio.unwrap_or_else(|| {
panic!(
"BUG: family ratio expansion missing merged table for unit '{unit_name}'"
)
});
&units
.get(unit_name)
.unwrap_or_else(|_| {
panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
})
.value
}
UnitFactorSource::Runtime { fallback, .. } => fallback.ratio_unit_factor(unit_name),
}
}
}
pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
match &lemma_type.specifications {
TypeSpecification::Ratio { .. } => lemma_type.clone(),
TypeSpecification::RatioRange { .. } => {
let element = range_element_type_specification(&lemma_type.specifications)
.expect("BUG: ratio range type must have ratio element specification");
let TypeSpecification::Ratio { units, .. } = element else {
panic!("BUG: ratio range element spec must be Ratio");
};
LemmaType::primitive(TypeSpecification::Ratio {
minimum: None,
maximum: None,
units,
unit: None,
help: String::new(),
})
}
_ => panic!(
"BUG: ratio_element_type_for_api called with non-ratio type {}",
lemma_type.name()
),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum LiteralUnitMapFailure {
Commit(crate::computation::rational::NumericFailure),
UnitConversion(crate::computation::rational::NumericFailure),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LiteralValue {
pub value: ValueKind,
}
impl LiteralValue {
#[inline]
pub fn new(value: ValueKind) -> Self {
Self { value }
}
fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
match signature {
[(unit_name, 1)] => Some(unit_name.as_str()),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TypedLiteral {
pub value: ValueKind,
pub lemma_type: Arc<LemmaType>,
}
impl TypedLiteral {
#[inline]
pub fn to_literal(&self) -> LiteralValue {
LiteralValue {
value: self.value.clone(),
}
}
#[inline]
pub fn into_literal(self) -> LiteralValue {
LiteralValue { value: self.value }
}
}
impl From<TypedLiteral> for LiteralValue {
fn from(typed: TypedLiteral) -> Self {
typed.into_literal()
}
}
impl From<&TypedLiteral> for LiteralValue {
fn from(typed: &TypedLiteral) -> Self {
typed.to_literal()
}
}
impl LiteralValue {
pub fn text(s: impl Into<Arc<str>>) -> Self {
Self {
value: ValueKind::Text(s.into()),
}
}
pub fn number(n: RationalInteger) -> Self {
Self {
value: ValueKind::Number(n),
}
}
pub fn number_from_decimal(decimal: Decimal) -> Self {
Self::number(
crate::literals::rational_from_parsed_decimal(decimal)
.expect("BUG: literal number from decimal must lift at boundary"),
)
}
pub fn measure(n: RationalInteger) -> Self {
Self {
value: ValueKind::Measure(n),
}
}
pub fn from_bool(b: bool) -> Self {
Self {
value: ValueKind::Boolean(b),
}
}
pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
Self::date(date_time_to_semantic(dt))
}
pub fn date(dt: SemanticDateTime) -> Self {
Self {
value: ValueKind::Date(dt),
}
}
pub fn time(t: SemanticTime) -> Self {
Self {
value: ValueKind::Time(t),
}
}
pub fn ratio(r: RationalInteger) -> Self {
Self {
value: ValueKind::Ratio(r),
}
}
pub fn ratio_from_decimal(r: Decimal) -> Self {
Self::ratio(
crate::literals::rational_from_parsed_decimal(r)
.expect("BUG: ratio literal from decimal must lift at boundary"),
)
}
pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
Self {
value: ValueKind::Range(Box::new(left), Box::new(right)),
}
}
pub fn display_value(&self) -> String {
match &self.value {
ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
format!("{}", self.value)
}
ValueKind::Range(left, right) => {
format!("{}...{}", left.display_value(), right.display_value())
}
ValueKind::List(rows) if rows.is_empty() => String::new(),
ValueKind::List(rows) => rows
.iter()
.map(|row| row.display_value())
.collect::<Vec<_>>()
.join(", "),
_ => format!("{}", self.value),
}
}
pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
self.display_value_with_scales(lemma_type, None)
}
pub fn display_value_with_scales(
&self,
lemma_type: &LemmaType,
runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
) -> String {
match &self.value {
ValueKind::Measure(n) => {
let signature = lemma_type.measure_runtime_signature();
format_measure_canonical_for_display(n, lemma_type, &signature, runtime_scales)
}
ValueKind::UnscaledMeasure { magnitude, unit } => {
format!(
"{} {unit}",
format_rational_magnitude_for_display(magnitude)
)
}
ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
ValueKind::Range(left, right) => {
format!(
"{}...{}",
left.to_literal()
.display_value_with_scales(&left.lemma_type, runtime_scales),
right
.to_literal()
.display_value_with_scales(&right.lemma_type, runtime_scales)
)
}
ValueKind::List(rows) if rows.is_empty() => lemma_type.specifications.to_string(),
ValueKind::List(rows) => rows
.iter()
.map(|row| {
row.to_literal()
.display_value_with_scales(&row.lemma_type, runtime_scales)
})
.collect::<Vec<_>>()
.join(", "),
_ => format!("{}", self.value),
}
}
pub fn byte_size(&self) -> usize {
self.value.structural_byte_size()
}
#[must_use]
pub fn magnitude_suggestion_for_decimal_prompt(
&self,
lemma_type: &LemmaType,
) -> Option<rust_decimal::Decimal> {
match &self.value {
ValueKind::Number(n) => Some(
lemma_type
.try_rational_as_decimal(n)
.expect("BUG: stored number literal must convert to decimal for prompt"),
),
ValueKind::Measure(n) => {
let signature = lemma_type.measure_runtime_signature();
let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
"BUG: measure prompt requires exactly one signature unit with exponent 1",
);
Some(
lemma_type
.try_measure_canonical_as_decimal_in_unit(n, unit_name)
.expect("BUG: stored measure literal must convert to decimal for prompt"),
)
}
ValueKind::Ratio(n) => {
if let Some(unit_name) = lemma_type.unit() {
Some(
lemma_type
.try_ratio_canonical_as_decimal_in_unit(n, unit_name)
.expect("BUG: stored ratio literal must convert to decimal for prompt"),
)
} else {
Some(lemma_type.try_rational_as_decimal(n).expect(
"BUG: stored bare ratio literal must convert to decimal for prompt",
))
}
}
_ => None,
}
}
#[must_use]
pub fn measure_units(
&self,
lemma_type: &LemmaType,
) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
if !matches!(self.value, ValueKind::Measure(_)) {
return None;
}
lemma_type.measure_unit_names()?;
let declared: Vec<&str> = lemma_type
.measure_unit_names()
.expect("BUG: measure_unit_names checked above");
lemma_type
.measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
.ok()
}
#[must_use]
pub fn ratio_units(
&self,
lemma_type: &LemmaType,
) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
if !matches!(self.value, ValueKind::Ratio(_)) {
return None;
}
let has_declared_units = match &lemma_type.specifications {
TypeSpecification::Ratio { units, .. } => !units.is_empty(),
TypeSpecification::RatioRange { .. } => true,
_ => return None,
};
if !has_declared_units {
return None;
}
let declared: Vec<&str> = lemma_type
.ratio_unit_names()
.expect("BUG: ratio units checked above");
lemma_type
.ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
.ok()
}
#[must_use]
pub fn magnitude_in_unit(
&self,
lemma_type: &LemmaType,
unit: &str,
) -> Option<rust_decimal::Decimal> {
self.measure_units(lemma_type)
.and_then(|map| map.get(unit).copied())
.or_else(|| {
self.ratio_units(lemma_type)
.and_then(|map| map.get(unit).copied())
})
}
pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
let ValueKind::Measure(magnitude) = &self.value else {
unreachable!(
"BUG: duration_canonical_seconds called with {:?}",
self.value
);
};
if !lemma_type.is_duration_like_measure() {
unreachable!(
"BUG: duration_canonical_seconds called with type {}",
lemma_type.name()
);
}
let factor = lemma_type.measure_unit_factor("second");
checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
}
pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
let ValueKind::Measure(magnitude) = &self.value else {
unreachable!(
"BUG: calendar_canonical_months called with {:?}",
self.value
);
};
if !lemma_type.is_calendar_like() {
unreachable!(
"BUG: calendar_canonical_months called with type {}",
lemma_type.name()
);
}
let factor = lemma_type.measure_unit_factor("month");
checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
}
}
impl TypedLiteral {
pub fn text(s: impl Into<Arc<str>>) -> Self {
Self {
value: ValueKind::Text(s.into()),
lemma_type: primitive_text_arc().clone(),
}
}
pub fn text_with_type(s: impl Into<Arc<str>>, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Text(s.into()),
lemma_type,
}
}
pub fn number(n: RationalInteger) -> Self {
Self {
value: ValueKind::Number(n),
lemma_type: primitive_number_arc().clone(),
}
}
pub fn number_from_decimal(decimal: Decimal) -> Self {
Self::number(
crate::literals::rational_from_parsed_decimal(decimal)
.expect("BUG: literal number from decimal must lift at boundary"),
)
}
pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Number(n),
lemma_type,
}
}
pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
Self::number_with_type(
crate::literals::rational_from_parsed_decimal(decimal)
.expect("BUG: literal number from decimal must lift at boundary"),
lemma_type,
)
}
pub fn measure_with_bound_unit(
n: RationalInteger,
unit_name: impl Into<String>,
lemma_type: Arc<LemmaType>,
) -> Self {
Self::measure_with_type(
n,
Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
)
}
pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Measure(n),
lemma_type,
}
}
pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Measure(n),
lemma_type,
}
}
pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
Self {
value: ValueKind::Measure(value),
lemma_type: Arc::new(anonymous_measure_type()),
}
}
pub fn from_bool(b: bool) -> Self {
Self {
value: ValueKind::Boolean(b),
lemma_type: primitive_boolean_arc().clone(),
}
}
pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
Self::date(date_time_to_semantic(dt))
}
pub fn date(dt: SemanticDateTime) -> Self {
Self {
value: ValueKind::Date(dt),
lemma_type: primitive_date_arc().clone(),
}
}
pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Date(dt),
lemma_type,
}
}
pub fn time(t: SemanticTime) -> Self {
Self {
value: ValueKind::Time(t),
lemma_type: primitive_time_arc().clone(),
}
}
pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Time(t),
lemma_type,
}
}
pub fn calendar(
value: RationalInteger,
unit: SemanticCalendarUnit,
lemma_type: Arc<LemmaType>,
) -> Self {
let unit_name = unit.to_string();
debug_assert_eq!(
semantic_calendar_unit_from_unit_name(&unit_name),
unit,
"BUG: calendar unit name must round-trip"
);
Self::measure_with_bound_unit(value, unit_name, lemma_type)
}
pub fn calendar_from_decimal(
value: Decimal,
unit: SemanticCalendarUnit,
lemma_type: Arc<LemmaType>,
) -> Self {
Self::calendar(
crate::literals::rational_from_parsed_decimal(value)
.expect("BUG: calendar literal from decimal must lift at boundary"),
unit,
lemma_type,
)
}
pub fn calendar_with_type(
value: RationalInteger,
unit: SemanticCalendarUnit,
lemma_type: Arc<LemmaType>,
) -> Self {
Self::calendar(value, unit, lemma_type)
}
pub fn ratio(r: RationalInteger) -> Self {
Self {
value: ValueKind::Ratio(r),
lemma_type: primitive_ratio_arc().clone(),
}
}
pub fn ratio_from_decimal(r: Decimal) -> Self {
Self::ratio(
crate::literals::rational_from_parsed_decimal(r)
.expect("BUG: ratio literal from decimal must lift at boundary"),
)
}
pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
Self {
value: ValueKind::Ratio(r),
lemma_type,
}
}
pub fn ratio_with_bound_unit(
r: RationalInteger,
unit_name: impl Into<String>,
lemma_type: Arc<LemmaType>,
) -> Self {
Self::ratio_with_type(
r,
Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
)
}
pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
let specifications =
range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
.unwrap_or_else(|| {
unreachable!(
"BUG: attempted to construct a range literal from incompatible endpoint types"
)
});
Self {
value: ValueKind::Range(Box::new(left), Box::new(right)),
lemma_type: Arc::new(LemmaType::primitive(specifications)),
}
}
pub fn display_value(&self) -> String {
self.to_literal().display_value_with_type(&self.lemma_type)
}
pub fn get_type(&self) -> &LemmaType {
&self.lemma_type
}
pub fn byte_size(&self) -> usize {
self.value.structural_byte_size()
}
}
impl fmt::Display for TypedLiteral {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_value())
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReferenceTarget {
Data(DataPath),
Rule(RulePath),
}
impl fmt::Display for ReferenceTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReferenceTarget::Data(path) => write!(f, "{path}"),
ReferenceTarget::Rule(path) => write!(f, "{path}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ReferenceEnd {
Promptable(DataPath),
Rule(RulePath),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DataOrigin {
Declared,
Bound,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ImportEffectivePin {
Data(DataPath),
Rule(RulePath),
}
#[derive(Clone, Debug)]
pub enum DataDefinition {
Value {
value: LiteralValue,
resolved_type: Arc<LemmaType>,
origin: DataOrigin,
source: Source,
},
TypeDeclaration {
resolved_type: Arc<LemmaType>,
declared_suggestion: Option<BoundValueKind>,
declared_value: Option<BoundValueKind>,
source: Source,
},
Import {
row: Arc<RowInstance>,
pin: Option<ImportEffectivePin>,
candidates: std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>>,
source: Source,
},
InstanceList {
resolved_type: Arc<LemmaType>,
origin: DataOrigin,
source: Source,
},
Reference {
target: ReferenceTarget,
resolved_type: Arc<LemmaType>,
local_constraints: Option<Vec<Constraint>>,
local_suggestion: Option<BoundValueKind>,
local_value: Option<BoundValueKind>,
origin: DataOrigin,
source: Source,
},
}
impl DataDefinition {
pub fn is_bound(&self) -> bool {
match self {
DataDefinition::Value { origin, .. }
| DataDefinition::InstanceList { origin, .. }
| DataDefinition::Reference { origin, .. } => *origin == DataOrigin::Bound,
DataDefinition::TypeDeclaration { .. } | DataDefinition::Import { .. } => false,
}
}
pub fn lemma_type(&self) -> Option<&LemmaType> {
self.resolved_type_arc().map(|arc| arc.as_ref())
}
pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
match self {
DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
DataDefinition::Import { .. } => None,
}
}
#[inline]
pub fn schema_type(&self) -> Option<&LemmaType> {
self.lemma_type()
}
pub fn value(&self) -> Option<LiteralValue> {
match self {
DataDefinition::Value { value, .. } => Some(value.clone()),
DataDefinition::TypeDeclaration {
declared_value: Some(dv),
..
} => Some(dv.to_literal()),
DataDefinition::Reference {
local_value: Some(dv),
resolved_type: _,
..
} => Some(dv.to_literal()),
DataDefinition::TypeDeclaration { .. }
| DataDefinition::Import { .. }
| DataDefinition::InstanceList { .. }
| DataDefinition::Reference { .. } => None,
}
}
pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
match self {
DataDefinition::TypeDeclaration {
declared_value: Some(dv),
..
}
| DataDefinition::Reference {
local_value: Some(dv),
..
} => Some(dv),
DataDefinition::Value { .. }
| DataDefinition::TypeDeclaration { .. }
| DataDefinition::Reference { .. }
| DataDefinition::InstanceList { .. }
| DataDefinition::Import { .. } => None,
}
}
pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
match self {
DataDefinition::TypeDeclaration {
declared_suggestion: Some(dv),
..
}
| DataDefinition::Reference {
local_suggestion: Some(dv),
..
} => Some(dv),
DataDefinition::Value { .. }
| DataDefinition::TypeDeclaration { .. }
| DataDefinition::Reference { .. }
| DataDefinition::InstanceList { .. }
| DataDefinition::Import { .. } => None,
}
}
pub fn source(&self) -> &Source {
match self {
DataDefinition::Value { source, .. } => source,
DataDefinition::TypeDeclaration { source, .. } => source,
DataDefinition::Import { source, .. } => source,
DataDefinition::InstanceList { source, .. } => source,
DataDefinition::Reference { source, .. } => source,
}
}
pub fn import_sole_row(&self) -> &Arc<RowInstance> {
let DataDefinition::Import { pin, row, .. } = self else {
panic!("BUG: import_sole_row on non-import data");
};
assert!(
pin.is_none(),
"BUG: dynamic import needs pin selection before reading a row"
);
row
}
pub fn import_candidates(&self) -> &std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>> {
let DataDefinition::Import {
pin, candidates, ..
} = self
else {
panic!("BUG: import_candidates on non-import data");
};
assert!(pin.is_some(), "BUG: static import has no candidate map");
candidates
}
pub fn import_pin(&self) -> Option<&ImportEffectivePin> {
match self {
DataDefinition::Import { pin, .. } => pin.as_ref(),
_ => None,
}
}
pub fn import_row_for(&self, identity: &SpecIdentity) -> &Arc<RowInstance> {
let DataDefinition::Import {
pin,
row,
candidates,
..
} = self
else {
panic!("BUG: import_row_for on non-import data");
};
if pin.is_none() {
return row;
}
candidates
.get(identity)
.unwrap_or_else(|| panic!("BUG: dynamic import has no row for identity '{identity}'"))
}
}
pub fn number_with_unit_to_value_kind(
magnitude: rust_decimal::Decimal,
unit_name: &str,
lemma_type: &LemmaType,
) -> Result<ValueKind, String> {
match &lemma_type.specifications {
TypeSpecification::Ratio { units, .. } => {
use crate::computation::rational::{checked_div, decimal_to_rational};
let unit = units.get(unit_name)?;
let magnitude_rational = decimal_to_rational(magnitude)
.map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
let canonical_rational = checked_div(&magnitude_rational, &unit.value)
.map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
Ok(ValueKind::Ratio(canonical_rational))
}
TypeSpecification::Measure { units, .. } => {
use crate::computation::rational::checked_mul;
let rational = lift_parser_decimal(magnitude)?;
let unit = units.get(unit_name)?;
match unit.literal_factor() {
Some(factor) => {
let canonical = checked_mul(&rational, factor).map_err(|failure| {
format!("measure canonicalization overflow: {failure}")
})?;
Ok(ValueKind::Measure(canonical))
}
None => Ok(ValueKind::UnscaledMeasure {
magnitude: rational,
unit: std::sync::Arc::from(unit_name),
}),
}
}
_ => Err(format!(
"Unit '{}' is defined on type '{}' which is not measure or ratio",
unit_name,
lemma_type.name()
)),
}
}
pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
matches!(
(type_spec, value),
(TypeSpecification::Number { .. }, ValueKind::Number(_))
| (TypeSpecification::Text { .. }, ValueKind::Text(_))
| (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
| (TypeSpecification::Date { .. }, ValueKind::Date(_))
| (TypeSpecification::Time { .. }, ValueKind::Time(_))
| (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
| (
TypeSpecification::Measure { .. },
ValueKind::UnscaledMeasure { .. }
)
| (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
| (
TypeSpecification::NumberRange { .. },
ValueKind::Range(_, _)
)
| (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
| (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
| (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
| (
TypeSpecification::MeasureRange { .. },
ValueKind::Range(_, _)
)
| (TypeSpecification::Veto { .. }, _)
| (TypeSpecification::Undetermined, _)
| (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
| (TypeSpecification::List { .. }, ValueKind::List(_))
)
}
fn parser_value_type_mismatch(
value: &crate::literals::Value,
type_spec: &TypeSpecification,
) -> String {
use crate::parsing::ast::AsLemmaSource;
let value_str = format!("{}", AsLemmaSource(value));
match type_spec {
TypeSpecification::Measure { units, .. } => {
let unit_hint = units
.iter()
.find(|u| u.is_canonical_factor())
.map(|u| u.name.as_str())
.or_else(|| units.iter().next().map(|u| u.name.as_str()))
.unwrap_or("unit");
format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
}
TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
let unit_hint = units
.iter()
.next()
.map(|u| u.name.as_str())
.unwrap_or("unit");
format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
}
_ => format!("cannot use {value_str} as {type_spec}"),
}
}
pub fn refresh_measure_literal_canonical_magnitude(
lit: &mut LiteralValue,
previous_type: &LemmaType,
resolved_type: &LemmaType,
) -> Result<(), crate::computation::rational::NumericFailure> {
use crate::computation::rational::{checked_div, checked_mul};
let ValueKind::Measure(magnitude) = &mut lit.value else {
if matches!(lit.value, ValueKind::UnscaledMeasure { .. }) {
return Ok(());
}
return Ok(());
};
let signature = previous_type.measure_runtime_signature();
let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
return Ok(());
};
if signature.len() != 1 {
return Ok(());
}
let stored_factor = previous_type.measure_unit_factor(unit_name);
let resolved_factor = resolved_type.measure_unit_factor(unit_name);
if stored_factor == resolved_factor {
return Ok(());
}
let scaled = checked_mul(magnitude, resolved_factor)?;
*magnitude = checked_div(&scaled, stored_factor)?;
Ok(())
}
fn number_with_unit_value_kind(
magnitude: rust_decimal::Decimal,
unit_name: &str,
type_spec: &TypeSpecification,
) -> Result<ValueKind, String> {
use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
match type_spec {
TypeSpecification::Ratio { units, .. } => {
let unit = units.get(unit_name)?;
let magnitude_rational = decimal_to_rational(magnitude)
.map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
let canonical_rational = checked_div(&magnitude_rational, &unit.value)
.map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
Ok(ValueKind::Ratio(canonical_rational))
}
TypeSpecification::Measure { units, .. } => {
let rational = lift_parser_decimal(magnitude)?;
let unit = units.get(unit_name)?;
match unit.literal_factor() {
Some(factor) => {
let canonical = checked_mul(&rational, factor).map_err(|failure| {
format!("measure canonicalization overflow: {failure}")
})?;
Ok(ValueKind::Measure(canonical))
}
None => Ok(ValueKind::UnscaledMeasure {
magnitude: rational,
unit: Arc::from(unit_name),
}),
}
}
_ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
}
}
fn parser_scalar_to_value_kind(
value: &crate::literals::Value,
type_spec: &TypeSpecification,
) -> Result<ValueKind, String> {
use crate::literals::Value;
match (value, type_spec) {
(Value::NumberWithUnit(magnitude, unit_name), _) => {
number_with_unit_value_kind(*magnitude, unit_name, type_spec)
}
(Value::Number(n), TypeSpecification::Number { .. }) => {
Ok(ValueKind::Number(lift_parser_decimal(*n)?))
}
(Value::Text(s), TypeSpecification::Text { .. }) => {
Ok(ValueKind::Text(Arc::from(s.as_str())))
}
(Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
(Value::Date(dt), TypeSpecification::Date { .. }) => {
Ok(ValueKind::Date(date_time_to_semantic(dt)))
}
(Value::Time(t), TypeSpecification::Time { .. }) => {
Ok(ValueKind::Time(time_to_semantic(t)))
}
(value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
}
}
pub fn parser_value_to_value_kind(
value: &crate::literals::Value,
lemma_type: &LemmaType,
resolve_unit: Option<&UnitTokenResolver<'_>>,
) -> Result<ValueKind, String> {
use crate::literals::Value;
match (value, &lemma_type.specifications) {
(
Value::Range(left, right),
TypeSpecification::NumberRange { .. }
| TypeSpecification::DateRange { .. }
| TypeSpecification::TimeRange { .. }
| TypeSpecification::RatioRange { .. }
| TypeSpecification::MeasureRange { .. },
) => {
let endpoint = match &lemma_type.specifications {
TypeSpecification::MeasureRange { .. } | TypeSpecification::RatioRange { .. } => {
lemma_type.range_element_type()
}
other => Arc::new(LemmaType::primitive(
range_element_type_specification(other).expect(
"BUG: range_element_type_specification missing arm for known range type",
),
)),
};
let left_lit = lift_range_endpoint(left, endpoint.as_ref(), resolve_unit)?;
let right_lit = lift_range_endpoint(right, endpoint.as_ref(), resolve_unit)?;
Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
}
(value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
}
}
pub(crate) fn typed_literal_from_parsed(
parsed: &crate::literals::Value,
lemma_type: &Arc<LemmaType>,
unit_index: &UnitIndex,
named_types: &IndexMap<String, Arc<LemmaType>>,
) -> Result<TypedLiteral, String> {
let resolve = UnitTokenResolver::Typed {
unit_index,
named_types,
lemma_type: lemma_type.as_ref(),
};
let kind = parser_value_to_value_kind(parsed, lemma_type.as_ref(), Some(&resolve))?;
let binding = match binding_unit_from_parser_value(parsed) {
Some(written) => Some(resolve.resolve(&written)?),
None => None,
};
Ok(typed_literal_with_binding_unit(kind, binding, lemma_type))
}
pub(crate) fn typed_literal_with_binding_unit(
kind: ValueKind,
binding_unit: Option<String>,
lemma_type: &Arc<LemmaType>,
) -> TypedLiteral {
let typed_type = match binding_unit {
Some(unit)
if matches!(
&lemma_type.specifications,
TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
) =>
{
Arc::new(lemma_type.as_ref().clone().with_unit(unit))
}
Some(_) | None => Arc::clone(lemma_type),
};
TypedLiteral {
value: kind,
lemma_type: typed_type,
}
}
fn binding_unit_from_parser_value(value: &crate::literals::Value) -> Option<String> {
use crate::literals::Value;
match value {
Value::NumberWithUnit(_, unit) => Some(unit.clone()),
Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
(Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
if left_unit == right_unit =>
{
Some(left_unit.clone())
}
_ => None,
},
_ => None,
}
}
pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
use crate::parsing::ast::Value;
Ok(match value {
Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
Value::Text(s) => ValueKind::Text(Arc::from(s.as_str())),
Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
Value::NumberWithUnit(_, _) => {
return Err(
"number_with_unit literal requires type context (measure or ratio)".to_string(),
);
}
Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
})
}
pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
SemanticDateTime {
year: dt.year,
month: dt.month,
day: dt.day,
hour: dt.hour,
minute: dt.minute,
second: dt.second,
microsecond: dt.microsecond,
timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
}
}
pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
SemanticTime {
hour: date.hour,
minute: date.minute,
second: date.second,
microsecond: date.microsecond,
timezone: date.timezone.clone(),
}
}
pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
SemanticTime {
hour: t.hour.into(),
minute: t.minute.into(),
second: t.second.into(),
microsecond: t.microsecond,
timezone: t.timezone.as_ref().map(SemanticTimezone::from),
}
}
pub(crate) fn compare_semantic_dates(
left: &SemanticDateTime,
right: &SemanticDateTime,
) -> std::cmp::Ordering {
left.year
.cmp(&right.year)
.then_with(|| left.month.cmp(&right.month))
.then_with(|| left.day.cmp(&right.day))
.then_with(|| left.hour.cmp(&right.hour))
.then_with(|| left.minute.cmp(&right.minute))
.then_with(|| left.second.cmp(&right.second))
.then_with(|| left.microsecond.cmp(&right.microsecond))
}
pub(crate) fn compare_semantic_times(
left: &SemanticTime,
right: &SemanticTime,
) -> std::cmp::Ordering {
left.hour
.cmp(&right.hour)
.then_with(|| left.minute.cmp(&right.minute))
.then_with(|| left.second.cmp(&right.second))
.then_with(|| left.microsecond.cmp(&right.microsecond))
}
pub fn conversion_target_to_semantic(
ct: &ConversionTarget,
unit_index: Option<&crate::planning::unit_index::UnitIndex>,
resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
) -> Result<SemanticConversionTarget, String> {
match ct {
ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
ConversionTarget::Unit { unit_name } => {
let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
let (bare, owning_type) = match resolved_types {
Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
None => index.resolve(unit_name)?,
};
Ok(SemanticConversionTarget::Unit {
unit_name: bare,
owning_type,
})
}
}
}
static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
#[must_use]
pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Boolean)
}
#[must_use]
pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Number)
}
#[must_use]
pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Text)
}
#[must_use]
pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Date)
}
#[must_use]
pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::DateRange)
}
#[must_use]
pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Time)
}
#[must_use]
pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
lemma_type_for_primitive(PrimitiveKind::Ratio)
}
#[must_use]
pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
let cell = match kind {
PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
PrimitiveKind::Number => &PRIMITIVE_NUMBER,
PrimitiveKind::Text => &PRIMITIVE_TEXT,
PrimitiveKind::Date => &PRIMITIVE_DATE,
PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
PrimitiveKind::Time => &PRIMITIVE_TIME,
PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
};
cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
}
#[must_use]
pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
match kind {
PrimitiveKind::Boolean => TypeSpecification::boolean(),
PrimitiveKind::Measure => TypeSpecification::measure(),
PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
PrimitiveKind::Number => TypeSpecification::number(),
PrimitiveKind::NumberRange => TypeSpecification::number_range(),
PrimitiveKind::Ratio => TypeSpecification::ratio(),
PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
PrimitiveKind::Text => TypeSpecification::text(),
PrimitiveKind::Date => TypeSpecification::date(),
PrimitiveKind::DateRange => TypeSpecification::date_range(),
PrimitiveKind::Time => TypeSpecification::time(),
PrimitiveKind::TimeRange => TypeSpecification::time_range(),
}
}
impl fmt::Display for PathSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let hop = self.input_key();
match &self.repository {
Some(repository) => write!(f, "{hop} → {} {}", repository, self.spec),
None => write!(f, "{hop} → {}", self.spec),
}
}
}
impl fmt::Display for DataPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.input_key())
}
}
impl fmt::Display for RulePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.input_key())
}
}
impl fmt::Display for LemmaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
pub(crate) fn format_decimal_for_api(decimal: rust_decimal::Decimal) -> rust_decimal::Decimal {
let normalized = decimal.normalize();
if normalized.fract().is_zero() {
normalized.trunc()
} else {
normalized
}
}
fn format_decimal_for_human_display(decimal: rust_decimal::Decimal) -> String {
decimal.normalize().to_string()
}
fn format_rational_for_human_display(
magnitude: &crate::computation::rational::RationalInteger,
) -> String {
match magnitude.try_to_decimal() {
Ok(decimal) => format_decimal_for_human_display(decimal),
Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
Err(_) => magnitude.display_str(),
}
}
pub(crate) fn measure_family_has_expressible_unit(
lemma_type: &LemmaType,
runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
) -> bool {
let Some(units) = lemma_type.measure_units_table() else {
return true;
};
if units.is_empty() {
return true;
}
units.iter().any(|unit| {
unit.literal_factor().is_some()
|| runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
})
}
fn measure_display_unit<'a>(
units: &'a crate::literals::MeasureUnits,
lemma_type: &LemmaType,
runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
) -> &'a crate::literals::MeasureUnit {
let factor_known = |unit: &crate::literals::MeasureUnit| {
unit.literal_factor().is_some()
|| runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
};
if let Some(binding) = lemma_type.unit() {
let unit = units
.iter()
.find(|unit| unit.name == binding)
.unwrap_or_else(|| {
panic!(
"BUG: measure display binding '{binding}' is not a declared unit on type {}",
lemma_type.name()
)
});
if factor_known(unit) {
return unit;
}
}
units
.iter()
.find(|unit| unit.is_canonical_factor() && factor_known(unit))
.or_else(|| {
units
.iter()
.find(|unit| unit.literal_factor().is_some() && factor_known(unit))
})
.or_else(|| units.iter().find(|unit| factor_known(unit)))
.unwrap_or_else(|| {
panic!(
"BUG: measure display of '{}' reached with no factor-known unit; callers must veto first",
lemma_type.name()
)
})
}
fn format_measure_canonical_for_display(
canonical: &crate::computation::rational::RationalInteger,
lemma_type: &LemmaType,
signature: &[(String, i32)],
runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
) -> String {
use crate::computation::rational::checked_div;
if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
if !units.is_empty() {
let unit = measure_display_unit(units, lemma_type, runtime_scales);
let factor = unit
.literal_factor()
.cloned()
.or_else(|| {
runtime_scales.and_then(|scales| scales.get(&unit.name).cloned())
})
.unwrap_or_else(|| {
panic!(
"BUG: display of unit '{}' on {} requires a literal factor or a settled runtime scale",
unit.name,
lemma_type.name()
)
});
let in_unit = checked_div(canonical, &factor)
.expect("BUG: de-canonicalization for measure display must not fail");
return format_number_with_unit_for_display(&in_unit, &unit.name);
}
}
let unit_label = match signature {
[] => String::new(),
[(name, 1)] => name.clone(),
_ => format_signature_operator_style(signature),
};
let formatted = format_rational_for_human_display(canonical);
if unit_label.is_empty() {
formatted
} else {
format!("{formatted} {unit_label}")
}
}
fn format_ratio_canonical_for_display(
canonical: &crate::computation::rational::RationalInteger,
lemma_type: &LemmaType,
) -> String {
use crate::computation::rational::checked_mul;
let units = match &lemma_type.specifications {
TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
units
}
_ => unreachable!(
"BUG: ratio display with non-ratio type {}",
lemma_type.name()
),
};
let display_unit = lemma_type
.unit()
.or_else(|| lemma_type.ratio_primary_unit());
let Some(unit_name) = display_unit else {
return format_rational_magnitude_for_display(canonical);
};
let unit = units.get(unit_name).unwrap_or_else(|_| {
panic!(
"BUG: ratio display unit '{unit_name}' is not declared on type {}",
lemma_type.name()
)
});
let in_unit = checked_mul(canonical, &unit.value)
.expect("BUG: de-canonicalization for ratio display must not fail");
format_number_with_unit_for_display(&in_unit, unit_name)
}
impl fmt::Display for LiteralValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.value {
ValueKind::Measure(_) | ValueKind::Ratio(_) => {
write!(f, "{}", self.value)
}
ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
_ => write!(f, "{}", self.value),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::computation::rational::decimal_to_rational;
use crate::literals::DateGranularity;
use crate::literals::Value;
use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
use rust_decimal::Decimal;
use std::str::FromStr;
fn meter_axis(exponent: i32) -> BaseMeasureVector {
[(Dimension::Family(test_type_family("meter")), exponent)]
.into_iter()
.collect()
}
#[test]
fn scale_decomposition_exponent_one_is_identity() {
let base = meter_axis(1);
assert_eq!(scale_decomposition(&base, 1), Some(base));
}
#[test]
fn scale_decomposition_squares_meter() {
assert_eq!(scale_decomposition(&meter_axis(1), 2), Some(meter_axis(2)));
}
#[test]
fn scale_decomposition_zero_is_empty() {
assert_eq!(
scale_decomposition(&meter_axis(1), 0),
Some(BaseMeasureVector::new())
);
}
#[test]
fn scale_decomposition_negative_inverts_meter() {
assert_eq!(
scale_decomposition(&meter_axis(1), -1),
Some(meter_axis(-1))
);
}
#[test]
fn scale_decomposition_at_exponent_cap() {
assert_eq!(
scale_decomposition(&meter_axis(32), 2),
Some(meter_axis(64))
);
}
#[test]
fn scale_decomposition_past_exponent_cap_is_none() {
assert_eq!(scale_decomposition(&meter_axis(1), 65), None);
assert_eq!(scale_decomposition(&meter_axis(32), 3), None);
}
#[must_use]
pub(crate) fn test_type_family(name: &str) -> TypeFamily {
TypeFamily {
repository: None,
spec: "test".to_string(),
effective: crate::parsing::ast::EffectiveDate::Origin,
name: name.to_string(),
}
}
#[must_use]
pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
}
#[must_use]
pub(crate) fn primitive_measure() -> &'static LemmaType {
primitive_measure_arc().as_ref()
}
#[test]
fn default_primitive_help_is_goal_oriented() {
let kinds = [
PrimitiveKind::Boolean,
PrimitiveKind::Measure,
PrimitiveKind::MeasureRange,
PrimitiveKind::Number,
PrimitiveKind::NumberRange,
PrimitiveKind::Ratio,
PrimitiveKind::RatioRange,
PrimitiveKind::Text,
PrimitiveKind::Date,
PrimitiveKind::DateRange,
PrimitiveKind::Time,
PrimitiveKind::TimeRange,
];
for kind in kinds {
let spec = type_spec_for_primitive(kind);
let help = match &spec {
TypeSpecification::Boolean { help, .. }
| TypeSpecification::Number { help, .. }
| TypeSpecification::NumberRange { help, .. }
| TypeSpecification::Text { help, .. }
| TypeSpecification::Measure { help, .. }
| TypeSpecification::MeasureRange { help, .. }
| TypeSpecification::Ratio { help, .. }
| TypeSpecification::RatioRange { help, .. }
| TypeSpecification::Date { help, .. }
| TypeSpecification::DateRange { help, .. }
| TypeSpecification::TimeRange { help, .. }
| TypeSpecification::Time { help, .. } => help,
TypeSpecification::Veto { .. }
| TypeSpecification::Undetermined
| TypeSpecification::Spec { .. }
| TypeSpecification::List { .. } => {
unreachable!(
"BUG: primitive kind {:?} mapped to non-primitive spec",
kind
)
}
};
assert!(!help.is_empty(), "help for {:?}", kind);
assert!(
!help.to_ascii_lowercase().contains("format:"),
"help for {:?} must not describe syntax: {:?}",
kind,
help
);
assert_eq!(help, default_help_for_primitive(kind));
}
}
#[test]
fn data_path_and_rule_path_display_are_input_key() {
let implicit = PathSegment {
uses: Arc::from("bag"),
row: None,
repository: None,
spec: Arc::from("bag"),
};
let explicit = PathSegment {
uses: Arc::from("bag"),
row: None,
repository: None,
spec: Arc::from("item_bag"),
};
let named_repo = PathSegment {
uses: Arc::from("calc"),
row: None,
repository: Some(Arc::from("alpha")),
spec: Arc::from("pricing"),
};
let row = PathSegment {
uses: Arc::from("staff"),
row: Some(2),
repository: None,
spec: Arc::from("employee"),
};
assert_eq!(
DataPath::new(vec![row.clone()], "salary".to_string()).to_string(),
"staff.2.salary"
);
assert_eq!(
RulePath::new(vec![row.clone()], "double".to_string()).to_string(),
"staff.2.double"
);
assert_eq!(row.to_string(), "staff.2 → employee");
assert_eq!(
SpecInstance {
prefix: vec![implicit.clone(), row]
}
.to_string(),
"bag.staff.2"
);
assert_eq!(
DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
"bag.weight"
);
assert_eq!(
DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
"bag.weight"
);
assert_eq!(
DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
"calc.price"
);
assert_eq!(
RulePath::new(vec![implicit], "cost".to_string()).to_string(),
"bag.cost"
);
assert_eq!(
RulePath::new(vec![explicit], "cost".to_string()).to_string(),
"bag.cost"
);
assert_eq!(
RulePath::new(vec![named_repo], "price".to_string()).to_string(),
"calc.price"
);
assert_eq!(
PathSegment {
uses: Arc::from("bag"),
row: None,
repository: None,
spec: Arc::from("item_bag"),
}
.to_string(),
"bag → item_bag"
);
assert_eq!(
PathSegment {
uses: Arc::from("calc"),
row: None,
repository: Some(Arc::from("alpha")),
spec: Arc::from("pricing"),
}
.to_string(),
"calc → alpha pricing"
);
}
#[test]
fn test_negated_comparison() {
assert_eq!(
negated_comparison(ComparisonComputation::LessThan),
ComparisonComputation::GreaterThanOrEqual
);
assert_eq!(
negated_comparison(ComparisonComputation::GreaterThanOrEqual),
ComparisonComputation::LessThan
);
assert_eq!(
negated_comparison(ComparisonComputation::Is),
ComparisonComputation::IsNot
);
assert_eq!(
negated_comparison(ComparisonComputation::IsNot),
ComparisonComputation::Is
);
}
#[test]
fn value_to_semantic_number_is_decimal() {
let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
}
#[test]
fn value_kind_measure_serializes_magnitude_only() {
let kind =
ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
assert_eq!(json["measure"]["value"], "99.5");
assert!(json["measure"].get("signature").is_none());
}
#[test]
fn value_kind_measure_roundtrips() {
let original =
ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
let api = crate::api::ValueKind::from(&original);
let json = serde_json::to_string(&api).unwrap();
let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
assert_eq!(api, parsed);
}
#[test]
fn value_kind_measure_empty_roundtrips() {
let original =
ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
let api = crate::api::ValueKind::from(&original);
let json = serde_json::to_string(&api).unwrap();
let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
assert_eq!(api, parsed);
}
#[test]
fn literal_value_number_serde_not_rational_array() {
let lit = LiteralValue::number_from_decimal(Decimal::from(20));
let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
let number = json
.get("value")
.and_then(|v| v.get("number"))
.expect("number field");
assert!(number.is_string());
assert_eq!(number.as_str(), Some("20"));
assert!(
!number.is_array(),
"stored number must not serialize as [n,d]"
);
}
#[test]
fn test_literal_value_to_primitive_type() {
let one = rational_new(1, 1);
assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
assert_eq!(
TypedLiteral::number(one.clone()).lemma_type.name(),
"number"
);
assert_eq!(
TypedLiteral::from_bool(bool::from(BooleanValue::True))
.lemma_type
.name(),
"boolean"
);
let dt = DateTimeValue {
year: 2024,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
microsecond: 0,
timezone: None,
granularity: DateGranularity::Full,
};
assert_eq!(
TypedLiteral::date(date_time_to_semantic(&dt))
.lemma_type
.name(),
"date"
);
assert_eq!(
TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
.lemma_type
.name(),
"ratio"
);
let dur_type = LemmaType::new(
"duration".to_string(),
TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![MeasureUnit {
name: "second".to_string(),
scale: UnitScale::Literal(crate::computation::rational::rational_one()),
derived_measure_factors: Vec::new(),
decomposition: BaseMeasureVector::new(),
minimum: None,
maximum: None,
suggestion_magnitude: None,
}]),
traits: vec![MeasureTrait::Duration],
decomposition: None,
unit: None,
help: String::new(),
},
TypeExtends::Primitive,
);
assert_eq!(
TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
.lemma_type
.name(),
"duration"
);
}
#[test]
fn test_type_display() {
let specs = TypeSpecification::text();
let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
assert_eq!(format!("{}", lemma_type), "name");
}
#[test]
fn test_type_serialization() {
let specs = TypeSpecification::number();
let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
let api = crate::api::LemmaType::from(&lemma_type);
let serialized = serde_json::to_string(&api).unwrap();
let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
assert_eq!(api, deserialized);
}
#[test]
fn empty_list_display_value_with_type_is_type_spec() {
let element = Arc::new(LemmaType::new(
"item".to_string(),
TypeSpecification::Spec {
spec: SpecIdentity {
repository: None,
spec: "item".to_string(),
effective: crate::parsing::ast::EffectiveDate::Origin,
slice: crate::parsing::ast::EffectiveDate::Origin,
},
},
TypeExtends::Primitive,
));
let list_ty = LemmaType::list_of(element);
let empty = LiteralValue::new(ValueKind::List(Vec::new().into()));
assert_eq!(empty.display_value_with_type(&list_ty), "item list");
assert_eq!(empty.display_value(), "");
}
#[test]
fn test_literal_value_display_value() {
let ten = rational_new(10, 1);
assert_eq!(
LiteralValue::text("hello".to_string()).display_value(),
"hello"
);
assert_eq!(LiteralValue::number(ten).display_value(), "10");
assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
let ten_percent_ratio = LiteralValue::ratio(
crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
.expect("ratio decimal"),
);
assert_eq!(
ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
"10%"
);
let time = TimeValue {
hour: 14,
minute: 30,
second: 0,
microsecond: 0,
timezone: None,
};
let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
assert!(time_display.contains("14"));
assert!(time_display.contains("30"));
}
#[test]
fn test_measure_display_trims_trailing_zeros() {
let money_type = LemmaType {
name: Some("money".to_string()),
specifications: TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![MeasureUnit {
name: "eur".to_string(),
scale: UnitScale::Literal(crate::computation::rational::rational_one()),
derived_measure_factors: Vec::new(),
decomposition: BaseMeasureVector::new(),
minimum: None,
maximum: None,
suggestion_magnitude: None,
}]),
traits: Vec::new(),
decomposition: None,
unit: None,
help: String::new(),
},
extends: TypeExtends::Primitive,
options: TypeOptions::default(),
};
let money_type = Arc::new(money_type);
let val =
LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.8 eur");
let more_precision = LiteralValue::measure(
decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
);
assert_eq!(
more_precision.display_value_with_type(money_type.as_ref()),
"1.8 eur"
);
let count_type = Arc::new(LemmaType {
name: Some("count".to_string()),
specifications: TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![MeasureUnit {
name: "items".to_string(),
scale: UnitScale::Literal(crate::computation::rational::rational_one()),
derived_measure_factors: Vec::new(),
decomposition: BaseMeasureVector::new(),
minimum: None,
maximum: None,
suggestion_magnitude: None,
}]),
traits: Vec::new(),
decomposition: None,
unit: None,
help: String::new(),
},
extends: TypeExtends::Primitive,
options: TypeOptions::default(),
});
let val_any = LiteralValue::measure(
decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
);
assert_eq!(
val_any.display_value_with_type(count_type.as_ref()),
"42.5 items"
);
}
#[test]
fn test_literal_value_time_type() {
let time = TimeValue {
hour: 14,
minute: 30,
second: 0,
microsecond: 0,
timezone: None,
};
let lit = TypedLiteral::time(time_to_semantic(&time));
assert_eq!(lit.lemma_type.name(), "time");
}
#[test]
fn test_measure_family_name_none_without_declaring_spec() {
let money_primitive = LemmaType::new(
"money".to_string(),
TypeSpecification::measure(),
TypeExtends::Primitive,
);
assert_eq!(money_primitive.measure_family_name(), None);
}
#[test]
fn test_measure_family_name_custom() {
let measure_spec = TypeSpecification::measure();
let money_custom = LemmaType::new(
"money".to_string(),
measure_spec,
TypeExtends::custom_local("money".to_string(), test_type_family("money")),
);
assert_eq!(money_custom.measure_family_name(), Some("money"));
}
#[test]
fn test_same_named_roots_in_different_specs_are_different_families() {
let declared_in = |spec: &str| TypeFamily {
repository: None,
spec: spec.to_string(),
effective: crate::parsing::ast::EffectiveDate::Origin,
name: "money".to_string(),
};
let pricing_money = LemmaType::new(
"money".to_string(),
TypeSpecification::measure(),
TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
);
let shop_money = LemmaType::new(
"money".to_string(),
TypeSpecification::measure(),
TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
);
assert!(!pricing_money.same_measure_family(&shop_money));
assert!(!shop_money.same_measure_family(&pricing_money));
}
#[test]
fn test_same_measure_family_parent_and_child() {
let measure_spec = TypeSpecification::measure();
let type_x = LemmaType::new(
"x".to_string(),
measure_spec.clone(),
TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
);
let type_x2 = LemmaType::new(
"x2".to_string(),
measure_spec,
TypeExtends::custom_local("x".to_string(), test_type_family("x")),
);
assert_eq!(type_x.measure_family_name(), Some("x"));
assert_eq!(type_x2.measure_family_name(), Some("x"));
assert!(type_x.same_measure_family(&type_x2));
assert!(type_x2.same_measure_family(&type_x));
}
#[test]
fn test_same_measure_family_siblings() {
let measure_spec = TypeSpecification::measure();
let type_x2_a = LemmaType::new(
"x2a".to_string(),
measure_spec.clone(),
TypeExtends::custom_local("x".to_string(), test_type_family("x")),
);
let type_x2_b = LemmaType::new(
"x2b".to_string(),
measure_spec,
TypeExtends::custom_local("x".to_string(), test_type_family("x")),
);
assert!(type_x2_a.same_measure_family(&type_x2_b));
}
#[test]
fn test_same_measure_family_different_families() {
let measure_spec = TypeSpecification::measure();
let money = LemmaType::new(
"money".to_string(),
measure_spec.clone(),
TypeExtends::Primitive,
);
let temperature = LemmaType::new(
"temperature".to_string(),
measure_spec,
TypeExtends::Primitive,
);
assert!(!money.same_measure_family(&temperature));
assert!(!temperature.same_measure_family(&money));
}
#[test]
fn test_same_measure_family_measure_vs_non_measure() {
let measure_spec = TypeSpecification::measure();
let number_spec = TypeSpecification::number();
let measure_type =
LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
assert!(!measure_type.same_measure_family(&number_type));
assert!(!number_type.same_measure_family(&measure_type));
}
#[test]
fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
assert!(!left.same_measure_family(&right));
assert!(left.compatible_with_anonymous_measure(&right));
}
#[test]
fn same_value_type_distinct_families_same_decomposition_are_not_same() {
let decomp: BaseMeasureVector = [
(Dimension::Family(test_type_family("length")), 1i32),
(Dimension::Family(test_type_family("elapsed")), -1i32),
]
.into_iter()
.collect();
let speed_spec = |name: &str, family: &str| {
let mut spec = TypeSpecification::measure();
if let TypeSpecification::Measure {
decomposition,
help,
..
} = &mut spec
{
*decomposition = Some(decomp.clone());
*help = String::new();
}
LemmaType::new(
name.to_string(),
spec,
TypeExtends::custom_local("measure".to_string(), test_type_family(family)),
)
};
let speed_a = speed_spec("speed_a", "speed_a");
let speed_b = speed_spec("speed_b", "speed_b");
assert_eq!(
speed_a.measure_type_decomposition(),
speed_b.measure_type_decomposition()
);
assert!(!speed_a.same_measure_family(&speed_b));
assert!(
!speed_a.same_value_type(&speed_b),
"same decomposition alone must not make distinct measure families same_value_type"
);
}
#[test]
fn test_measure_family_name_non_measure_returns_none() {
let number_spec = TypeSpecification::number();
let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
assert_eq!(number_type.measure_family_name(), None);
}
#[test]
fn test_lemma_type_inequality_local_vs_import_same_shape() {
let measure_spec = TypeSpecification::measure();
let local = LemmaType::new(
"t".to_string(),
measure_spec.clone(),
TypeExtends::custom_local("money".to_string(), test_type_family("money")),
);
let imported = LemmaType::new(
"t".to_string(),
measure_spec,
TypeExtends::Custom {
parent: "money".to_string(),
family: test_type_family("money"),
defining_spec: TypeDefiningSpec::Import,
},
);
assert_ne!(local, imported);
}
#[test]
fn test_lemma_type_equality_import_unit_variant() {
let measure_spec = TypeSpecification::measure();
let left = LemmaType::new(
"t".to_string(),
measure_spec.clone(),
TypeExtends::Custom {
parent: "money".to_string(),
family: test_type_family("money"),
defining_spec: TypeDefiningSpec::Import,
},
);
let right = LemmaType::new(
"t".to_string(),
measure_spec,
TypeExtends::Custom {
parent: "money".to_string(),
family: test_type_family("money"),
defining_spec: TypeDefiningSpec::Import,
},
);
assert_eq!(left, right);
}
fn month_suggestion_arg() -> CommandArg {
CommandArg::Literal(crate::literals::Value::NumberWithUnit(
Decimal::ONE,
"month".to_string(),
))
}
fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
[
CommandArg::Label(name.to_string()),
CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
]
}
#[test]
fn default_calendar_on_text_reports_hint() {
let mut specs = TypeSpecification::text();
let mut default = None;
let err = specs
.apply_constraint(
ConstraintNames {
type_name: "notes",
data_name: "notes",
},
TypeConstraintCommand::Suggest,
&[month_suggestion_arg()],
&mut default,
&mut None,
None,
)
.unwrap_err();
assert!(err.contains("Unit 'month' is for calendar data"));
assert!(err.contains("double quotes"));
}
#[test]
fn default_calendar_on_duration_reports_valid_units() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("second", 1),
&mut None,
&mut None,
None,
)
.unwrap();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("week", 604_800),
&mut None,
&mut None,
None,
)
.unwrap();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Trait,
&[CommandArg::Label("duration".to_string())],
&mut None,
&mut None,
None,
)
.unwrap();
let mut default = None;
let err = specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Suggest,
&[month_suggestion_arg()],
&mut default,
&mut None,
None,
)
.unwrap_err();
assert!(err.contains("Unit 'month' is for calendar data"));
assert!(err.contains("Valid 'duration' units are"));
assert!(err.contains("week"));
}
#[test]
fn default_valid_duration_weeks_accepted() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("second", 1),
&mut None,
&mut None,
None,
)
.unwrap();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("week", 604_800),
&mut None,
&mut None,
None,
)
.unwrap();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Trait,
&[CommandArg::Label("duration".to_string())],
&mut None,
&mut None,
None,
)
.unwrap();
let mut default = None;
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Suggest,
&[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
Decimal::from(4),
"week".to_string(),
))],
&mut default,
&mut None,
None,
)
.unwrap();
assert!(matches!(
default,
Some(RawSuggestion::Measure {
unit_name,
..
}) if unit_name == "week"
));
}
#[test]
fn default_unknown_unit_on_duration_lists_valid_units() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("second", 1),
&mut None,
&mut None,
None,
)
.unwrap();
specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Trait,
&[CommandArg::Label("duration".to_string())],
&mut None,
&mut None,
None,
)
.unwrap();
let mut default = None;
let err = specs
.apply_constraint(
ConstraintNames {
type_name: "duration",
data_name: "duration",
},
TypeConstraintCommand::Suggest,
&[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
Decimal::ONE,
"fortnight".to_string(),
))],
&mut default,
&mut None,
None,
)
.unwrap_err();
assert!(err.contains("fortnight"));
assert!(err.contains("not defined on 'duration'"));
assert!(err.contains("Valid units are"));
}
fn money_measure_type() -> LemmaType {
LemmaType::new(
"Money".to_string(),
TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![
MeasureUnit {
name: "eur".to_string(),
scale: UnitScale::Literal(crate::computation::rational::rational_one()),
derived_measure_factors: Vec::new(),
decomposition: BaseMeasureVector::new(),
minimum: None,
maximum: None,
suggestion_magnitude: None,
},
MeasureUnit {
name: "usd".to_string(),
scale: UnitScale::Literal(
crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
.expect("factor"),
),
derived_measure_factors: Vec::new(),
decomposition: BaseMeasureVector::new(),
minimum: None,
maximum: None,
suggestion_magnitude: None,
},
]),
traits: Vec::new(),
decomposition: None,
unit: None,
help: String::new(),
},
TypeExtends::Primitive,
)
}
#[test]
fn measure_unit_names_for_named_measure() {
let money = money_measure_type();
assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
}
fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
}
#[test]
fn combine_signatures_multiply_adds_exponents() {
let left = sig(&[("eur", 1)]);
let right = sig(&[("hour", -1)]);
let result = combine_signatures(&left, &right, true);
assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
}
#[test]
fn combine_signatures_divide_subtracts_exponents() {
let left = sig(&[("eur", 1)]);
let right = sig(&[("hour", 1)]);
let result = combine_signatures(&left, &right, false);
assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
}
#[test]
fn combine_signatures_cancels_to_empty() {
let left = sig(&[("ce", 1), ("minute", -1)]);
let right = sig(&[("minute", 1)]);
let result = combine_signatures(&left, &right, true);
assert_eq!(result, sig(&[("ce", 1)]));
}
#[test]
fn combine_signatures_output_is_canonical_form() {
let left = sig(&[("eur", 1), ("hour", 1)]);
let right = sig(&[("minute", 1)]);
let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
assert_eq!(result, expected);
}
#[test]
fn canonicalize_signature_drops_zero_exponents() {
let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
let result = canonicalize_signature(&sig_with_zero);
assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
}
#[test]
fn canonicalize_signature_sorts_by_name() {
let unsorted = sig(&[("minute", -1), ("eur", 1)]);
let result = canonicalize_signature(&unsorted);
assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
}
#[test]
fn format_signature_operator_style_numerator_only() {
let signature = sig(&[("eur", 1)]);
let result = format_signature_operator_style(&signature);
assert_eq!(result, "eur");
}
#[test]
fn format_signature_operator_style_with_denominator() {
let signature = sig(&[("eur", 1), ("hour", -1)]);
let result = format_signature_operator_style(&signature);
assert_eq!(result, "eur/hour");
}
#[test]
fn format_signature_operator_style_denominator_only() {
let signature = sig(&[("meter", -1)]);
let result = format_signature_operator_style(&signature);
assert_eq!(result, "1/meter");
}
#[test]
fn format_signature_operator_style_with_exponents() {
let signature = sig(&[("meter", 2), ("second", -2)]);
let result = format_signature_operator_style(&signature);
assert_eq!(result, "meter^2/second^2");
}
#[test]
fn calendar_unit_factor_table_completeness() {
for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
let name = unit.to_string();
assert!(
calendar_unit_factor(&name).is_some(),
"calendar_unit_factor('{}') must return Some",
name
);
}
}
#[test]
fn semantic_calendar_unit_display_returns_singular() {
assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
}
#[test]
fn signature_factor_with_calendar_units() {
let calendar = test_calendar_type_for_signature_factor();
let unit_index = crate::planning::unit_index::UnitIndex::new();
let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
.expect("must not overflow");
let expected = rational_new(1, 12);
assert_eq!(factor, expected, "month/year factor must be 1/12");
}
fn test_calendar_type_for_signature_factor() -> LemmaType {
use crate::computation::rational::{decimal_to_rational, rational_one};
use crate::literals::{MeasureUnit, MeasureUnits};
use rust_decimal::Decimal;
LemmaType::new(
"calendar".to_string(),
TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![
MeasureUnit {
name: "month".to_string(),
scale: UnitScale::Literal(rational_one()),
minimum: None,
maximum: None,
suggestion_magnitude: None,
decomposition: calendar_decomposition(),
derived_measure_factors: Vec::new(),
},
MeasureUnit {
name: "year".to_string(),
scale: UnitScale::Literal(
decimal_to_rational(Decimal::from(12)).expect("year factor"),
),
minimum: None,
maximum: None,
suggestion_magnitude: None,
decomposition: calendar_decomposition(),
derived_measure_factors: Vec::new(),
},
]),
traits: vec![MeasureTrait::Calendar],
decomposition: Some(calendar_decomposition()),
unit: None,
help: String::new(),
},
TypeExtends::Primitive,
)
}
#[test]
#[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
fn signature_factor_panics_on_unresolved_name() {
let unit_index = crate::planning::unit_index::UnitIndex::new();
let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
signature_factor(&bad_sig, &unit_index, None)
.expect("BUG: signature_factor must panic before returning a failure");
}
#[test]
fn signature_factor_uses_owner_when_expression_index_empty() {
let money = test_money_type_for_signature_factor();
let expression_units = crate::planning::unit_index::UnitIndex::new();
let sig_usd = sig(&[("usd", 1)]);
let factor =
signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
assert_eq!(factor, rational_new(91, 100));
}
fn test_money_type_for_signature_factor() -> LemmaType {
use crate::computation::rational::decimal_to_rational;
use crate::literals::{MeasureUnit, MeasureUnits};
use rust_decimal::Decimal;
LemmaType::new(
"money".to_string(),
TypeSpecification::Measure {
minimum: None,
maximum: None,
units: MeasureUnits::from(vec![
MeasureUnit {
name: "eur".to_string(),
scale: UnitScale::Literal(crate::computation::rational::rational_one()),
minimum: None,
maximum: None,
suggestion_magnitude: None,
decomposition: BaseMeasureVector::new(),
derived_measure_factors: Vec::new(),
},
MeasureUnit {
name: "usd".to_string(),
scale: UnitScale::Literal(
decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
),
minimum: None,
maximum: None,
suggestion_magnitude: None,
decomposition: BaseMeasureVector::new(),
derived_measure_factors: Vec::new(),
},
]),
traits: Vec::new(),
decomposition: None,
unit: None,
help: String::new(),
},
TypeExtends::Primitive,
)
}
fn measure_type_with_kilogram() -> TypeSpecification {
use crate::computation::rational::rational_one;
use crate::literals::{MeasureUnit, MeasureUnits};
let mut units = MeasureUnits::new();
units.push(MeasureUnit {
name: "kilogram".to_string(),
scale: UnitScale::Literal(rational_one()),
minimum: None,
maximum: None,
suggestion_magnitude: None,
decomposition: BaseMeasureVector::new(),
derived_measure_factors: Vec::new(),
});
TypeSpecification::Measure {
minimum: None,
maximum: None,
units,
traits: Vec::new(),
decomposition: None,
unit: None,
help: String::new(),
}
}
#[test]
fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
let ten = Value::Number(Decimal::from(10));
let measure = LemmaType::primitive(measure_type_with_kilogram());
let err = parser_value_to_value_kind(&ten, &measure, None)
.expect_err("bare number must not bind to measure");
assert!(
err.contains("kilogram"),
"error must hint expected unit, got: {err}"
);
}
#[test]
fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
let measure = LemmaType::primitive(measure_type_with_kilogram());
let kind = parser_value_to_value_kind(&ten_kg, &measure, None)
.expect("10 kilogram must bind to measure");
assert!(matches!(kind, ValueKind::Measure(_)));
}
#[test]
fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
let ten = Value::Number(Decimal::from(10));
let ratio = LemmaType::primitive(TypeSpecification::ratio());
let err = parser_value_to_value_kind(&ten, &ratio, None)
.expect_err("bare number must not bind to ratio");
assert!(
err.contains("percent"),
"error must hint expected unit, got: {err}"
);
}
#[test]
fn value_kind_matches_spec_rejects_number_for_measure() {
let n = ValueKind::Number(rational_new(10, 1));
assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
}
#[test]
fn value_kind_matches_spec_rejects_number_for_ratio() {
let n = ValueKind::Number(rational_new(1, 2));
assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
}
#[test]
fn apply_constraint_rejects_inherited_unit_factor_change() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("eur", 1),
&mut None,
&mut None,
None,
)
.expect("seed eur");
let err = specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&[
CommandArg::Label("eur".to_string()),
CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
],
&mut None,
&mut None,
None,
)
.expect_err("must not change inherited unit factor");
assert!(err.contains("eur"), "error must name unit, got: {err}");
assert!(
err.contains("inherited") || err.contains("cannot change"),
"error must reject factor change, got: {err}"
);
}
#[test]
fn apply_constraint_allows_additive_unit_on_inherited_spec() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("eur", 1),
&mut None,
&mut None,
None,
)
.expect("seed eur");
specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("usd", 1),
&mut None,
&mut None,
None,
)
.expect("add usd");
match &specs {
TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
other => panic!("expected Measure, got {other:?}"),
}
}
#[test]
fn apply_constraint_idempotent_inherited_unit_redeclare() {
let mut specs = TypeSpecification::measure();
specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("eur", 1),
&mut None,
&mut None,
None,
)
.expect("seed eur");
specs
.apply_constraint(
ConstraintNames {
type_name: "money",
data_name: "money",
},
TypeConstraintCommand::Unit,
&unit_factor_arg("eur", 1),
&mut None,
&mut None,
None,
)
.expect("idempotent eur");
match &specs {
TypeSpecification::Measure { units, .. } => {
assert_eq!(units.len(), 1);
assert_eq!(
*units
.iter()
.find(|u| u.name == "eur")
.expect("eur")
.require_literal_factor(),
crate::computation::rational::rational_one()
);
}
other => panic!("expected Measure, got {other:?}"),
}
}
#[test]
fn element_from_range_returns_element_for_every_range_primitive() {
type RangeElementMatcher = fn(&TypeSpecification) -> bool;
let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
(PrimitiveKind::NumberRange, |element| {
matches!(element, TypeSpecification::Number { .. })
}),
(PrimitiveKind::MeasureRange, |element| {
matches!(element, TypeSpecification::Measure { .. })
}),
(PrimitiveKind::RatioRange, |element| {
matches!(element, TypeSpecification::Ratio { .. })
}),
(PrimitiveKind::DateRange, |element| {
matches!(element, TypeSpecification::Date { .. })
}),
(PrimitiveKind::TimeRange, |element| {
matches!(element, TypeSpecification::Time { .. })
}),
];
for (kind, matches_element) in cases {
let range_spec = type_spec_for_primitive(kind);
let element = range_spec
.element_from_range()
.unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
assert!(
matches_element(&element),
"{kind:?} element must match documented mapping, got {element:?}"
);
}
}
#[test]
fn element_from_range_returns_none_for_non_range_primitives() {
let non_range = [
type_spec_for_primitive(PrimitiveKind::Boolean),
type_spec_for_primitive(PrimitiveKind::Measure),
TypeSpecification::Undetermined,
TypeSpecification::veto(),
];
for spec in non_range {
assert!(
spec.element_from_range().is_none(),
"{spec:?} must not define element_from_range"
);
}
}
fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
LemmaType::primitive(TypeSpecification::Spec {
spec: SpecIdentity {
repository: None,
spec: name.to_string(),
effective: effective.clone(),
slice: effective,
},
})
}
#[test]
fn same_value_type_spec_identity() {
use crate::parsing::ast::EffectiveDate;
let origin = EffectiveDate::Origin;
let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
year: 2026,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
microsecond: 0,
timezone: None,
granularity: crate::literals::DateGranularity::Full,
});
let tax = spec_type("tax_bracket", origin.clone());
let tax_again = spec_type("tax_bracket", origin.clone());
let other = spec_type("menu_item", origin);
let tax_dated = spec_type("tax_bracket", dated.clone());
assert!(tax.same_value_type(&tax_again));
assert!(!tax.same_value_type(&other));
assert!(!tax.same_value_type(&tax_dated));
let mut tax_later_slice = tax.clone();
match &mut tax_later_slice.specifications {
TypeSpecification::Spec { spec } => spec.slice = dated,
other => panic!("expected Spec, got {other:?}"),
}
assert!(!tax.same_value_type(&tax_later_slice));
let mut other_repo = tax.clone();
match &mut other_repo.specifications {
TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
other => panic!("expected Spec, got {other:?}"),
}
assert!(!tax.same_value_type(&other_repo));
}
#[test]
fn spec_identity_displays_repository_space_spec_space_effective() {
use crate::parsing::ast::EffectiveDate;
let workspace = SpecIdentity {
repository: None,
spec: "tax_bracket".to_string(),
effective: EffectiveDate::Origin,
slice: EffectiveDate::Origin,
};
assert_eq!(workspace.to_string(), "tax_bracket");
let start =
EffectiveDate::DateTimeValue(DateTimeValue::from_str("2026-01-01").expect("date"));
let dated = SpecIdentity {
repository: Some("@jack/finance".to_string()),
spec: "tax_bracket".to_string(),
effective: start.clone(),
slice: start,
};
assert_eq!(
dated.to_string(),
format!(
"@jack/finance tax_bracket {}",
DateTimeValue::from_str("2026-01-01").expect("date")
)
);
let later_slice = SpecIdentity {
slice: EffectiveDate::DateTimeValue(
DateTimeValue::from_str("2026-07-01").expect("date"),
),
..workspace
};
assert_eq!(
later_slice.to_string(),
format!(
"tax_bracket slice {}",
DateTimeValue::from_str("2026-07-01").expect("date")
)
);
}
#[test]
fn type_family_displays_declaring_spec_then_type() {
let workspace = TypeFamily {
repository: None,
spec: "pricing".to_string(),
effective: crate::parsing::ast::EffectiveDate::Origin,
name: "money".to_string(),
};
assert_eq!(workspace.to_string(), "pricing.money");
let stdlib = TypeFamily {
repository: Some("lemma".to_string()),
spec: "units".to_string(),
effective: crate::parsing::ast::EffectiveDate::Origin,
name: "mass".to_string(),
};
assert_eq!(stdlib.to_string(), "lemma units.mass");
let dated = TypeFamily {
repository: None,
spec: "pricing".to_string(),
effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
DateTimeValue::from_str("2024-01-01").expect("date"),
),
name: "money".to_string(),
};
assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
}
#[test]
fn piecewise_type_keeps_spec_identity() {
use crate::parsing::ast::EffectiveDate;
use crate::planning::typing::{piecewise_type, MeasureScope};
use crate::planning::unit_index::UnitIndex;
let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
let unit_index = UnitIndex::new();
let signature_index = crate::computation::arithmetic::SignatureIndex::new();
let named_types = IndexMap::new();
let scope = MeasureScope {
unit_index: &unit_index,
signature_index: &signature_index,
named_types: &named_types,
};
let picked = piecewise_type([Arc::new(LemmaType::veto_type()), Arc::clone(&tax)], &scope);
assert!(picked.same_value_type(&tax));
}
#[test]
#[should_panic(expected = "BUG: literal factor required")]
fn measure_unit_factor_panics_on_reference_scale() {
let units = crate::literals::MeasureUnits::from(vec![
crate::literals::MeasureUnit::from_reference(
"gbp".to_string(),
"rate".to_string(),
Vec::new(),
"rate".to_string(),
),
]);
let lemma_type = LemmaType::new(
"money".to_string(),
TypeSpecification::Measure {
units,
traits: vec![],
decomposition: None,
minimum: None,
maximum: None,
unit: None,
help: String::new(),
},
TypeExtends::Primitive,
);
let _ = lemma_type.measure_unit_factor("gbp");
}
}