diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 51a156d..1b8d8a9 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -126,10 +126,13 @@ pub const ExprTyper = struct { if (info.ty) |t| return t; } } - // Numeric-limit accessor: `.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: `.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; } } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a83feb0..e4951f9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4904,33 +4904,45 @@ pub const Lowering = struct { return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); } - /// Numeric-limit accessor intercept (`.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 (`.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); } diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 3b673a1..aed74e5 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -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; + } + + /// `.` → 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`,