Project 9: Physical Units Lib (Type-Safe Engineering)
Project 9: Physical Units Lib (Type-Safe Engineering)
Goal: Build a compile-time units library where dimensional analysis errors are impossible. Learn to encode physics in Rustโs type system, making the compiler your co-engineer.
- Main Programming Language: Rust
- Coolness Level: Level 3: Genuinely Clever
- Difficulty: Level 3: Advanced
- Knowledge Area: Type Systems / Metaprogramming
- Estimated Time: 1 week
Learning Objectives
By completing this project, you will be able to:
- Explain the Mars Climate Orbiter disaster and why type-safe units prevent $125M mistakes
- Describe dimensional analysis and how the 7 SI base units combine into derived units
- Use PhantomData to carry type information with zero runtime cost
- Implement operator overloading using
std::opstraits for custom types - Design type-level arithmetic where unit multiplication/division produces correct derived types
- Verify zero-cost abstractions by inspecting generated assembly
- Create ergonomic APIs with extension traits like
.meters()and.seconds() - Apply const generics to represent dimensional exponents at the type level
- Write compile-fail tests that verify invalid operations are rejected
- Connect theory to practice by understanding real-world crates like
uomanddimensioned
Deep Theoretical Foundation
Before writing any code, you must understand why this problem matters and how Rustโs type system makes the impossible possible.
The $125 Million Bug: Mars Climate Orbiter
On September 23, 1999, NASAโs Mars Climate Orbiter was lost. The spacecraft was supposed to orbit Mars at an altitude of 140-150 km. Instead, it entered the atmosphere at approximately 57 km and was destroyed by atmospheric stresses.
The cause? A unit conversion error.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MARS CLIMATE ORBITER FAILURE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Lockheed Martin Software NASA Navigation Software โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Thruster data sent โ โ โ Expected data in โ โ
โ โ in POUND-FORCE โ โ NEWTON-SECONDS โ โ
โ โ SECONDS โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 1 lbfยทs = 4.44822 Nยทs โ
โ โ
โ Result: Trajectory calculations were off by factor of ~4.45 โ
โ Spacecraft entered Mars atmosphere too low and was destroyed โ
โ โ
โ Cost: $125,000,000 USD โ
โ Cause: Software assumed metric; hardware sent imperial โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ

