Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Units of measure: spike evidence

Spikes 1–8 ran on the release compiler built from main on 2026-11-02, before issue #701, and were rerun on eb772202 (after issue #701) with identical output except spike 5. They support the units of measure proposal. The built-in quantity section shows the same cases with Quantity[T, D]. Outputs are exact except that error paths are shortened to the file name.

Spike 1: Same-dimension arithmetic

Phantom tags give distinct types; @newtype forwards +, -, < and unary -.

# Phantom-tagged generic newtype: same-dimension arithmetic, distinct types.
class Length:
    pass
class Time:
    pass

@copy
@newtype
class Q[T, D]:
    value: T

def main() -> Result[(), IoError]:
    a = Q[f64, Length](3.0)
    b = Q[f64, Length](4.5)
    print(a + b, a - b, a < b, -a)?
    t = Q[f64, Time](2.0)
    print(t)?
    Ok(())

Output:

Q[f64, Length](value=7.5) Q[f64, Length](value=-1.5) True Q[f64, Length](value=-3.0)
Q[f64, Time](value=2.0)

Spike 2: Mixed dimensions are rejected

The forwarding rule already rejects Length + Time.

class Length:
    pass
class Time:
    pass
@copy
@newtype
class Q[T, D]:
    value: T
def main() -> Result[(), IoError]:
    a = Q[f64, Length](3.0)
    t = Q[f64, Time](2.0)
    print(a + t)?
    Ok(())

Output:

error: s2.plenty:12:11: `@newtype` class `Q[f64, Length]` forwards `+` only between two `Q[f64, Length]` values, not `Q[f64, Length]` and `Q[f64, Time]`; read the field, or declare an operator hook for these operands

Spike 3: Domain storage loses the unit on -

Positive - Positive returns i64, so d is a bare i64; the annotation n: i64 = d compiles.

# Domain storage through a newtype: does Positive survive + and -?
class Length:
    pass
@copy
@newtype
class Q[T, D]:
    value: T
def main() -> Result[(), IoError]:
    a = Q[Positive[i64], Length](3)
    b = Q[Positive[i64], Length](4)
    s = a + b
    d = a - b
    print(s, d)?
    n: i64 = d
    Ok(())

Output:

Q[Positive[i64], Length](value=7) -1

Spike 4: A generic operator hook is accepted when unused

The restriction is checked only at a use.

# Generic operator hook: can __mul__ take a different dimension?
class Q[D]:
    value: f64
    def __mul__[E](self, other: &Q[E]) -> f64:
        self.value * other.value
def main() -> () :
    ()

Output:

(compiles; no output)

Spike 4b: A generic operator hook is rejected when used

Length * Time cannot be written with one generic __mul__.

class L:
    pass
class T:
    pass
class Q[D]:
    value: f64
    def __mul__[E](self, other: &Q[E]) -> f64:
        self.value * other.value
def main() -> Result[(), IoError]:
    a = Q[L](2.0)
    b = Q[T](3.0)
    print(a * b)?
    Ok(())

Output:

error: s4b.plenty:7:5: operator hooks cannot have method type parameters, defaults, or yield

Spike 5: No float domain

Positive[f64] is rejected. On 2026-11-02 the error also suggested NonZero[f64], itself rejected; issue #700 removed that hint (local PR #883).

# Positive over floats?
def f(x: Positive[f64]) -> f64:
    x.get()
def main() -> ():
    ()

Output:

error: s5.plenty:2:10: `Positive` requires a signed integer type

Spike 5b: NonZero is integer-only

def f(x: NonZero[f64]) -> f64:
    x.get()
def main() -> Result[(), IoError]:
    print(f(2.0))?
    Ok(())

Output:

error: s5b.plenty:1:10: f64 does not satisfy IntType

Spike 6: Hand-written classes per dimension

Works, but each class has one __mul__ signature, so Metres * Metres and Metres * f64 cannot both use it; the scalar goes through __rmul__.

