Skip to content

uom (Rust)

The mainstream Rust units library: a macro-generated Quantity<D, U, V> family whose dimensions are typenum type-level integer vectors, checked entirely by rustc's trait solver, with no runtime representation of dimensions at all.

FieldValue
LanguageRust (MSRV 1.68.0; no_std-capable — std is a default feature)
LicenseApache-2.0 OR MIT
Repositoryiliekturtles/uom
Documentationdocs.rs/uom · crates.io
Key authorsMike Boutin (iliekturtles, original author) and contributors
CategoryLibrary-level compile-time checking (no compiler support; declarative macro DSL)
Mechanismmacro_rules!-generated Quantity<D, U, V> where D is a dyn Dimension trait object carrying one typenum exponent per base quantity plus a Kind tag
Exponent domainℤ⁷typenum type-level integers (N1, Z0, P1, P2, …); rational exponents not representable
Checking timeCompile time (trait solving + associated-type equality); zero runtime checks
Analyzed versiona465bcc (pinned clone, 2026-04-04; version 0.38.0 + post-release merges)
Latest releasev0.38.0 (2026-02-13)

NOTE

uom is this survey's canonical data point for the type-level-integers + trait-arithmetic mechanism: the checker evaluates dimension arithmetic that the author spelled out, in contrast with the Kennedy lineage where the checker solves for it (F#, uom-plugin — see Kennedy's type system and the mechanism taxonomy). Its Rust sibling dimensioned uses the same typenum exponents with a different (struct-generic, non-macro-DSL) surface; the C++ analogues are Boost.Units, mp-units, and Au. See the comparison capstone for the cross-system synthesis.


Overview

What it solves

uom ("units of measurement") gives Rust programs dimensional analysis with no runtime cost and no new compiler machinery: dimensions live in the type system as phantom parameters, so adding metres to seconds is a type error and multiplying metres by metres is square metres. The crate-root docs state the positioning in the first sentence (src/lib.rs L1–7):

"Units of measurement is a crate that does automatic type-safe zero-cost dimensional analysis. You can create your own systems or use the pre-built International System of Units (SI) which is based on the International System of Quantities (ISQ) and includes numerous quantities (length, mass, time, ...) with conversion factors for even more numerous measurement units (meter, kilometer, foot, mile, ...). No more crashing your climate orbiter!"

The shipped SI implementation is large: 118 quantity modules under src/si/ (length through gyromagnetic_ratio), each with dozens of units, all generated by three declarative macros — system!, quantity!, and unit! — from short data-like declarations.

Design philosophy

Two decisions define uom against the other systems in this survey.

Quantities over units. A value is stored normalized to the quantity's base unit, and the unit name appears only at construction and extraction (src/lib.rs L107–113):

"Rather than working with measurement units (meter, kilometer, foot, mile, ...) uom works with quantities (length, mass, time, ...). This simplifies usage because units are only involved at interface boundaries: the rest of your code only needs to be concerned about the quantities involved. This also makes operations on quantities (+, -, *, /, ...) have zero runtime cost over using the raw storage type (e.g. f32)."

This is the opposite pole from mp-units and Au, which keep the unit in the type and convert lazily; uom converts eagerly at the boundary (new/get) and its arithmetic core never touches conversion factors. The cost is representability: storing 1 centimeter in an i32-backed Length normalized to metres truncates to 0 — the crate docs call this out explicitly (src/lib.rs L122–128).

Storage-type genericity. The value slot V of Quantity<D, U, V> is a feature- selected storage type — f32/f64, all signed/unsigned integer widths, BigInt, Rational/BigRational, and complex types, 22 in total (src/lib.rs L61–102, Cargo.toml features). Each enabled feature stamps out a parallel module tree (si::f32::*, si::f64::*, …) via the storage_types! macro.


How it works

Three macro layers generate the whole library; there is no procedural macro and no build step (the separate uom-macros crate is "currently a placeholder for future development efforts" per its README).

quantity! — one quantity, its dimension vector, its units

A quantity module declares its dimension as an exponent vector over the system's base quantities, spelled with typenum names — P1 = +1, Z0 = 0, N1 = −1 (src/si/length.rs, and the macro's own doc example in src/quantity.rs L31–49):

