From a0390a63ab90923201b0c244c8c801abbd9078af Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 09:02:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(diag):=20generic=20VALUE=20param=20($N:=20u?= =?UTF-8?q?32)=20used=20as=20a=20field/annotation=20type=20=E2=86=92=20dia?= =?UTF-8?q?gnostic=20(no=20.unresolved=20LLVM=20panic)=20[stdlib=20E3=20at?= =?UTF-8?q?tempt-3]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../0172-types-value-param-as-field-type.sx | 29 ++++++++++ .../0172-types-value-param-as-field-type.exit | 1 + ...172-types-value-param-as-field-type.stderr | 5 ++ ...172-types-value-param-as-field-type.stdout | 1 + src/ir/semantic_diagnostics.zig | 58 +++++++++++++++++-- 5 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 examples/0172-types-value-param-as-field-type.sx create mode 100644 examples/expected/0172-types-value-param-as-field-type.exit create mode 100644 examples/expected/0172-types-value-param-as-field-type.stderr create mode 100644 examples/expected/0172-types-value-param-as-field-type.stdout diff --git a/examples/0172-types-value-param-as-field-type.sx b/examples/0172-types-value-param-as-field-type.sx new file mode 100644 index 0000000..f4efaa7 --- /dev/null +++ b/examples/0172-types-value-param-as-field-type.sx @@ -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; +} diff --git a/examples/expected/0172-types-value-param-as-field-type.exit b/examples/expected/0172-types-value-param-as-field-type.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0172-types-value-param-as-field-type.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0172-types-value-param-as-field-type.stderr b/examples/expected/0172-types-value-param-as-field-type.stderr new file mode 100644 index 0000000..f68fb41 --- /dev/null +++ b/examples/expected/0172-types-value-param-as-field-type.stderr @@ -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; + | ^ diff --git a/examples/expected/0172-types-value-param-as-field-type.stdout b/examples/expected/0172-types-value-param-as-field-type.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0172-types-value-param-as-field-type.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index fb936bb..78805bf 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -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