← Back to AILP Home

Fractions — frac

Widths: frac8, frac16, frac32, frac64

Overview

Fraction types store exact rational numbers as mixed fractions: whole part + numerator + denominator. Unlike floating point, fractions suffer zero representational drift — ⅓ is exactly ⅓, not 0.333... Built on TBB internally, inheriting sticky ERR propagation for safe overflow handling.

Use fractions when you need: - Exact arithmetic — recipe scaling, financial ratios, musical intervals - Zero-drift accumulation — repeated operations produce exact results - Human-readable output — fractions display as 3 + 1/4 rather than 3.25

Specifications

Type Component Type Storage Whole Range Notes
frac8 int8 3 bytes -127..127 {int8:whole, int8:num, int8:denom}
frac16 int16 6 bytes -32767..32767
frac32 int32 12 bytes -2147483647..2147483647 Most common width
frac64 int64 24 bytes ±9.2×10¹⁸ Full 64-bit precision

Representation

A frac value {whole, num, denom} represents whole + num/denom:

{1, 1, 3}  = 1⅓  (exactly 1 + 1/3)
{0, 1, 3}  = ⅓   (exactly — not 0.333...)
{0, 0, 1}  = 0
{-2, 1, 4} = -1¾ (i.e., -2 + 1/4 = -7/4)

Canonical Form Invariants

After every operation, fractions auto-normalize to canonical form:

  1. Positive denominator: denom > 0 (always)
  2. Non-negative numerator: 0 ≤ num < denom
  3. Sign in whole: The sign is carried by whole; num is always non-negative
  4. Fully reduced: GCD(num, denom) == 1 (lowest terms)
  5. Canonical zero: Zero is always {0, 0, 1}, never {0, 0, N}

Declaration & Literal Syntax

Object Literal (struct form)

frac8:half = {0, 1, 2};       // 0 + 1/2
frac16:third = {0, 1, 3};     // 0 + 1/3
frac32:mixed = {1, 1, 3};     // 1 + 1/3
frac64:precise = {100, 7, 13}; // 100 + 7/13

Expression Literal (constant folding)

The compiler folds int + int/int expressions in frac context into object literals:

frac32:a = 2 + 1/3;   // Constant-folded to {2, 1, 3}
frac32:b = 1/7;        // {0, 1, 7}
frac32:c = 5;           // {5, 0, 1}  — integer promotion

Integer Initialization

Bare integer literals are automatically promoted:

frac32:n = 42;          // {42, 0, 1}

Arithmetic Operations

All arithmetic operators work on frac types, producing GCD-normalized results:

frac32:a = 2 + 1/3;     // {2, 1, 3}
frac32:b = 1 + 1/2;     // {1, 1, 2}

frac32:sum  = a + b;     // 3 + 5/6 = {3, 5, 6}
frac32:diff = a - b;     // 5/6 = {0, 5, 6}
frac32:prod = a * b;     // 3 + 1/2 = {3, 1, 2}
frac32:quot = a / b;     // 1 + 5/9 = {1, 5, 9}

Algorithm

  1. Convert both operands to improper fractions: W*D + N / D
  2. Apply the operation:
  3. Add: (a_num * b_den + b_num * a_den) / (a_den * b_den)
  4. Sub: (a_num * b_den - b_num * a_den) / (a_den * b_den)
  5. Mul: (a_num * b_num) / (a_den * b_den)
  6. Div: (a_num * b_den) / (a_den * b_num)
  7. Normalize: reduce by GCD, split into mixed form {W, N, D}

Comparison Operators

Comparisons use cross-multiplication for exact comparison without conversion:

if (a == b) { /* Equality: a_num*b_den == b_num*a_den */ }
if (a != b) { /* Inequality */ }
if (a < b)  { /* Less than */ }
if (a <= b) { /* Less than or equal */ }
if (a > b)  { /* Greater than */ }
if (a >= b) { /* Greater than or equal */ }

Type Conversions

Using @cast

// frac → integer (truncates fractional part)
int32:val = @cast<int32>(my_frac);

// integer → frac (exact, {value, 0, 1})
frac32:from_int = @cast<frac32>(42);

// frac → float (introduces rounding)
flt64:approx = @cast<flt64>(my_frac);

// float → frac (captures the float's exact rational value)
frac32:from_flt = @cast<frac32>(3.14);

// frac → fix256 (exact for representable values)
fix256:fixed = @cast<fix256>(my_frac);

// fix256 → frac
frac32:from_fixed = @cast<frac32>(my_fix256);

Width Conversions