rust
// uom: examples/mks.rs — a custom quantity in a custom 3-dimensional system
quantity! {
    /// Length (base unit meter, m).
    quantity: Length; "length";
    /// Length dimension, m.
    dimension: Q<
        P1,  // length
        Z0,  // mass
        Z0>; // time
    units {
        @meter: 1.0E0; "m", "meter", "meters";
        @foot: 3.048E-1; "ft", "foot", "feet";
    }
}

Each @unit: coefficient; "abbr", "singular", "plural"; line becomes an empty struct implementing a Unit marker trait and a Conversion<V> giving the multiplicative factor to the base unit (plus an optional additive constant — used only by degree_celsius/degree_fahrenheit, see Expressiveness edges). The macro also emits the public alias pub type Length<U, V> = Quantity<Dimension, U, V>, the new::<unit>() / get::<unit>() accessors, a runtime Units enum, and a FromStr implementation (src/quantity.rs L108–452).

system! — the Dimension/Units traits and the Quantity struct

The system! macro (src/system.rs, 1 658 lines) is instantiated once per system of quantities — the SI instantiation is src/si/mod.rs L10–170. It generates a Dimension trait with one typenum::Integer associated type per base quantity, plus a Kind tag:

rust
// uom: src/system.rs L126-141 (generated per system; abridged)
pub trait Dimension: Send + Sync + Unpin + /* unwind-safety bounds */ {
    /// Quantity dimension.
    type L: crate::typenum::Integer;   // one per base quantity: L, M, T, I, Th, N, J
    // ...

    /// Kind of the quantity. Quantities of the same dimension but differing kinds
    /// are not comparable.
    type Kind: ?Sized;
}

and the single value-carrying struct of the whole library:

rust
// uom: src/system.rs L251-266 (abridged)
#[repr(transparent)]
pub struct Quantity<D, U, V>
where
    D: Dimension + ?Sized,
    U: Units<V> + ?Sized,
    V: crate::num::Num + crate::Conversion<V>,
{
    pub dimension: PhantomData<D>,  // exponent vector + kind (type-level only)
    pub units: PhantomData<U>,      // base units of the system (type-level only)
    pub value: V,                   // the one real field, in base units
}

A dimension is then named by a trait-object type with associated-type bindings — type ISQ<L, M, T, I, Th, N, J, K = dyn Kind> = dyn Dimension<L = L, …, Kind = K> (src/system.rs L279–281) — so Area is literally the type Quantity<ISQ<P2, Z0, Z0, Z0, Z0, Z0, Z0>, SI<f32>, f32> (src/system.rs L206–216). Arithmetic is trait impls whose output type does typenum arithmetic on the exponents:

rust
// uom: src/system.rs L490-514 (generated Mul/Div between quantities; abridged)
impl<Dl, Dr, U, V> Mul<Quantity<Dr, U, V>> for Quantity<Dl, U, V>
where
    Dl::L: Add<Dr::L>,                      // one bound per base-quantity symbol,
    <Dl::L as Add<Dr::L>>::Output: Integer, // repeated for M, T, I, Th, N, J
    Dl::Kind: crate::marker::Mul,
    Dr::Kind: crate::marker::Mul,
    /* ... */
{
    type Output = Quantity<ISQ<Sum<Dl::L, Dr::L>, /* …, per symbol */>, U, V>;

    #[inline(always)]
    fn mul(self, rhs: Quantity<Dr, U, V>) -> Self::Output {
        Quantity { dimension: PhantomData, units: PhantomData,
                   value: self.value * rhs.value }
    }
}

Multiplication adds exponent vectors; division subtracts them — the free-abelian-group model computed by the trait solver. Addition/subtraction require both operands to have the same D (same exponents and same Kind), so a mismatch is an ordinary E0308 type error.

ISQ! — storage and base-unit aliases

The system! macro also emits a per-system alias macro (named after the system, ISQ! for SI) that stamps out the user-facing type aliases for a chosen storage type and, optionally, a non-default set of base units (src/system.rs L1604–1629):

