DEV Community

Henrique Sasaki Yuya
Henrique Sasaki Yuya

Posted on

I Built a Type-Safe SI Unit Library in Swift — And the Compiler Catches Your Physics Mistakes

You've seen this bug before:

let speed = distance + time  // Compiles. Runs. Produces nonsense.
Enter fullscreen mode Exit fullscreen mode

Or worse — the Mars Climate Orbiter kind, where pound-seconds and newton-seconds silently mix, and a $327 million spacecraft burns up in the atmosphere.

I built SystemeInternational to make that class of bug extinct in Swift — at compile time, with zero runtime cost.

What If the Type System Knew Physics?

SystemeInternational encodes physical dimensions, units, and even the distinction between absolute positions and intervals into Swift's type parameters. Every quantity is:

Quantity<Scalar, Unit, Space>
Enter fullscreen mode Exit fullscreen mode

That's it. 8 bytes. The Unit and Space are phantom types — they exist only at compile time and vanish completely in the binary.

import UnitesSI

let distance = try Quantity<Double, Kilometer>(36)
let time     = try Quantity<Double, Second>(3_600)
let speed    = try distance / time  // ✅ Compiles — dimension is Length/Time

let nonsense = try distance + time  // ❌ Compile error — no overload
Enter fullscreen mode Exit fullscreen mode

No runtime checks. No if unit == .meter. The compiler simply won't let you add meters to seconds.

Five Things That Make This Different

