fix(diag): generic VALUE param ($N: u32) used as a field/annotation type → diagnostic (no .unresolved LLVM panic) [stdlib E3 attempt-3]

The generic-struct field checker (attempt-2) accepted ALL struct type
params as valid type-name leaves, including VALUE params. The parser
marks any reference to a struct's own param `is_generic` (so `x: T`
resolves without `$`), and it marks a value param `$N: u32` the same
way — so `Bad :: struct($N: u32) { x: N; }` instantiated `Bad(3)` slipped
past the unknown-type walk, resolved the field's type leaf to the
`.unresolved` sentinel, and panicked at LLVM emission instead of
diagnosing.

Distinguish TYPE params (`$T: Type`, `$T: SomeProtocol`, the `..$Ts`
pack) from VALUE params (`$N: u32`) using the binder's own classification
rule (lower.zig). A value param named in a type position now gets the
tailored "'N' is a value parameter, not a type" hint, exit 1, before
codegen. Two dispatch paths covered: the `is_generic` struct-field path
(reportIfValueParamInTypePosition) and the non-generic annotation path
(reportIfUnknownType in-scope filter). A value param in a VALUE position
(array dim `[N]u8`, `Vector` lane) still resolves.

Regression: 0172-types-value-param-as-field-type (panic-before / clean
diagnostic-after). 0171 and 0759 stay green; 499 markers, prior
byte-identical.
This commit is contained in:
agra
2026-06-08 09:02:54 +03:00
parent a4906975bd
commit a0390a63ab
5 changed files with 90 additions and 4 deletions

View File

@@ -0,0 +1,29 @@
// A generic struct's VALUE param (`$N: u32`) is a compile-time integer, not a
// type. Naming it in a TYPE position — here a field type `x: N` — must emit a
// clean diagnostic, NOT silently compile.
//
// The parser marks any reference to a struct's own type param `is_generic`
// (so `x: T` for a real `$T: Type` resolves without a `$`). That marking is
// the same for a value param, so the unknown-type walk used to skip `x: N`
// entirely; the field's type leaf then resolved to the `.unresolved` sentinel
// and PANICKED at LLVM emission ("unresolved type reached LLVM emission").
//
// The checker now distinguishes TYPE params (`$T: Type`, `$T: SomeProtocol`,
// the `..$Ts` pack) from VALUE params (`$N: u32`) using the binder's own rule:
// a value param named in a type position gets the tailored hint. A value param
// in a VALUE position (a `[N]u8` array dimension, a `Vector` lane count) still
// works (see 0147 / 0201).
//
// Expected: `error: 'N' is a value parameter, not a type`; exit 1.
// Regression (stdlib E3).
#import "modules/std.sx";
Bad :: struct($N: u32) {
x: N;
}
main :: () -> s32 {
b : Bad(3) = .{ x = 1 };
print("{}\n", b.x);
return 0;
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: 'N' is a value parameter, not a type; introduce a generic type parameter with `$N: Type`
--> examples/0172-types-value-param-as-field-type.sx:22:8
|
22 | x: N;
| ^

View File

@@ -0,0 +1 @@

View File

@@ -669,6 +669,38 @@ pub const UnknownTypeChecker = struct {
}
}
/// True when a generic param names a TYPE (so its name may appear in a type
/// position), false for a VALUE param (`$N: u32`) whose name is a
/// compile-time integer. A type param is `..$Ts` (the `[]Type` pack), or a
/// `Type`/protocol-constrained `$T` (`$T: Type`, `$T: Lerpable`). Mirrors the
/// binder's template-param classification (`lower.zig`); the protocol cases
/// keep a `$T: SomeProtocol` field type from being wrongly rejected.
fn isTypeParam(self: UnknownTypeChecker, tp: ast.StructTypeParam) bool {
if (tp.is_variadic) return true;
if (tp.constraint.data == .type_expr) {
const cname = tp.constraint.data.type_expr.name;
return std.mem.eql(u8, cname, "Type") or
self.index.protocol_decl_map.contains(cname) or
self.index.protocol_ast_map.contains(cname);
}
return false;
}
/// A struct field / fn annotation that names an in-scope generic VALUE param
/// (`$N: u32`) in a TYPE position is invalid — the name is a compile-time
/// integer, not a type. The parser marks such a reference `is_generic`
/// (same as a real type param), so the unknown-type walk would otherwise skip
/// it and let it reach the `.unresolved` sentinel. Emit the tailored hint; a
/// genuine type-param reference (or a fresh inline `$R`, not in scope) passes.
fn reportIfValueParamInTypePosition(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span, in_scope: []const ast.StructTypeParam) void {
for (in_scope) |tp| {
if (!std.mem.eql(u8, tp.name, name)) continue;
if (self.isTypeParam(tp)) return;
self.diagnostics.addFmt(.err, span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
return;
}
}
/// True when arg `i` of a parameterized type `base(...)` is a VALUE
/// parameter (a compile-time integer such as a `Vector` lane count or a
/// generic `$N: u32` arg), not a type. Such a position must be skipped by
@@ -708,9 +740,17 @@ pub const UnknownTypeChecker = struct {
type_vals: []const []const u8,
) void {
switch (node.data) {
// A `$`-prefixed name (`-> $R`) introduces/references a generic type
// param inline — always valid in a type position.
.type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals, te.is_raw),
// A `$`-prefixed / struct-param-matched name (`-> $R`, or a field
// `x: T` naming the struct's own `$T`) is marked `is_generic` by the
// parser and is normally a valid type-param reference. But the parser
// marks a struct VALUE param (`$N: u32`) the SAME way, so `x: N` would
// slip past the unknown-type check and reach the `.unresolved`
// sentinel (LLVM panic). Catch that one case; a genuine type-param
// reference still passes.
.type_expr => |te| if (!te.is_generic)
self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals, te.is_raw)
else
self.reportIfValueParamInTypePosition(te.name, node.span, in_scope),
.identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals, id.is_raw),
.pointer_type_expr => |pt| self.checkTypeNodeForUnknown(pt.pointee_type, declared, in_scope, type_vals),
.many_pointer_type_expr => |mp| self.checkTypeNodeForUnknown(mp.element_type, declared, in_scope, type_vals),
@@ -766,7 +806,17 @@ pub const UnknownTypeChecker = struct {
// error. Skip the builtin-name exemption that would otherwise wave a
// bare `s2` through (issue 0089).
if (!is_raw and isBuiltinTypeName(name)) return;
for (in_scope) |tp| if (std.mem.eql(u8, tp.name, name)) return;
for (in_scope) |tp| {
if (!std.mem.eql(u8, tp.name, name)) continue;
// A TYPE param (`$T: Type`, `$T: SomeProtocol`, the `..$Ts` pack)
// names a type and is valid in this position. A VALUE param
// (`$N: u32`) is a compile-time integer, NOT a type — accepting it
// would let the field's type leaf resolve to the `.unresolved`
// sentinel and panic at LLVM emission. Emit the tailored hint.
if (self.isTypeParam(tp)) return;
self.diagnostics.addFmt(.err, span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ name, name });
return;
}
if (declared.contains(name)) return;
// Registered as a real (non-stub) type — covers imported concrete
// structs / enums / unions absent from the main-file decl list. A