rust
// uom: examples/base.rs — CGS aliases over the same ISQ system
mod cgs {
    ISQ!(uom::si, f32, (centimeter, gram, second, ampere, kelvin, mole, candela));
}

With the default autoconvert feature, quantities over different base units of the same system interoperate — binary ops insert a change_base conversion on the right operand (src/system.rs L352–375). Disabling autoconvert restricts ops to identical U, which exists precisely because the conversion is not always free (src/lib.rs L88–91):

"autoconvert -- Feature to enable automatic conversion between base units in binary operators. Disabling the feature only allows for quantities with the same base units to directly interact. The feature exists to account for compiler limitations where zero-cost code is not generated for non-floating point underlying storage types."


Dimension representation

The dimension of a quantity is a ℤ⁷ exponent vector encoded as seven typenum::Integer associated types on the Dimension trait, one per ISQ base quantity (L, M, T, I, Th, N, J), carried by a PhantomData field (src/system.rs L126–141, L251–266). typenum integers are binary-coded types (P1 is an alias for PInt<UInt<UTerm, B1>>), so exponents are unbounded in range but strictly integers — the associated-type bound is $crate::typenum::Integer, and nothing in the crate can express L^(1/2). Rational exponents are absent by construction, not by omission — a finding shared with dimensioned and contrasted by mp-units' rational powers.

Two representation choices are distinctive:

  • The dimension is a dyn Trait type, not a nominal struct. uom names dimensions as trait-object types with associated-type bindings (dyn Dimension<L = P1, M = Z0, …, Kind = dyn Kind>). Trait objects with the same (canonically sorted) binding list are the same type, so structural equality of exponent vectors is literal type identity — normalization comes for free, with no reduction step and no "same dimension, different spelling" trap. The ?Sized bounds on D and U throughout the crate exist because trait objects are unsized. The price is visible in diagnostics (see Diagnostics).
  • Kind is an eighth, non-group slot. Alongside the seven integers sits type Kind: ?Sized — an open-set tag used to keep same-dimension quantities apart (angle vs ratio, torque vs energy). Unlike the exponents it does not obey group arithmetic; see Expressiveness edges for how it propagates.

Dimension-one is the all-zero vector: pub type DimensionOne = DN<Z0> (src/system.rs L269–277), the type behind the Ratio quantity.

Checking & inference

All checking is ordinary Rust trait solving — there is no plugin, no build.rs, and no custom solver. Addition demands identical D on both sides (associated-type equality); multiplication's output type evaluates Sum<Dl::L, Dr::L> per symbol via typenum's closed type-level arithmetic. The process is decidable and terminating (trait resolution over concrete types with a recursion limit); nothing is ever solved for — in the taxonomy of this survey's theory tree, uom sits in the "checker evaluates, never solves" row, the opposite of the AG-unification built into F# and retrofitted by uom-plugin.

Rust's local type inference still gives pleasant forward inference: in the crate's own README example, let velocity = length / time; infers Velocity without annotation (README.md L42–63). What it cannot do is invert arithmetic — the argument type of a square root cannot be inferred from a desired result type, and no Kennedy-style principal-type generalization exists.

Dimensional polymorphism is expressible, with the arithmetic spelled out as bounds. The generic sqr : α → α² exists in the crate as Quantity::powi, whose signature must carry one Mul bound per base-quantity symbol (seven for SI), because the trait solver needs each component's arithmetic stated explicitly (src/system.rs L887–901):

rust
// uom: src/system.rs L887-901 (abridged) — dimension-polymorphic integer power
pub fn powi<E>(self, _e: E)
    -> Quantity<ISQ<Prod<D::L, E>, /* …, per symbol */>, U, V>
where
    D::L: Mul<E>, <D::L as Mul<E>>::Output: Integer,  // ×7, one per symbol
    D::Kind: crate::marker::Mul,
    E: Integer,
{
    Quantity { /* … */ value: self.value.powi(E::to_i32()) }
}

let a: Area = Length::new::<meter>(3.0).powi(P2::new()); type-checks; user-written generic functions over dimension are possible in exactly the same style and inherit the same seven-fold where-clauses. The bounds are per-system (a 3-dimensional mks system needs three), which is why such signatures cannot be written once generically over all systems.

