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:
agra
2026-06-04 13:16:39 +03:00
parent e8cc9d03de
commit e03c087e5a
33 changed files with 384 additions and 41 deletions

View 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]);
}

View 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
}

View 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
}

View 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);
}

View 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);
}

View 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");
}

View 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]);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
a len=4 a0=10 a3=40
b len=4 b1=21
c len=4 c2=32

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
a.len=3 b.len=4

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
sum 0..M = 3

View File

@@ -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 = ---;
| ^

View File

@@ -0,0 +1 @@
1

View File

@@ -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 = ---;
| ^^^^

View File

@@ -0,0 +1 @@
1

View File

@@ -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) = ---;
| ^^^^^^^^^^

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
v0=1.000000 v2=3.000000 v3=4.000000