From 04f46ef384858ed3fe9070465f1b09833894bc2f Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 16:14:06 +0300 Subject: [PATCH] feat(lang): integer numeric-limit accessors (s64.max, u8.min, s3.max) [NL.1] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- examples/0148-types-int-numeric-limits.sx | 65 +++++++++++++++ .../0149-types-int-numeric-limits-errors.sx | 18 ++++ .../0148-types-int-numeric-limits.exit | 1 + .../0148-types-int-numeric-limits.stderr | 1 + .../0148-types-int-numeric-limits.stdout | 18 ++++ .../0149-types-int-numeric-limits-errors.exit | 1 + ...149-types-int-numeric-limits-errors.stderr | 17 ++++ ...149-types-int-numeric-limits-errors.stdout | 1 + readme.md | 6 ++ specs.md | 37 +++++++++ src/ir/expr_typer.zig | 17 ++++ src/ir/lower.zig | 38 +++++++++ src/ir/program_index.zig | 21 +++-- src/ir/type_resolver.test.zig | 74 +++++++++++++++++ src/ir/type_resolver.zig | 83 +++++++++++++++++-- 15 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 examples/0148-types-int-numeric-limits.sx create mode 100644 examples/0149-types-int-numeric-limits-errors.sx create mode 100644 examples/expected/0148-types-int-numeric-limits.exit create mode 100644 examples/expected/0148-types-int-numeric-limits.stderr create mode 100644 examples/expected/0148-types-int-numeric-limits.stdout create mode 100644 examples/expected/0149-types-int-numeric-limits-errors.exit create mode 100644 examples/expected/0149-types-int-numeric-limits-errors.stderr create mode 100644 examples/expected/0149-types-int-numeric-limits-errors.stdout diff --git a/examples/0148-types-int-numeric-limits.sx b/examples/0148-types-int-numeric-limits.sx new file mode 100644 index 0000000..f623bb0 --- /dev/null +++ b/examples/0148-types-int-numeric-limits.sx @@ -0,0 +1,65 @@ +// Integer numeric-limit accessors: `.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; +} diff --git a/examples/0149-types-int-numeric-limits-errors.sx b/examples/0149-types-int-numeric-limits-errors.sx new file mode 100644 index 0000000..522cf96 --- /dev/null +++ b/examples/0149-types-int-numeric-limits-errors.sx @@ -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; +} diff --git a/examples/expected/0148-types-int-numeric-limits.exit b/examples/expected/0148-types-int-numeric-limits.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0148-types-int-numeric-limits.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0148-types-int-numeric-limits.stderr b/examples/expected/0148-types-int-numeric-limits.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0148-types-int-numeric-limits.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0148-types-int-numeric-limits.stdout b/examples/expected/0148-types-int-numeric-limits.stdout new file mode 100644 index 0000000..f6e3b70 --- /dev/null +++ b/examples/expected/0148-types-int-numeric-limits.stdout @@ -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 diff --git a/examples/expected/0149-types-int-numeric-limits-errors.exit b/examples/expected/0149-types-int-numeric-limits-errors.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0149-types-int-numeric-limits-errors.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0149-types-int-numeric-limits-errors.stderr b/examples/expected/0149-types-int-numeric-limits-errors.stderr new file mode 100644 index 0000000..7ad725e --- /dev/null +++ b/examples/expected/0149-types-int-numeric-limits-errors.stderr @@ -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; + | ^^^^^^^^ diff --git a/examples/expected/0149-types-int-numeric-limits-errors.stdout b/examples/expected/0149-types-int-numeric-limits-errors.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0149-types-int-numeric-limits-errors.stdout @@ -0,0 +1 @@ + diff --git a/readme.md b/readme.md index 24cff5b..16d0632 100644 --- a/readme.md +++ b/readme.md @@ -86,6 +86,12 @@ Options: | `struct`, `enum`, `union` | Composite types | | `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 ```sx diff --git a/specs.md b/specs.md index 5fbf70f..93e175d 100644 --- a/specs.md +++ b/specs.md @@ -117,6 +117,43 @@ GLSL; - `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`). +### 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 1–64 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 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). diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index ee05d26..51a156d 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -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: `.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))) { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ddb0210..a83feb0 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4823,6 +4823,13 @@ pub const Lowering = struct { } } + // Numeric-limit accessor: `.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 (`.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 diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 4ba77bf..e629189 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -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: { - // `.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| { + // `.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); + // `.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) { diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index 0d415bb..51b0208 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -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); +} diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index e9075e3..3b673a1 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -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 + } + + /// `.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| {