Finish NL.2 on top of the WIP compiler impl (2e9e4fe): f32/f64 expose
.min/.max plus the float-only .epsilon/.min_positive/.true_min/.inf/.nan,
folded via the shared lowerNumericLimit intercept + builder.constFloat.
- examples/0159: pins every f32/f64 accessor by untagged-union bit
reinterpret against exact IEEE-754 hex (true_min read before any
arithmetic — FTZ/DAZ), plus the defining-property checks
((1+eps)!=1 / (1+eps/2)==1, inf>max, min==-max, true_min<min_positive,
true_min>0, nan!=nan).
- examples/0160: float-only accessor on an int (s32.epsilon/u8.inf/
s64.true_min) and any accessor on a non-numeric type compile-error
cleanly (exit 1, pinned stderr).
- type_resolver.test.zig: floatLimitFor bit-pattern + property tests for
f32/f64, isLimitField coverage, null for non-float/non-limit fields.
- specs.md Numeric Limits: float accessors + the min=-max / min_positive=
smallest-normal / epsilon=ULP-of-1.0 / true_min=smallest-subnormal
clarifications, with the mandatory FTZ/DAZ flush-to-zero caveat.
readme.md overview updated.
90 lines
4.5 KiB
Plaintext
90 lines
4.5 KiB
Plaintext
// Float numeric-limit accessors: `f32`/`f64` expose `.min` / `.max` (sibling of
|
|
// the integer `.min`/`.max`, NL.1) plus the float-only `.epsilon`,
|
|
// `.min_positive`, `.true_min`, `.inf`, and `.nan`. Each folds, at compile time,
|
|
// to a constant of the QUERIED float type via the same `lowerNumericLimit`
|
|
// intercept as the integer case (`builder.constFloat` + the `std.math`
|
|
// constants), driven by `TypeResolver.floatLimitFor`.
|
|
//
|
|
// The lexer has no exponent notation and the default float formatter is crude
|
|
// (issue 0090), so these limits can be pinned NEITHER by literal comparison NOR
|
|
// by printing. Every accessor is asserted instead by reinterpreting its bits
|
|
// through an untagged union and comparing against the exact IEEE-754 hex
|
|
// pattern — plus the defining-property checks that no other value could satisfy.
|
|
//
|
|
// Semantics (Agra-ruled, consistent with the integer accessors):
|
|
// .min = most-NEGATIVE finite (= -max), NOT C's DBL_MIN
|
|
// .max = largest finite
|
|
// .epsilon = ULP of 1.0 (next f after 1.0 minus 1.0), NOT C#'s denormal Epsilon
|
|
// .min_positive = smallest positive NORMAL (= C DBL_MIN / Rust MIN_POSITIVE)
|
|
// .true_min = smallest positive SUBNORMAL (next value above 0.0)
|
|
// .inf = +infinity
|
|
// .nan = a quiet NaN
|
|
//
|
|
// Regression (issue 0091): `f64.nan != f64.nan` is true — native float `!=`
|
|
// lowers UNORDERED, so a NaN compares unequal to everything including itself.
|
|
#import "modules/std.sx";
|
|
|
|
// `bits` mirrors each float's raw IEEE-754 storage. f64 needs 64 bits, f32 32.
|
|
// The f64 union uses a `u64` view so the all-ones-ish positive patterns read as
|
|
// their true magnitude; the negative `f64.min` pattern (0xFFEF…) overflows the
|
|
// i64 literal parser, so it is pinned by the `min == -max` property instead.
|
|
Uf64 :: union { f: f64; bits: u64; }
|
|
Uf32 :: union { f: f32; bits: u32; }
|
|
|
|
main :: () -> s32 {
|
|
o : Uf64 = ---;
|
|
|
|
// Read `.true_min` (a subnormal) FIRST and through the union only — never via
|
|
// arithmetic. Under flush-to-zero / denormals-are-zero CPU modes a subnormal
|
|
// can flush to 0.0 on the first arithmetic op, so the bit reinterpret is the
|
|
// only reliable channel for it.
|
|
o.f = f64.true_min;
|
|
print("f64.true_min {}\n", o.bits == 0x0000000000000001); // true
|
|
|
|
o.f = f64.max;
|
|
print("f64.max {}\n", o.bits == 0x7FEFFFFFFFFFFFFF); // true
|
|
o.f = f64.epsilon;
|
|
print("f64.epsilon {}\n", o.bits == 0x3CB0000000000000); // true
|
|
o.f = f64.min_positive;
|
|
print("f64.min_positive {}\n", o.bits == 0x0010000000000000); // true
|
|
o.f = f64.inf;
|
|
print("f64.inf {}\n", o.bits == 0x7FF0000000000000); // true
|
|
|
|
p : Uf32 = ---;
|
|
p.f = f32.true_min;
|
|
print("f32.true_min {}\n", p.bits == 0x00000001); // true
|
|
p.f = f32.max;
|
|
print("f32.max {}\n", p.bits == 0x7F7FFFFF); // true
|
|
p.f = f32.min;
|
|
print("f32.min {}\n", p.bits == 0xFF7FFFFF); // true
|
|
p.f = f32.epsilon;
|
|
print("f32.epsilon {}\n", p.bits == 0x34000000); // true
|
|
p.f = f32.min_positive;
|
|
print("f32.min_positive {}\n", p.bits == 0x00800000); // true
|
|
p.f = f32.inf;
|
|
print("f32.inf {}\n", p.bits == 0x7F800000); // true
|
|
|
|
// Defining-property checks — true epsilon is the ULP of 1.0: adding it to 1.0
|
|
// changes the value, adding half of it does not (round-to-nearest-even).
|
|
print("(1+eps)!=1 {}\n", (1.0 + f64.epsilon) != 1.0); // true
|
|
print("(1+eps/2)==1 {}\n", (1.0 + f64.epsilon/2.0) == 1.0); // true
|
|
print("inf>max {}\n", f64.inf > f64.max); // true
|
|
// f64.min = -max (the 0xFFEF… bit pattern overflows the i64 literal parser).
|
|
print("min==-max {}\n", f64.min == -f64.max); // true
|
|
print("true_min<min_pos {}\n", f64.true_min < f64.min_positive); // true
|
|
print("true_min>0 {}\n", f64.true_min > 0.0); // true
|
|
// Quiet NaN: unequal to everything, itself included (mantissa bits not pinned).
|
|
print("nan!=nan {}\n", f64.nan != f64.nan); // true
|
|
|
|
// Result carries the QUERIED type: each binding is declared with the float
|
|
// type and round-trips, so a mistyped fold (boxed as Any / wrong width) would
|
|
// not type-check here.
|
|
e64 : f64 = f64.epsilon;
|
|
e32 : f32 = f32.epsilon;
|
|
q : Uf64 = ---; q.f = e64;
|
|
r : Uf32 = ---; r.f = e32;
|
|
print("typed eps bits {}\n", q.bits == 0x3CB0000000000000 and r.bits == 0x34000000); // true
|
|
|
|
return 0;
|
|
}
|