Extensibility

Extension has three tiers, each grounded in a shipped example:

  • New units on an existing quantity — cheap. The unit! macro adds units to any quantity of a compiled system, including the shipped SI, from user code (src/unit.rs L1–120, demonstrated in examples/unit.rs):

    rust
    // uom: src/unit.rs doc example — adding kilometers to a length quantity
    unit! {
        system: crate::mks;
        quantity: crate::mks::length;
    
        @kilometer: 1.0E-03; "km", "kilometer", "kilometers";
    }

    The macro doc pitches it precisely at this gap — "When using the pre-built SI system included with uom this macro allows for new units to quickly be defined without requiring a release" — and notes the trade-off: manually defined units "will not be included in the quantity unit enum or associated functions, or in the FromStr implementation" (src/unit.rs L1–8).

  • New quantities in an existing system — a quantity! invocation plus registration in the system! unit list. SI prefixes come from a prefix! macro of literal factors ((kilo) => { 1.0_E3 }, plus binary kibiyobisrc/si/prefix.rs), so a typical unit line is @kilometer: prefix!(kilo); "km", ….

  • A whole new system of quantities — system! from scratch.examples/mks.rs builds a three-base-quantity system in ~80 lines. The base-dimension set is closed per system: the exponent-vector length is fixed by the system! invocation, so adding an eighth base quantity to SI means editing the library (or forking the system), not extending it from user code. There is also no interop between systems — two system! invocations produce unrelated Dimension traits and Quantity structs; a mks length and an si length are foreign types with no conversion path. Within one system, alternative base units (CGS over ISQ) are supported via ISQ!/autoconvert, with the documented restriction that "a unit with a non-zero constant factor is not currently supported as a base unit" (src/system.rs L1529–1533).

