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:
agra
2026-06-04 16:14:06 +03:00
parent bc9777d81f
commit 04f46ef384
15 changed files with 384 additions and 14 deletions

View File

@@ -0,0 +1,65 @@
// Integer numeric-limit accessors: `<IntType>.min` / `.max` fold to a
// compile-time constant of the QUERIED integer type, driven by the
// (width, signedness) arithmetic (`sN`: min=-(2^(N-1)), max=2^(N-1)-1; `uN`:
// min=0, max=2^N-1) — every width 1..64, not just the power-of-two ones, plus
// `usize`/`isize` (target-width). Usable in expressions and in array-dimension
// position via the comptime-int path (`[u8.max]T`).
//
// The extreme values that the s64-based integer formatter cannot render
// directly — `s64.min` (i64::MIN) and the all-ones `u64.max`/`usize.max` — are
// asserted EXACTLY via comparison and untagged-union bit reinterpret, never via
// the formatter (which prints i64::MIN as a bare "-" and u64.max as "-1").
#import "modules/std.sx";
// Untagged union for the exact u64.max bit-reinterpret check.
UU :: union { u: u64; s: s64; }
main :: () -> s32 {
// Sub-byte widths — arbitrary bit-width arithmetic, not a per-name table.
print("s1.min={} s1.max={}\n", s1.min, s1.max); // -1 0
print("s2.min={} s2.max={}\n", s2.min, s2.max); // -2 1
print("s3.max={}\n", s3.max); // 3
print("u1.min={} u1.max={}\n", u1.min, u1.max); // 0 1
print("u2.max={}\n", u2.max); // 3
// Byte / word widths.
print("s8.min={} s8.max={}\n", s8.min, s8.max); // -128 127
print("u8.max={}\n", u8.max); // 255
print("s32.min={} s32.max={}\n", s32.min, s32.max); // -2147483648 2147483647
// s64 extremes: max prints; min (i64::MIN) is pinned by relation since the
// formatter cannot render it (this is independent of this feature).
print("s64.max={}\n", s64.max); // 9223372036854775807
print("s64.min+1 == -(s64.max): {}\n", s64.min + 1 == -9223372036854775807); // true
print("s64.min + s64.max == -1: {}\n", s64.min + s64.max == -1); // true
// u64.max / usize.max = all-ones (18446744073709551615); reinterpret to s64
// to confirm the bit pattern is -1 (and NOT a mangled value).
o : UU = ---;
o.u = u64.max;
print("u64.max as s64 == -1: {}\n", o.s == -1); // true
o.u = usize.max;
print("usize.max as s64 == -1: {}\n", o.s == -1); // true (host = u64)
print("usize.max == u64.max: {}\n", usize.max == u64.max); // true
print("isize.min == s64.min: {}\n", isize.min == s64.min); // true (host = s64)
// Result carries the QUERIED type: each binding is declared with the queried
// type and round-trips, so a mistyped fold (e.g. boxed as Any / widened)
// would not type-check here.
m3 : s3 = s3.max;
mu : u8 = u8.max;
ms : s8 = s8.min;
print("typed: m3={} mu={} ms={}\n", m3, mu, ms); // 3 255 -128
// Array-dimension / comptime-int path: `[u8.max]T` and `[s16.max]T` are
// valid counts (255 and 32767), usable end-to-end.
a : [u8.max]u8 = ---;
a[254] = 7;
print("[u8.max]u8 len={} a[254]={}\n", a.len, a[254]); // 255 7
b : [s16.max]u8 = ---;
b[32766] = 9;
print("[s16.max]u8 len={} b[32766]={}\n", b.len, b[32766]); // 32767 9
return 0;
}

View File

