fix(ir): integral-float counts + range-checked value-param binds (0083)
Item 2 (Agra ruling): a compile-time INTEGRAL float (`4.0`, `N : f64 : 4.0`, `N :: 4.0`) used as an array dimension / Vector lane / generic value-param count / `inline for` bound now folds to its integer at the shared leaf — `program_index.floatToIntExact`, used by both the `.float_literal` arm of `evalConstIntExpr` and `moduleConstInt`. All four consumers route through the one evaluator, so `[4.0]s64` lays out the same `[4]s64` uniformly; a non-integral (`4.5`) or negative value stays rejected by the downstream `foldDimU32` gate. Pass-0 now pre-registers float-valued module consts for forward-alias parity with int consts. Item 1: a generic value-param bind (`Box($K: u32)`) never range-checked the folded arg, so `Box(5_000_000_000)` compiled and ran. The bind now range-checks against the param's declared type — a `u32` count through the shared `foldDimU32` gate (making program_index's "single u32 gate for value-param counts" doc true), any other integer type through the new `program_index.intTypeRange` — and emits a clean "value N does not fit in u32 parameter K" otherwise. The declared type is threaded via a new `TemplateParam.value_type`. Regressions: examples 0145 (integral-float array dim), 1504 (Vector lane), 0611 (inline-for bound), 0209 (value-param integral-float), 1132 (non-integral float dim rejected), 1133 (negative float dim rejected), 1134 (oversized u32 value-param rejected) + program_index float-fold unit tests. Gate: zig build, zig build test, 406/0 run_examples.
This commit is contained in:
29
examples/0145-types-integral-float-array-dim.sx
Normal file
29
examples/0145-types-integral-float-array-dim.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// An array dimension accepts any compile-time numeric constant whose value is a
|
||||
// positive INTEGRAL number — an integral float (`4.0`) folds to its integer just
|
||||
// like `4`. A float-typed const (`N : f64 : 4.0`), an untyped-float const
|
||||
// (`M :: 4.0`), and a direct float literal (`[4.0]s64`) all lay out the same
|
||||
// `[4]s64` as the integer spelling, so element store/read is in bounds.
|
||||
//
|
||||
// Regression (issue 0083 / F0.4 attempt 8, Agra ruling): an integral float used
|
||||
// as a dimension was wrongly rejected "must be a compile-time integer constant".
|
||||
// The shared const-int evaluator now folds an integral float literal (and a
|
||||
// float-typed module const) via `program_index.floatToIntExact`; a non-integral
|
||||
// float (`4.5`) is still rejected (see 1132).
|
||||
#import "modules/std.sx";
|
||||
|
||||
N : f64 : 4.0; // float-typed const
|
||||
M :: 4.0; // untyped float const
|
||||
|
||||
main :: () {
|
||||
a : [N]s64 = ---; // dim from a float-typed const
|
||||
a[0] = 10; a[3] = 40;
|
||||
print("a len={} a0={} a3={}\n", a.len, a[0], a[3]);
|
||||
|
||||
b : [M]s64 = ---; // dim from an untyped float const
|
||||
b[1] = 21;
|
||||
print("b len={} b1={}\n", b.len, b[1]);
|
||||
|
||||
c : [4.0]s64 = ---; // direct integral-float-literal dim
|
||||
c[2] = 32;
|
||||
print("c len={} c2={}\n", c.len, c[2]);
|
||||
}
|
||||
19
examples/0209-generics-value-param-integral-float.sx
Normal file
19
examples/0209-generics-value-param-integral-float.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
// A generic value parameter (`$K: u32`) binds a literal (`Vec(3, s64)`) and an
|
||||
// integral-float named const (`Vec(L, s64)` with `L : f64 : 4.0`) to the same
|
||||
// integer a plain `4` would — the value-param arg folds through the shared
|
||||
// const-int evaluator, so the integral-float rule (F0.4 attempt 8, Agra ruling)
|
||||
// reaches value params too. The folded value is the array length `[K]s64`.
|
||||
//
|
||||
// The bind is range-checked against the declared `u32` (an out-of-range arg is a
|
||||
// clean compile error — see 1134); a valid in-range value binds normally.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Vec :: struct ($K: u32, $T: Type) { data: [K]T; }
|
||||
|
||||
L : f64 : 4.0;
|
||||
|
||||
main :: () {
|
||||
a : Vec(3, s64) = ---; // literal value param
|
||||
b : Vec(L, s64) = ---; // integral-float named-const value param → 4
|
||||
print("a.len={} b.len={}\n", a.data.len, b.data.len); // 3 and 4
|
||||
}
|
||||
14
examples/0611-comptime-integral-float-inline-for.sx
Normal file
14
examples/0611-comptime-integral-float-inline-for.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// An `inline for 0..M` bound accepts an integral float constant — `M :: 3.0`
|
||||
// unrolls the same three iterations as `M :: 3`. The inline-for bound folder
|
||||
// (`evalComptimeInt`) delegates to the shared const-int evaluator, so the
|
||||
// integral-float rule (issue 0083 / F0.4 attempt 8, Agra ruling) applies here
|
||||
// too.
|
||||
#import "modules/std.sx";
|
||||
|
||||
M :: 3.0;
|
||||
|
||||
main :: () {
|
||||
s := 0;
|
||||
inline for 0..M: (i) { s += i; }
|
||||
print("sum 0..M = {}\n", s); // 0 + 1 + 2 = 3
|
||||
}
|
||||
14
examples/1132-diagnostics-array-dim-non-integral-float.sx
Normal file
14
examples/1132-diagnostics-array-dim-non-integral-float.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// A NON-integral float constant (`4.5`) used as an array dimension is a hard
|
||||
// error — only an integral float (`4.0`) folds to a count. Clean diagnostic +
|
||||
// non-zero exit, NOT a fabricated length.
|
||||
//
|
||||
// Regression (F0.4 attempt 8, Agra ruling): the integral-float rule accepts
|
||||
// `4.0` as a dimension but must keep rejecting `4.5` (it is not an integer).
|
||||
#import "modules/std.sx";
|
||||
|
||||
N : f64 : 4.5;
|
||||
|
||||
main :: () {
|
||||
a : [N]s64 = ---;
|
||||
print("unreachable: {}\n", a.len);
|
||||
}
|
||||
12
examples/1133-diagnostics-array-dim-negative-float.sx
Normal file
12
examples/1133-diagnostics-array-dim-negative-float.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// A NEGATIVE integral float (`-2.0`) used as an array dimension is a hard error.
|
||||
// The integral-float rule folds and negates it to `-2`, then the shared u32 dim
|
||||
// gate rejects a below-minimum dimension — a clean diagnostic + non-zero exit.
|
||||
//
|
||||
// Regression (F0.4 attempt 8, Agra ruling): integral floats fold, but a negative
|
||||
// result is still rejected (a dimension must be non-negative).
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
a : [-2.0]s64 = ---;
|
||||
print("unreachable: {}\n", a.len);
|
||||
}
|
||||
17
examples/1134-diagnostics-value-param-u32-overflow.sx
Normal file
17
examples/1134-diagnostics-value-param-u32-overflow.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// A generic value-param arg that does not fit the param's declared integer type
|
||||
// (`Box(5_000_000_000)` for `$K: u32`) is a hard error — a clean diagnostic +
|
||||
// non-zero exit, NOT a silent truncating bind.
|
||||
//
|
||||
// Regression (F0.4 attempt 8, item 1): `resolveValueParamArg` bound the folded
|
||||
// i64 without range-checking the declared type, so an out-of-u32 arg compiled
|
||||
// and ran. The bind now routes a `u32` count through the shared
|
||||
// `program_index.foldDimU32` gate (the same one array dims / Vector lanes use),
|
||||
// so an oversized value is rejected before instantiation.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct ($K: u32) { value: s64; }
|
||||
|
||||
main :: () {
|
||||
b : Box(5000000000) = ---;
|
||||
print("unreachable\n");
|
||||
}
|
||||
12
examples/1504-vectors-integral-float-lane.sx
Normal file
12
examples/1504-vectors-integral-float-lane.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// A Vector lane count accepts an integral float constant — `L : f64 : 4.0` lays
|
||||
// out the same `Vector(4, f32)` as the literal `4`. The lane resolver shares the
|
||||
// const-int evaluator with the array-dim path, so the integral-float rule
|
||||
// (issue 0083 / F0.4 attempt 8, Agra ruling) applies uniformly.
|
||||
#import "modules/std.sx";
|
||||
|
||||
L : f64 : 4.0;
|
||||
|
||||
main :: () {
|
||||
v : Vector(L, f32) = .[1.0, 2.0, 3.0, 4.0];
|
||||
print("v0={} v2={} v3={}\n", v[0], v[2], v[3]);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
a len=4 a0=10 a3=40
|
||||
b len=4 b1=21
|
||||
c len=4 c2=32
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
a.len=3 b.len=4
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sum 0..M = 3
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: array dimension must be a compile-time integer constant
|
||||
--> examples/1132-diagnostics-array-dim-non-integral-float.sx:12:10
|
||||
|
|
||||
12 | a : [N]s64 = ---;
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: array dimension must be non-negative, got -2
|
||||
--> examples/1133-diagnostics-array-dim-negative-float.sx:10:10
|
||||
|
|
||||
10 | a : [-2.0]s64 = ---;
|
||||
| ^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: value 5000000000 does not fit in u32 parameter K
|
||||
--> examples/1134-diagnostics-value-param-u32-overflow.sx:15:13
|
||||
|
|
||||
15 | b : Box(5000000000) = ---;
|
||||
| ^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/1504-vectors-integral-float-lane.exit
Normal file
1
examples/expected/1504-vectors-integral-float-lane.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
v0=1.000000 v2=3.000000 v3=4.000000
|
||||
@@ -143,6 +143,30 @@
|
||||
> Regression: `examples/1131-diagnostics-array-dim-oversized-u32-alias.sx`
|
||||
> (oversized dim via alias → "does not fit in u32", matching direct example 1130;
|
||||
> 1129 still proves the non-const path keeps the generic message).
|
||||
>
|
||||
> **Integral-float counts + value-param range gate (attempt 8, Agra ruling).**
|
||||
> Two finishing items on the shared count path. (1) An *integral* compile-time
|
||||
> FLOAT used as a count (array dim, Vector lane, value-param, `inline for` bound)
|
||||
> was wrongly rejected — `N : f64 : 4.0`, `N :: 4.0`, and `[4.0]s64` all said
|
||||
> "must be a compile-time integer constant". The shared evaluator now folds an
|
||||
> integral float to its integer at the single leaf
|
||||
> (`program_index.floatToIntExact`, used by both the `.float_literal` arm of
|
||||
> `evalConstIntExpr` and `moduleConstInt`), so every consumer accepts `4.0` ≡ `4`
|
||||
> while a non-integral (`4.5`) or negative value is still rejected by the
|
||||
> downstream `foldDimU32` gate. (2) A generic value-param bind (`Box($K: u32)`)
|
||||
> never range-checked the folded arg against its declared type, so
|
||||
> `Box(5_000_000_000)` compiled and ran; the bind now routes a `u32` count
|
||||
> through the same `foldDimU32` gate (and any other declared integer type through
|
||||
> `program_index.intTypeRange`), so an out-of-range arg is a clean compile error
|
||||
> ("value 5000000000 does not fit in u32 parameter K"). Files:
|
||||
> `src/ir/program_index.zig` (+`.test.zig`), `src/ir/lower.zig`, `specs.md`.
|
||||
> Regressions: `examples/0145-types-integral-float-array-dim.sx`,
|
||||
> `examples/1504-vectors-integral-float-lane.sx`,
|
||||
> `examples/0611-comptime-integral-float-inline-for.sx`,
|
||||
> `examples/0209-generics-value-param-integral-float.sx`,
|
||||
> `examples/1132-diagnostics-array-dim-non-integral-float.sx`,
|
||||
> `examples/1133-diagnostics-array-dim-negative-float.sx`,
|
||||
> `examples/1134-diagnostics-value-param-u32-overflow.sx`.
|
||||
|
||||
## Symptom
|
||||
A fixed array whose dimension is a module-global integer constant (`N :: 16;
|
||||
|
||||
6
specs.md
6
specs.md
@@ -651,6 +651,12 @@ Arrays can also be constructed programmatically with the `Array` builtin:
|
||||
MyArr :: Array(5, s32); // equivalent to [5]s32
|
||||
```
|
||||
|
||||
An array dimension — and likewise a `Vector` lane count, a generic value-param
|
||||
count, and an `inline for` bound — accepts any compile-time numeric constant
|
||||
whose value is a positive integral number. An integral float (`4.0`, or a
|
||||
float-typed const `N : f64 : 4.0`) folds to its integer (`[4.0]s64` ≡ `[4]s64`);
|
||||
a non-integral float (`4.5`) or a negative value is rejected.
|
||||
|
||||
### Slice Types
|
||||
A slice `[]T` is a fat pointer `{ptr, i64}` referencing a contiguous sequence of `T` elements. Same runtime layout as `string`.
|
||||
```sx
|
||||
|
||||
131
src/ir/lower.zig
131
src/ir/lower.zig
@@ -653,22 +653,27 @@ pub const Lowering = struct {
|
||||
|
||||
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
|
||||
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
||||
// Pass 0: register every integer-valued module const (`N :: 16` and the
|
||||
// typed `N : s64 : 16`) BEFORE any type alias is resolved below. A type
|
||||
// alias whose dimension is a named const (`Arr :: [N]T`) resolves its
|
||||
// dimension eagerly here, on the stateless registration path; that path
|
||||
// can only read `module_const_map`. Untyped consts would otherwise be
|
||||
// registered only in declaration order (pass 1) and typed ones only after
|
||||
// the alias fixpoint (pass 2) — so an alias declared before its const, or
|
||||
// any alias over a typed const, saw an empty table and miscompiled the
|
||||
// dimension to length 0 (issue 0083). The dimension only needs the value,
|
||||
// so a placeholder type is fine; pass 2 overwrites typed consts with the
|
||||
// resolved annotation type (issue 0070).
|
||||
// Pass 0: register every numeric-literal module const (`N :: 16` and the
|
||||
// typed `N : s64 : 16`, plus float-valued `N :: 4.0` / `N : f64 : 4.0`)
|
||||
// BEFORE any type alias is resolved below. A type alias whose dimension is
|
||||
// a named const (`Arr :: [N]T`) resolves its dimension eagerly here, on
|
||||
// the stateless registration path; that path can only read
|
||||
// `module_const_map`. Untyped consts would otherwise be registered only in
|
||||
// declaration order (pass 1) and typed ones only after the alias fixpoint
|
||||
// (pass 2) — so an alias declared before its const, or any alias over a
|
||||
// typed const, saw an empty table and miscompiled the dimension to length
|
||||
// 0 (issue 0083). A float-valued const resolves to a dimension only when
|
||||
// its value is integral (`floatToIntExact`); pre-registering it keeps the
|
||||
// forward-alias float path identical to the int path. The dimension only
|
||||
// needs the value, so a placeholder type is fine; pass 2 overwrites typed
|
||||
// consts with the resolved annotation type (issue 0070).
|
||||
for (decls) |decl| {
|
||||
if (decl.data != .const_decl) continue;
|
||||
const cd = decl.data.const_decl;
|
||||
if (cd.value.data == .int_literal) {
|
||||
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {};
|
||||
switch (cd.value.data) {
|
||||
.int_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {},
|
||||
.float_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
for (decls) |decl| {
|
||||
@@ -11852,17 +11857,65 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a generic value-param argument (`$N: u32`) to its compile-time
|
||||
/// integer through the shared `evalConstIntExpr` folder, so a module/generic
|
||||
/// const arg (`Vec(N, f32)`) binds the same value — and mangles to the same
|
||||
/// instantiation — a literal (`Vec(3, f32)`) would. A non-const arg emits a
|
||||
/// clean diagnostic and returns null; the caller bails rather than
|
||||
/// fabricating a 0 binding under a wrong mangled name.
|
||||
fn resolveValueParamArg(self: *Lowering, arg_node: *const Node) ?i64 {
|
||||
if (program_index_mod.evalConstIntExpr(arg_node, self)) |v| return v;
|
||||
/// Resolve a generic value-param argument (`$K: u32`) to its compile-time
|
||||
/// integer AND verify it fits the param's declared integer type. The folded
|
||||
/// value is bound and mangled into the instantiation name, so a module/generic
|
||||
/// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an
|
||||
/// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the
|
||||
/// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a
|
||||
/// `u32` param) or a non-const arg emits a clean diagnostic and returns null;
|
||||
/// the caller bails rather than binding a truncated / fabricated value under a
|
||||
/// wrong mangled name.
|
||||
///
|
||||
/// `type_name` is the param's declared constraint type (`"u32"`, null if
|
||||
/// unknown). A `u32` count routes through the shared
|
||||
/// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim /
|
||||
/// Vector lane uses — so the documented "single u32 gate for value-param
|
||||
/// counts" holds; any other integer type range-checks against
|
||||
/// `program_index.intTypeRange`; an unrecognised type folds without bounding.
|
||||
fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 {
|
||||
if (type_name) |tn| {
|
||||
if (std.mem.eql(u8, tn, "u32")) {
|
||||
switch (program_index_mod.foldDimU32(arg_node, self, 0)) {
|
||||
.ok => |n| return n,
|
||||
.not_const => {
|
||||
self.diagValueParamNotConst(arg_node, param_name);
|
||||
return null;
|
||||
},
|
||||
.below_min => |v| {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
},
|
||||
.too_large => |v| {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
const v = program_index_mod.evalConstIntExpr(arg_node, self) orelse {
|
||||
self.diagValueParamNotConst(arg_node, param_name);
|
||||
return null;
|
||||
};
|
||||
if (type_name) |tn| {
|
||||
if (program_index_mod.intTypeRange(tn)) |r| {
|
||||
if (v < r.min or v > r.max) {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, arg_node.span, "generic value parameter must be a compile-time integer constant", .{});
|
||||
return null;
|
||||
d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name});
|
||||
}
|
||||
|
||||
fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name });
|
||||
}
|
||||
|
||||
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
||||
@@ -11999,8 +12052,9 @@ pub const Lowering = struct {
|
||||
const tname = self.formatTypeName(ty);
|
||||
name_parts.appendSlice(self.alloc, tname) catch {};
|
||||
} else {
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer.
|
||||
const val = self.resolveValueParamArg(args[i]) orelse return .unresolved;
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
||||
// and range-check against its declared type.
|
||||
const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved;
|
||||
cvb.put(tp.name, val) catch {};
|
||||
var val_buf: [32]u8 = undefined;
|
||||
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
||||
@@ -12093,8 +12147,10 @@ pub const Lowering = struct {
|
||||
const tname = self.formatTypeName(ty);
|
||||
name_parts.appendSlice(self.alloc, tname) catch {};
|
||||
} else {
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer.
|
||||
const val = self.resolveValueParamArg(args[i]) orelse return null;
|
||||
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
||||
// and range-check against its declared type.
|
||||
const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null;
|
||||
const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return null;
|
||||
cvb.put(tp.name, val) catch {};
|
||||
var val_buf: [32]u8 = undefined;
|
||||
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
||||
@@ -12309,19 +12365,26 @@ pub const Lowering = struct {
|
||||
// Build owned type_params
|
||||
const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return;
|
||||
for (sd.type_params, 0..) |tp, i| {
|
||||
const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
|
||||
const cname = tp.constraint.data.type_expr.name;
|
||||
// "Type" or a protocol name → type param
|
||||
break :blk std.mem.eql(u8, cname, "Type") or
|
||||
self.program_index.protocol_decl_map.contains(cname) or
|
||||
self.program_index.protocol_ast_map.contains(cname);
|
||||
} else false);
|
||||
tps[i] = .{
|
||||
.name = self.alloc.dupe(u8, tp.name) catch return,
|
||||
// $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
|
||||
// `..$Ts: []Type` (variadic) is a type-pack param. Only value
|
||||
// params like $N: u32 are non-type.
|
||||
.is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
|
||||
const cname = tp.constraint.data.type_expr.name;
|
||||
// "Type" or a protocol name → type param
|
||||
break :blk std.mem.eql(u8, cname, "Type") or
|
||||
self.program_index.protocol_decl_map.contains(cname) or
|
||||
self.program_index.protocol_ast_map.contains(cname);
|
||||
} else false),
|
||||
.is_type_param = is_type_param,
|
||||
.is_variadic = tp.is_variadic,
|
||||
// Capture a value param's declared type name (`$K: u32` →
|
||||
// "u32") so instantiation can range-check the folded arg.
|
||||
.value_type = if (!is_type_param and tp.constraint.data == .type_expr)
|
||||
(self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
|
||||
else
|
||||
null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,9 @@ const DimCtx = struct {
|
||||
fn nLit(v: i64) ast.Node {
|
||||
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = v } } };
|
||||
}
|
||||
fn nFloat(v: f64) ast.Node {
|
||||
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .float_literal = .{ .value = v } } };
|
||||
}
|
||||
fn nIdent(name: []const u8) ast.Node {
|
||||
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = name } } };
|
||||
}
|
||||
@@ -191,3 +194,42 @@ test "evalConstIntExpr folds constant-expression array dimensions, halts on non-
|
||||
try std.testing.expect(eval(&cmp, ctx) == null);
|
||||
try std.testing.expect(eval(&ovf, ctx) == null);
|
||||
}
|
||||
|
||||
test "floatToIntExact accepts integral floats, rejects the rest" {
|
||||
const f = pi.floatToIntExact;
|
||||
// Integral floats (positive, zero, negative) fold to their exact integer.
|
||||
try std.testing.expectEqual(@as(?i64, 4), f(4.0));
|
||||
try std.testing.expectEqual(@as(?i64, 0), f(0.0));
|
||||
try std.testing.expectEqual(@as(?i64, -2), f(-2.0));
|
||||
// Non-integral / non-finite → null (the caller's clean halt).
|
||||
try std.testing.expect(f(4.5) == null);
|
||||
try std.testing.expect(f(0.1) == null);
|
||||
try std.testing.expect(f(std.math.inf(f64)) == null);
|
||||
try std.testing.expect(f(-std.math.inf(f64)) == null);
|
||||
try std.testing.expect(f(std.math.nan(f64)) == null);
|
||||
// Out-of-i64-range integral floats → null (no @intFromFloat range panic).
|
||||
// `-2^63` is exactly the i64 minimum and IS representable.
|
||||
try std.testing.expectEqual(@as(?i64, std.math.minInt(i64)), f(-9223372036854775808.0));
|
||||
try std.testing.expect(f(9223372036854775808.0) == null); // 2^63, just past maxInt(i64)
|
||||
try std.testing.expect(f(1.0e30) == null);
|
||||
}
|
||||
|
||||
test "evalConstIntExpr folds an integral float literal, halts on a fractional one" {
|
||||
const eval = pi.evalConstIntExpr;
|
||||
const ctx = DimCtx{};
|
||||
|
||||
var f4 = nFloat(4.0);
|
||||
var f45 = nFloat(4.5);
|
||||
var one = nLit(1);
|
||||
|
||||
// A direct integral float dimension (`[4.0]T`) folds; `4.5` does not.
|
||||
try std.testing.expectEqual(@as(?i64, 4), eval(&f4, ctx));
|
||||
try std.testing.expect(eval(&f45, ctx) == null);
|
||||
|
||||
// It composes inside an expression dimension (`4.0 + 1` → 5); a fractional
|
||||
// operand poisons the whole fold to null.
|
||||
var add = nBin(.add, &f4, &one);
|
||||
var addbad = nBin(.add, &f45, &one);
|
||||
try std.testing.expectEqual(@as(?i64, 5), eval(&add, ctx));
|
||||
try std.testing.expect(eval(&addbad, ctx) == null);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ pub const TemplateParam = struct {
|
||||
name: []const u8,
|
||||
is_type_param: bool, // true for $T: Type, false for $N: u32
|
||||
is_variadic: bool = false, // `..$Ts: []Type` — binds remaining type args as a pack
|
||||
// Declared constraint type NAME for a value (non-type) param (`$K: u32` →
|
||||
// "u32"), used to range-check the folded arg at instantiation; null for a
|
||||
// type/variadic param or when the constraint isn't a plain type name.
|
||||
value_type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const ProtocolMethodInfo = struct {
|
||||
@@ -41,6 +45,24 @@ pub const ModuleConstInfo = struct {
|
||||
ty: TypeId,
|
||||
};
|
||||
|
||||
/// A finite, INTEGRAL `f64` (`4.0`) → its exact `i64` value; a non-integral
|
||||
/// (`4.5`), infinite, NaN, or out-of-`i64`-range float → null. THE single place
|
||||
/// the "an integral float counts as an integer count" rule lives, shared by the
|
||||
/// `.float_literal` leaf of `evalConstIntExpr` (a direct `[4.0]T` dim) and
|
||||
/// `moduleConstInt` (a float-typed module const `N : f64 : 4.0` used as a
|
||||
/// count). One source, so an integral float resolves to the SAME integer at
|
||||
/// every dimension / lane / count / value-param / inline-for site; positivity
|
||||
/// and u32-range are still enforced downstream by `foldDimU32`.
|
||||
pub fn floatToIntExact(v: f64) ?i64 {
|
||||
if (!std.math.isFinite(v)) return null;
|
||||
if (@trunc(v) != v) return null;
|
||||
// `-2^63` is exactly representable and is `minInt(i64)`; `2^63` is the first
|
||||
// f64 above `maxInt(i64)`. Guard both so `@intFromFloat`'s range assert can
|
||||
// never trip on a valid-but-oversized integral float.
|
||||
if (v < -9223372036854775808.0 or v >= 9223372036854775808.0) return null;
|
||||
return @intFromFloat(v);
|
||||
}
|
||||
|
||||
/// A name bound to a module-global integer constant → its value, else null.
|
||||
/// SINGLE source for both array-dimension resolvers — the stateful
|
||||
/// body-lowering path (`Lowering.comptimeIntNamed`) and the stateless
|
||||
@@ -48,12 +70,17 @@ pub const ModuleConstInfo = struct {
|
||||
/// which named consts a `[N]T` dimension resolves to; if they diverge, an array
|
||||
/// laid out via a type alias (`Arr :: [N]T`, stateless) gets a different length
|
||||
/// than the direct form (`a : [N]T`, stateful) — the issue-0083 miscompile.
|
||||
/// Untyped (`N :: 16`) and typed (`N : s64 : 16`) consts both store an
|
||||
/// `.int_literal` value node, so both resolve here identically.
|
||||
/// Untyped (`N :: 16`) and typed (`N : s64 : 16`) consts store an `.int_literal`
|
||||
/// value node; a float-typed const (`N : f64 : 4.0`, `N :: 4.0`) stores a
|
||||
/// `.float_literal` and resolves iff its value is an integral float (via
|
||||
/// `floatToIntExact`) — `4.5` is not an integer → null.
|
||||
pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8) ?i64 {
|
||||
const ci = consts.get(name) orelse return null;
|
||||
if (ci.value.data == .int_literal) return ci.value.data.int_literal.value;
|
||||
return null;
|
||||
return switch (ci.value.data) {
|
||||
.int_literal => |lit| lit.value,
|
||||
.float_literal => |lit| floatToIntExact(lit.value),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Evaluate a constant integer expression to its value. THE single
|
||||
@@ -62,9 +89,10 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: [
|
||||
/// args (`Vec(N, f32)`), and `inline for 0..M` bounds all route here so they
|
||||
/// cannot disagree on what a given expression evaluates to (the issue-0083
|
||||
/// two-resolver class of bug). Folds integer `+ - * / %` and unary negate over
|
||||
/// int literals and named module / comptime consts — recursively, so nested and
|
||||
/// parenthesised forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)`
|
||||
/// carries no AST node; the parser returns the inner expression).
|
||||
/// int literals, integral float literals (`[4.0]T` → 4, via `floatToIntExact`),
|
||||
/// and named module / comptime consts — recursively, so nested and parenthesised
|
||||
/// forms (`[M + N - 1]`, `[(M + 1) * 2]`) fold (a grouping `(…)` carries no AST
|
||||
/// node; the parser returns the inner expression).
|
||||
///
|
||||
/// Leaves resolve through the ctx, so each call site shares the SAME folding
|
||||
/// logic while contributing its own bindings:
|
||||
@@ -83,6 +111,8 @@ pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: [
|
||||
pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
|
||||
return switch (node.data) {
|
||||
.int_literal => |lit| lit.value,
|
||||
// An integral float literal (`[4.0]T`) folds to its integer; `4.5` → null.
|
||||
.float_literal => |lit| floatToIntExact(lit.value),
|
||||
.identifier => |id| ctx.lookupDimName(id.name),
|
||||
.type_expr => |te| ctx.lookupDimName(te.name),
|
||||
.field_access => |fa| blk: {
|
||||
@@ -166,6 +196,32 @@ pub fn reportDimError(diag: *errors.DiagnosticList, span: ?ast.Span, result: Dim
|
||||
}
|
||||
}
|
||||
|
||||
/// The inclusive `[min, max]` integer range a value of a fixed-width integer
|
||||
/// type can hold, addressed by the type NAME as written on a generic value-param
|
||||
/// constraint (`$K: u32`). null for a non-integer / unrecognised name — the
|
||||
/// caller then skips the range check (folds without bounding) rather than
|
||||
/// guessing. Bounds are clamped into `i64`: a `u64`/`usize` ceiling exceeds
|
||||
/// `i64`, but a folded value-param arg is already an `i64`, so `maxInt(i64)` is
|
||||
/// its effective ceiling and the only failure a `u64` param can have is a
|
||||
/// negative arg. THE single declared-type → range map for the value-param gate,
|
||||
/// so the bound at every binding site agrees. The `u32` count case is gated
|
||||
/// through `foldDimU32` instead (the documented dim/lane/value-param u32 gate);
|
||||
/// both encode the same `[0, maxInt(u32)]`.
|
||||
pub const IntRange = struct { min: i64, max: i64 };
|
||||
pub fn intTypeRange(name: []const u8) ?IntRange {
|
||||
const eql = std.mem.eql;
|
||||
if (eql(u8, name, "u8")) return .{ .min = 0, .max = std.math.maxInt(u8) };
|
||||
if (eql(u8, name, "u16")) return .{ .min = 0, .max = std.math.maxInt(u16) };
|
||||
if (eql(u8, name, "u32")) return .{ .min = 0, .max = std.math.maxInt(u32) };
|
||||
if (eql(u8, name, "u64") or eql(u8, name, "usize")) return .{ .min = 0, .max = std.math.maxInt(i64) };
|
||||
if (eql(u8, name, "s8")) return .{ .min = std.math.minInt(i8), .max = std.math.maxInt(i8) };
|
||||
if (eql(u8, name, "s16")) return .{ .min = std.math.minInt(i16), .max = std.math.maxInt(i16) };
|
||||
if (eql(u8, name, "s32")) return .{ .min = std.math.minInt(i32), .max = std.math.maxInt(i32) };
|
||||
if (eql(u8, name, "s64") or eql(u8, name, "isize") or eql(u8, name, "int"))
|
||||
return .{ .min = std.math.minInt(i64), .max = std.math.maxInt(i64) };
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
|
||||
|
||||
/// Single lowering access point for declaration-name / import / visibility
|
||||
|
||||
Reference in New Issue
Block a user