# One hook signature: scalar via __rmul__, quantity*quantity via __mul__.
@copy
class Metres:
    value: f64
    def __mul__(self, other: &Metres) -> SquareMetres:
        SquareMetres(self.value * other.value)
    def __rmul__(self, k: f64) -> Metres:
        Metres(k * self.value)
    def __truediv__(self, other: &Seconds) -> MetresPerSecond:
        MetresPerSecond(self.value / other.value)
@copy
class Seconds:
    value: f64
@copy
class SquareMetres:
    value: f64
@copy
class MetresPerSecond:
    value: f64
def main() -> Result[(), IoError]:
    m = Metres(3.0)
    print(m * m, 2.0 * m, m / Seconds(1.5))?
    Ok(())

Output:

SquareMetres(value=9.0) Metres(value=6.0) MetresPerSecond(value=2.0)

Spike 7: Code generic over one dimension

Additive unit-polymorphic code works today.

# Unit-polymorphic code over a phantom dimension, today.
class Length:
    pass
@copy
@newtype
class Q[T, D]:
    value: T
def total[T, D](xs: &list[Q[T, D]]) -> Q[T, D]:
    mut sum = xs[0]
    for i in range(1, len(xs)):
        sum = sum + xs[i]
    sum
def main() -> Result[(), AllocError | IoError]:
    xs = [Q[f64, Length](1.5), Q[f64, Length](2.0)]?
    print(total(&xs))?
    Ok(())

Output:

Q[f64, Length](value=3.5)

Spike 8: NonZero storage

* keeps NonZero; + returns a bare i64 and drops the unit.

# Positive survives + inside a newtype; NonZero divisor through a newtype.
class Count:
    pass
@copy
@newtype
class Q[T, D]:
    value: T
def main() -> Result[(), IoError]:
    a = Q[NonZero[i64], Count](3)
    b = Q[NonZero[i64], Count](-2)
    print(a * b, a + b)?
    Ok(())

Output:

Q[NonZero[i64], Count](value=-6) 1

Built-in quantity after issue #701

Run on eb772202. Spike 4b’s product and spike 3’s subtraction now work, and the proposal’s opening example, with local unit constants in place of std.units, prints the output the proposal gives.

dimension Length("m")
dimension Time("s")
dimension Velocity = Length / Time
const metre: Quantity[f64, Length] = Quantity[f64, Length](1.0)
const second: Quantity[f64, Time] = Quantity[f64, Time](1.0)
const km: Quantity[f64, Length] = 1000.0 * metre
const minute: Quantity[f64, Time] = 60.0 * second
const hour: Quantity[f64, Time] = 3600.0 * second

def speed(d: Quantity[f64, Length], t: Quantity[f64, Time]) -> Quantity[f64, Velocity]:
    d / t

def main() -> Result[(), IoError]:
    d = 3.2 * km
    t = 41.0 * minute
    v = speed(d, t)
    print(v, v.to(km / hour))?
    a = Quantity[f64, Length](2.0)
    b = Quantity[f64, Time](3.0)
    print(a * b)?
    p = Quantity[Positive[i64], Length](5)
    print(p - p, p * p)?
    Ok(())

Output:

1.3008130081300813 m/s 4.682926829268292
6.0 m*s
0 m 25 m^2

Mixed dimensions:

dimension Length("m")
dimension Time("s")
def main() -> Result[(), IoError]:
    d = Quantity[f64, Length](1.0)
    t = Quantity[f64, Time](1.0)
    print(d + t)?
    Ok(())
error: q2.plenty:6:11: quantity `+` requires equal dimensions, not `Length` and `Time`

p - p keeps the unit and drops the domain:

dimension Length("m")
def main() -> Result[(), IoError]:
    p = Quantity[Positive[i64], Length](5)
    q: Quantity[Positive[i64], Length] = p - p
    print(q)?
    Ok(())
error: q3.plenty:4:5: expected Quantity[Positive[i64], Length], got Quantity[i64, Length]