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:
65
examples/0148-types-int-numeric-limits.sx
Normal file
65
examples/0148-types-int-numeric-limits.sx
Normal 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;
|
||||||
|
}
|
||||||
18
examples/0149-types-int-numeric-limits-errors.sx
Normal file
18
examples/0149-types-int-numeric-limits-errors.sx
Normal 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;
|
||||||
|
}
|
||||||
1
examples/expected/0148-types-int-numeric-limits.exit
Normal file
1
examples/expected/0148-types-int-numeric-limits.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
examples/expected/0148-types-int-numeric-limits.stderr
Normal file
1
examples/expected/0148-types-int-numeric-limits.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
18
examples/expected/0148-types-int-numeric-limits.stdout
Normal file
18
examples/expected/0148-types-int-numeric-limits.stdout
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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;
|
||||||
|
| ^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
37
specs.md
37
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.
|
- `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 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
|
### 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).
|
||||||
|
|
||||||
|
|||||||
@@ -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))) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
Reference in New Issue
Block a user