use std::collections::BTreeMap;
use std::sync::Arc;
use num_rational::Ratio;
use crate::diag::Span;
use crate::eval::mag::Mag;
use crate::dim::Dimension;
use crate::quantity::UnitExpr;
use crate::{Diag, Resolver};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Symbol(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EquationProvenance {
pub pack_id: String,
pub title: String,
pub edition: String,
pub license: String,
pub namespace: String,
pub equation_id: String,
pub cite: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolConstraint {
pub dim: Dimension,
pub sites: Vec<Span>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConstraintSet {
pub symbol_dims: BTreeMap<Symbol, SymbolConstraint>,
}
impl ConstraintSet {
pub fn new() -> Self {
Self::default()
}
pub fn pin_at(&mut self, sym: Symbol, dim: Dimension, span: Span) -> Result<(), Diag> {
use crate::diag::{Diagnostic, ErrorCode, Hint};
if let Some(existing) = self.symbol_dims.get(&sym) {
if existing.dim != dim {
let primary = existing.sites.first().copied().unwrap_or(span);
return Err(Diag::new(
Diagnostic::error(
ErrorCode::DimMismatch,
format!("symbol `{}` has conflicting dimension constraints", sym.0),
primary,
)
.with_hints(vec![
Hint::ExpectedDimension(format!("{:?}", existing.dim)),
Hint::FoundDimension(format!("{dim:?}")),
Hint::RelatedSpan(span),
]),
));
}
return Ok(());
}
self.symbol_dims.insert(
sym,
SymbolConstraint {
dim,
sites: vec![span],
},
);
Ok(())
}
pub fn pin(&mut self, sym: Symbol, dim: Dimension) -> Result<(), Diag> {
self.pin_at(sym, dim, Span::empty(0))
}
pub fn dimension_of(&self, sym: &Symbol) -> Option<Dimension> {
self.symbol_dims.get(sym).map(|c| c.dim.clone())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymUnaryOp {
Neg,
Sqrt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymBinaryOp {
Add,
Sub,
Mul,
Div,
Pow,
}
#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)]
pub enum SymNode {
Known(Quantity),
Symbol(Symbol),
Unary {
op: SymUnaryOp,
operand: Box<SymNode>,
},
Binary {
op: SymBinaryOp,
left: Box<SymNode>,
right: Box<SymNode>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct SymExpr {
pub root: SymNode,
pub text: String,
pub free_symbols: Vec<Symbol>,
pub constraints: ConstraintSet,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Known(Quantity),
Symbolic(SymExpr),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Quantity {
pub mag: Mag,
pub unit: UnitExpr,
pub dim: Dimension,
pub provenance: Option<Arc<EquationProvenance>>,
}
impl Quantity {
pub fn new(mag: Mag, unit: UnitExpr, dim: Dimension) -> Self {
Self {
mag,
unit,
dim,
provenance: None,
}
}
pub fn from_exact(magnitude: Ratio<i128>, unit: UnitExpr, dim: Dimension) -> Self {
Self::new(Mag::exact(magnitude), unit, dim)
}
pub fn from_int(n: i128, unit: impl Into<String>, dim: Dimension) -> Self {
Self::from_exact(Ratio::from_integer(n), UnitExpr::named(unit), dim)
}
#[allow(clippy::result_unit_err)]
pub fn from_float(f: f64, unit: UnitExpr, dim: Dimension) -> Result<Self, ()> {
Ok(Self::new(Mag::float(f)?, unit, dim))
}
pub fn is_exact(&self) -> bool {
self.mag.is_exact()
}
pub fn as_f64(&self) -> f64 {
self.mag.as_f64()
}
pub fn exact_ratio(&self) -> Option<Ratio<i128>> {
self.mag.exact_ratio()
}
pub fn convert_to(
&self,
unit: &UnitExpr,
registry: &crate::Registry,
) -> Result<Quantity, Diag> {
let mut lints = crate::eval::lint_sink::LintSink::new();
crate::eval::units::convert_quantity(
self,
unit,
registry,
&mut lints,
crate::diag::Span::empty(0),
)
}
pub fn display(&self, registry: &crate::Registry, opts: &crate::FmtOptions) -> String {
crate::fmt::format_quantity(self, registry, opts)
}
}
impl Value {
pub fn bind(&self, resolver: &dyn Resolver) -> Result<Value, Diag> {
match self {
Self::Known(_) => Ok(self.clone()),
Self::Symbolic(s) => {
let bound = crate::eval::partial::bind_symbolic(s, resolver)?;
crate::eval::partial::finalize(bound, Span::empty(0))
}
}
}
pub fn free_symbols(&self) -> &[Symbol] {
match self {
Self::Known(_) => &[],
Self::Symbolic(s) => &s.free_symbols,
}
}
pub fn constraints(&self) -> &ConstraintSet {
match self {
Self::Known(_) => empty_constraints(),
Self::Symbolic(s) => &s.constraints,
}
}
pub fn quantity(&self) -> Option<&Quantity> {
match self {
Self::Known(q) => Some(q),
Self::Symbolic(_) => None,
}
}
}
#[macro_export]
macro_rules! qty {
($n:expr, $unit:expr) => {
$crate::eval::value::Quantity::from_int(
$n,
$unit,
$crate::dim::Dimension::dimensionless(),
)
};
($n:expr, $unit:expr, $dim:expr) => {
$crate::eval::value::Quantity::from_int($n, $unit, $dim)
};
}
#[allow(dead_code)]
pub mod rational {
use super::*;
use crate::diag::{Diag, Diagnostic, LintCode, Span};
use num_traits::CheckedAdd;
use num_traits::CheckedMul;
use num_traits::CheckedSub;
pub fn add(a: Ratio<i128>, b: Ratio<i128>) -> Result<Ratio<i128>, (f64, Diag)> {
match a.checked_add(&b) {
Some(r) => Ok(r),
None => {
let fa = ratio_to_f64(a);
let fb = ratio_to_f64(b);
let lint = Diag::new(Diagnostic::lint(
LintCode::RationalOverflow,
"rational addition overflowed i128; using float",
Span::empty(0),
));
Err((fa + fb, lint))
}
}
}
pub fn mul(a: Ratio<i128>, b: Ratio<i128>) -> Result<Ratio<i128>, (f64, Diag)> {
match a.checked_mul(&b) {
Some(r) => Ok(r),
None => {
let lint = Diag::new(Diagnostic::lint(
LintCode::RationalOverflow,
"rational multiplication overflowed i128; using float",
Span::empty(0),
));
Err((ratio_to_f64(a) * ratio_to_f64(b), lint))
}
}
}
pub fn sub(a: Ratio<i128>, b: Ratio<i128>) -> Result<Ratio<i128>, (f64, Diag)> {
match a.checked_sub(&b) {
Some(r) => Ok(r),
None => {
let lint = Diag::new(Diagnostic::lint(
LintCode::RationalOverflow,
"rational subtraction overflowed i128; using float",
Span::empty(0),
));
Err((ratio_to_f64(a) - ratio_to_f64(b), lint))
}
}
}
fn ratio_to_f64(r: Ratio<i128>) -> f64 {
let n: f64 = num_traits::ToPrimitive::to_f64(r.numer()).unwrap_or(0.0);
let d: f64 = num_traits::ToPrimitive::to_f64(r.denom()).unwrap_or(1.0);
n / d
}
}
fn empty_constraints() -> &'static ConstraintSet {
use std::sync::OnceLock;
static EMPTY: OnceLock<ConstraintSet> = OnceLock::new();
EMPTY.get_or_init(ConstraintSet::new)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dim::BaseDim;
use num_traits::One;
#[test]
fn quantity_exactness() {
let q = Quantity::from_int(12, "ft", Dimension::single(BaseDim::Length, Ratio::one()));
assert!(q.is_exact());
assert_eq!(q.as_f64(), 12.0);
}
#[test]
fn constraint_pin_conflict() {
let mut cs = ConstraintSet::new();
let sym = Symbol("x".into());
cs.pin(
sym.clone(),
Dimension::single(BaseDim::Force, Ratio::one()),
)
.unwrap();
assert!(cs
.pin(sym, Dimension::single(BaseDim::Length, Ratio::one()))
.is_err());
}
}