feat(lang): integer numeric-limit accessors (s64.max, u8.min, s3.max) [NL.1]
A field-like access on a builtin INTEGER type name folds to a compile-time
constant of the queried type, driven by (width, signedness) arithmetic:
sN: min=-(2^(N-1)), max=2^(N-1)-1; uN: min=0, max=2^N-1
for every width s1..s64 / u1..u64 (not just power-of-two), plus usize/isize.
- type_resolver.zig: extract the single width parser (parseWidthInt) reused by
resolveNamed AND the new accessors (no second parser — issue-0083 class);
add resolveBuiltinName / integerWidthSign / integerLimitBits / integerLimitFor.
- lower.zig: lowerNumericLimit intercept beside the error.X / Struct.CONST /
pack-arity identifier-receiver intercepts; folds ints via constInt, emits a
clean diagnostic for a non-numeric receiver (bool/string/void/Any/noreturn),
falls through for floats (NL.2).
- expr_typer.zig: mirror the result type so inferExprType reports the queried type.
- program_index.zig: recognize the accessors in the comptime-int / array-dim path
so [u8.max]T (255) / [s16.max]T (32767) work; [u64.max]T is rejected oversized.
- u64.max / usize.max stored as the all-ones bit pattern with TYPE u64 (i64 -1),
asserted via union { u: u64; s: s64 } reinterpret.
Docs: specs.md numeric-limits subsection (formulas + result-type + u64 note);
readme.md language overview. Examples 0148 (positive) / 0149 (negative-receiver).
Unit tests for the value computation in type_resolver.test.zig.
Gate: zig build, zig build test (359/359), tests/run_examples.sh (416 ok, 0 failed).
This commit is contained in:
@@ -6,6 +6,7 @@ const lower = @import("lower.zig");
|
||||
const Node = ast.Node;
|
||||
const TypeId = types.TypeId;
|
||||
const Lowering = lower.Lowering;
|
||||
const TypeResolver = @import("type_resolver.zig").TypeResolver;
|
||||
|
||||
/// AST-level expression typing (architecture phase A3.1), extracted from
|
||||
/// `Lowering.inferExprType`. Owns the structural / non-call expression shapes —
|
||||
@@ -125,6 +126,22 @@ 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).
|
||||
{
|
||||
const type_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| id.name,
|
||||
.type_expr => |te| te.name,
|
||||
else => null,
|
||||
};
|
||||
if (type_name) |tn| {
|
||||
if (TypeResolver.integerLimitFor(tn, fa.field) != null) {
|
||||
if (TypeResolver.resolveBuiltinName(tn, &self.l.module.types)) |t| return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
// M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
|
||||
if (std.mem.eql(u8, fa.field, "class")) {
|
||||
if (self.l.objc().isObjcClassPointer(self.l.inferExprType(fa.object))) {
|
||||
|
||||
@@ -4823,6 +4823,13 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric-limit accessor: `<IntType>.min` / `.max` folds to a comptime
|
||||
// const of the queried type (sibling of the identifier-receiver
|
||||
// intercepts above). Placed AFTER `Struct.CONST` so a user const named
|
||||
// `min`/`max` wins on its own struct; a builtin type name can never
|
||||
// name a user struct (reserved — issue 0076), so they never collide.
|
||||
if (self.lowerNumericLimit(fa, span)) |ref| return ref;
|
||||
|
||||
// M1.3 — `obj.class` on any Obj-C-class pointer lowers to
|
||||
// `object_getClass(obj)`. Sugar; the receiver is opaque so
|
||||
// we don't auto-deref. Returns `Class` (alias for *void;
|
||||
@@ -4897,6 +4904,37 @@ 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).
|
||||
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;
|
||||
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 (self.diagnostics) |d| {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Lower each pack element to a Ref: `pack_name[i]` when `method` is null,
|
||||
/// or `pack_name[i].method()` when given. Synthesizes the index/field/call
|
||||
/// AST per element and lowers it (substitution turns `xs[i]` into the
|
||||
|
||||
@@ -3,6 +3,7 @@ const ast = @import("../ast.zig");
|
||||
const types = @import("types.zig");
|
||||
const inst = @import("inst.zig");
|
||||
const errors = @import("../errors.zig");
|
||||
const type_resolver = @import("type_resolver.zig");
|
||||
|
||||
const Node = ast.Node;
|
||||
const TypeId = types.TypeId;
|
||||
@@ -156,12 +157,22 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
||||
.identifier => |id| ctx.lookupDimName(id.name),
|
||||
.type_expr => |te| ctx.lookupDimName(te.name),
|
||||
.field_access => |fa| blk: {
|
||||
// `<pack>.len` resolves to the monomorphised arity (e.g. an
|
||||
// `inline for 0..xs.len` bound). Any other field access is not a
|
||||
// compile-time integer leaf.
|
||||
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
|
||||
break :blk ctx.lookupPackLen(fa.object.data.identifier.name);
|
||||
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| id.name,
|
||||
.type_expr => |te| te.name,
|
||||
else => null,
|
||||
};
|
||||
if (obj_name) |on| {
|
||||
// `<pack>.len` resolves to the monomorphised arity (e.g. an
|
||||
// `inline for 0..xs.len` bound).
|
||||
if (std.mem.eql(u8, fa.field, "len")) break :blk ctx.lookupPackLen(on);
|
||||
// `<IntType>.min` / `.max` — the same fold the value path uses
|
||||
// (type_resolver), so `[u8.max]T` agrees with `u8.max` in
|
||||
// expression position. A `u64.max` (= -1 as i64) folds here too;
|
||||
// `foldDimU32` then rejects it as a negative array dimension.
|
||||
if (type_resolver.TypeResolver.integerLimitFor(on, fa.field)) |v| break :blk v;
|
||||
}
|
||||
// Any other field access is not a compile-time integer leaf.
|
||||
break :blk null;
|
||||
},
|
||||
.unary_op => |u| switch (u.op) {
|
||||
|
||||
@@ -160,3 +160,77 @@ test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" {
|
||||
// never `.unresolved`, which is reserved for failed *generic* resolution).
|
||||
try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved);
|
||||
}
|
||||
|
||||
test "TypeResolver.parseWidthInt: every width 1..64, both signs; rejects out-of-range / non-int" {
|
||||
// The single width parser — covers the named primitives (s8/u64/…) too.
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 1, .signed = true }), TypeResolver.parseWidthInt("s1"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 3, .signed = true }), TypeResolver.parseWidthInt("s3"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 64, .signed = true }), TypeResolver.parseWidthInt("s64"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 1, .signed = false }), TypeResolver.parseWidthInt("u1"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 64, .signed = false }), TypeResolver.parseWidthInt("u64"));
|
||||
// Width 0 and >64, and non-`s`/`u` names, are not width-ints.
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("s0") == null);
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("u65") == null);
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("usize") == null);
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("f32") == null);
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("sx") == null);
|
||||
try std.testing.expect(TypeResolver.parseWidthInt("s") == null);
|
||||
}
|
||||
|
||||
test "TypeResolver.integerWidthSign: width-ints plus usize/isize, null for non-integers" {
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 64, .signed = false }), TypeResolver.integerWidthSign("usize"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 64, .signed = true }), TypeResolver.integerWidthSign("isize"));
|
||||
try std.testing.expectEqual(@as(?TypeResolver.WidthInt, .{ .width = 8, .signed = false }), TypeResolver.integerWidthSign("u8"));
|
||||
// Non-integer builtins and user names are not integer types.
|
||||
try std.testing.expect(TypeResolver.integerWidthSign("f64") == null);
|
||||
try std.testing.expect(TypeResolver.integerWidthSign("bool") == null);
|
||||
try std.testing.expect(TypeResolver.integerWidthSign("void") == null);
|
||||
try std.testing.expect(TypeResolver.integerWidthSign("MyStruct") == null);
|
||||
}
|
||||
|
||||
test "TypeResolver.integerLimitFor: pinned min/max across widths and extremes" {
|
||||
const L = struct {
|
||||
fn v(name: []const u8, field: []const u8) i64 {
|
||||
return TypeResolver.integerLimitFor(name, field).?;
|
||||
}
|
||||
};
|
||||
// Sub-byte widths (arbitrary bit-width arithmetic, not a per-name table).
|
||||
try std.testing.expectEqual(@as(i64, -1), L.v("s1", "min"));
|
||||
try std.testing.expectEqual(@as(i64, 0), L.v("s1", "max"));
|
||||
try std.testing.expectEqual(@as(i64, -2), L.v("s2", "min"));
|
||||
try std.testing.expectEqual(@as(i64, 1), L.v("s2", "max"));
|
||||
try std.testing.expectEqual(@as(i64, 3), L.v("s3", "max"));
|
||||
try std.testing.expectEqual(@as(i64, 0), L.v("u1", "min"));
|
||||
try std.testing.expectEqual(@as(i64, 1), L.v("u1", "max"));
|
||||
try std.testing.expectEqual(@as(i64, 3), L.v("u2", "max"));
|
||||
// Byte / word.
|
||||
try std.testing.expectEqual(@as(i64, -128), L.v("s8", "min"));
|
||||
try std.testing.expectEqual(@as(i64, 127), L.v("s8", "max"));
|
||||
try std.testing.expectEqual(@as(i64, 255), L.v("u8", "max"));
|
||||
try std.testing.expectEqual(@as(i64, -2147483648), L.v("s32", "min"));
|
||||
try std.testing.expectEqual(@as(i64, 2147483647), L.v("s32", "max"));
|
||||
// s64 extremes = i64 extremes.
|
||||
try std.testing.expectEqual(std.math.minInt(i64), L.v("s64", "min"));
|
||||
try std.testing.expectEqual(std.math.maxInt(i64), L.v("s64", "max"));
|
||||
// u63.max fits i64; u64.max is all-ones (= -1 as i64, maxInt(u64) as u64).
|
||||
try std.testing.expectEqual(std.math.maxInt(i64), L.v("u63", "max"));
|
||||
try std.testing.expectEqual(@as(i64, -1), L.v("u64", "max"));
|
||||
try std.testing.expectEqual(std.math.maxInt(u64), @as(u64, @bitCast(L.v("u64", "max"))));
|
||||
try std.testing.expectEqual(@as(i64, 0), L.v("u64", "min"));
|
||||
// usize/isize track u64/s64 on the host.
|
||||
try std.testing.expectEqual(L.v("u64", "max"), L.v("usize", "max"));
|
||||
try std.testing.expectEqual(@as(i64, 0), L.v("usize", "min"));
|
||||
try std.testing.expectEqual(L.v("s64", "min"), L.v("isize", "min"));
|
||||
try std.testing.expectEqual(L.v("s64", "max"), L.v("isize", "max"));
|
||||
}
|
||||
|
||||
test "TypeResolver.integerLimitFor: null for non-integer receivers and non-limit fields" {
|
||||
// Float / non-numeric / user names are not integer-limit folds.
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("f64", "max") == null);
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("bool", "max") == null);
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("void", "min") == null);
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("MyStruct", "min") == null);
|
||||
// A builtin int with a non-limit field is not a fold here.
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("s64", "len") == null);
|
||||
try std.testing.expect(TypeResolver.integerLimitFor("u8", "epsilon") == null);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,76 @@ pub const TypeResolver = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// An arbitrary-bit-width integer type NAME (`s1`–`s64`, `u1`–`u64`, which
|
||||
/// also subsumes `s8`/`u8`/…/`s64`/`u64`): its width + signedness, else
|
||||
/// null. THE single width parser — `resolveBuiltinName` (to intern the
|
||||
/// `TypeId`) and the numeric-limit accessors (`.min`/`.max`, via
|
||||
/// `integerWidthSign`) both classify through here, so the recognized width
|
||||
/// set cannot diverge (the issue-0083 two-resolver defect class).
|
||||
pub const WidthInt = struct { width: u8, signed: bool };
|
||||
pub fn parseWidthInt(name: []const u8) ?WidthInt {
|
||||
if (name.len < 2) return null;
|
||||
if (name[0] != 's' and name[0] != 'u') return null;
|
||||
const width = std.fmt.parseInt(u8, name[1..], 10) catch return null;
|
||||
if (width < 1 or width > 64) return null;
|
||||
return .{ .width = width, .signed = name[0] == 's' };
|
||||
}
|
||||
|
||||
/// A bare name → its builtin `TypeId` (primitive keyword OR arbitrary-width
|
||||
/// integer), WITHOUT the named-struct / alias / stub fallthrough of
|
||||
/// `resolveNamed`. null for any non-builtin name. The shared builtin
|
||||
/// classifier: `resolveNamed` resolves through it first (then continues to
|
||||
/// struct/alias resolution), and the numeric-limit accessor intercept uses
|
||||
/// it to recover the queried type.
|
||||
pub fn resolveBuiltinName(name: []const u8, table: *TypeTable) ?TypeId {
|
||||
if (resolvePrimitive(name)) |id| return id;
|
||||
if (parseWidthInt(name)) |wi| {
|
||||
return if (wi.signed) table.intern(.{ .signed = wi.width }) else table.intern(.{ .unsigned = wi.width });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Width + signedness of a builtin INTEGER type NAME: the `s`/`u` widths via
|
||||
/// `parseWidthInt`, plus `usize`/`isize` (target-width = 64 on the host).
|
||||
/// null for a non-integer name (floats, `bool`, `string`, a user type, …) —
|
||||
/// so the `.min`/`.max` fold fires for integers only.
|
||||
pub fn integerWidthSign(name: []const u8) ?WidthInt {
|
||||
if (parseWidthInt(name)) |wi| return wi;
|
||||
if (std.mem.eql(u8, name, "usize")) return .{ .width = 64, .signed = false };
|
||||
if (std.mem.eql(u8, name, "isize")) return .{ .width = 64, .signed = true };
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The two's-complement bit pattern (as a raw `i64`) of a fixed-width integer
|
||||
/// type's `.min`/`.max`. Pure `(width, signedness)` arithmetic — never a
|
||||
/// per-name table. `sN`: min = -(2^(N-1)), max = 2^(N-1)-1. `uN`: min = 0,
|
||||
/// max = 2^N-1. The all-ones `u64.max`/`usize.max` (18446744073709551615)
|
||||
/// exceeds `i64`'s max, so it is returned as its bit pattern (`-1` as `i64`);
|
||||
/// the caller pairs it with the `u64`/`usize` `TypeId` so no signed path
|
||||
/// re-signs it.
|
||||
pub fn integerLimitBits(wi: WidthInt, want_max: bool) i64 {
|
||||
if (wi.signed) {
|
||||
const half_shift: u6 = @intCast(wi.width - 1);
|
||||
const half: u64 = @as(u64, 1) << half_shift; // 2^(width-1)
|
||||
return if (want_max) @bitCast(half - 1) else @bitCast(0 -% half);
|
||||
}
|
||||
if (!want_max) return 0; // unsigned min
|
||||
const lead: u6 = @intCast(64 - @as(u8, wi.width));
|
||||
return @bitCast((~@as(u64, 0)) >> lead); // 2^width - 1
|
||||
}
|
||||
|
||||
/// `<IntType>.min` / `.max` → the type's limit as a raw `i64`, or null when
|
||||
/// `name` is not a builtin integer type or `field` is not `min`/`max`. THE
|
||||
/// single name+field → value fold, shared by the value path (lower.zig) and
|
||||
/// the comptime-int / array-dim path (program_index.evalConstIntExpr) so the
|
||||
/// two cannot disagree on what `u8.max` evaluates to.
|
||||
pub fn integerLimitFor(name: []const u8, field: []const u8) ?i64 {
|
||||
const want_max = std.mem.eql(u8, field, "max");
|
||||
if (!want_max and !std.mem.eql(u8, field, "min")) return null;
|
||||
const wi = integerWidthSign(name) orelse return null;
|
||||
return integerLimitBits(wi, want_max);
|
||||
}
|
||||
|
||||
/// 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`,
|
||||
@@ -175,15 +245,10 @@ pub const TypeResolver = struct {
|
||||
/// stub fall-through preserves long-standing behavior for as-yet-
|
||||
/// unregistered names.
|
||||
pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId {
|
||||
if (resolvePrimitive(name)) |id| return id;
|
||||
// Arbitrary bit-width integers: s1-s64, u1-u64.
|
||||
if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) {
|
||||
if (std.fmt.parseInt(u8, name[1..], 10)) |width| {
|
||||
if (width >= 1 and width <= 64) {
|
||||
return if (name[0] == 's') table.intern(.{ .signed = width }) else table.intern(.{ .unsigned = width });
|
||||
}
|
||||
} else |_| {}
|
||||
}
|
||||
// Builtin primitive keyword or arbitrary-width integer (`s1`-`s64`,
|
||||
// `u1`-`u64`) — the single builtin classifier, also reused by the
|
||||
// numeric-limit accessor intercept.
|
||||
if (resolveBuiltinName(name, table)) |id| return id;
|
||||
// Sentinel-terminated slice: [:0]u8 → string.
|
||||
if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
|
||||
if (std.mem.indexOfScalar(u8, name, ']')) |close| {
|
||||
|
||||
Reference in New Issue
Block a user