Using Jai's Unique and Powerful Compiler for Typesafe Units

erenon1 pts0 comments

Using Jai's Unique and Powerful Compiler for Typesafe Units

Using Jai's Unique and Powerful Compiler for Typesafe Units

May 24th, 2024

I've always wanted a nice library for writing typesafe math. For example if you're solving for distance in<br>Meters but somehow wind up with MetersPerSecond you know you did something wrong. This<br>can prevent issues such as when NASA infamously lost a Mars Orbiter due to mixing imperial and<br>metric units!

Many units libraries exist in various languages. None of them have sparked my joy. They're complex, have bad<br>error messages, and make trade-offs I don't like.

Awhile back I learned Jai via<br>Advent of Code. Since then I haven't done much with Jai. But over the past year I've found myself missing<br>several of its features.

Feeling a spark of inspiration I wrote a small, proof-of-life units library in Jai. In doing so I learned about<br>some pretty crazy compiler features that kinda blew me away. It makes C++ templates and Rust traits feel<br>disgustingly archaic, imho.

I'm honestly not sure if this post is for Jai programmers, the Jai curious, or C++/Rust programmers. It's a<br>mix.

What are Typesafe Units?

What do I mean when I say "typesafe units". This is best served with an example.

Metersfloat> CalcBallisticRange(<br>MetersPerSecondfloat> speed,<br>MetersPerSecond2float> gravity,<br>Metersfloat> height)<br>float d2r = 0.01745329252f;<br>float angle = 45.0f * d2r;<br>float cos = std::cos(angle);<br>float sin = std::sin(angle);

auto range = (speed*cos / gravity) *<br>(speed*sin + std::sqrt(<br>speed*speed*sin*sin<br>+ 2.0f*gravity*initial_height));<br>return range;

This is a simple function to compute the maximum range of a ballistic trajecy. The<br>computation is non-trivial and involves a variety of intermediate units. However all the terms cancel out and<br>the final result should be Meters. If it's not Meters then we have a bug and should<br>get a compile error.

I have a few requirements. Units logic must be strictly compile-time with zero run-time cost in either memory<br>or cpu. The storage type should be user specified - float, int64, etc. Values should<br>not be normalized to base units (meters, grams, seconds). Types must be compiler generated and must not be<br>manually declared. And the code should be readable by humans - no inscrutable templates or macros.

A Rust Implementation

Way back in 2019 I wrote my own units library in Rust. There are existing crates like uom but none of them meet all my requirements.

My Rust implementation is built entirely in the type system. It relies heavily on the popular typenum crate. Which exists because Rust const generics are<br>still woefully incomplete.

// Declare a quantity struct the stores a value and units<br>pub struct QuantityTT, U><br>where T: Amount<br>amount: T,<br>_u: PhantomDataU>,

// Declare SI types<br>pub trait Ratio {<br>fn numerator() -> i64;<br>fn denominator() -> i64;

pub trait SIRatios {<br>type Length: Ratio;<br>type Mass: Ratio;<br>type Time: Ratio;

pub trait SIExponents {<br>type Length: Integer;<br>type Mass: Integer;<br>type Time: Integer;

pub trait SIUnits {<br>type Ratios: SIRatios;<br>type Exponents: SIExponents;

pub struct SIUnitsTRATIOS, EXPONENTS><br>where<br>RATIOS: SIRatios,<br>EXPONENTS: SIExponents,<br>_r: PhantomDataRATIOS>,<br>_e: PhantomDataEXPONENTS>,

These types and traits let us declare types such as:

// via helpers, nice<br>type Kilometers = LengthKilo>;

// expanded type, blech<br>type Kilometers = QuantityTf32, SIUnitsT<br>SIRatiosTRatioTExpP10, P3>,P1>, RatioTZ0,Z0>, RatioTZ0,Z0>>,<br>SIExponentsTP1, Z0, Z0>>>;

The end result is pretty delightful at least.

fn calc_ballistic_range(<br>speed: MetersPerSecondf32>,<br>gravity: MetersPerSecond2f32>,<br>initial_height: Metersf32>,<br>) -> Metersf32> {<br>let d2r = 0.01745329252;<br>let angle: f32 = 45.0 * d2r;<br>let cos = angle.cos();<br>let sin = angle.sin();

let range = (speed * cos / gravity)<br>* (speed*sin<br>+ (speed*speed*sin*sin + 2.0*gravity*initial_height)<br>.sqrt());<br>range

Unfortunately the implementation is trait hell. For example:

/// Multiplying a pair of SIUnits is complicated<br>/// Ratios: must be XOR-able<br>/// meters * meters is fine.<br>/// meters * kilometers is not. The output type is ambiguous.<br>/// Exponents: can be anything. will be added together<br>/// meters * meters = meters squared<br>/// However exponents which become zero must zero out their corresponding ratio<br>/// 10 kilometers per hour * 2 hours = 20 kilometers. The time Ratio was hours, but needs to be zero'd out.<br>implR0, E0, R1, E1> std::ops::MulSIUnitsTR1, E1>> for SIUnitsTR0, E0><br>where<br>R0: SIRatios + XorR1>,<br>E0: SIExponents + std::ops::AddE1>,<br>R1: SIRatios,<br>E1: SIExponents,<br>XorOutputR0, R1>: SIRatios + ReduceWithAddOutputE0, E1>> + Default,<br>AddOutputE0, E1>: SIExponents + Default,<br>ReduceWithOutputXorOutputR0, R1>, AddOutputE0, E1>>: SIRatios + Default,<br>ReduceOutputSIUnitsTXorOutputR0, R1>, AddOutputE0, E1>>>: Default,<br>type Output = ReduceOutputSIUnitsTXorOutputR0, R1>, AddOutputE0, E1>>>;

fn mul(self, _other: SIUnitsTR1, E1>) -> Self::Output {<br>Default::default()

The nice thing is we're using the type...

type units meters speed siratios typesafe

Related Articles