feat(lang): float numeric-limit accessors — examples, unit tests, docs [NL.2]

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.
This commit is contained in:
agra
2026-06-04 23:30:41 +03:00
parent b7069801bd
commit 463557990f
11 changed files with 310 additions and 1 deletions

View File

@@ -283,6 +283,64 @@ n := u64.max; // 18446744073709551615 (all-ones)
`string`, a pointer, a `struct`, `void`, an `enum`) is a compile error, never
a silent value.
The **float** types `f32` and `f64` expose the same `.min` / `.max` plus a set of
float-only accessors. Each folds, at compile time, to a constant of the queried
float type (the same `lowerNumericLimit` intercept, via `builder.constFloat`):
```sx
hi := f64.max; // largest finite double
lo := f64.min; // most-NEGATIVE finite = -max (NOT C's DBL_MIN)
eps := f64.epsilon; // ULP of 1.0 (f64 = 2^-52, f32 = 2^-23)
mp := f64.min_positive; // smallest positive NORMAL (= C DBL_MIN / Rust MIN_POSITIVE)
tm := f64.true_min; // smallest positive SUBNORMAL (next value above 0.0)
pin := f64.inf; // +infinity
qn := f64.nan; // a quiet NaN
```
- **Receiver.** `f32` or `f64`.
- **Shared with integers.** `.min` / `.max` are valid on BOTH integer and float
types. `.min` is the most-NEGATIVE finite value, i.e. `-max` — consistent with
the integer `.min`, and deliberately **NOT** C's `DBL_MIN`/`FLT_MIN` (which is
the smallest positive normal; that is `.min_positive` here).
- **Float-only accessors.**
- `.epsilon` — the ULP of `1.0`: the gap between `1.0` and the next
representable value (`f64 = 2^-52 ≈ 2.22e-16`, `f32 = 2^-23`). This is the
**machine epsilon** used for relative-tolerance comparisons, **NOT** C#'s
`Double.Epsilon` (which is the smallest denormal — that is `.true_min` here).
Defining property: `1.0 + epsilon != 1.0` while `1.0 + epsilon/2.0 == 1.0`.
- `.min_positive` — the smallest positive **NORMAL** value (`f64 = 2^-1022`,
`f32 = 2^-126`). Equals C's `DBL_MIN` / Rust's `MIN_POSITIVE`.
- `.true_min` — the smallest positive **SUBNORMAL**: the next value above `0.0`
(`f64` bits `0x0000000000000001 = 2^-1074`, `f32` bits `0x00000001 = 2^-149`).
Named `true_min` (after Zig's `floatTrueMin`) to avoid the Java/Go/JS
`MIN_VALUE` footgun, where a bare `MIN_VALUE` names the smallest *subnormal*
yet reads like the most-negative value.
- **FTZ/DAZ caveat.** Subnormals are exactly the values that vanish under
flush-to-zero (FTZ) / denormals-are-zero (DAZ) CPU modes. If such a mode is
active, a loaded `.true_min` can flush to `0.0` on the **first arithmetic
operation** that touches it. The folded constant always carries the exact
subnormal bit pattern; read or store it through a bit reinterpret *before*
any arithmetic if you need the true value to survive. Numerical-library
authors who toggle FTZ/DAZ should not be surprised when `true_min * 1.0`
reads back as `0.0`.
- `.inf` — positive infinity (`inf > max`).
- `.nan` — a quiet NaN. The exact mantissa bits are not pinned; the only
guaranteed property is that it is unequal to everything, itself included
(`nan != nan` is `true` — native float `!=` lowers unordered, issue 0091).
- **Float-only on an integer is an error.** `.epsilon` / `.min_positive` /
`.true_min` / `.inf` / `.nan` applied to an integer type (`s32.epsilon`,
`u8.inf`, `s64.true_min`) is a clean compile error — integer types expose only
`.min` / `.max`.
- **Pinning the values.** The lexer has no exponent notation and the default
float formatter is crude (issue 0090), so float limits can be asserted neither
by literal comparison nor by printing. Reinterpret the bits through an untagged
union (`union { f: f64; bits: u64 }`) and compare against the exact IEEE-754
pattern — `f64.max = 0x7FEFFFFFFFFFFFFF`, `min = 0xFFEFFFFFFFFFFFFF`,
`epsilon = 0x3CB0000000000000`, `min_positive = 0x0010000000000000`,
`true_min = 0x0000000000000001`, `inf = 0x7FF0000000000000`; the `f32` set is
`0x7F7FFFFF` / `0xFF7FFFFF` / `0x34000000` / `0x00800000` / `0x00000001` /
`0x7F800000`.
### Enum Types
User-defined sum types with named variants. Variants may optionally carry typed data (tagged unions). Internally, payload-less enums are represented as `i64` (variant index). Enums with payloads are represented as `{ i64, [max_payload_size x i8] }` (tag + data).