In a dynamically typed or weakly typed system, this bug is invisible:
// C code - compiles fine, causes spacecraft loss
double thrust = get_thruster_data(); // Returns pound-force-seconds
double velocity = calculate_delta_v(thrust); // Expects newton-seconds
// No error. No warning. $125M lost.
In Rust with a proper units library:
// Rust with type-safe units - doesn't compile
let thrust: Value<f64, PoundForceSeconds> = get_thruster_data();
let velocity = calculate_delta_v(thrust); // Error: expected NewtonSeconds
// COMPILE ERROR: Cannot convert PoundForceSeconds to NewtonSeconds implicitly
This project teaches you to build that safety net.
Dimensional Analysis: The Physics Behind the Types
Every physical quantity can be expressed as a combination of 7 SI base units:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE 7 SI BASE UNITS โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Quantity โ Unit โ Symbol โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Length โ meter โ m โ
โ Time โ second โ s โ
โ Mass โ kilogram โ kg โ
โ Electric curr. โ ampere โ A โ
โ Temperature โ kelvin โ K โ
โ Amount of sub. โ mole โ mol โ
โ Luminous int. โ candela โ cd โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Derived units are combinations of base units through multiplication and division:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DERIVED UNITS EXAMPLES โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Quantity โ Formula โ SI Representation โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Velocity โ ฮx/ฮt โ m/s = mยนยทsโปยน โ
โ Acceleration โ ฮv/ฮt โ m/sยฒ = mยนยทsโปยฒ โ
โ Force โ mยทa โ N = kgยนยทmยนยทsโปยฒ โ
โ Energy โ Fยทd โ J = kgยนยทmยฒยทsโปยฒ โ
โ Power โ E/t โ W = kgยนยทmยฒยทsโปยณ โ
โ Pressure โ F/A โ Pa = kgยนยทmโปยนยทsโปยฒ โ
โ Frequency โ 1/t โ Hz = sโปยน โ
โ Area โ l ร w โ mยฒ = mยฒ โ
โ Volume โ l ร w ร h โ mยณ = mยณ โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Notice the pattern: every derived unit is a product of base units raised to integer powers.
Dimensional Exponents: The Key Insight
We can represent any unit as a tuple of 7 integers (the exponents):
Unit<M, S, KG, A, K, MOL, CD> where each parameter is an integer exponent
Examples:
Meter = Unit<1, 0, 0, 0, 0, 0, 0> (mยน)
Second = Unit<0, 1, 0, 0, 0, 0, 0> (sยน)
Kilogram = Unit<0, 0, 1, 0, 0, 0, 0> (kgยน)
Velocity = Unit<1, -1, 0, 0, 0, 0, 0> (mยนยทsโปยน)
Acceleration = Unit<1, -2, 0, 0, 0, 0, 0> (mยนยทsโปยฒ)
Force = Unit<1, -2, 1, 0, 0, 0, 0> (mยนยทsโปยฒยทkgยน)
Dimensionless= Unit<0, 0, 0, 0, 0, 0, 0> (pure number)
Unit multiplication = exponent addition:
Velocity ร Time = Distance
(mยนยทsโปยน) ร (sยน) = mยนยทsโฐ = mยน
Exponents: [1, -1, 0, ...] + [0, 1, 0, ...] = [1, 0, 0, ...]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Velocity Time Distance
Unit division = exponent subtraction:
Distance รท Time = Velocity
(mยน) รท (sยน) = mยนยทsโปยน
Exponents: [1, 0, 0, ...] - [0, 1, 0, ...] = [1, -1, 0, ...]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Distance Time Velocity
Type-Level Programming in Rust
Rust allows us to encode values in types. This happens at compile time, not runtime.
// Runtime value - exists when program runs
let x: i32 = 5;
// Type-level value - exists only at compile time
struct Meters;
struct Seconds;
// Const generic - value embedded in type
struct Matrix<const ROWS: usize, const COLS: usize>;
For our units library, we use marker types (types that carry information but have no data):
// Marker types for base units
struct Meter;
struct Second;
struct Kilogram;
// These types have SIZE = 0 bytes!
assert_eq!(std::mem::size_of::<Meter>(), 0);
PhantomData: The Ghost in the Machine
PhantomData<T> is a zero-sized type that โpretendsโ to own a T without actually storing one.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PhantomData Explained โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ struct Value<T, U> { โ
โ val: T, // Actual data (e.g., f64) โ
โ _unit: PhantomData<U>, // Type marker, 0 bytes โ
โ } โ
โ โ
โ Memory Layout: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ val: f64 (8 bytes) โ Total size: 8 bytes โ
โ โโโโโโโโโโโโโโโโโโโโโโโ PhantomData adds NOTHING to memory โ
โ โ
โ But the TYPE carries unit information: โ
โ โ
โ Value<f64, Meter> โ The compiler knows this is "meters" โ
โ Value<f64, Second> โ The compiler knows this is "seconds" โ
โ โ
โ Different types = Cannot mix accidentally! โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why does PhantomData exist?
- Variance: Tells the compiler how
Value<T, U>relates toValue<T, V>in terms of lifetime/type covariance - Drop checking: Affects how the compiler reasons about when data can be dropped
- Zero runtime cost: The marker type is optimized away completely
use std::marker::PhantomData;
struct Value<T, U> {
val: T,
_unit: PhantomData<U>,
}
// Construction requires specifying the unit type
let distance: Value<f64, Meter> = Value {
val: 100.0,
_unit: PhantomData,
};
Zero-Sized Types (ZSTs) and Their Optimization
When a type has no fields (or only ZST fields), Rust allocates zero bytes for it:
// All of these are ZSTs (0 bytes):
struct Meter;
struct Second;
struct Unit<const M: i8, const S: i8, const KG: i8>;
fn main() {
println!("Meter size: {} bytes", std::mem::size_of::<Meter>()); // 0
println!("PhantomData size: {} bytes",
std::mem::size_of::<PhantomData<Meter>>()); // 0
println!("Value<f64, Meter> size: {} bytes",
std::mem::size_of::<Value<f64, Meter>>()); // 8 (just the f64)
}
This is the magic: the unit information exists only in the type system, not in memory.
Operator Overloading with std::ops Traits
Rust allows you to define how operators work on custom types via traits:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ std::ops Traits โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Trait โ Operator โ Method โ Example โ
โ โโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ โ
โ Add<RHS> โ + โ fn add(...) โ a + b โ
โ Sub<RHS> โ - โ fn sub(...) โ a - b โ
โ Mul<RHS> โ * โ fn mul(...) โ a * b โ
โ Div<RHS> โ / โ fn div(...) โ a / b โ
โ Neg โ - โ fn neg(...) โ -a โ
โ โ
โ Each trait has an associated type `Output`: โ
โ โ
โ impl Mul<Time> for Distance { โ
โ type Output = ???; // What type results from Distance ร Time? โ
โ fn mul(self, rhs: Time) -> Self::Output { ... } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
For units:
AddandSubshould only work for same units (meters + meters = meters)MulandDivshould produce derived units (meters / seconds = velocity)
Trait Arithmetic: The Pattern for Unit Multiplication
Here is the key pattern that makes dimensional analysis work at compile time:
// For each pair of unit types, define what their product is
trait UnitMul<RHS> {
type Output; // The resulting unit type
}
// Meter ร Second = MeterSecond
impl UnitMul<Second> for Meter {
type Output = MeterSecond;
}
// Distance / Time = Velocity
trait UnitDiv<RHS> {
type Output;
}
impl UnitDiv<Second> for Meter {
type Output = Velocity; // m / s = mยทsโปยน
}
But manually implementing every combination is tedious. The elegant solution uses const generics:
// Unit type with dimensional exponents as const generics
struct Unit<const M: i8, const S: i8, const KG: i8>;
// Type aliases for readability
type Meter = Unit<1, 0, 0>; // mยน
type Second = Unit<0, 1, 0>; // sยน
type Velocity = Unit<1, -1, 0>; // mยนยทsโปยน
type Acceleration = Unit<1, -2, 0>; // mยนยทsโปยฒ
type Force = Unit<1, -2, 1>; // mยนยทsโปยฒยทkgยน
// Multiplication: add exponents
// Unit<M1, S1, KG1> ร Unit<M2, S2, KG2> = Unit<M1+M2, S1+S2, KG1+KG2>
// This requires const generic expressions (nightly feature)
// Or we use type-level integers with traits (stable)
Dimensional Exponent Arithmetic (ASCII Diagram)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UNIT MULTIPLICATION: EXPONENT ADDITION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Velocity ร Time = Distance โ
โ โ
โ Unit<M=1, S=-1, KG=0> ร Unit<M=0, S=1, KG=0> โ
โ โ โ โ
โ [1, -1, 0] + [0, 1, 0] โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ [1 + 0, -1 + 1, 0 + 0] โ
โ โ โ
โ [1, 0, 0] โ
โ โ โ
โ Unit<M=1, S=0, KG=0> = Meter (Distance) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ UNIT DIVISION: EXPONENT SUBTRACTION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Force รท Mass = Acceleration โ
โ โ
โ Unit<M=1, S=-2, KG=1> รท Unit<M=0, S=0, KG=1> โ
โ โ โ โ
โ [1, -2, 1] - [0, 0, 1] โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ [1 - 0, -2 - 0, 1 - 1] โ
โ โ โ
โ [1, -2, 0] โ
โ โ โ
โ Unit<M=1, S=-2, KG=0> = Acceleration โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Solution Architecture
Your library should have the following structure:
The Value Wrapper
use std::marker::PhantomData;
use std::ops::{Add, Sub, Mul, Div};
/// A value tagged with its unit type.
/// The unit type U is a phantom type - it exists only at compile time.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Value<T, U> {
val: T,
_unit: PhantomData<U>,
}
impl<T, U> Value<T, U> {
/// Create a new value with the given unit type
pub fn new(val: T) -> Self {
Value {
val,
_unit: PhantomData,
}
}
/// Get the raw value (use sparingly - breaks type safety!)
pub fn raw(&self) -> &T {
&self.val
}
}
Base Unit Types (Marker Types)
// Option 1: Simple marker structs
pub struct Meter;
pub struct Second;
pub struct Kilogram;
pub struct Ampere;
pub struct Kelvin;
pub struct Mole;
pub struct Candela;
// Option 2: Const generic dimensional representation
pub struct Unit<
const M: i8, // Meter exponent
const S: i8, // Second exponent
const KG: i8, // Kilogram exponent
// ... other base units
>;
// Type aliases for readability
pub type Dimensionless = Unit<0, 0, 0>;
pub type Length = Unit<1, 0, 0>;
pub type Time = Unit<0, 1, 0>;
pub type Mass = Unit<0, 0, 1>;
pub type Velocity = Unit<1, -1, 0>;
pub type Acceleration = Unit<1, -2, 0>;
pub type Force = Unit<1, -2, 1>;
Trait Bounds for Valid Operations
/// Trait for unit multiplication (at the type level)
pub trait UnitMul<RHS> {
type Output;
}
/// Trait for unit division (at the type level)
pub trait UnitDiv<RHS> {
type Output;
}
// Example implementations (simplified for marker types)
impl UnitMul<Second> for Meter {
type Output = MeterSecond; // m ร s = mยทs
}
impl UnitDiv<Second> for Meter {
type Output = Velocity; // m รท s = m/s
}
Operator Implementations
// Addition: Only for same units
impl<T: Add<Output = T>, U> Add for Value<T, U> {
type Output = Value<T, U>;
fn add(self, rhs: Self) -> Self::Output {
Value::new(self.val + rhs.val)
}
}
// Multiplication: Produces derived unit
impl<T, U1, U2> Mul<Value<T, U2>> for Value<T, U1>
where
T: Mul<Output = T>,
U1: UnitMul<U2>,
{
type Output = Value<T, <U1 as UnitMul<U2>>::Output>;
fn mul(self, rhs: Value<T, U2>) -> Self::Output {
Value::new(self.val * rhs.val)
}
}
// Division: Produces derived unit
impl<T, U1, U2> Div<Value<T, U2>> for Value<T, U1>
where
T: Div<Output = T>,
U1: UnitDiv<U2>,
{
type Output = Value<T, <U1 as UnitDiv<U2>>::Output>;
fn div(self, rhs: Value<T, U2>) -> Self::Output {
Value::new(self.val / rhs.val)
}
}
Convenience Extension Traits
/// Extension trait for ergonomic unit construction
pub trait LengthExt {
fn meters(self) -> Value<Self, Length> where Self: Sized;
fn kilometers(self) -> Value<Self, Length> where Self: Sized;
}
impl LengthExt for f64 {
fn meters(self) -> Value<Self, Length> {
Value::new(self)
}
fn kilometers(self) -> Value<Self, Length> {
Value::new(self * 1000.0) // Convert to meters
}
}
pub trait TimeExt {
fn seconds(self) -> Value<Self, Time> where Self: Sized;
fn hours(self) -> Value<Self, Time> where Self: Sized;
}
impl TimeExt for f64 {
fn seconds(self) -> Value<Self, Time> {
Value::new(self)
}
fn hours(self) -> Value<Self, Time> {
Value::new(self * 3600.0) // Convert to seconds
}
}
Phased Implementation Guide
Phase 1: Define Base Unit Types and Value Wrapper (Day 1)
Objectives:
- Create the
Value<T, U>struct withPhantomData - Define marker types for base SI units
- Verify zero-sized type optimization
Tasks:
- Create a new library crate:
cargo new --lib physics-units - Define the
Valuestruct - Define at least 3 base unit marker types:
Meter,Second,Kilogram - Add a test that verifies
size_of::<Value<f64, Meter>>() == size_of::<f64>() - Implement
Debug,Clone,Copy,PartialEqforValue
Verification:
#[test]
fn test_zero_cost() {
use std::mem::size_of;
// The unit wrapper should add NO memory overhead
assert_eq!(size_of::<Value<f64, Meter>>(), size_of::<f64>());
assert_eq!(size_of::<Value<f32, Second>>(), size_of::<f32>());
// Marker types should be zero-sized
assert_eq!(size_of::<Meter>(), 0);
assert_eq!(size_of::<Second>(), 0);
}
Phase 2: Implement Add/Sub for Same Units (Day 2)
Objectives:
- Implement
Addtrait forValuetypes with matching units - Implement
Subtrait similarly - Verify that adding different units fails to compile
Tasks:
- Implement
Add<Value<T, U>> for Value<T, U>whereT: Add<Output = T> - Implement
Sub<Value<T, U>> for Value<T, U>whereT: Sub<Output = T> - Write tests for addition and subtraction
- Create a compile-fail test (using
trybuildcrate) for adding different units
Verification:
#[test]
fn test_same_unit_addition() {
let a: Value<f64, Meter> = Value::new(5.0);
let b: Value<f64, Meter> = Value::new(3.0);
let c = a + b;
assert_eq!(*c.raw(), 8.0);
}
// This should NOT compile:
// let x: Value<f64, Meter> = Value::new(5.0);
// let y: Value<f64, Second> = Value::new(3.0);
// let z = x + y; // ERROR!
Phase 3: Implement Mul/Div with Derived Units (Days 3-4)
Objectives:
- Define
UnitMulandUnitDivtraits - Implement trait instances for common unit combinations
- Implement
MulandDivforValuethat produce correct derived types
Tasks:
- Define
UnitMul<RHS>trait with associatedOutputtype - Define
UnitDiv<RHS>trait with associatedOutputtype - Define derived unit types:
Velocity,Acceleration,Force,Area,Volume - Implement unit multiplication/division relationships:
Meter / Second = VelocityVelocity / Second = AccelerationKilogram * Acceleration = ForceMeter * Meter = Area
- Implement
MulandDivforValue
Verification:
#[test]
fn test_velocity_calculation() {
let distance: Value<f64, Meter> = Value::new(100.0);
let time: Value<f64, Second> = Value::new(10.0);
let velocity = distance / time;
// velocity should be Value<f64, Velocity>
assert_eq!(*velocity.raw(), 10.0);
}
#[test]
fn test_force_calculation() {
let mass: Value<f64, Kilogram> = Value::new(10.0);
let accel: Value<f64, Acceleration> = Value::new(9.8);
let force = mass * accel;
// force should be Value<f64, Force>
assert_eq!(*force.raw(), 98.0);
}
Phase 4: Add Convenience Methods (Day 5)
Objectives:
- Create extension traits for ergonomic API:
.meters(),.seconds(), etc. - Support unit prefixes:
.kilometers(),.milliseconds() - Add display formatting
Tasks:
- Define
LengthExttrait with methods like.meters(),.kilometers(),.centimeters() - Define
TimeExttrait with.seconds(),.hours(),.milliseconds() - Define
MassExttrait with.kilograms(),.grams() - Implement
DisplayforValuethat shows the unit name - Add more derived unit calculations
Verification:
#[test]
fn test_ergonomic_api() {
use physics_units::prelude::*;
let d = 100.0.meters();
let t = 10.0.seconds();
let v = d / t;
assert_eq!(*v.raw(), 10.0); // 10 m/s
}
#[test]
fn test_unit_conversions() {
let km = 1.0.kilometers();
let m = 1000.0.meters();
// Both represent 1000 meters internally
assert_eq!(*km.raw(), *m.raw());
}
Phase 5: Verify Zero Runtime Cost (Days 6-7)
Objectives:
- Inspect generated assembly to confirm no overhead
- Benchmark against raw
f64operations - Document the zero-cost abstraction
Tasks:
- Create a benchmark comparing
Value<f64, Meter>operations vs rawf64 - Use
cargo asmorcargo show-asmto inspect generated code - Verify that the assembly for unit-tagged and untagged operations is identical
- Write documentation explaining the zero-cost nature
Verification:
# Install cargo-show-asm
$ cargo install cargo-show-asm
# Look at the assembly for a simple function
$ cargo asm physics_units::benchmark_functions::calculate_kinetic_energy
# The assembly should be identical whether using Value<f64, _> or plain f64
// These two functions should generate identical assembly:
pub fn calculate_kinetic_energy_typed(
mass: Value<f64, Kilogram>,
velocity: Value<f64, Velocity>,
) -> Value<f64, Energy> {
let v_squared = velocity.raw() * velocity.raw();
Value::new(0.5 * mass.raw() * v_squared)
}
pub fn calculate_kinetic_energy_untyped(mass: f64, velocity: f64) -> f64 {
0.5 * mass * velocity * velocity
}
Testing Strategy
Unit Tests (Runtime Correctness)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
let a = 5.0.meters();
let b = 3.0.meters();
assert_eq!(*(a + b).raw(), 8.0);
}
#[test]
fn test_velocity() {
let d = 100.0.meters();
let t = 10.0.seconds();
let v = d / t;
assert!((v.raw() - 10.0).abs() < 1e-10);
}
#[test]
fn test_newtons_second_law() {
// F = m * a
let mass = 10.0.kilograms();
let accel = 9.8.meters_per_second_squared();
let force = mass * accel;
assert!((force.raw() - 98.0).abs() < 1e-10);
}
}
Compile-Fail Tests (Type Safety)
Use the trybuild crate to verify that invalid operations fail to compile:
// tests/compile_fail/add_different_units.rs
fn main() {
use physics_units::prelude::*;
let distance = 100.0.meters();
let time = 10.0.seconds();
// This should fail to compile!
let invalid = distance + time;
}
// tests/ui.rs
#[test]
fn compile_fail_tests() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/compile_fail/*.rs");
}
Assembly Inspection (Zero-Cost Verification)
// src/lib.rs
#[no_mangle]
pub fn typed_multiply(a: Value<f64, Meter>, b: f64) -> Value<f64, Meter> {
Value::new(a.raw() * b)
}
#[no_mangle]
pub fn untyped_multiply(a: f64, b: f64) -> f64 {
a * b
}
// Run: cargo asm physics_units::typed_multiply
// Run: cargo asm physics_units::untyped_multiply
// Compare: They should be identical!
Expected assembly output:
; Both functions should compile to essentially:
physics_units::typed_multiply:
mulsd xmm0, xmm1 ; SSE multiply
ret
physics_units::untyped_multiply:
mulsd xmm0, xmm1 ; Identical!
ret
Common Pitfalls and Debugging
1. Forgetting PhantomData in Constructor
Problem:
impl<T, U> Value<T, U> {
pub fn new(val: T) -> Self {
Value { val } // Error: missing field `_unit`
}
}
Solution:
use std::marker::PhantomData;
impl<T, U> Value<T, U> {
pub fn new(val: T) -> Self {
Value {
val,
_unit: PhantomData, // Always include this!
}
}
}
2. Orphan Rule Violations
Problem: You cannot implement foreign traits for foreign types.
// This is illegal! Mul is foreign, and f64 is foreign.
impl Mul<f64> for f64 { ... }
Solution: Always implement for your own types:
// Legal: Value is our type
impl<T: Mul<Output = T>, U1, U2> Mul<Value<T, U2>> for Value<T, U1>
where U1: UnitMul<U2>
{
type Output = Value<T, <U1 as UnitMul<U2>>::Output>;
// ...
}
3. Missing UnitMul/UnitDiv Implementations
Problem:
let area = length * width;
// Error: the trait `UnitMul<Meter>` is not implemented for `Meter`
Solution: You must implement every unit combination you want to support:
impl UnitMul<Meter> for Meter {
type Output = Area; // m * m = m^2
}
4. Const Generic Expression Limitations (Stable Rust)
Problem: You want Unit<M1 + M2, S1 + S2, ...> but that requires nightly.
Solution (Stable): Use type-level integers via traits:
// Instead of const generics for arithmetic, use typenum crate
use typenum::{Integer, P1, N1, Z0}; // Positive 1, Negative 1, Zero
struct Unit<M: Integer, S: Integer, KG: Integer>;
type Velocity = Unit<P1, N1, Z0>; // m^1 * s^-1
5. Ambiguous Type Inference
Problem:
let x = Value::new(5.0); // Error: cannot infer type for U
Solution: Always specify the unit type explicitly or use extension methods:
let x: Value<f64, Meter> = Value::new(5.0); // Explicit type
let y = 5.0.meters(); // Extension method (cleaner)
6. Copy/Clone Not Derived
Problem:
let a = 5.0.meters();
let b = a + a; // Error: use of moved value `a`
Solution: Derive Copy and Clone:
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Value<T, U> {
val: T,
_unit: PhantomData<U>,
}
Extensions and Challenges
Challenge 1: Temperature Units (Affine Transformations)
Temperature is special because Celsius and Fahrenheit require offsets, not just scaling:
Celsius to Kelvin: K = C + 273.15
Fahrenheit to Kelvin: K = (F - 32) * 5/9 + 273.15
Challenge: Implement a type-safe temperature system that:
- Prevents adding Celsius + Fahrenheit directly
- Allows conversion between scales
- Represents temperature differences vs absolute temperatures differently
// Hint: Use separate types for absolute vs relative temperature
struct AbsoluteTemp<Scale>(f64, PhantomData<Scale>);
struct TempDifference<Scale>(f64, PhantomData<Scale>);
// AbsoluteTemp - AbsoluteTemp = TempDifference
// AbsoluteTemp + TempDifference = AbsoluteTemp
// TempDifference + TempDifference = TempDifference
Challenge 2: Unit Prefixes (Kilo, Milli, Micro)
Add support for SI prefixes:
Prefix Symbol Factor
โโโโโโโโโโโโโโโโโโโโโโ
tera T 10^12
giga G 10^9
mega M 10^6
kilo k 10^3
(base) - 10^0
milli m 10^-3
micro ฮผ 10^-6
nano n 10^-9
pico p 10^-12
Challenge: Design a system where:
1.0.kilometers() + 500.0.meters()works correctly- The internal representation is consistent (e.g., always base units)
- Display shows the most appropriate prefix
Challenge 3: Const Generics for Dimensional Exponents
Use nightly Rustโs generic_const_exprs feature:
#![feature(generic_const_exprs)]
struct Unit<const M: i8, const S: i8, const KG: i8>;
impl<const M1: i8, const S1: i8, const KG1: i8,
const M2: i8, const S2: i8, const KG2: i8>
Mul<Unit<M2, S2, KG2>> for Unit<M1, S1, KG1>
where
[(); (M1 + M2) as usize]:, // Const expression trick
{
type Output = Unit<{M1 + M2}, {S1 + S2}, {KG1 + KG2}>;
// ...
}
Challenge 4: Dimensional Homogeneity Checking
Ensure that all terms in an equation have the same dimensions:
// This is valid physics: d = v*t + 0.5*a*t^2
let d = velocity * time + 0.5 * acceleration * time * time;
// This is invalid: cannot add distance to velocity
let invalid = distance + velocity; // Should fail to compile
Challenge 5: Integration with num-traits
Make your library work with any numeric type, not just f64:
use num_traits::{Float, Num};
impl<T, U1, U2> Mul<Value<T, U2>> for Value<T, U1>
where
T: Num + Copy, // Works with f32, f64, i32, etc.
U1: UnitMul<U2>,
{
type Output = Value<T, <U1 as UnitMul<U2>>::Output>;
fn mul(self, rhs: Value<T, U2>) -> Self::Output {
Value::new(self.val * rhs.val)
}
}
Real-World Connections
The uom Crate (Units of Measurement)
The uom crate is the production-grade solution for Rust:
use uom::si::f64::*;
use uom::si::length::meter;
use uom::si::time::second;
use uom::si::velocity::meter_per_second;
let length = Length::new::<meter>(100.0);
let time = Time::new::<second>(10.0);
let velocity: Velocity = length / time;
println!("{:?}", velocity.get::<meter_per_second>()); // 10.0
Study this crate to see how professionals handle:
- All 7 SI base units
- Hundreds of derived units
- Unit conversions
- Multiple unit systems (SI, CGS, imperial)
The dimensioned Crate
An alternative approach using procedural macros:
#[macro_use]
extern crate dimensioned;
use dimensioned::si::*;
let length = 5.0 * M; // 5 meters
let time = 2.0 * S; // 2 seconds
let velocity = length / time; // 2.5 m/s
Scientific Computing Applications
This pattern is used in:
- Physics simulations (particle physics, astrophysics)
- Engineering calculations (structural analysis, fluid dynamics)
- Financial modeling (currency units, risk metrics)
- Game development (coordinate systems, physics engines)
- Embedded systems (sensor data with units)
The Core Question Youโre Answering
โHow can the compiler check my physics homework?โ
By encoding units in the type parameters, the compilerโs trait solver becomes a dimensional analysis engine. Youโre building a system where:
- Adding meters to seconds is a compile error
- Dividing distance by time automatically produces velocity
- The Mars Climate Orbiter disaster becomes impossible
- All of this happens at zero runtime cost
The Interview Questions Theyโll Ask
- โWhat is a zero-sized type and how is it used in this library?โ
- โHow do you prevent users from creating invalid units?โ
- โCan you explain how the compiler optimizes away these wrappers?โ
- โWhat is
PhantomDataand why is it necessary?โ - โHow does trait arithmetic enable type-level dimensional analysis?โ
- โWhat are the limitations of using const generics for unit exponents in stable Rust?โ
- โHow would you implement unit conversions (e.g., kilometers to meters)?โ
- โCan you explain the difference between marker types and const generics for units?โ
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Type-safe units | โIdiomatic Rustโ | Ch. 5: Advanced Traits |
| Generic Math | โProgramming Rustโ | Ch. 11: Traits and Generics |
| Zero-Cost Wrappers | โEffective Rustโ | Item 5: Understand Newtype Patterns |
| PhantomData | โRust for Rustaceansโ | Ch. 3: Types |
| Operator Overloading | โProgramming Rustโ | Ch. 12: Operator Overloading |
Summary
This project teaches you to:
- Prevent real-world disasters by making unit confusion impossible
- Master PhantomData and zero-sized types for compile-time information
- Implement operator overloading with complex trait bounds
- Design type-level arithmetic using traits or const generics
- Verify zero-cost abstractions through assembly inspection
- Create ergonomic APIs with extension traits
By the end, youโll have built a library that catches dimensional analysis errors at compile time with no runtime overhead - the ultimate demonstration of Rustโs โzero-cost abstractionsโ philosophy.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PROJECT COMPLETE WHEN: โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ Adding meters + seconds fails to compile โ
โ โ distance / time produces Velocity type automatically โ
โ โ size_of::<Value<f64, Meter>>() == size_of::<f64>() โ
โ โ Generated assembly is identical to raw f64 operations โ
โ โ Newton's laws (F=ma) type-check correctly โ
โ โ Compile-fail tests verify type safety โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