fix(ir): converge the comptime-int count surface (0083)
Three adjacent cells of the shared count surface still diverged from the rest; all now route through the same leaf+fold+narrow+diagnose path. 1. Aliased integer constraint bypassed the value-param range gate — only builtin constraint names matched intTypeRange, so Box(5_000_000_000) with `$K: Count` (Count :: u32) compiled and bound a truncated value. resolveValueParamArg (shared by both the struct AND type-fn binder) now resolves the constraint to its underlying builtin via canonicalIntConstraintName (Count -> u32, Small -> s8) before range-checking, so an aliased integer constraint behaves exactly like the builtin it names. 2. A named const with an expression RHS (M :: 2; N :: M + 1) did not fold as a count — moduleConstInt read only a literal RHS node. It now folds every const's RHS through the shared evalConstIntExpr, cycle-guarded (mutual / self cycles fold to null, not a stack overflow), and pass-0 pre-registers expression-RHS consts. N :: M + 1 == 3 at every consumer: dim (direct + alias), Vector lane, value-param (struct + type-fn), inline for. 3. Stateful resolveArrayLen still fabricated length 0 after a failed fold; it now returns null -> the .unresolved sentinel (no fabrication). The binding's lowering never reaches sizeOf (alloca defers it; hasErrors aborts first) and a field access on an already-diagnosed .unresolved value is poison-suppressed (emitFieldError), so a failed-fold dim emits ONE clean diagnostic with no panic. Regressions: examples/0146 (full positive matrix — every consumer x leaf form), 1135 (aliased u32 + s8 overflow), 1136 (direct non-const dim halts cleanly). The cascade cleanup also tightened 1502/1503 to one diagnostic. Unit test added for moduleConstInt expression-folding + cycle detection.
This commit is contained in:
68
examples/0146-types-comptime-count-matrix.sx
Normal file
68
examples/0146-types-comptime-count-matrix.sx
Normal file
@@ -0,0 +1,68 @@
|
||||
// The comptime-int COUNT surface is uniform: every count consumer — array
|
||||
// dimension (direct `[N]T` and via type alias), `Vector` lane, generic
|
||||
// value-param (struct AND type-fn binder), and `inline for 0..N` — folds the
|
||||
// SAME leaf forms to the SAME value through one shared evaluator
|
||||
// (`program_index.evalConstIntExpr` / `moduleConstInt`). The leaf forms
|
||||
// exercised here: untyped int const (`M`), a named const with an EXPRESSION RHS
|
||||
// (`N :: M + 1`), a typed-int const (`S : s64 : 5`), an integral float const
|
||||
// (`F :: 4.0` ≡ 4), and an ALIASED integer constraint (`Count :: u32`,
|
||||
// `Small :: s8`) on a value-param.
|
||||
//
|
||||
// Regression (issue 0083): two cells of this surface diverged from the rest.
|
||||
// (1) A named const whose RHS is an expression (`N :: M + 1`) did not fold as a
|
||||
// count ("not a compile-time integer constant") — `moduleConstInt` read only a
|
||||
// literal RHS; it now folds the RHS through the shared `evalConstIntExpr`. (2) An
|
||||
// aliased integer constraint (`$K: Count`) bypassed the value-param range gate,
|
||||
// which only matched builtin constraint names; the constraint now resolves to
|
||||
// its underlying builtin before range-checking, so `$K: Count` behaves exactly
|
||||
// like `$K: u32`.
|
||||
#import "modules/std.sx";
|
||||
|
||||
M :: 2; // untyped int const
|
||||
N :: M + 1; // named const, EXPRESSION RHS (== 3)
|
||||
S : s64 : 5; // typed-int const
|
||||
KU : u32 : 3; // typed-u32 const
|
||||
F :: 4.0; // integral float const (== 4)
|
||||
Count :: u32; // integer ALIAS — value-param constraint
|
||||
Small :: s8; // integer ALIAS — value-param constraint
|
||||
|
||||
ArrN :: [N]s64; // array dim via alias: expression const (3)
|
||||
ArrF :: [F]s64; // array dim via alias: integral float (4)
|
||||
ArrS :: [S]s64; // array dim via alias: typed const (5)
|
||||
|
||||
Buf :: struct ($K: u32, $T: Type) { data: [K]T; }
|
||||
BufC :: struct ($K: Count, $T: Type) { data: [K]T; } // ALIASED u32 constraint
|
||||
BufS :: struct ($K: Small, $T: Type) { data: [K]T; } // ALIASED s8 constraint
|
||||
|
||||
Make :: ($K: u32, $T: Type) -> Type { return [K]T; } // type-fn value-param
|
||||
|
||||
main :: () {
|
||||
// array dimension — DIRECT
|
||||
a : [N]s64 = ---; a[0] = 7; a[2] = 9;
|
||||
print("dim.direct.expr: len={} a0={} a2={}\n", a.len, a[0], a[2]);
|
||||
f : [F]s64 = ---; f[3] = 40;
|
||||
print("dim.direct.float: len={} f3={}\n", f.len, f[3]);
|
||||
|
||||
// array dimension — via type ALIAS
|
||||
aa : ArrN = ---; aa[2] = 99; print("dim.alias.expr: len={} aa2={}\n", aa.len, aa[2]);
|
||||
af : ArrF = ---; print("dim.alias.float: len={}\n", af.len);
|
||||
az : ArrS = ---; print("dim.alias.typed: len={}\n", az.len);
|
||||
|
||||
// Vector lane — expression const (3) and integral float (4)
|
||||
v3 : Vector(N, f32) = .[1.0, 2.0, 3.0];
|
||||
print("lane.expr3: {} {} {}\n", v3.x, v3.y, v3.z);
|
||||
v4 : Vector(F, f32) = .[1.0, 2.0, 3.0, 4.0];
|
||||
print("lane.float4: {}\n", v4.w);
|
||||
|
||||
// generic value-param — struct binder: expr const, aliased u32, aliased s8
|
||||
bn : Buf(N, s64) = ---; bn.data[2] = 30; print("vp.struct.expr: len={} v={}\n", bn.data.len, bn.data[2]);
|
||||
bc : BufC(KU, s64) = ---; bc.data[2] = 31; print("vp.struct.alias.u32: len={} v={}\n", bc.data.len, bc.data[2]);
|
||||
bs : BufS(4, s64) = ---; bs.data[3] = 32; print("vp.struct.alias.s8: len={} v={}\n", bs.data.len, bs.data[3]);
|
||||
|
||||
// generic value-param — type-fn binder: expr const
|
||||
mk : Make(N, s64) = ---; mk[2] = 33; print("vp.typefn.expr: len={} v={}\n", mk.len, mk[2]);
|
||||
|
||||
// inline-for bound — expr const (3) and integral float (4)
|
||||
s := 0; inline for 0..N: (i) { s += i; } print("for.expr: {}\n", s); // 0+1+2 = 3
|
||||
t := 0; inline for 0..F: (i) { t += i; } print("for.float: {}\n", t); // 0+1+2+3 = 6
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// A generic value-param arg that does not fit the param's declared integer type
|
||||
// is a hard error even when that type is reached through a type ALIAS
|
||||
// (`$K: Count` where `Count :: u32`, `$K: Small` where `Small :: s8`) — a clean
|
||||
// diagnostic + non-zero exit, NOT a silent truncating bind.
|
||||
//
|
||||
// Regression (issue 0083): the value-param range gate matched only BUILTIN
|
||||
// constraint names, so an aliased constraint slipped past `intTypeRange` and
|
||||
// `Box(5_000_000_000)` with `$K: Count` compiled and bound a truncated value.
|
||||
// The constraint now resolves to its underlying builtin (`Count` → u32,
|
||||
// `Small` → s8) before range-checking, so an aliased integer constraint behaves
|
||||
// exactly like the builtin it names — at both the struct and type-fn binders.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Count :: u32;
|
||||
Small :: s8;
|
||||
Box :: struct ($K: Count) { value: s64; }
|
||||
Tiny :: struct ($K: Small) { value: s64; }
|
||||
|
||||
main :: () {
|
||||
b : Box(5000000000) = ---;
|
||||
t : Tiny(300) = ---;
|
||||
print("unreachable {} {}\n", b.value, t.value);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// A DIRECT array dimension that is genuinely not a compile-time integer (a
|
||||
// runtime call) is a hard error — ONE clean diagnostic + non-zero exit. Crucially
|
||||
// it must NOT fabricate a length and must NOT crash later in lowering: the bad
|
||||
// var is used downstream (element store + read, `.len`), and lowering has to bail
|
||||
// gracefully on the `.unresolved` type rather than `@panic` in `sizeOf` or pile
|
||||
// on cascade errors.
|
||||
//
|
||||
// Regression (issue 0083): the stateful `resolveArrayLen` emitted the diagnostic
|
||||
// then `return 0` — fabricating a 0-length array (0-byte alloca, OOB access) to
|
||||
// dodge the `sizeOf` panic. It now returns null → the `.unresolved` sentinel; the
|
||||
// binding's lowering bails on it (a field access on an already-diagnosed
|
||||
// `.unresolved` value stays silent), so the single real diagnostic aborts the
|
||||
// build with no fabrication and no panic.
|
||||
#import "modules/std.sx";
|
||||
|
||||
get :: () -> s64 { return 5; }
|
||||
|
||||
main :: () {
|
||||
a : [get()]s64 = ---;
|
||||
a[0] = 7;
|
||||
print("unreachable: {} {}\n", a.len, a[0]);
|
||||
}
|
||||
1
examples/expected/0146-types-comptime-count-matrix.exit
Normal file
1
examples/expected/0146-types-comptime-count-matrix.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
13
examples/expected/0146-types-comptime-count-matrix.stdout
Normal file
13
examples/expected/0146-types-comptime-count-matrix.stdout
Normal file
@@ -0,0 +1,13 @@
|
||||
dim.direct.expr: len=3 a0=7 a2=9
|
||||
dim.direct.float: len=4 f3=40
|
||||
dim.alias.expr: len=3 aa2=99
|
||||
dim.alias.float: len=4
|
||||
dim.alias.typed: len=5
|
||||
lane.expr3: 1.000000 2.000000 3.000000
|
||||
lane.float4: 4.000000
|
||||
vp.struct.expr: len=3 v=30
|
||||
vp.struct.alias.u32: len=3 v=31
|
||||
vp.struct.alias.s8: len=4 v=32
|
||||
vp.typefn.expr: len=3 v=33
|
||||
for.expr: 3
|
||||
for.float: 6
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,11 @@
|
||||
error: value 5000000000 does not fit in u32 parameter K
|
||||
--> examples/1135-diagnostics-value-param-alias-constraint-overflow.sx:20:13
|
||||
|
|
||||
20 | b : Box(5000000000) = ---;
|
||||
| ^^^^^^^^^^
|
||||
|
||||
error: value 300 does not fit in s8 parameter K
|
||||
--> examples/1135-diagnostics-value-param-alias-constraint-overflow.sx:21:14
|
||||
|
|
||||
21 | t : Tiny(300) = ---;
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: array dimension must be a compile-time integer constant
|
||||
--> examples/1136-diagnostics-array-dim-nonconst-direct-no-crash.sx:19:10
|
||||
|
|
||||
19 | a : [get()]s64 = ---;
|
||||
| ^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -3,9 +3,3 @@ error: Vector lane count must be a positive compile-time integer constant
|
||||
|
|
||||
14 | v : Vector(lanes(), f32) = ---;
|
||||
| ^^^^^^^
|
||||
|
||||
error: field 'x' not found on type 'unresolved'
|
||||
--> examples/1502-vectors-runtime-lane-not-const.sx:15:32
|
||||
|
|
||||
15 | print("unreachable: {}\n", v.x);
|
||||
| ^^^
|
||||
|
||||
@@ -3,9 +3,3 @@ error: Vector lane count 5000000000 does not fit in u32
|
||||
|
|
||||
13 | v : Vector(5000000000, f32) = ---;
|
||||
| ^^^^^^^^^^
|
||||
|
||||
error: field 'x' not found on type 'unresolved'
|
||||
--> examples/1503-vectors-oversized-lane-not-u32.sx:14:32
|
||||
|
|
||||
14 | print("unreachable: {}\n", v.x);
|
||||
| ^^^
|
||||
|
||||
@@ -167,6 +167,34 @@
|
||||
> `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`.
|
||||
>
|
||||
> **Convergence — the last three count-surface cells (attempt 9).** Three
|
||||
> adjacent cells of the SAME shared count surface still diverged. (1) An ALIASED
|
||||
> integer constraint (`Count :: u32`; `$K: Count`) bypassed the value-param range
|
||||
> gate — only BUILTIN constraint names matched `intTypeRange`, so
|
||||
> `Box(5_000_000_000)` with `$K: Count` compiled and bound a truncated value. The
|
||||
> gate (`Lowering.resolveValueParamArg`, shared by BOTH binders — struct +
|
||||
> type-fn) now resolves the constraint to its underlying builtin
|
||||
> (`canonicalIntConstraintName`: `Count` → u32, `Small` → s8) before
|
||||
> range-checking, so an aliased integer constraint behaves exactly like the
|
||||
> builtin it names. (2) A named const with an EXPRESSION RHS (`M :: 2; N :: M + 1`)
|
||||
> did not fold as a count — `program_index.moduleConstInt` read only a LITERAL RHS
|
||||
> node. It now folds every const's RHS through the shared `evalConstIntExpr`
|
||||
> (cycle-guarded so `N :: N` / mutual cycles fold to null, not a stack overflow),
|
||||
> and scanDecls pass-0 pre-registers expression-RHS consts; so `N :: M + 1` == 3
|
||||
> at every count consumer (dim direct + alias, Vector lane, value-param struct +
|
||||
> type-fn, `inline for`). (3) The stateful `Lowering.resolveArrayLen` STILL
|
||||
> fabricated length 0 after a failed fold; it now returns null → the `.unresolved`
|
||||
> sentinel (no fabrication), and the binding's lowering bails on it cleanly — a
|
||||
> field access on an already-diagnosed `.unresolved` value stays silent
|
||||
> (`emitFieldError`), so a failed-fold dim emits ONE clean diagnostic and never
|
||||
> reaches the `sizeOf` panic. Files: `src/ir/program_index.zig` (+`.test.zig`),
|
||||
> `src/ir/lower.zig`. Regressions: `examples/0146-types-comptime-count-matrix.sx`
|
||||
> (the full positive matrix — every consumer × representative leaf form),
|
||||
> `examples/1135-diagnostics-value-param-alias-constraint-overflow.sx` (aliased
|
||||
> u32 + s8 overflow), `examples/1136-diagnostics-array-dim-nonconst-direct-no-crash.sx`
|
||||
> (direct non-const dim halts cleanly, no fabrication / panic); the cascade
|
||||
> cleanup also tightened `examples/1502`/`1503` to one diagnostic each.
|
||||
|
||||
## Symptom
|
||||
A fixed array whose dimension is a module-global integer constant (`N :: 16;
|
||||
|
||||
@@ -673,6 +673,13 @@ pub const Lowering = struct {
|
||||
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 {},
|
||||
// A const whose RHS is an integer EXPRESSION over other consts
|
||||
// (`M :: 2; N :: M + 1`) is itself a usable count: register it so
|
||||
// `moduleConstInt` can fold the RHS through `evalConstIntExpr`
|
||||
// (issue 0083). Placeholder `.s64` type — the count consumers read
|
||||
// only the value; if the expression doesn't fold (references a
|
||||
// non-const), `moduleConstInt` yields null and the use diagnoses.
|
||||
.binary_op, .unary_op => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@@ -11676,13 +11683,17 @@ pub const Lowering = struct {
|
||||
if (result == .ok) return result.ok;
|
||||
// A non-const / oversized / negative dim is a hard error. Emit the
|
||||
// shared diagnostic (single wording source — `program_index.reportDimError`,
|
||||
// also used by the stateless alias path so the two cannot diverge), then
|
||||
// return a harmless `0` so body lowering finishes without touching the
|
||||
// `.unresolved` sentinel (which would `@panic` in `sizeOf` mid-lowering,
|
||||
// before the diagnostic surfaces). The diagnostic — not the returned
|
||||
// length — guarantees no garbage ships (issue 0083).
|
||||
// also used by the stateless alias path so the two cannot diverge) and
|
||||
// return null so `resolveCompound` yields the `.unresolved` sentinel — NO
|
||||
// fabricated length (issue 0083: a `0` here gives a 0-byte alloca and OOB
|
||||
// element access). Lowering the binding never computes the failed type's
|
||||
// size: `alloca` records the type but defers `sizeOf` to LLVM emission,
|
||||
// which the emitted diagnostic pre-empts via `hasErrors()`, and a
|
||||
// downstream use of the `.unresolved`-typed value is poison-suppressed (a
|
||||
// field access stays silent — `emitFieldError`). So the failure surfaces
|
||||
// as ONE clean diagnostic and never reaches the `sizeOf` panic.
|
||||
if (self.diagnostics) |d| program_index_mod.reportDimError(d, len_node.span, result);
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Leaf-name lookup for the shared dimension evaluator: a name bound to a
|
||||
@@ -11874,7 +11885,14 @@ pub const Lowering = struct {
|
||||
/// 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| {
|
||||
// Resolve an ALIASED integer constraint (`$K: Count` where `Count :: u32`,
|
||||
// `$K: Small` where `Small :: s8`) to its underlying builtin so the range
|
||||
// gate below treats it exactly like `$K: u32` / `$K: s8` (issue 0083 — an
|
||||
// alias previously slipped past `intTypeRange`, so `Box(5_000_000_000)`
|
||||
// with `$K: Count` bound a truncated value). A non-integer / unrecognised
|
||||
// constraint yields null → no range bound (fold only), as before.
|
||||
const tn_canon: ?[]const u8 = if (type_name) |tn| self.canonicalIntConstraintName(tn) else null;
|
||||
if (tn_canon) |tn| {
|
||||
if (std.mem.eql(u8, tn, "u32")) {
|
||||
switch (program_index_mod.foldDimU32(arg_node, self, 0)) {
|
||||
.ok => |n| return n,
|
||||
@@ -11897,7 +11915,7 @@ pub const Lowering = struct {
|
||||
self.diagValueParamNotConst(arg_node, param_name);
|
||||
return null;
|
||||
};
|
||||
if (type_name) |tn| {
|
||||
if (tn_canon) |tn| {
|
||||
if (program_index_mod.intTypeRange(tn)) |r| {
|
||||
if (v < r.min or v > r.max) {
|
||||
self.diagValueParamRange(arg_node, param_name, tn, v);
|
||||
@@ -11908,6 +11926,23 @@ pub const Lowering = struct {
|
||||
return v;
|
||||
}
|
||||
|
||||
/// Resolve a generic value-param constraint type NAME to its canonical builtin
|
||||
/// integer type name, chasing a type alias (`Count :: u32` → "u32",
|
||||
/// `Small :: s8` → "s8") so an ALIASED integer constraint range-checks exactly
|
||||
/// like the builtin it names. Returns the name unchanged when it is already a
|
||||
/// builtin integer; null when it isn't an integer type (directly or via alias)
|
||||
/// — the caller then folds without a range bound rather than guessing. The
|
||||
/// alias map + type table are the same single sources every other resolver
|
||||
/// reads, so this can't diverge from how the alias is laid out elsewhere.
|
||||
fn canonicalIntConstraintName(self: *Lowering, name: []const u8) ?[]const u8 {
|
||||
if (program_index_mod.intTypeRange(name) != null) return name;
|
||||
if (self.program_index.type_alias_map.get(name)) |tid| {
|
||||
const canon = self.module.types.typeName(tid);
|
||||
if (program_index_mod.intTypeRange(canon) != null) return canon;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 '{s}' must be a compile-time integer constant", .{param_name});
|
||||
@@ -14104,9 +14139,18 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
|
||||
if (self.diagnostics) |diags| {
|
||||
const ty_name = self.formatTypeName(obj_ty);
|
||||
diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name });
|
||||
// A field access on an already-`.unresolved` object is a cascade from an
|
||||
// upstream type-resolution failure that was ALREADY diagnosed (e.g. an
|
||||
// unresolvable / oversized array dimension — issue 0083). The
|
||||
// `.unresolved` sentinel never exists without an accompanying error, so
|
||||
// piling a second "field not found on unresolved" onto the real one is
|
||||
// pure noise; stay silent and return a placeholder so lowering finishes
|
||||
// and `hasErrors()` aborts the build on the genuine diagnostic.
|
||||
if (obj_ty != .unresolved) {
|
||||
if (self.diagnostics) |diags| {
|
||||
const ty_name = self.formatTypeName(obj_ty);
|
||||
diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name });
|
||||
}
|
||||
}
|
||||
return self.emitPlaceholder(field);
|
||||
}
|
||||
|
||||
@@ -214,6 +214,53 @@ test "floatToIntExact accepts integral floats, rejects the rest" {
|
||||
try std.testing.expect(f(1.0e30) == null);
|
||||
}
|
||||
|
||||
test "moduleConstInt folds expression-RHS consts and rejects cycles" {
|
||||
var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator);
|
||||
defer map.deinit();
|
||||
|
||||
// M :: 2 (literal), N :: M + 1 (expression), P :: N * 2 (expression over an
|
||||
// expression const), F :: 4.0 (integral float), G :: 4.5 (fractional).
|
||||
var m_val = nLit(2);
|
||||
var m_id = nIdent("M");
|
||||
var one = nLit(1);
|
||||
var n_val = nBin(.add, &m_id, &one);
|
||||
var n_id = nIdent("N");
|
||||
var two = nLit(2);
|
||||
var p_val = nBin(.mul, &n_id, &two);
|
||||
var f_val = nFloat(4.0);
|
||||
var g_val = nFloat(4.5);
|
||||
|
||||
try map.put("M", .{ .value = &m_val, .ty = .s64 });
|
||||
try map.put("N", .{ .value = &n_val, .ty = .s64 });
|
||||
try map.put("P", .{ .value = &p_val, .ty = .s64 });
|
||||
try map.put("F", .{ .value = &f_val, .ty = .f64 });
|
||||
try map.put("G", .{ .value = &g_val, .ty = .f64 });
|
||||
|
||||
try std.testing.expectEqual(@as(?i64, 2), pi.moduleConstInt(&map, "M"));
|
||||
try std.testing.expectEqual(@as(?i64, 3), pi.moduleConstInt(&map, "N"));
|
||||
try std.testing.expectEqual(@as(?i64, 6), pi.moduleConstInt(&map, "P"));
|
||||
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, "F"));
|
||||
try std.testing.expect(pi.moduleConstInt(&map, "G") == null);
|
||||
try std.testing.expect(pi.moduleConstInt(&map, "absent") == null);
|
||||
|
||||
// A cyclic const has no compile-time integer value, and folding it must not
|
||||
// recurse forever: mutual `A :: B + 0; B :: A + 0` and self `C :: C + 0` all
|
||||
// fold to null via the frame-based cycle guard.
|
||||
var a_id = nIdent("A");
|
||||
var b_id = nIdent("B");
|
||||
var c_id = nIdent("C");
|
||||
var zero = nLit(0);
|
||||
var a_val = nBin(.add, &b_id, &zero);
|
||||
var b_val = nBin(.add, &a_id, &zero);
|
||||
var c_val = nBin(.add, &c_id, &zero);
|
||||
try map.put("A", .{ .value = &a_val, .ty = .s64 });
|
||||
try map.put("B", .{ .value = &b_val, .ty = .s64 });
|
||||
try map.put("C", .{ .value = &c_val, .ty = .s64 });
|
||||
try std.testing.expect(pi.moduleConstInt(&map, "A") == null);
|
||||
try std.testing.expect(pi.moduleConstInt(&map, "B") == null);
|
||||
try std.testing.expect(pi.moduleConstInt(&map, "C") == null);
|
||||
}
|
||||
|
||||
test "evalConstIntExpr folds an integral float literal, halts on a fractional one" {
|
||||
const eval = pi.evalConstIntExpr;
|
||||
const ctx = DimCtx{};
|
||||
|
||||
@@ -63,6 +63,49 @@ pub fn floatToIntExact(v: f64) ?i64 {
|
||||
return @intFromFloat(v);
|
||||
}
|
||||
|
||||
/// A frame in the chain of module consts currently being folded by
|
||||
/// `moduleConstInt`. Stack-allocated (each recursive frame lives on the Zig
|
||||
/// call stack), so cycle detection needs no allocation.
|
||||
const ModuleConstFrame = struct {
|
||||
name: []const u8,
|
||||
parent: ?*const ModuleConstFrame,
|
||||
};
|
||||
|
||||
fn moduleConstFrameContains(frame: ?*const ModuleConstFrame, name: []const u8) bool {
|
||||
var cur = frame;
|
||||
while (cur) |c| : (cur = c.parent) {
|
||||
if (std.mem.eql(u8, c.name, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Folding context for a module-const EXPRESSION RHS (`N :: M + 1`): a leaf name
|
||||
/// resolves to another module const via `moduleConstInt`, recursively, so the
|
||||
/// SAME shared `evalConstIntExpr` that folds an inline dim expression (`[M + 1]`)
|
||||
/// also folds an expression hidden behind a const name. `frame` is the chain of
|
||||
/// const names currently being resolved; a name already on it is a cyclic
|
||||
/// definition (`N :: N`; `N :: M + 1; M :: N`) — which has no compile-time
|
||||
/// integer value — so it folds to null (→ the clean "not a compile-time integer
|
||||
/// constant" diagnostic) rather than recursing forever. No pack arity at module
|
||||
/// scope, so `lookupPackLen` is always null.
|
||||
const ModuleConstCtx = struct {
|
||||
consts: *const std.StringHashMap(ModuleConstInfo),
|
||||
frame: ?*const ModuleConstFrame,
|
||||
pub fn lookupDimName(self: ModuleConstCtx, name: []const u8) ?i64 {
|
||||
return moduleConstIntFramed(self.consts, name, self.frame);
|
||||
}
|
||||
pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fn moduleConstIntFramed(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8, parent: ?*const ModuleConstFrame) ?i64 {
|
||||
if (moduleConstFrameContains(parent, name)) return null;
|
||||
const ci = consts.get(name) orelse return null;
|
||||
var frame = ModuleConstFrame{ .name = name, .parent = parent };
|
||||
return evalConstIntExpr(ci.value, ModuleConstCtx{ .consts = consts, .frame = &frame });
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -70,17 +113,14 @@ pub fn floatToIntExact(v: f64) ?i64 {
|
||||
/// 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 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.
|
||||
/// Every const's RHS is folded through the shared `evalConstIntExpr`, so an
|
||||
/// untyped (`N :: 16`) / typed (`N : s64 : 16`) literal, an integral float
|
||||
/// (`N : f64 : 4.0` → 4, via `floatToIntExact`; `4.5` → null), AND an expression
|
||||
/// RHS over other consts (`M :: 2; N :: M + 1` → 3) all resolve identically and
|
||||
/// everywhere a count is accepted. Cyclic consts fold to null (see
|
||||
/// `ModuleConstCtx`).
|
||||
pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8) ?i64 {
|
||||
const ci = consts.get(name) orelse return null;
|
||||
return switch (ci.value.data) {
|
||||
.int_literal => |lit| lit.value,
|
||||
.float_literal => |lit| floatToIntExact(lit.value),
|
||||
else => null,
|
||||
};
|
||||
return moduleConstIntFramed(consts, name, null);
|
||||
}
|
||||
|
||||
/// Evaluate a constant integer expression to its value. THE single
|
||||
|
||||
Reference in New Issue
Block a user