// Widening: always lossless
frac64:wide = @cast<frac64>(my_frac32);

// Narrowing: may overflow → ERR
frac8:narrow = @cast<frac8>(my_frac32);

Widening (e.g., frac32→frac64): All 12 widening conversions are lossless static_cast operations with ERR sentinel translation.

Narrowing (e.g., frac64→frac32): All 12 narrowing conversions range-check each component (whole, num, denom). If any component overflows the target width, the result is the ERR sentinel.

Dimensional Analysis

frac supports dimensional type annotations for compile-time unit checking:

frac32<Meters>:distance = {10, 1, 2};      // 10½ meters
frac32<Seconds>:time = 5;                    // 5 seconds
frac32<Meters>:sum = distance + distance;    // Same-unit addition ✓

// Scalar multiplication preserves dimension
frac32:scale = {2, 0, 1};
frac32<Meters>:scaled = distance * scale;    // 21 meters

// Division of same units yields dimensionless
frac32:ratio = distance / distance;          // Dimensionless ✓

At the IR level, dimensional types are erased — they share the same LLVM struct as the base frac type. All checking is compile-time only.

For full details on dimensional types, see the Dimensional Analysis User Guide.

Error Handling & The ERR Sentinel

frac uses a sticky ERR sentinel to indicate invalid operations safely.

What is ERR?

The ERR sentinel is {0, 0, 0} — a denominator of zero is impossible in valid fractions, making it an unambiguous error marker.

Triggers for ERR

Sticky Propagation

If any operand is ERR, the result is ERR:

frac32:bad = a / zero;       // bad is ERR
frac32:worse = bad + valid;  // worse is ERR — propagated
frac32:still = worse * b;    // still ERR — it never goes away

Checking for ERR

ERR follows TBB identity rules: - ERR == ERR evaluates to false - ERR != ERR evaluates to true - All ordered comparisons with ERR evaluate to false

Runtime Intrinsics

The following C runtime functions implement frac operations (called by the compiler, not typically invoked directly):

Function Description
npk_frac*_add(r, a, b) Addition with GCD normalization
npk_frac*_sub(r, a, b) Subtraction with GCD normalization
npk_frac*_mul(r, a, b) Multiplication with GCD normalization
npk_frac*_div(r, a, b) Division (zero → ERR) with GCD normalization
npk_frac*_neg(r, a) Negation
npk_frac*_cmp(a, b) Three-way comparison (-1, 0, +1)
npk_frac*_normalize(r, a) Normalize to canonical form
npk_frac*_to_int(a) Convert to integer (truncate)
npk_frac*_to_float(a) Convert to double (may lose precision)
npk_frac*_from_int(r, v) Convert integer to frac
npk_frac*_from_float(r, v) Convert double to frac
npk_frac*_to_fix256(r, a) Convert to fix256
npk_fix256_to_frac*(r, a) Convert from fix256

All functions use pointer-based calling convention (sret pattern) — the result is written through the first pointer parameter.

Performance Characteristics

Operation frac32 fix256 flt64
Add/Sub GCD + normalize Limb add/carry Hardware FPU
Multiply Cross-multiply + GCD 512-bit intermediate Hardware FPU
Divide Reciprocal + GCD Knuth Algorithm D Hardware FPU
Storage 12 bytes 32 bytes 8 bytes
Precision Exact rationals 2^-128 fixed ~15 decimal digits
Deterministic Yes Yes Platform-dependent

When to use frac: Exact rational arithmetic with moderate numerics (recipe scaling, financial ratios, musical intervals, probability).

When to use fix256: High-precision deterministic computation (physics, cryptography, scientific simulation).

When to use flt64: Maximum throughput where slight imprecision is acceptable.

Why Not Floats?

flt64:  1/3 = 0.333333333333... (truncated, drift accumulates)
frac32: 1/3 = {0, 1, 3}        (exact, zero drift)

flt64:  1/3 + 1/3 + 1/3 ≈ 0.999999...  (not exactly 1.0)
frac32: 1/3 + 1/3 + 1/3 = {1, 0, 1}    (exactly 1)

K-Semantics Coverage

The frac type is fully formalized in the K framework (v0.49.7 FRAC-038..043):

Rule Set K Status
Value constructors (FRAC8..64) ✅ Full
ERR sentinel & propagation ✅ Full
GCD normalization ✅ Full
Arithmetic (+, -, ×, ÷) ✅ Full (frac32)
Comparison (==, !=, <, ≤, >, ≥) ✅ Full (frac32)
Conversions (int↔frac) ✅ Full
Dimensional DIM wrappers ✅ Full

Related