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:

  1. Explain the Mars Climate Orbiter disaster and why type-safe units prevent $125M mistakes
  2. Describe dimensional analysis and how the 7 SI base units combine into derived units
  3. Use PhantomData to carry type information with zero runtime cost
  4. Implement operator overloading using std::ops traits for custom types
  5. Design type-level arithmetic where unit multiplication/division produces correct derived types
  6. Verify zero-cost abstractions by inspecting generated assembly
  7. Create ergonomic APIs with extension traits like .meters() and .seconds()
  8. Apply const generics to represent dimensional exponents at the type level
  9. Write compile-fail tests that verify invalid operations are rejected
  10. Connect theory to practice by understanding real-world crates like uom and dimensioned

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             โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Mars Climate Orbiter Failure - Unit Conversion Error

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?

  1. Variance: Tells the compiler how Value<T, U> relates to Value<T, V> in terms of lifetime/type covariance
  2. Drop checking: Affects how the compiler reasons about when data can be dropped
  3. 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:

  • Add and Sub should only work for same units (meters + meters = meters)
  • Mul and Div should 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 with PhantomData
  • Define marker types for base SI units
  • Verify zero-sized type optimization

Tasks:

  1. Create a new library crate: cargo new --lib physics-units
  2. Define the Value struct
  3. Define at least 3 base unit marker types: Meter, Second, Kilogram
  4. Add a test that verifies size_of::<Value<f64, Meter>>() == size_of::<f64>()
  5. Implement Debug, Clone, Copy, PartialEq for Value

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 Add trait for Value types with matching units
  • Implement Sub trait similarly
  • Verify that adding different units fails to compile

Tasks:

  1. Implement Add<Value<T, U>> for Value<T, U> where T: Add<Output = T>
  2. Implement Sub<Value<T, U>> for Value<T, U> where T: Sub<Output = T>
  3. Write tests for addition and subtraction
  4. Create a compile-fail test (using trybuild crate) 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 UnitMul and UnitDiv traits
  • Implement trait instances for common unit combinations
  • Implement Mul and Div for Value that produce correct derived types

Tasks:

  1. Define UnitMul<RHS> trait with associated Output type
  2. Define UnitDiv<RHS> trait with associated Output type
  3. Define derived unit types: Velocity, Acceleration, Force, Area, Volume
  4. Implement unit multiplication/division relationships:
    • Meter / Second = Velocity
    • Velocity / Second = Acceleration
    • Kilogram * Acceleration = Force
    • Meter * Meter = Area
  5. Implement Mul and Div for Value

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:

  1. Define LengthExt trait with methods like .meters(), .kilometers(), .centimeters()
  2. Define TimeExt trait with .seconds(), .hours(), .milliseconds()
  3. Define MassExt trait with .kilograms(), .grams()
  4. Implement Display for Value that shows the unit name
  5. 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 f64 operations
  • Document the zero-cost abstraction

Tasks:

  1. Create a benchmark comparing Value<f64, Meter> operations vs raw f64
  2. Use cargo asm or cargo show-asm to inspect generated code
  3. Verify that the assembly for unit-tagged and untagged operations is identical
  4. 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:

  1. Physics simulations (particle physics, astrophysics)
  2. Engineering calculations (structural analysis, fluid dynamics)
  3. Financial modeling (currency units, risk metrics)
  4. Game development (coordinate systems, physics engines)
  5. 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

  1. โ€œWhat is a zero-sized type and how is it used in this library?โ€
  2. โ€œHow do you prevent users from creating invalid units?โ€
  3. โ€œCan you explain how the compiler optimizes away these wrappers?โ€
  4. โ€œWhat is PhantomData and why is it necessary?โ€
  5. โ€œHow does trait arithmetic enable type-level dimensional analysis?โ€
  6. โ€œWhat are the limitations of using const generics for unit exponents in stable Rust?โ€
  7. โ€œHow would you implement unit conversions (e.g., kilometers to meters)?โ€
  8. โ€œ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:

  1. Prevent real-world disasters by making unit confusion impossible
  2. Master PhantomData and zero-sized types for compile-time information
  3. Implement operator overloading with complex trait bounds
  4. Design type-level arithmetic using traits or const generics
  5. Verify zero-cost abstractions through assembly inspection
  6. 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                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