Floating-Point
A floating-point type holds a binary fraction with an exponent, covering a vast range at a fixed number of significant digits. Rux has eight widths: the two IEEE 754 formats every supported machine computes in hardware, float32 and float64, and six more that the compiler computes itself.
The floating-point types
| Type | Bits | Bytes | Format | Significant digits | Largest finite value | Suffix |
|---|---|---|---|---|---|---|
float8 | 8 | 1 | E4M3: 4 exponent, 3 fraction bits | about 1 | 240 | f8 |
float16 | 16 | 2 | IEEE 754 binary16 | about 3 | 65,504 | f16 |
float32 | 32 | 4 | IEEE 754 binary32 | about 7 | ≈ 3.40 × 1038 | f32 |
float64 | 64 | 8 | IEEE 754 binary64 | about 16 | ≈ 1.80 × 10308 | f64 |
float80 | 80 | 16 | x87 extended precision | about 19 | ≈ 1.19 × 104932 | f80 |
float128 | 128 | 16 | IEEE 754 binary128 | about 34 | ≈ 1.19 × 104932 | f128 |
float256 | 256 | 32 | IEEE 754 binary256 | about 71 | ≈ 1.61 × 1078913 | f256 |
float512 | 512 | 64 | binary512: 23 exponent bits | about 147 | ≈ 2.07 × 101262611 | f512 |
float is a built-in alias of float64, and float64 is the type of an unsuffixed floating-point literal. Use float64 by default, and float32 where storage or bandwidth matters more than precision, such as large arrays of samples. The other widths are for data that arrives in their format and for precision beyond float64. They cost more: the compiler computes them with integer instructions rather than the machine's floating-point unit.
float8is the E4M3 format: one sign bit, four exponent bits with a bias of 7, and three fraction bits. It keeps IEEE-style infinities and NaNs in the all-ones exponent, so its largest finite value is 240.float80is the x87 extended format: a 64-bit significand with its integer bit stored, and a 15-bit exponent. Its 80 bits of value are held in 16 bytes, sofloat80::Bitsis 80 whilefloat80::Bytesandsizeof(float80)are 16.float512continues the IEEE 754 pattern past binary256, with a 23-bit exponent and 489 bits of precision.
Associated constants
Imported from Core like the integer constants, every width has the same eight:
| Constant | Meaning |
|---|---|
Bits | Width of the value in bits (uint) |
Bytes | Storage size in bytes (uint) |
Max | Largest finite value |
Lowest | Most negative finite value, -Max |
MinPositive | Smallest positive normal value |
Epsilon | Gap between 1.0 and the next value up |
Infinity | Positive infinity, which prints as Inf |
NaN | A quiet not-a-number, which prints as NaN |
| Type | Max | MinPositive | Epsilon |
|---|---|---|---|
float8 | 240 | 2−6 = 0.015625 | 2−3 = 0.125 |
float16 | 65,504 | 2−14 ≈ 6.10 × 10−5 | 2−10 ≈ 9.77 × 10−4 |
float32 | 3.4028235e+38 | 1.1754944e-38 | 1.1920929e-07 |
float64 | 1.7976931348623157e+308 | 2.2250738585072014e-308 | 2.220446049250313e-16 |
float80 | ≈ 1.19 × 104932 | 2−16382 ≈ 3.36 × 10−4932 | 2−63 ≈ 1.08 × 10−19 |
float128 | ≈ 1.19 × 104932 | 2−16382 ≈ 3.36 × 10−4932 | 2−112 ≈ 1.93 × 10−34 |
float256 | ≈ 1.61 × 1078913 | 2−262142 ≈ 2.48 × 10−78913 | 2−236 ≈ 9.06 × 10−72 |
float512 | ≈ 2.07 × 101262611 | 2−4194302 ≈ 1.94 × 10−1262611 | 2−488 ≈ 1.25 × 10−147 |
Core writes the limits of float8 through float64 as literals. Infinity and NaN at every width, and every limit of float80 and wider, are intrinsic constants whose values the compiler takes from the format itself.
import Core::{ float16, float64 };
import Io::PrintLine;
func Main() -> int {
PrintLine("{} {}", float64::Max, float64::Epsilon);
PrintLine("{} {}", float64::Infinity, -float64::Infinity); // Inf -Inf
PrintLine("{} {}", float16::Max, float16::Epsilon); // 65500.0 0.000977
return 0;
}
float16::Max prints as 65500.0 because {} writes the shortest decimal that reads back as the same value at its own width, and no other float16 is nearer to 65,500 than 65,504 is.
Printing the wide widths
float8 through float64 implement Display and Debug, so {} prints them. float80 and wider do not: the exact arithmetic that finds their digits allocates, and those protocols offer nowhere to pass an allocator. PrintLine("{}", 1.0f80) is therefore an error:
error: no matching overload for 'PrintLine' with argument types (char8[..], float80)
Render one through the Format package's Float80 to Float512 values instead, which take the allocator explicitly:
import Allocator::{ Allocator, SystemAllocator };
import Core::{ Succeeded, float128 };
import Format::{ ByteCursor, Float128 };
import Io::PrintLine;
import Text::{ FormatSpec, TextWriter };
func Main() -> int {
let third = 1.0f128 / 3.0f128;
var system = SystemAllocator();
let allocator: Allocator = system;
var storage: char8[64];
var cursor = ByteCursor(storage[..]);
let writer: &var TextWriter = cursor;
if !Succeeded(Float128::FromValue(third).FormatWith(allocator, writer, FormatSpec::Plain())) {
return 1;
}
PrintLine("{}", cursor.Written()); // 0.3333333333333333333333333333333333
return 0;
}
Literals
A floating-point literal has digits on both sides of a point, an exponent, or a float suffix: 3.14, 2.998e8, 1.6e-19, 0.5f32, 1f16. A suffixed literal is read exactly at its own width: 0.1f128 is the float128 nearest to one tenth, not a float64 widened. Without a suffix a literal is a float64, whatever the context:
let single: float32 = 0.75;
error: cannot assign 'float64' to 'float32'
Write 0.75f32. A literal too large for its width is an infinity: 300.0f8 is Inf. Literals gives the full grammar and every suffix.
Operations
| Operators | Result | Rules |
|---|---|---|
+ - * / | The operand type | IEEE 754, rounded to nearest with ties to even |
% | The operand type | Remainder of truncated division: 7.5 % 2.0 is 1.5 |
prefix - | The operand type | Flips the sign, including of zero and infinity |
== != < <= > >= | bool | IEEE 754 comparison |
At every width, % is the exact remainder of truncating division, with the dividend's sign, as C's fmod computes it. Its non-NaN result agrees bit for bit with constant folding on every target, including large quotients. On x86-64, float32 and float64 remainder use x87 fprem; on AArch64 they use compiler-generated helpers.
Floating-point arithmetic never panics, at any width. Results follow IEEE 754:
| Expression | Result |
|---|---|
1.0 / 0.0 | Inf |
-1.0 / 0.0 | -Inf |
0.0 / 0.0 | NaN |
A NaN is unordered: every comparison with it is false except !=, so x != x is true exactly when x is a NaN. The Core functions IsNaN, IsInfinite, IsFinite, IsZero and IsNegativeZero test for special values at every width.
The bitwise and shift operators do not apply to floats: a & 1.0 is error: operator '&' requires an integer, bool, or character left operand, but found 'float64'.
Most decimal fractions have no exact binary form, so arithmetic on them rounds: 0.1 + 0.2 == 0.3 is false. Compare against a tolerance, or count in integer units such as cents.
Mixed widths
A float32 operand meets a float64 operand at float64. No other two widths mix: with half: float16 and single: float32, half + single is error: operator '+' cannot combine left operand 'float16' with right operand 'float32'. Convert one operand with as.
Because an unsuffixed literal is a float64, single * 2.0 is a float64, and half * 2.0 is an error. Write single * 2.0f32 to stay in float32, and half * 2.0f16.
How each width is computed
float32 and float64 use the machine's floating-point instructions. Every other width is computed by helper functions the compiler synthesizes from integer operations while it lowers the program. They are named __rux_float_f<bits>_<operation>, generated only for the operations a program uses, and private to it. They depend on no package, Core included, so arithmetic at every width works in a program with no dependencies.
float80 on x86-64 is the exception, because the x87 unit computes in exactly that format. +, -, *, /, %, the comparisons, conversion between float80 and float32 or float64, and conversion from a signed integer of at most 64 bits run on x87. Around each of these that rounds, the compiler sets the x87 control word to 64-bit precision with round-to-nearest and restores the caller's word afterwards, since Windows leaves the unit at 53-bit precision. --emit asm shows this code. Every other float80 conversion goes through the helpers, as all of float80 does on AArch64.
For float8, float16 and float80 through float512, every result that is not a NaN is bit-identical on every target, and identical to what the compiler computes when it folds the same expression while compiling. A NaN result is only guaranteed to be a NaN: its sign and payload may differ between targets, and between folding and running. A program that needs a NaN writes T::NaN, and tests for one with x != x or IsNaN, never by its bits.
Conversions
float32 converts to float64 implicitly; every float32 value is exactly a float64 value. That is the only implicit conversion between floating-point widths. Every other one, and every conversion between a float and another type, is written with as:
| Conversion | Result |
|---|---|
| To another float width | Rounded to nearest with ties to even; a value beyond its range becomes an infinity |
| To an integer type | Truncated toward zero; beyond the range, saturated at Min or Max; NaN becomes 0 |
| From an integer type | The nearest representable value |
| To a boolean type | false for a zero of either sign; true for anything else, NaN included |
let ratio = 3.9;
let whole = ratio as int32; // 3
let down = -3.9 as int32; // -3
let capped = 1e10 as int32; // 2147483647
let floor = -1.0 as uint8; // 0
let single = 3.141592653589793 as float32; // 3.1415927
let back = 7 as float64; // 7.0
let tenth = 0.1 as float16; // 0.1, stored as 0.0999755859375
let zero = -0.0f16 as bool; // false
Float-to-integer conversion is the same for every float width and every integer width, on every target, and whether it runs or is folded at compile time. It never wraps: 1e20 as int32 is int32::Max, not its low bits, and float512::Infinity as int512 is int512::Max. Round first, with the Math package, when rounding rather than truncation is meant.
The C boundary
C's float and double are float32 and float64, and they are the only floating-point widths the C calling convention knows. The others are passed by the compiler's own convention, which matches no C type — not _Float16, long double or __float128 — so one of them in an extern or #Abi signature, or as a C-variadic argument, is an error. See Foreign Function Interface.
See also
- Integers — the other numeric family
- Arithmetic and Casts
- Intrinsics — the compiler-supplied constants
- API: Core floating-point types,
Float128 - Float and Float special — lessons on precision, infinities and NaN
- Math — rounding, powers and roots