1. Hertz ≠ Becquerel (Even Though They're Both 1/s)

The SI system has unit pairs with identical dimensions but completely different physical meanings. Most unit libraries treat them as interchangeable. SystemeInternational doesn't:

let frequency: Quantity<Double, Hertz, Linear>      // Radio signal
let activity:  Quantity<Double, Becquerel, Linear>   // Radioactive decay

// These are DIFFERENT types. You cannot assign one to the other.
// To explicitly reinterpret:
let freq = canonicalQuantity.interpreted(as: Hertz.self)
Enter fullscreen mode Exit fullscreen mode

Same goes for Gray/Sievert (absorbed dose vs. equivalent dose). The library respects BIPM's semantic distinctions.

2. You Can't Add Two Temperatures

Adding 20°C + 25°C is physically meaningless — you can't add two absolute positions. But subtracting them to get a temperature difference is perfectly valid.

SystemeInternational models this with affine space algebra:

let room    = try CelsiusTemperatureValue(20)       // Affine (absolute position)
let boiling = try CelsiusTemperatureValue(100)       // Affine
let rise    = try boiling - room                     // ✅ Linear (interval): 80°C

let shifted = try room + rise                        // ✅ Affine: 100°C
let oops    = try room + boiling                     // ❌ Compile error
Enter fullscreen mode Exit fullscreen mode
Expression Result Meaning
Point − Point Vector Distance between positions
Point + Vector Point Shift a position
Point + Point compile error Adding positions is meaningless

And yes — it validates against absolute zero at runtime:

try CelsiusTemperatureValue(-274)  
// throws QuantityError.belowAbsoluteZero
Enter fullscreen mode Exit fullscreen mode

3. Exact Integer Arithmetic

Need precise timing in embedded systems? Use integer scalars:

let duration = try Quantity<Int, Millisecond>(exactly: 2_000)
let seconds  = try duration.convertedIfExactly(to: Second.self)
print(seconds.exactValue)  // Optional(2)

let oneMeter = try Quantity<Int, Meter>(exactly: 1)
try oneMeter.convertedIfExactly(to: Kilometer.self)  // throws — 0.001 isn't an Int
Enter fullscreen mode Exit fullscreen mode

No silent truncation. No floating-point drift. It throws when the math doesn't work out exactly.

4. Rational Scale Factors (Not Floating-Point)

Unit scales are stored as rational numbers with decimal exponents, preserving precision that Double arithmetic would destroy:

// 1° = π/180 radians, stored as:
UnitScale(numerator: 3_141_592_653_589_793, denominator: 180, decimalExponent: -15)

// 1 eV = 1.602176634 × 10⁻¹⁹ J (exact by 2019 SI redefinition), stored as:
UnitScale(numerator: 1_602_176_634, denominator: 1, decimalExponent: -28)
Enter fullscreen mode Exit fullscreen mode

This means conversions like Degree → Radian or ElectronVolt → Joule carry the full precision of the defining constants.

5. Thin Abstractions the Optimizer Can See Through

Hot-path accessors and arithmetic are annotated with @inlinable, so the compiler can inline across module boundaries and apply full optimizations in Release builds:

Benchmark Debug Release Speedup
convert_kilometer_to_meter 225 ns/op 3 ns/op ~75×
semantic_lumen_operator 291 ns/op 83 ns/op ~3.5×
semantic_lux_operator 435 ns/op 155 ns/op ~2.8×

Same-unit operations like linear_add_same_unit and multiply_canonical_area measured 0 ns/op in Release — the optimizer eliminated them entirely via dead code elimination, confirming that the phantom-type abstractions add no barriers to standard compiler optimizations. Real-world code that uses the results will still pay the cost of a bare Double operation, but nothing more.

The key takeaway: the type-safety layer is transparent to the optimizer. You get compile-time unit checking without runtime overhead beyond the underlying arithmetic.

The Module Architecture

SystemeInternational is split into focused, composable modules:

UnitesSI              ← Main facade (import this)
├── UnitesDeBaseDuSI  ← Core: Quantity, dimensions, 7 base units
├── PrefixesDuSI      ← All 20 SI prefixes (Quetta → Quecto)
└── UnitesDeriveesDuSI← Named derived units + temperature scales

UtiliseesNonSI        ← 16 BIPM-accepted non-SI units
UnitesSICompat        ← Foundation.Measurement bridge
UtiliseesNonSICompat  ← Non-SI Foundation bridge
Enter fullscreen mode Exit fullscreen mode

For most use cases, import UnitesSI gives you everything. The modular design means you only link what you use.

Quick Tour

Prefixed Units — All 20 SI Prefixes

import UnitesSI

let distance = try Quantity<Double, Kilometer>(5.2)
let tiny     = try Quantity<Double, Microgram>(0.3)
let huge     = try Quantity<Double, Gigahertz>(2.4)

// Mass follows SI convention: Kilogram is base, but prefixes come from Gram
let mg = try Quantity<Double, Milligram>(500)
print(mg.converted(to: Kilogram.self).value)  // 0.0005
Enter fullscreen mode Exit fullscreen mode

Derived Units with Semantic Operators

let intensity  = try Quantity<Double, Candela>(1_200)
let solidAngle = try Quantity<Double, Steradian>(1.5)
let luminous   = try intensity * solidAngle  // → Lumen

let area = try Quantity<Double, Meter>(3) * Quantity<Double, Meter>(2)  // → m²
let illuminance = try luminous / area  // → Lux
Enter fullscreen mode Exit fullscreen mode

Foundation.Measurement Interop

import Foundation
import UnitesSICompat

// SystemeInternational → Foundation
let road = try Quantity<Double, Kilometer>(12.3).foundationMeasurement()
print(road.unit.symbol)  // "km"

// Foundation → SystemeInternational
let temp = try Measurement(value: 25, unit: UnitTemperature.celsius)
    .absoluteTemperature(as: DegreeCelsius.self)
print(temp.converted(to: Kelvin.self).value)  // 298.15
Enter fullscreen mode Exit fullscreen mode

Non-SI Accepted Units

import UtiliseesNonSI

let water      = try Quantity<Double, Milliliter>(500)
let rightAngle = try Quantity<Double, Degree>(90)
let gain       = try Quantity<Double, Decibel>(20)

print(water.converted(to: Liter.self).value)       // 0.5
print(rightAngle.converted(to: Radian.self).value) // 1.5707963267948966
Enter fullscreen mode Exit fullscreen mode

Why "Systeme International"?

The name comes from the French Système International d'Unités — the official name of the SI system, maintained by the Bureau International des Poids et Mesures (BIPM). The module names follow the same convention: Unités de base du SI, Préfixes du SI, Utilisées non-SI.

Requirements & Installation

  • Swift 6.2+
  • No dependencies — pure Swift, no external packages
// Package.swift
dependencies: [
    .package(url: "https://github.com/moriturus/SystemeInternational.git", from: "1.0.0"),
]
Enter fullscreen mode Exit fullscreen mode

306 tests. 100% coverage target. Apache 2.0 licensed.


If you work with physical quantities in Swift — whether it's scientific computing, IoT sensor data, robotics, game physics, or just making sure your app doesn't confuse kilometers with miles — give SystemeInternational a look.

Star the repo on GitHub if you think the type system should catch your physics bugs.

GitHub logo moriturus / SystemeInternational

SI Units for Swift: Strongly typed, static type safe, (almost) zero-cost abstruction

SystemeInternational

A type-safe SI unit system for Swift — catch unit misuse at compile time, not at runtime.

Swift 6.2 License

Overview

SystemeInternational models physical quantities, units, and dimensions using Swift's strong type system. Units are marker types — you never instantiate Meter() directly; instead you write Quantity<Double, Meter, Linear> or refer to Meter.self.

Key characteristics:

  • Different physical meanings are different types — no raw Double or typealias ambiguity
  • Unit conversions are type-checked and use exact rational scale metadata
  • Named SI derived units with distinct semantics (Hertz vs Becquerel) are never implicitly convertible
  • Affine-space algebra distinguishes absolute positions (points) from intervals (vectors) at compile time
  • Temperature uses the same unified Quantity type with a Space parameter, not a separate type hierarchy
  • Foundation Measurement interoperability is available through dedicated compatibility modules

Requirements

  • Swift 6.2+

Installation

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/moriturus/SystemeInternational.git
Enter fullscreen mode Exit fullscreen mode

P.S. To our friends still measuring things in feet, inches, pounds, ounces, fluid ounces (US), fluid ounces (UK), short tons, long tons, nautical miles, statute miles, furlongs, and—my personal favorite—slugs: the year is 2026. The rest of the world moved on. Even the UK went metric (mostly). Even NASA went metric (after that incident). This library does not, and will never, include Foot, Slug, or Hogshead. You know where to find Foundation.Measurement — it's right there, waiting, with all 14 of your competing gallon definitions. 🫡

Top comments (0)