Custom Kinds are user-definable in the same pattern the SI uses (a marker trait extending uom::Kind, named in a quantity!'s optional kind: field — src/quantity.rs L14–16, L457–460).

Expressiveness edges

  • Fractional powers: absent. Exponents are typenum::Integers; sqrt exists only for quantities whose every exponent is divisible by two, enforced by a PartialDiv<P2> bound per symbol, and cbrt by P3 (src/system.rs L922–937, L821–836). The crate's own compile-fail doctest documents the edge: Length::new::<meter>(4.0).sqrt() fails with error[E0271]: type mismatch resolving … (src/system.rs L912–919). Quantity<L^(1/2)> is unwritable; there is no uom type for, say, the V/√Hz noise-density idiom.

  • Affine quantities: modeled by kind + a hand-written torsor.ThermodynamicTemperature (points) and TemperatureInterval (differences) share the dimension Th but differ in kind; TemperatureKind deliberately omits the marker::Add/marker::Sub capabilities, so point + point does not compile, while point ± interval is implemented by hand-written impls (src/si/thermodynamic_temperature.rs L50–174, src/si/temperature_interval.rs). The module doc states the semantics:

    "Thermodynamic temperature has the same dimensions as temperature interval but is not directly comparable. Thermodynamic temperature is the absolute measure of temperature ... Temperature interval is the measure of relative temperature difference between thermodynamic temperatures."

    degree_celsius on the point type is the one place the conversion constant slot is used (@degree_celsius: 1.0_E0, 273.15_E0;src/si/thermodynamic_temperature.rs L90); on the interval type Celsius is a pure scale factor. This is a concrete instance of the torsor / affine-quantity model, restricted to temperature — no general point-vs-difference machinery exists (no position-vs-displacement, no timestamp-vs-duration; contrast Au's QuantityPoint and mp-units' quantity_point).

  • Logarithmic quantities: absent. No decibel, neper, or level-of-field/power quantity exists anywhere under src/si/ (verified by search of the pinned clone) — dB ergonomics are out of scope entirely, as in most systems in this survey (Pint being the notable exception).

  • Angle and information: dimensionless-with-kind. Angle has the all-zero dimension vector plus kind: dyn crate::si::marker::AngleKind (src/si/angle.rs L6–18); Information (bits, bytes, and their decimal / binary multiples) likewise carries InformationKind (src/si/mod.rs L213–224). Kinds gate comparability — Angle == Ratio does not compile — while explicit From conversions re-connect them (let r: Ratio = a.into();, src/si/mod.rs L187–198). Trigonometry lives on Angle (sin/cos/tan return Ratiosrc/si/angle.rs L68–120).

  • Kind-vs-dimension disambiguation is real but shallow. The Kind mechanism (src/system.rs L138–141: "Quantities of the same dimension but differing kinds are not comparable") separates the classic collision pairs: Torque (L²MT⁻², AngleKind) from Energy (default kind), and three distinct T⁻¹ quantities — Frequency (default kind), Radioactivity (tagged ConstituentConcentrationKind, an implementation quirk: the becquerel borrows a chemistry kind rather than getting its own — src/si/radioactivity.rs L15), and AngularVelocity (AngleKind). So Hz vs Bq and J vs N·m are compile-time-distinct: a genuine capability most integer-vector systems lack.

  • But kind is erased by quantity × quantity. The generated Mul/Div output type passes no kind argument, so it defaults to dyn Kind (src/system.rs L502–504): multiplying or dividing any two quantities yields a default-kind result. AngularVelocity × Time is default-kind dimensionless (Ratio), not Angle; Torque × Angle lands on Energy (dimensionally right, but by erasure rather than by an angle-aware algebra). Only scalar-by-quantity multiplication preserves kind (src/system.rs L554–579; changelog v0.23.0, issue #138). Kinds are comparability tags, not elements of the dimension group.

  • No frame/origin tracking beyond temperature. The Torque docs are candid about the boundary (src/si/torque.rs L5–9):

    "Torque is a moment, the product of distance and force. Moments are inherently dependent on the distance from a fixed reference point. This library does not capture this dependency. As a consequence, there are no compile time guarantees that only moments of the same frame can be combined."

Zero-cost story

The claim "zero-cost" is structural, not benchmark-backed — there is no benches/ directory in the repository. The evidence in the clone:

  • #[repr(transparent)] on Quantity<D, U, V> (src/system.rs L251): the struct is guaranteed ABI-identical to its single non-zero-sized field value: V. The two PhantomData fields are zero-sized; a Length is an f64 at runtime, in registers and in memory. Locally verified: size_of::<Length>() == size_of::<f64>() holds (rustc 1.91.1, 2026-07-03, against the pinned clone).
  • #[inline(always)] on every arithmetic impl and on the to_base/from_base conversion helpers (src/system.rs passim), so the phantom-typed wrapper ops collapse to the underlying scalar ops.
  • Eager normalization means steady-state arithmetic touches no conversion factors: new::<kilometer>(5.0) multiplies once at the boundary (src/system.rs L328–350), and l1 + l2 on same-U quantities compiles to a bare + (src/system.rs L409–427, the non-autoconvert impl).
  • The documented exception: with autoconvert enabled, cross-base-unit ops route through change_base, which computes coefficient().powi(exp) per base quantity (src/system.rs L352–375). For float storage these are constant-foldable literals; for integer/rational storage they are Ratio<V> operations that the optimizer does not eliminate — which is precisely why the feature exists as an opt-out ("compiler limitations where zero-cost code is not generated for non-floating point underlying storage types", src/lib.rs L88–91). The zero-cost claim is cleanly true for the default float-storage, same-base-unit path; it is qualified elsewhere, and the crate says so itself.

Runtime artifacts do exist — the per-quantity Units enum, FromStr, and the formatting machinery (src/quantity.rs L158–206, L315–451) — but they are opt-in call sites, not overhead on arithmetic.

Diagnostics

The mandated experiment — adding a Length to a Time — against the pinned clone (path dependency on $REPOS/rust/uom @ a465bcc):

rust
// locally reproduced — mismatch/src/main.rs
use uom::si::f64::*;
use uom::si::length::meter;
use uom::si::time::second;

fn main() {
    let l = Length::new::<meter>(1.0);
    let t = Time::new::<second>(1.0);
    let err = l + t;
    println!("{:?}", err);
}
text
error[E0308]: mismatched types
 --> src/main.rs:8:19
  |
8 |     let err = l + t;
  |                   ^ expected `Quantity<..., _, f64>`, found `Quantity<..., ..., f64>`
  |
  = note: expected struct `Quantity<dyn Dimension<I = Z0, J = Z0, Kind = (dyn Kind + 'static), L = PInt<UInt<UTerm, B1>>, M = Z0, N = Z0, T = Z0, Th = Z0>, _, _>`
             found struct `Quantity<..., ..., f64>`
  = note: the full name for the type has been written to '.../target/debug/deps/mismatch-....long-type-5624342945472187492.txt'
  = note: consider using `--verbose` to print the full type name to the console

For more information about this error, try `rustc --explain E0308`.

[reproduced locally, rustc 1.91.1 / cargo 1.91.0 (nixpkgs), 2026-07-03]

The signal is correct and precise — the expected type shows length's +1 exponent as L = PInt<UInt<UTerm, B1>> (that is typenum's binary encoding of P1) against the found type's T = PInt<UInt<UTerm, B1>> — but the notorious unreadability is on full display: rustc truncates the "found" type to Quantity<..., ..., f64> and dumps the real name to a side file. That file's single line, verbatim, is what the user must decode:

text
Quantity<dyn Dimension<I = Z0, J = Z0, Kind = (dyn Kind + 'static), L = Z0, M = Z0, N = Z0, T = PInt<UInt<UTerm, B1>>, Th = Z0>, dyn uom::si::Units<f64, amount_of_substance = uom::si::amount_of_substance::mole, electric_current = uom::si::electric_current::ampere, length = uom::si::length::meter, luminous_intensity = uom::si::luminous_intensity::candela, mass = uom::si::mass::kilogram, thermodynamic_temperature = uom::si::thermodynamic_temperature::kelvin, time = uom::si::time::second>, f64>

No uom-side rendering intervenes — there is nothing like mp-units' symbol-text types or F#'s measure pretty-printer to say "expected m, found s". The pinned clone corroborates the expected errors as rung-2 evidence in its own compile-fail doctests: Length + Time and let v: Velocity = Length * Time are both annotated // error[E0308]: mismatched types (src/system.rs L227–243), Length::new::<second>(…) is // error[E0277]: the trait bound 'second: length::Unit' is not satisfied (src/system.rs L218–225), and the temperature-point addition compile-fail lives in src/si/thermodynamic_temperature.rs L9–31.

The valid-path counterpart, run against the same pinned clone [reproduced locally, rustc 1.91.1, 2026-07-03]:

rust
// locally reproduced — same-dimension addition, mixed units; L/T -> Velocity
let l1 = Length::new::<meter>(500.0);
let l2 = Length::new::<kilometer>(1.5);
let sum = l1 + l2;                                 // OK: same dimension
let v: Velocity = sum / Time::new::<second>(4.0);  // OK: L/T evaluates to Velocity
assert_eq!(v.get::<meter_per_second>(), 500.0);    // passes; prints "v = 500 m/s"

Ergonomics & compile-time cost

Declaration overhead is low for the common cases. Using the shipped SI is use uom::si::f64::*; plus unit imports; a new unit is a 4-line unit!; a new quantity is a ~15-line quantity!. Only a new system requires understanding the three-macro architecture, and examples/mks.rs keeps even that under 100 lines. The macro DSL's data-like surface is a genuine ergonomic win over template-metaprogramming equivalents (Boost.Units).

Error readability is the weak flank, as quoted above: typenum binary encodings (PInt<UInt<UTerm, B1>> for +1), eight associated-type bindings printed in alphabetical order (I, J, Kind, L, M, N, T, Th — not the conventional L, M, T, …), the dyn Units<…> base-unit record repeated in full, and rustc's long-type truncation-to-file behavior. Errors are sound and point at the right operand; they are just written in the implementation's language rather than the domain's.

Compile-time cost is real, measured, and actively worked on. Locally, a clean debug build of the default feature set (autoconvert, f32, f64, si, std — i.e. the full 118-quantity SI, twice, plus typenum and num-traits) takes 16.1 s wall / ~1.1 GB peak RSS for the uom crate and its dependency graph [measured locally, rustc 1.91.1, dev profile, 2026-07-03]. Each enabled storage-type feature multiplies the generated code via storage_types!. The project tracks this: the v0.38.0 changelog reports that the release "compiles 15 % +/- 1 % faster than v0.37.0 even after the numerous quantity and unit additions", attributing the win to "simplifying macro patterns or call trees" (CHANGELOG.md #533, #536, #537).


Strengths

  • True zero-cost core, structurally guaranteed#[repr(transparent)] + PhantomData + #[inline(always)]; a quantity is its storage scalar at the ABI level, verified by size_of and stated by the docs.
  • The Kind mechanism — same-dimension quantities (torque/energy, Hz/Bq, angle/ratio, temperature point/interval) are compile-time distinct; rare among integer-vector libraries and ahead of F#'s pure Kennedy model.
  • Breadth of the shipped SI — 118 quantity modules, thousands of units with vetted conversion factors (SI Brochure / NIST SP 811 cited as the contribution references, src/lib.rs L130–135), maintained continuously since 2017.
  • Storage genericity — the same dimensional machinery over floats, all integer widths, big/rational/complex types; no_std support for embedded use.
  • Affine temperature done honestly — point vs interval as types, with the Celsius/Fahrenheit constant handled at the unit-conversion layer.
  • Declarative, readable definitionsquantity!/unit! declarations read like data tables; extension by downstream crates needs no fork for new units.

Weaknesses

  • Integer-only exponents — no powers, so sqrt is partial (even exponents only) and half-integer dimensions are unwritable; see Dimension representation.
  • Unreadable errorstypenum-mangled, alphabetized, truncated-to-file type names with no domain-language rendering; among the survey's least readable diagnostics (only dimensioned's nameless positional arrays are worse), quoted in Diagnostics.
  • Kind erasure under multiplication — kinds don't compose; every quantity-times-quantity result resets to the default kind, so the angle/information distinctions vanish exactly where an angle-aware algebra would need them.
  • Closed base-dimension set, no cross-system interop — extending SI with a new base quantity (currency, pixels) means a new system! world with no bridge to the old one.
  • Eager normalization has representational costs — integer/rational storage can't represent sub-base-unit values (1 cm as i32 metres is 0); float storage pays precision at the boundary; and autoconvert's conversions are documented as not-zero-cost off the float path.
  • No dimensional inference, verbose polymorphism — generic-over-dimension code carries seven-fold typenum where-clauses per operation; nothing like Kennedy-style principal types.
  • Compile-time weight — tens of seconds and ~1 GB RSS for the full SI in a clean build, scaling with enabled storage features.

Key design decisions and trade-offs

DecisionRationaleTrade-off
typenum type-level exponents, trait-solver arithmeticWorks on stable Rust since 2017 (pre-const-generics); decidable; no compiler pluginInteger-only powers; PInt<UInt<UTerm, B1>> leaks into every error; 7-fold bounds on generic code
Dimension as dyn Dimension<…> trait object + PhantomDataStructural identity of exponent vectors — normalization for free; one Quantity struct serves every dimension?Sized bounds everywhere; trait-object names dominate diagnostics
Quantities-over-units: eager normalization to base unitsArithmetic core never converts; ops are bare scalar ops (#[inline(always)], #[repr(transparent)])Boundary rounding; integer storage can't hold sub-base values; unit identity is forgotten after construction
Kind associated type (v0.20.0)Distinguishes same-dimension quantities (torque/energy, Hz/Bq, angle/ratio) without inventing fake dimensionsKinds erase under ×/÷; ad-hoc reuse (becquerel tagged with a chemistry kind); From boilerplate to cross kinds
macro_rules! DSL (system!/quantity!/unit!), no proc macrosDeclarations read as data; no build-dependency cost; downstream unit! extensionthe 1 658-line system.rs macro is hard to evolve; uom-macros proc-macro successor still a placeholder
Storage-type genericity via feature-gated storage_types!One library serves f32BigRational, no_std embedded targetsCode size and compile time multiply per enabled feature
autoconvert default-onMixed base-unit systems (CGS vs MKS aliases) interoperate silentlyOff the float path the inserted conversions are not zero-cost — acknowledged by the feature's own existence

Sources