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:
- Positive denominator:
denom > 0(always) - Non-negative numerator:
0 ≤ num < denom - Sign in whole: The sign is carried by
whole;numis always non-negative - Fully reduced:
GCD(num, denom) == 1(lowest terms) - 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
- Convert both operands to improper fractions:
W*D + N/D - Apply the operation:
- Add:
(a_num * b_den + b_num * a_den) / (a_den * b_den) - Sub:
(a_num * b_den - b_num * a_den) / (a_den * b_den) - Mul:
(a_num * b_num) / (a_den * b_den) - Div:
(a_num * b_den) / (a_den * b_num) - 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
- Division by zero:
a / {0, 0, 1}→ ERR - TBB overflow: Component arithmetic (from underlying TBB operations) that exceeds the width's range → ERR
- Narrowing overflow: Width conversion where a component doesn't fit → 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
- tbb.md — underlying component type with ERR semantics
- fix256.md — deterministic fixed-point alternative
- flt.md — IEEE 754 floating point
- dimensional.md — dimensional type annotations