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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user