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

@@ -246,3 +246,74 @@ test "TypeResolver.integerLimitFor: null for non-integer receivers and non-limit
try std.testing.expect(TypeResolver.integerLimitFor("s64", "len") == null);
try std.testing.expect(TypeResolver.integerLimitFor("u8", "epsilon") == null);
}
test "TypeResolver.isLimitField: the accessor set, nothing else" {
// The full numeric-limit surface — int .min/.max plus the float-only ones.
for ([_][]const u8{ "min", "max", "epsilon", "min_positive", "true_min", "inf", "nan" }) |f| {
try std.testing.expect(TypeResolver.isLimitField(f));
}
// Ordinary fields / near-misses are not limit accessors.
try std.testing.expect(!TypeResolver.isLimitField("len"));
try std.testing.expect(!TypeResolver.isLimitField("ptr"));
try std.testing.expect(!TypeResolver.isLimitField("maximum"));
try std.testing.expect(!TypeResolver.isLimitField("Min"));
try std.testing.expect(!TypeResolver.isLimitField(""));
}
test "TypeResolver.floatLimitFor: pinned f64 bit patterns" {
const L = struct {
fn bits(name: []const u8, field: []const u8) u64 {
return @bitCast(TypeResolver.floatLimitFor(name, field).?);
}
};
// Exact IEEE-754 double bit patterns (the same values the example pins via
// a runtime bit reinterpret).
try std.testing.expectEqual(@as(u64, 0x7FEFFFFFFFFFFFFF), L.bits("f64", "max"));
try std.testing.expectEqual(@as(u64, 0xFFEFFFFFFFFFFFFF), L.bits("f64", "min")); // -max
try std.testing.expectEqual(@as(u64, 0x3CB0000000000000), L.bits("f64", "epsilon"));
try std.testing.expectEqual(@as(u64, 0x0010000000000000), L.bits("f64", "min_positive"));
try std.testing.expectEqual(@as(u64, 0x0000000000000001), L.bits("f64", "true_min"));
try std.testing.expectEqual(@as(u64, 0x7FF0000000000000), L.bits("f64", "inf"));
// .min = -max (NOT C's DBL_MIN, which is min_positive); the ordering holds.
const v = struct {
fn f(name: []const u8, field: []const u8) f64 {
return TypeResolver.floatLimitFor(name, field).?;
}
};
try std.testing.expectEqual(v.f("f64", "min"), -v.f("f64", "max"));
try std.testing.expect(v.f("f64", "true_min") < v.f("f64", "min_positive"));
try std.testing.expect(v.f("f64", "true_min") > 0.0);
try std.testing.expect(std.math.isInf(v.f("f64", "inf")));
// Quiet NaN: unequal to itself; exact mantissa bits intentionally not pinned.
try std.testing.expect(std.math.isNan(v.f("f64", "nan")));
}
test "TypeResolver.floatLimitFor: pinned f32 bit patterns (widened value narrows losslessly)" {
const L = struct {
// floatLimitFor widens every f32 limit to f64; narrowing back is lossless
// (the codegen path does the same via constFloat at the queried width).
fn bits(name: []const u8, field: []const u8) u32 {
return @bitCast(@as(f32, @floatCast(TypeResolver.floatLimitFor(name, field).?)));
}
};
try std.testing.expectEqual(@as(u32, 0x7F7FFFFF), L.bits("f32", "max"));
try std.testing.expectEqual(@as(u32, 0xFF7FFFFF), L.bits("f32", "min")); // -max
try std.testing.expectEqual(@as(u32, 0x34000000), L.bits("f32", "epsilon"));
try std.testing.expectEqual(@as(u32, 0x00800000), L.bits("f32", "min_positive"));
try std.testing.expectEqual(@as(u32, 0x00000001), L.bits("f32", "true_min"));
try std.testing.expectEqual(@as(u32, 0x7F800000), L.bits("f32", "inf"));
const nan_v: f32 = @floatCast(TypeResolver.floatLimitFor("f32", "nan").?);
try std.testing.expect(std.math.isNan(nan_v));
}
test "TypeResolver.floatLimitFor: null for non-float receivers and non-limit fields" {
// Integer / non-numeric / user names are not float-limit folds.
try std.testing.expect(TypeResolver.floatLimitFor("s32", "epsilon") == null);
try std.testing.expect(TypeResolver.floatLimitFor("u64", "max") == null);
try std.testing.expect(TypeResolver.floatLimitFor("usize", "min") == null);
try std.testing.expect(TypeResolver.floatLimitFor("bool", "nan") == null);
try std.testing.expect(TypeResolver.floatLimitFor("MyStruct", "inf") == null);
// A builtin float with a non-limit field is not a fold here.
try std.testing.expect(TypeResolver.floatLimitFor("f64", "len") == null);
try std.testing.expect(TypeResolver.floatLimitFor("f32", "ptr") == null);
}