WIP: float numeric-limit accessors (NL.2) — blocked on 0091 (nan != nan); examples/docs pending

This commit is contained in:
agra
2026-06-04 16:48:34 +03:00
parent b5a2535ab6
commit 2e9e4fe873
3 changed files with 81 additions and 21 deletions

View File

@@ -126,10 +126,13 @@ pub const ExprTyper = struct {
if (info.ty) |t| return t;
}
}
// Numeric-limit accessor: `<IntType>.min` / `.max` is a comptime
// const of the queried integer type — mirrors the lowerFieldAccess
// intercept so inference reports the same type (without it the
// const would be mistyped, e.g. boxed into an Any slot).
// Numeric-limit accessor: `<Type>.min`/`.max` (int or float) or a
// float-only `.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`
// is a comptime const of the queried type — mirrors the
// lowerFieldAccess intercept so inference reports the same type
// (without it the const would be mistyped, e.g. boxed into an Any
// slot). Only valid folds carry a type here; the cross-type error
// cases fall through (lowerNumericLimit emits the diagnostic).
{
const type_name: ?[]const u8 = switch (fa.object.data) {
.identifier => |id| id.name,
@@ -137,7 +140,9 @@ pub const ExprTyper = struct {
else => null,
};
if (type_name) |tn| {
if (TypeResolver.integerLimitFor(tn, fa.field) != null) {
if (TypeResolver.integerLimitFor(tn, fa.field) != null or
TypeResolver.floatLimitFor(tn, fa.field) != null)
{
if (TypeResolver.resolveBuiltinName(tn, &self.l.module.types)) |t| return t;
}
}

View File

@@ -4904,33 +4904,45 @@ pub const Lowering = struct {
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
}
/// Numeric-limit accessor intercept (`<IntType>.min` / `.max`), a sibling of
/// the `error.X` / `Struct.CONST` / pack-arity identifier-receiver intercepts
/// in `lowerFieldAccess`. Folds an integer type's `.min`/`.max` to a comptime
/// const of that type via the shared `TypeResolver` width logic (no second
/// width parser) + the existing `constInt` const path. Returns null when this
/// is not an integer-limit access, so the caller continues normal field
/// lowering. A `.min`/`.max` on a builtin NON-numeric receiver
/// (`bool`/`string`/`void`/`Any`/`noreturn`) is a clean diagnostic here (then
/// a placeholder, so lowering finishes and `hasErrors()` aborts the build); a
/// float receiver falls through (float limits are NL.2).
/// Numeric-limit accessor intercept (`<Type>.min`/`.max`/`.epsilon`/
/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` /
/// `Struct.CONST` / pack-arity identifier-receiver intercepts in
/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried
/// type via the shared `TypeResolver` logic (no second computor) + the
/// existing `constInt` / `constFloat` const paths:
/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`);
/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/
/// `.nan` → `constFloat` (via `floatLimitFor`).
/// Returns null when the field is not a limit accessor, or the receiver is not
/// a builtin type (a user struct → ordinary field lowering reports
/// field-not-found). Two clean diagnostics (then a placeholder, so lowering
/// finishes and `hasErrors()` aborts the build):
/// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`);
/// - any accessor on a builtin NON-numeric receiver
/// (`bool`/`string`/`void`/`Any`/`noreturn`).
fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref {
const name = switch (fa.object.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => return null,
};
if (!std.mem.eql(u8, fa.field, "min") and !std.mem.eql(u8, fa.field, "max")) return null;
if (!TypeResolver.isLimitField(fa.field)) return null;
const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null;
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
return self.builder.constInt(value, ty);
}
// A builtin receiver that is not an integer: floats are NL.2 (fall
// through), every other builtin (bool/string/void/Any/noreturn) has no
// numeric limit.
if (ty == .f32 or ty == .f64) return null;
if (TypeResolver.floatLimitFor(name, fa.field)) |value| {
return self.builder.constFloat(value, ty);
}
// The field is a limit accessor, but it does not apply to this type.
if (self.diagnostics) |d| {
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
if (TypeResolver.integerWidthSign(name) != null) {
// Integer receiver + a float-only accessor.
d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field });
} else {
// Non-numeric builtin receiver (bool/string/void/Any/noreturn).
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
}
}
return self.emitPlaceholder(fa.field);
}

View File

@@ -137,6 +137,49 @@ pub const TypeResolver = struct {
return integerLimitBits(wi, want_max);
}
/// The full numeric-limit accessor field set: `.min`/`.max` (valid on int AND
/// float) plus the float-only `.epsilon`/`.min_positive`/`.true_min`/`.inf`/
/// `.nan`. THE single trigger for the `lowerNumericLimit` intercept — only a
/// field in this set is treated as a limit access; anything else falls through
/// to ordinary field lowering. Keeps the accessor name set in one place so the
/// intercept and `expr_typer` can't recognize different surfaces.
pub fn isLimitField(field: []const u8) bool {
const names = [_][]const u8{ "min", "max", "epsilon", "min_positive", "true_min", "inf", "nan" };
for (names) |n| if (std.mem.eql(u8, field, n)) return true;
return false;
}
/// `<FloatType>.<field>` → the limit as an `f64` value (the queried type is
/// `f32`/`f64`; every f32 limit is exactly representable in f64, so widening
/// is lossless and the caller pairs the value with the queried `TypeId` —
/// `builder.constFloat` narrows it back at emit), or null when `name` is not a
/// builtin float type or `field` is not a limit accessor. Values come straight
/// from `std.math` (`floatMax`/`floatEps`/`floatMin`/`floatTrueMin`/`inf`/`nan`):
/// - `.min` = most-NEGATIVE finite (`-max`, NOT C's DBL_MIN)
/// - `.max` = largest finite
/// - `.epsilon` = ULP of 1.0 (`floatEps`; f64 = 2^-52, f32 = 2^-23)
/// - `.min_positive` = smallest positive NORMAL (`floatMin`; = C DBL_MIN)
/// - `.true_min` = smallest positive SUBNORMAL (`floatTrueMin`)
/// - `.inf` = +infinity, `.nan` = a quiet NaN
/// THE single name+field → float fold, shared by the value path (lower.zig)
/// and `expr_typer` so they can't disagree.
pub fn floatLimitFor(name: []const u8, field: []const u8) ?f64 {
if (std.mem.eql(u8, name, "f64")) return floatLimitValue(f64, field);
if (std.mem.eql(u8, name, "f32")) return floatLimitValue(f32, field);
return null;
}
fn floatLimitValue(comptime T: type, field: []const u8) ?f64 {
if (std.mem.eql(u8, field, "min")) return -@as(f64, std.math.floatMax(T));
if (std.mem.eql(u8, field, "max")) return @as(f64, std.math.floatMax(T));
if (std.mem.eql(u8, field, "epsilon")) return @as(f64, std.math.floatEps(T));
if (std.mem.eql(u8, field, "min_positive")) return @as(f64, std.math.floatMin(T));
if (std.mem.eql(u8, field, "true_min")) return @as(f64, std.math.floatTrueMin(T));
if (std.mem.eql(u8, field, "inf")) return @as(f64, std.math.inf(T));
if (std.mem.eql(u8, field, "nan")) return @as(f64, std.math.nan(T));
return null;
}
/// Single owner of structural AST-type-shape construction. Builds the
/// shapes whose `TypeId` is fully determined by their node kind plus their
/// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`,