GF32: a single
You will learn
The 32-bit GoldenFloat, 1 + 12 + 19, bias 2047, and the two invariants that compare it with IEEE single.
gf32.t27 holds 32 bits: 1 sign, 12 exponent, 19 mantissa bits, and bias 2047. IEEE single spends 8 bits on the exponent and 23 on the mantissa; the spec keeps two invariants that say so, EXP_BITS > 8 and MANT_BITS < 23, so GF32 trades precision for range at the same width. Its PHI_DISTANCE is 0.0135, under the test bound of 0.015, and MEMORY_RATIO_VS_FP32 is 1. A comment calls GF32 second after GF12; by the distances in this course it is closer than GF12, and GF14 is closer still.
Try it
Find the two invariants of gf32.t27 that compare it with IEEE single. Then check EXP_BIAS against 2^(12 - 1) - 1 and compute 12 / 19.

GF32: 32 bits as the spec lays them out, beside IEEE single, read from gf32.t27. Lesson 16 of the GoldenFloat course.
specs/numeric/gf32.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/numeric/gf32.t27
// GoldenFloat32 — 32-bit φ-structured floating point
// NUMERIC-STANDARD-001 — Agent 8 (P1)
module GF32 {
// Import base format family
use numeric::goldenfloat_family;
use numeric::phi_ratio;
// ═════════════════════════════════════════════════════════════════
// 1. Format Definition
// ═════════════════════════════════════════════════════════════════════════
// GF32 bit layout: [S|EEEE EEEE EEEE|MMM MMMM MMMM MMMM MMMM MMM]
// S: 1 bit (sign)
// E: 12 bits (exponent)
// M: 19 bits (mantissa)
const BITS : u8 = 32;
const SIGN_BITS : u8 = 1;
const EXP_BITS : u8 = 12;
const MANT_BITS : u8 = 19;
// Bias for exponent (2^(12-1) - 1 = 2047)
const EXP_BIAS : u16 = 2047;
// φ-ratio: exp/mant = 12/19 ≈ 0.632 (phi_distance = 0.014)
// This is the second-best φ-approximation after GF12
const PHI_DISTANCE : f64 = 0.01354495894042812;
// ═════════════════════════════════════════════════════════════════
// 2. GoldenFloat32 Type
// ═════════════════════════════════════════════════════════════════════════
struct GF32 {
raw : u32, // 32-bit raw value
}
// ═════════════════════════════════════════════════════════════════
// 3. Encoding/Decoding
// ═════════════════════════════════════════════════════════════════════════
// Encode f32 to GF32
fn encode(value: f32) -> GF32 {
if (value == 0.0) {
return GF32{ raw = 0 };
}
const sign = if (value < 0.0) { 1u32 } else { 0u32 };
const abs_val = if (value < 0.0) { -value } else { value };
// Extract exponent (unbiased)
const exp_unbiased = floor_log2(abs_val) as i16;
const exp_biased = (exp_unbiased + EXP_BIAS as i16) as u16;
// Clamp exponent
const exp_clamped = clamp_u16(exp_biased, 0, (1u16 << EXP_BITS) - 1);
// Extract mantissa (19 bits)
const mant = extract_mantissa(abs_val, exp_unbiased, MANT_BITS);
return GF32{
raw = (sign << 31) |
((exp_clamped as u32) << MANT_BITS) |
(mant as u32)
};
}
// Decode GF32 to f32
fn decode(gf: GF32) -> f32 {
const sign = (gf.raw >> 31) as u8;
const exp_biased = ((gf.raw >> MANT_BITS) & 0xFFF) as u16;
const mant = (gf.raw & 0x7FFFF) as u32;
// Zero
if (exp_biased == 0 && mant == 0) {
return 0.0;
}
// Exponent
const exp_unbiased = if (exp_biased == 0) {
-(EXP_BIAS as i16) + 1
} else {
(exp_biased as i16) - EXP_BIAS as i16
};
// Mantissa
const mant_normalized = if (exp_biased == 0) {
(mant as f32) / 524288.0
} else {
1.0 + (mant as f32) / 524288.0
};
const value = mant_normalized * pow(2.0, exp_unbiased as f32);
if (sign != 0) {
return -value;
}
return value;
}
// ═════════════════════════════════════════════════════════════════
// 4. Format Properties
// ═════════════════════════════════════════════════════════════════════════
fn max_value() -> f32 {
const mant_max = 1.0 + 524287.0 / 524288.0;
const exp_max = (1i16 << EXP_BITS) - 1 - EXP_BIAS as i16;
return mant_max * pow(2.0, exp_max as f32);
}
fn min_positive() -> f32 {
const mant_min = 1.0 / 524288.0;
const exp_min = -(EXP_BIAS as i16) + 1;
return mant_min * pow(2.0, exp_min as f32);
}
fn epsilon() -> f32 {
return 1.0 / 524288.0; // 0.000001907
}
// ═════════════════════════════════════════════════════════════════
// 5. Validation
// ═════════════════════════════════════════════════════════════════════════
fn validate_format() -> bool {
const fmt = goldenfloat_family::get_format_by_name("GF32");
return (fmt != null) &&
(fmt.?.bits == BITS) &&
(fmt.?.exp_bits == EXP_BITS) &&
(fmt.?.mant_bits == MANT_BITS);
}
// ═════════════════════════════════════════════════════════════════
// 6. Use Cases
// ═════════════════════════════════════════════════════════════════════════
// GF32 is optimal for:
// - Near-IEEE 754 precision with φ-optimized layout
// - 12-bit exponent (vs IEEE's 8-bit) for wider dynamic range
// - 19-bit mantissa (vs IEEE's 23-bit) - still good precision
// - Same memory footprint as FP32, better φ-ratio
// Comparison with IEEE FP32:
// - IEEE: 1 sign, 8 exp, 23 mant → exp/mant = 0.348 (phi_distance = 0.270)
// - GF32: 1 sign, 12 exp, 19 mant → exp/mant = 0.632 (phi_distance = 0.014)
// Memory: 32 bits = 4 bytes (same as FP32)
const MEMORY_RATIO_VS_FP32 : f32 = 1.0;
// ═════════════════════════════════════════════════════════════════
// 7. Helper Functions
// ═════════════════════════════════════════════════════════════════════════
fn floor_log2(x: f32) -> i16 {
if (x <= 0.0) { return -32768; }
let mut exp : i16 = 0;
while (x >= 2.0) {
x = x / 2.0;
exp = exp + 1;
}
while (x < 1.0) {
x = x * 2.0;
exp = exp - 1;
}
return exp;
}
fn extract_mantissa(value: f32, exp: i16, mant_bits: u8) -> u32 {
const normalized = value / pow(2.0, exp as f32);
const frac = normalized - 1.0;
const max_mant = (1u32 << mant_bits) - 1;
return (frac * (max_mant as f32 + 1.0)) as u32;
}
fn clamp_u16(x: u16, min: u16, max: u16) -> u16 {
if (x < min) { return min; }
if (x > max) { return max; }
return x;
}
fn pow(base: f32, exp: f32) -> f32 {
// Efficient power function for GF32
// Integer exponent: binary exponentiation
// Fractional exponent: use logarithm approximation
if (base <= 0.0 || exp == 0.0) {
if (exp == 0.0) {
return 1.0;
}
if (base == 0.0 && exp > 0.0) {
return 0.0;
}
return 0.0 / 0.0; // NaN for negative base with non-integer exp
}
// Check if exponent is (approximately) integer
const is_integer = exp == floor(exp);
if (is_integer) {
// Binary exponentiation for integer exponents
let exp_int = exp as i32;
let mut result = 1.0;
let mut base_acc = base;
let mut e = exp_int;
if (e < 0) {
e = -e;
base_acc = 1.0 / base_acc;
}
while (e > 0) {
if (e % 2 == 1) {
result = result * base_acc;
}
base_acc = base_acc * base_acc;
e = e / 2;
}
return result;
}
// Fractional exponent: x^y = exp(y * ln(x))
const ln_val = ln_approx(base);
return exp_approx(exp * ln_val);
}
// Natural logarithm approximation
fn ln_approx(x: f32) -> f32 {
if (x <= 0.0) {
return 0.0 / 0.0; // NaN
}
if (x == 1.0) {
return 0.0;
}
// Series: ln(x) = 2 * ((x-1)/(x+1) + 1/3*((x-1)/(x+1))^3 + ...)
const t = (x - 1.0) / (x + 1.0);
const t2 = t * t;
const t3 = t2 * t;
const t5 = t3 * t2;
const t7 = t5 * t2;
return 2.0 * (t + t3 / 3.0 + t5 / 5.0 + t7 / 7.0);
}
// Exponential approximation
fn exp_approx(x: f32) -> f32 {
if (x == 0.0) {
return 1.0;
}
// Taylor series: e^x = 1 + x + x^2/2! + x^3/3! + ...
let mut result = 1.0;
let mut term = 1.0;
let mut exp_x = x;
// Scale down for large inputs
if (exp_x > 5.0 || exp_x < -5.0) {
const k = floor(exp_x / 5.0) as i32;
exp_x = exp_x - (k as f32) * 5.0;
}
for (i in 1..=8) {
term = term * exp_x / (i as f32);
result = result + term;
}
// Scale back if needed
if (x > 5.0 || x < -5.0) {
const k = floor(x / 5.0) as i32;
if (k > 0) {
for (i in 0..k) {
result = result * exp_approx(5.0);
}
} else if (k < 0) {
for (i in k..0) {
result = result / exp_approx(5.0);
}
}
}
return result;
}
// Floor function
fn floor(x: f32) -> f32 {
let xi = x as i32;
if (x >= 0.0 || x == xi as f32) {
return xi as f32;
}
return (xi - 1) as f32;
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// TDD-Inside-Spec: Tests and Invariants for GF32
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
test gf32_decode_zero
given gf = GF32{ raw = 0 }
when value = decode(gf)
then value == 0.0
test gf32_encode_zero_roundtrip
given original = 0.0
and encoded = encode(original)
and decoded = decode(encoded)
then decoded == original
test gf32_bits_sum_correct
given total = SIGN_BITS + EXP_BITS + MANT_BITS
then total == BITS
test gf32_max_value_positive
given max_val = max_value()
then max_val > 0.0
test gf32_min_positive_greater_than_zero
given min_pos = min_positive()
then min_pos > 0.0
test gf32_epsilon_positive
given eps = epsilon()
then eps > 0.0
test gf32_phi_distance_near_optimal
given phi_dist = PHI_DISTANCE
then phi_dist < 0.015
test gf32_memory_ratio_equals_one
given ratio = MEMORY_RATIO_VS_FP32
then ratio == 1.0
test gf32_validate_format_success
given valid = validate_format()
then valid == true
invariant gf32_bits_constant
assert BITS == 32
invariant gf32_sign_bits_is_one
assert SIGN_BITS == 1
invariant gf32_exp_bits_is_twelve
assert EXP_BITS == 12
invariant gf32_mant_bits_is_nineteen
assert MANT_BITS == 19
invariant gf32_max_ge_min_positive
assert max_value() >= min_positive()
invariant gf32_phi_distance_near_optimal
assert PHI_DISTANCE < 0.015
invariant gf32_exp_bias_positive
assert EXP_BIAS > 0
invariant gf32_exp_wider_than_ieee
assert EXP_BITS > 8 // IEEE FP32 has 8-bit exponent
invariant gf32_mant_narrower_than_ieee
assert MANT_BITS < 23 // IEEE FP32 has 23-bit mantissa
test gf32_pow_zero_exponent_returns_one
given result = pow(2.0, 0.0)
then abs(result - 1.0) < 1e-6
test gf32_pow_one_exponent_returns_base
given result = pow(5.0, 1.0)
then abs(result - 5.0) < 1e-6
test gf32_pow_positive_integer_exponent
given result = pow(2.0, 5.0)
and expected = 32.0
then abs(result - expected) < 1e-5
test gf32_pow_negative_integer_exponent
given result = pow(2.0, -3.0)
and expected = 0.125
then abs(result - expected) < 1e-5
test gf32_pow_fractional_exponent
given result = pow(4.0, 0.5)
and expected = 2.0
then abs(result - expected) < 1e-4
test gf32_pow_zero_base_positive_exponent
given result = pow(0.0, 5.0)
then result == 0.0
test gf32_pow_one_base_any_exponent
given result1 = pow(1.0, 10.0)
and result2 = pow(1.0, -5.0)
then abs(result1 - 1.0) < 1e-6 and abs(result2 - 1.0) < 1e-6
test gf32_ln_approx_of_one
given result = ln_approx(1.0)
then abs(result) < 1e-6
test gf32_ln_approx_of_e
given e = 2.718281828459045 as f32
and result = ln_approx(e)
then abs(result - 1.0) < 0.01
test gf32_ln_approx_negative_returns_nan
given result = ln_approx(-1.0)
then result != result // NaN check
test gf32_exp_approx_zero
given result = exp_approx(0.0)
then abs(result - 1.0) < 1e-6
test gf32_exp_approx_one
given e = 2.718281828459045 as f32
and result = exp_approx(1.0)
then abs(result - e) < 0.01
test gf32_exp_approx_negative
given result = exp_approx(-1.0)
and expected = 1.0 / 2.718281828459045 as f32
then abs(result - expected) < 0.01
test gf32_floor_positive
given result = floor(3.7)
then abs(result - 3.0) < 1e-6
test gf32_floor_negative
given result = floor(-3.2)
then abs(result - (-4.0)) < 1e-6
test gf32_floor_integer
given result = floor(5.0)
then abs(result - 5.0) < 1e-6
invariant gf32_pow_zero_exponent_identity
// For every positive x; checked at three points, one on each side of 1.
given x1 = 0.5
and x2 = 2.5
and x3 = 7.0
assert abs(pow(x1, 0.0) - 1.0) < 1e-6 and abs(pow(x2, 0.0) - 1.0) < 1e-6 and abs(pow(x3, 0.0) - 1.0) < 1e-6
invariant gf32_pow_one_exponent_identity
// For every valid x; checked at three points.
given x1 = 0.5
and x2 = 2.5
and x3 = 7.0
assert abs(pow(x1, 1.0) - x1) < 1e-5 and abs(pow(x2, 1.0) - x2) < 1e-5 and abs(pow(x3, 1.0) - x3) < 1e-5
invariant gf32_ln_exp_inversion
given x = 2.0
and y = ln_approx(x)
then abs(exp_approx(y) - x) < 0.01
invariant gf32_floor_returns_integer
// floor returns a whole number, and the floor of a whole number is itself.
given r1 = floor(3.7)
and r2 = floor(-3.2)
assert abs(floor(r1) - r1) < 1e-6 and abs(floor(r2) - r2) < 1e-6
invariant gf32_floor_monotonic
given x1 = 2.5
and x2 = 3.5
assert floor(x1) <= floor(x2)
bench gf32_pow_integer_exponent
measure: nanoseconds to compute pow(2.0, 10.0)
target: < 500ns
bench gf32_ln_latency
measure: nanoseconds to compute ln_approx(2.0)
target: < 300ns
bench gf32_exp_latency
measure: nanoseconds to compute exp_approx(1.0)
target: < 500ns
bench gf32_floor_latency
measure: nanoseconds to compute floor(3.7)
target: < 50ns
bench gf32_encode_latency
measure: nanoseconds to encode(1.0)
target: < 300ns
bench gf32_decode_latency
measure: nanoseconds to decode(GF32{raw = 1065353216})
target: < 250ns
}
All lessons
Module 1 · The rule and its numbers
One rule splits every width, the ratio it aims at, and the Lucas numbers behind the 3.
Module 2 · Why phi, why three
Why the split is phi, why base three, and how a spec checks GF16 keeps phi.
Module 3 · The small rungs: GF4 to GF8
GF4, GF6 and GF8, the fewest bits, where rounding to whole bits costs the most.
Module 4 · Ten to fourteen bits
GF10, GF12 and GF14, and how the distance from 1 / phi moves as the word grows.
Module 5 · GF16 at work
The primary 16-bit format, a two-term dot product in GF-T16, then GF20 and GF24.
Module 6 · GF32 to GF64
GF32 beside IEEE single, GF48 with no IEEE twin, GF64 beside IEEE double.
Module 7 · GF96 to GF256
GF96, GF128 and GF256, where the specs hold the layout with invariants.
Module 8 · The widest rungs, then trits
GF512 and GF1024, the two widest rungs, then GF-T8, where the exponent moves to trits.
Module 9 · More trits, then the decode
GF-T16 and GF-T32, then why fixed fields decode in parallel and a posit does not.