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:
29
examples/0172-types-value-param-as-field-type.sx
Normal file
29
examples/0172-types-value-param-as-field-type.sx
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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;
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user