Fixed Point — fix256
Overview
fix256 is a Q128.128 deterministic fixed-point type: 128-bit integer part + 128-bit fractional part = 256 bits total. It provides bit-exact identical results across all platforms (x86-64, ARM64, RISC-V, CUDA) by avoiding floating-point drift. This is critical for systems like robotic control and consciousness substrates where non-determinism can lead to cascading failures.
Specifications
| Property | Value |
|---|---|
| Total size | 256 bits (32 bytes) |
| Integer part | 128 bits (signed, two's complement) |
| Fractional part | 128 bits |
| Precision | 2^-128 ≈ 2.9×10^-39 |
| Range | ±2^127 ≈ ±1.7×10^38 |
| Storage | 4 × 64-bit limbs |
| MSB/Sign | limb[3] bit 63 |
| Deterministic | Yes — zero drift, bit-exact across platforms |
| ERR Sentinel | 0x8000000000000000 in limb[3], zeros elsewhere |
Internal Representation
fix256 is stored internally as a 256-bit two's complement integer, representing the value multiplied by 2^128. The 4 64-bit limbs are ordered little-endian:
- limb[0]: Fractional bits 0-63
- limb[1]: Fractional bits 64-127
- limb[2]: Integer bits 0-63
- limb[3]: Integer bits 64-127 (includes sign bit)
Declaration
You can declare fix256 variables using literal syntax or via explicit conversion functions:
// Using literal suffix
fix256:pi = 3.1415926535fix256;
fix256:negative = -10.5fix256;
// From integers
fix256:a = fix256_from_int(10);
fix256:b = fix256_from_int(-42);
// From floats (note: conversion from float imports float non-determinism at the boundary)
fix256:c = fix256_from_float(3.0);
Arithmetic Operations
All arithmetic operations are fully deterministic and handle overflow, underflow, and errors safely.
fix256:sum = a + b;
fix256:diff = a - b;
fix256:prod = a * b;
fix256:quot = a / b;
fix256:rem = a % b;
- Addition/Subtraction: Limb-wise addition/subtraction with carry/borrow.
- Multiplication: Full 512-bit intermediate product, right-shifted by 128 bits to realign the decimal point.
- Division: Implemented using Knuth's Algorithm D with full precision.
Comparison Operators
fix256 supports standard signed comparison operations. Note that comparisons involving the ERR sentinel follow sticky error semantics (see Error Handling below).
if (a == b) { /* Equality */ }
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 */ }
Bitwise Operations
Bitwise operations operate on the underlying 256-bit representation.
// Shifts operate on the entire 256-bit value
fix256:shifted_left = a << 1; // Equivalent to multiplying by 2
fix256:shifted_right = a >> 1; // Arithmetic shift right (preserves sign)
Math Intrinsics
fix256:f = fix256_floor(a); // Round towards negative infinity
fix256:t = fix256_trunc(a); // Truncate fractional part (round towards zero)
Error Handling & The ERR Sentinel
fix256 employs a sticky ERR sentinel to indicate invalid operations without crashing the program (Undefined Behavior).
What is ERR?
The ERR sentinel is the minimum signed 256-bit value: limb[3] = 0x8000000000000000, with all other limbs set to 0. It cannot be produced by valid arithmetic.
Triggers for ERR:
- Division by zero (a / 0 → ERR)
- Arithmetic overflow (fix256_MAX() + 1 → ERR)
- Arithmetic underflow (subtracting beyond minimum valid value → ERR)
Sticky Propagation:
If any operand is ERR, the result of the operation is ERR. This ensures errors are not silently absorbed.
fix256:bad = a / 0; // bad is ERR
fix256:worse = bad + 10; // worse is ERR
Checking for ERR:
Since ERR represents an invalid state, it follows TBB identity rules:
- ERR == ERR evaluates to false
- ERR != ERR evaluates to true
- ERR < x evaluates to true for any valid x (as it is the minimum representable value)
To safely check if a value is valid, use the ok() function:
if (ok(bad) == 0) {
// bad is ERR
}
Constants
fix256:max_val = fix256_MAX(); // Largest positive value
fix256:err_val = fix256_ERR(); // The ERR sentinel
fix256:eps = fix256_EPSILON(); // Smallest positive non-zero value (2^-128)
Type Conversions
Converting back to primitive types strips the fractional component:
// Truncates fractional part
int64:val = fix256_to_int(sum);
// Using @cast
int32:val32 = @cast<int32>(sum);
Dimensional Analysis Variants
fix256 supports dimensional type annotations for physics/engineering, allowing the compiler to enforce unit correctness at compile-time.
fix256<Joules>:energy = 100.0fix256;
fix256<Meters>:distance = 50.0fix256;
For full details on dimensional types and operations, see the Dimensional Analysis User Guide.
Performance
While fix256 provides exact determinism and massive precision, it comes with a performance overhead compared to hardware floating-point operations:
- CPU: Operations are emulated using 64-bit integer limbs. Addition/subtraction are fast (a few instructions), but multiplication and division require multi-limb algorithms (Knuth's Algorithm D).
- GPU: Full PTX implementations are provided for CUDA, enabling massively parallel execution.
If absolute determinism is not required and performance is the sole bottleneck, consider IEEE 754 floating point types (flt32, flt64).
Why Not Floats?
- Float problem:
0.1 + 0.2 ≠ 0.3(IEEE 754 representation error) - Float problem: Results differ across platforms (x87 vs SSE, ARM vs x86)
- Float problem: Non-associative:
(a + b) + c ≠ a + (b + c) - fix256: Same bits in → same bits out, every time, every platform
Real-World Example
// Deterministic physics update loop
func:update_physics = void(fix256:dt) {
fix256:velocity = 10.5fix256;
fix256:position = 0.0fix256;
for (int32:i = 0; i < 1000; i++) {
fix256:delta = (velocity * dt) ? fix256_ERR();
if (ok(delta) == 0) {
!!! ERR_PHYSICS_CORRUPTION;
}
position = (position + delta) ? fix256_ERR();
}
}
Related
- dimensional.md — Dimensional analysis with fix256
- flt.md — IEEE 754 floating point (faster but non-deterministic)
- frac.md — Exact rational arithmetic
- int.md — LBIM integers used internally