---
title: "I Built a Type-Safe SI Unit Library in Swift — And the Compiler Catches Your Physics Mistakes"
published: true
description: "SystemeInternational uses phantom types, affine spaces, and zero-cost abstractions to make unit misuse a compile-time error. 306 tests, 8 bytes per quantity, and unit conversions at 3 ns/op in Release."
tags: swift, programming, opensource, types
---
You've seen this bug before:
```swift
let speed = distance + time // Compiles. Runs. Produces nonsense.
```
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](https://github.com/moriturus/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:
```swift
Quantity<Scalar, Unit, Space>
```
That's it. **8 bytes.** The `Unit` and `Space` are phantom types — they exist only at compile time and vanish completely in the binary.
```swift
import UnitesSI
let distance = try Quantity<Double, Kilometer, Linear>(36)
let time = try Quantity<Double, Second, Linear>(3_600)
let speed = try distance / time // ✅ Compiles — dimension is Length/Time
let nonsense = try distance + time // ❌ Compile error — no overload
```
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:
```swift
import UnitesSI
let reciprocalRate = try Quantity<Double, CanonicalUnit<QuotientDimension<Dimensionless, TimeDimension>>, Linear>(50)
let frequency = reciprocalRate.interpreted(as: Hertz.self)
let activity = reciprocalRate.interpreted(as: Becquerel.self)
print(frequency.value) // 50.0
print(activity.value) // 50.0
```
`Hertz` and `Becquerel` are different public types even though they share the same exponent dimension. The same idea applies to angular frequency (`rad/s`) versus cyclic frequency (`Hz`), and to `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**:
```swift
import UnitesSI
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
```
| 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:
```swift
try CelsiusTemperatureValue(-274)
// throws QuantityError.belowAbsoluteZero
```
### 3. Exact Integer Arithmetic
Need precise timing in embedded systems? Use integer scalars:
```swift
import UnitesSI
let duration = try Quantity<Int, Millisecond, Linear>(exactly: 2_000)
let seconds = try duration.convertedIfExactly(to: Second.self)
print(seconds.exactValue) // Optional(2)
let oneMeter = try Quantity<Int, Meter, Linear>(exactly: 1)
try oneMeter.convertedIfExactly(to: Kilometer.self) // throws — 0.001 isn't an Int
```
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:
```text
// 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)
```
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:
```plaintext
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
```
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
```swift
import UnitesSI
let distance = try Quantity<Double, Kilometer, Linear>(5.2)
let tiny = try Quantity<Double, Microgram, Linear>(0.3)
let huge = try Quantity<Double, Gigahertz, Linear>(2.4)
// Mass follows SI convention: Kilogram is base, but prefixes come from Gram
let mg = try Quantity<Double, Milligram, Linear>(500)
print(mg.converted(to: Kilogram.self).value) // 0.0005
```
### Derived Units with Semantic Operators
```swift
import UnitesSI
let intensity = try Quantity<Double, Candela, Linear>(1_200)
let solidAngle = try Quantity<Double, Steradian, Linear>(1.5)
let luminous = try intensity * solidAngle // → Lumen
let area = try Quantity<Double, Meter, Linear>(3) * Quantity<Double, Meter, Linear>(2) // → m²
let illuminance = try luminous / area // → Lux
```
### Foundation.Measurement Interop
```swift
import Foundation
import UnitesSICompat
// SystemeInternational → Foundation
let road = try Quantity<Double, Kilometer, Linear>(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
```
### Non-SI Accepted Units
```swift
import UtiliseesNonSI
let water = try Quantity<Double, Milliliter, Linear>(500)
let rightAngle = try Quantity<Double, Degree, Linear>(90)
let gain = try Quantity<Double, Decibel, Linear>(20)
print(water.converted(to: Liter.self).value) // 0.5
print(rightAngle.converted(to: Radian.self).value) // 1.5707963267948966
```
## 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)](https://www.bipm.org/en/measurement-units/si). 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
```swift
// Package.swift
dependencies: [
.package(url: "https://github.com/moriturus/SystemeInternational.git", from: "0.1.0"),
]
```
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](https://github.com/moriturus/SystemeInternational)** a look.
⭐ **[Star the repo on GitHub](https://github.com/moriturus/SystemeInternational)** if you think the type system should catch your physics bugs.
{% github moriturus/SystemeInternational %}
---
**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. 🫡