@@ -0,0 +1,18 @@
// Numeric-limit accessors apply only to numeric types. `.min`/`.max` on a
// NON-numeric receiver is a clean compile error (never a silent value, never
// the `.unresolved` sentinel reaching codegen):
// - a builtin non-numeric type (`bool`, `void`, `string`) → a dedicated
// "type 'X' has no '.min'/'.max'" diagnostic from the accessor intercept;
// - a user struct (`MyStruct`) → the type name is not a builtin, so the
// intercept stays out and the existing field-not-found path reports it.
// Each case is accurate and located at the access; the program exits non-zero.
#import "modules/std.sx";
MyStruct :: struct { a: s64; }
main :: () -> s32 {
b := bool.max;
s := MyStruct.min;
v := void.max;
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,18 @@
s1.min=-1 s1.max=0
s2.min=-2 s2.max=1
s3.max=3
u1.min=0 u1.max=1
u2.max=3
s8.min=-128 s8.max=127
u8.max=255
s32.min=-2147483648 s32.max=2147483647
s64.max=9223372036854775807
s64.min+1 == -(s64.max): true
s64.min + s64.max == -1: true
u64.max as s64 == -1: true
usize.max as s64 == -1: true
usize.max == u64.max: true
isize.min == s64.min: true
typed: m3=3 mu=255 ms=-128
[u8.max]u8 len=255 a[254]=7
[s16.max]u8 len=32767 b[32766]=9

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
error: type 'bool' has no '.max' — numeric limits apply only to integer and float types
--> examples/0149-types-int-numeric-limits-errors.sx:14:10
|
14 | b := bool.max;
| ^^^^^^^^
error: field 'min' not found on type 'Any'
--> examples/0149-types-int-numeric-limits-errors.sx:15:10
|
15 | s := MyStruct.min;
| ^^^^^^^^^^^^
error: type 'void' has no '.max' — numeric limits apply only to integer and float types
--> examples/0149-types-int-numeric-limits-errors.sx:16:10
|
16 | v := void.max;
| ^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -86,6 +86,12 @@ Options:
| `struct`, `enum`, `union` | Composite types | | `struct`, `enum`, `union` | Composite types |
| `Closure(args) -> ret` | Closure type | | `Closure(args) -> ret` | Closure type |
**Numeric limits.** A field-like access on a builtin integer type name folds to
a compile-time constant of that type: `s64.max``9223372036854775807`,
`u8.min``0`, `s3.max``3`. It works for every width `s1`..`s64` / `u1`..`u64`
plus `usize`/`isize`, and is usable anywhere a constant of that type is — including
array dimensions (`[u8.max]T` is a 255-element array).
### Declarations ### Declarations
```sx ```sx

View File

@@ -117,6 +117,43 @@ GLSL;
- `Any` — type-erased value, represented as `{ i64, i64 }` (type tag + payload). Used for variadic arguments and runtime type dispatch. - `Any` — type-erased value, represented as `{ i64, i64 }` (type tag + payload). Used for variadic arguments and runtime type dispatch.
- `Type` — compile-time type value. At runtime, represented as an `i64` type tag (same tag space as `Any`). - `Type` — compile-time type value. At runtime, represented as an `i64` type tag (same tag space as `Any`).
### Numeric Limits
A field-like access on a builtin **integer** type name folds, at compile time, to
that type's smallest/largest representable value:
```sx
maxS64 := s64.max; // 9223372036854775807
minS32 := s32.min; // -2147483648
maxU8 := u8.max; // 255
minU8 := u8.min; // 0
m3 := s3.max; // 3 (arbitrary width)
n := u64.max; // 18446744073709551615 (all-ones)
```
- **Receiver.** Any builtin integer type: every signed width `s1`..`s64`, every
unsigned width `u1`..`u64` (arbitrary 164 bit widths, not only the
power-of-two ones), plus `usize`/`isize` (target-width — `u64`/`s64` on a
64-bit host).
- **Value.** 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`
- **Result type.** The constant has the **queried** type: `s3.max` is an `s3`,
`u64.max` is a `u64`. So it is usable anywhere a constant of that type is
legal — initializers, `::` / `:=` bindings, and larger expressions — and in
array-dimension / count position via the compile-time integer path
(`[u8.max]T` is a 255-element array; `[s16.max]T` a 32767-element one). A
count that does not fit (`[u64.max]T`) is rejected as an oversized dimension.
- **Representation note.** `u64.max` / `usize.max` is the all-ones 64-bit value
(`18446744073709551615`), which exceeds the signed `i64` range used for
integer constants; it is stored as that exact bit pattern carrying the `u64`
type (it reinterprets to `-1` as an `s64`). It cannot be written as a decimal
literal, and the default integer formatter (which is `s64`-based) prints it as
`-1`; assert it exactly through a bit reinterpret (`union { u: u64; s: s64 }`).
- **Non-numeric receivers.** `.min` / `.max` on a non-numeric type (`bool`,
`string`, a pointer, a `struct`, `void`, an `enum`) is a compile error, never
a silent value.
### Enum Types ### 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). 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).

View File

@@ -6,6 +6,7 @@ const lower = @import("lower.zig");
const Node = ast.Node; const Node = ast.Node;
const TypeId = types.TypeId; const TypeId = types.TypeId;
const Lowering = lower.Lowering; const Lowering = lower.Lowering;
const TypeResolver = @import("type_resolver.zig").TypeResolver;
/// AST-level expression typing (architecture phase A3.1), extracted from /// AST-level expression typing (architecture phase A3.1), extracted from
/// `Lowering.inferExprType`. Owns the structural / non-call expression shapes — /// `Lowering.inferExprType`. Owns the structural / non-call expression shapes —
@@ -125,6 +126,22 @@ pub const ExprTyper = struct {
if (info.ty) |t| return t; 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). // M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
if (std.mem.eql(u8, fa.field, "class")) { if (std.mem.eql(u8, fa.field, "class")) {
if (self.l.objc().isObjcClassPointer(self.l.inferExprType(fa.object))) { if (self.l.objc().isObjcClassPointer(self.l.inferExprType(fa.object))) {

View File

@@ -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 // M1.3 — `obj.class` on any Obj-C-class pointer lowers to
// `object_getClass(obj)`. Sugar; the receiver is opaque so // `object_getClass(obj)`. Sugar; the receiver is opaque so
// we don't auto-deref. Returns `Class` (alias for *void; // 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); 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, /// 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 /// or `pack_name[i].method()` when given. Synthesizes the index/field/call
/// AST per element and lowers it (substitution turns `xs[i]` into the /// AST per element and lowers it (substitution turns `xs[i]` into the

View File

@@ -3,6 +3,7 @@ const ast = @import("../ast.zig");
const types = @import("types.zig"); const types = @import("types.zig");
const inst = @import("inst.zig"); const inst = @import("inst.zig");
const errors = @import("../errors.zig"); const errors = @import("../errors.zig");
const type_resolver = @import("type_resolver.zig");
const Node = ast.Node; const Node = ast.Node;
const TypeId = types.TypeId; const TypeId = types.TypeId;
@@ -156,12 +157,22 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
.identifier => |id| ctx.lookupDimName(id.name), .identifier => |id| ctx.lookupDimName(id.name),
.type_expr => |te| ctx.lookupDimName(te.name), .type_expr => |te| ctx.lookupDimName(te.name),
.field_access => |fa| blk: { .field_access => |fa| blk: {
// `<pack>.len` resolves to the monomorphised arity (e.g. an const obj_name: ?[]const u8 = switch (fa.object.data) {
// `inline for 0..xs.len` bound). Any other field access is not a .identifier => |id| id.name,
// compile-time integer leaf. .type_expr => |te| te.name,
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { else => null,
break :blk ctx.lookupPackLen(fa.object.data.identifier.name); };
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; break :blk null;
}, },
.unary_op => |u| switch (u.op) { .unary_op => |u| switch (u.op) {

View File

@@ -160,3 +160,77 @@ test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" {
// never `.unresolved`, which is reserved for failed *generic* resolution). // never `.unresolved`, which is reserved for failed *generic* resolution).
try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved); 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);
}

View File

@@ -67,6 +67,76 @@ pub const TypeResolver = struct {
return null; 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 /// Single owner of structural AST-type-shape construction. Builds the
/// shapes whose `TypeId` is fully determined by their node kind plus their /// shapes whose `TypeId` is fully determined by their node kind plus their
/// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`, /// 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- /// stub fall-through preserves long-standing behavior for as-yet-
/// unregistered names. /// unregistered names.
pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId { pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId {
if (resolvePrimitive(name)) |id| return id; // Builtin primitive keyword or arbitrary-width integer (`s1`-`s64`,
// Arbitrary bit-width integers: s1-s64, u1-u64. // `u1`-`u64`) — the single builtin classifier, also reused by the
if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) { // numeric-limit accessor intercept.
if (std.fmt.parseInt(u8, name[1..], 10)) |width| { if (resolveBuiltinName(name, table)) |id| return id;
if (width >= 1 and width <= 64) {
return if (name[0] == 's') table.intern(.{ .signed = width }) else table.intern(.{ .unsigned = width });
}
} else |_| {}
}
// Sentinel-terminated slice: [:0]u8 → string. // Sentinel-terminated slice: [:0]u8 → string.
if (name.len >= 5 and name[0] == '[' and name[1] == ':') { if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
if (std.mem.indexOfScalar(u8, name, ']')) |close| { if (std.mem.indexOfScalar(u8, name, ']')) |close| {