fix(diag): imported generic struct field with bad type → diagnostic, not .unresolved/silent stub [stdlib E3 attempt-4]
attempt-3 closed the MAIN-file value-param-as-type quadrant (0172) in the
UnknownTypeChecker, but the checker only walks main-file decls — an IMPORTED
generic struct's field with a bad type name was never checked. Worse, the
generic-struct INSTANTIATION resolved its field type nodes in the (possibly
cross-module) instantiation site's source context, not the template's module.
So for `Bad :: struct($N: u32) { x: N; }` declared in an imported module and
used as `Bad(3)` from main, the field `x: N` resolved against the main file:
the value-param-as-type leaf poisoned it with `.unresolved` and PANICKED at
LLVM emission, and the genuinely-undeclared sibling (`y: Missing` in a generic
import, distinct from the non-generic 0759 case) silently fabricated a 0-field
stub.
Root cause + uniform fix: capture the declaring module on each StructTemplate
and resolve its field type nodes in THAT source context during
instantiateGenericStruct. The source-aware nominal leaf then classifies main vs
imported by the TEMPLATE's file, so both failure modes are diagnosed at the
right authority for every quadrant — main + imported, undeclared name + value
param used as a type:
- imported `.undeclared` field → the existing leaf emits "unknown type 'X'"
(now reached because `from` is the template's module, not main).
- imported value-param-as-type → the `is_generic` leaf, when the name is bound
as a comptime VALUE (`comptime_value_bindings`), emits the tailored
"'N' is a value parameter, not a type" hint (gated to non-main; the
UnknownTypeChecker owns the main-file case). Caught in every type position
(`x: N`, `*N`, `[3]N`, `?N`). A genuinely-unbound type param (`$R`) stays a
silent `.unresolved`.
No `.unresolved` reaches LLVM for these cases (hasErrors halts after lowering);
the emit_llvm `.unresolved` @panic tripwire stays as the last-resort sentinel.
Valid value-param VALUE positions (`[N]u8` dim, `Vector(N,T)` lane) and
`$T:Type`/`$T:Protocol` type-param fields still resolve.
Regressions:
- 0760-modules-imported-generic-value-param-as-field-type (panic-before / clean
diagnostic-after).
- 0761-modules-imported-generic-undeclared-field (silent-compile-before / clean
diagnostic-after).
0171/0172/0759 stay green; main-file quadrants emit exactly one error.
Gate: zig build; zig build test (423/423 + LSP corpus sweep); run_examples 501
passed / 0 failed (prior 499 byte-identical); m3te ios-sim build exit 0.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
// A generic struct's VALUE param (`$N: u32`) named in a TYPE position must emit
|
||||
// a clean diagnostic even when the struct is declared in an IMPORTED module —
|
||||
// not just the main file (examples/0172 covers the main-file case). Before the
|
||||
// fix the imported template's field `x: N` resolved to the `.unresolved`
|
||||
// sentinel and PANICKED at LLVM emission ("unresolved type reached LLVM
|
||||
// emission"); the field type leaf now diagnoses the imported value-param misuse
|
||||
// at the reference, the same way the main-file `UnknownTypeChecker` does.
|
||||
//
|
||||
// Expected: `error: 'N' is a value parameter, not a type` pointing into lib.sx;
|
||||
// exit 1. Regression (stdlib E3).
|
||||
#import "modules/std.sx";
|
||||
#import "0760-modules-imported-generic-value-param-as-field-type/lib.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
b : Bad(3) = .{ x = 1 };
|
||||
print("{}\n", b.x);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Flat-imported generic struct whose VALUE param `$N: u32` is named in a TYPE
|
||||
// position (`x: N`). Because this module is imported (not the main file), the
|
||||
// `UnknownTypeChecker` trusts it and never walks it — so the field type leaf is
|
||||
// the sole guard. `instantiateGenericStruct` resolves the template's field nodes
|
||||
// in THIS source context, so the leaf classifies the value-param misuse as
|
||||
// imported and emits the tailored hint instead of poisoning the field with the
|
||||
// `.unresolved` sentinel (which panicked at LLVM emission before the fix).
|
||||
Bad :: struct($N: u32) {
|
||||
x: N;
|
||||
}
|
||||
19
examples/0761-modules-imported-generic-undeclared-field.sx
Normal file
19
examples/0761-modules-imported-generic-undeclared-field.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
// A genuinely-undeclared type in an IMPORTED GENERIC struct field must emit
|
||||
// "unknown type" even when the struct is instantiated from the main file —
|
||||
// 0759 only covered a non-generic imported struct instantiated in-module.
|
||||
// Before the fix the generic template's fields resolved in the main-file
|
||||
// instantiation context, so the leaf trusted them as main-file decls and
|
||||
// silently stubbed `Missing` (the program compiled and printed `b.x`). The
|
||||
// template's fields now resolve in the template's own source context, so the
|
||||
// undeclared name surfaces.
|
||||
//
|
||||
// Expected: `error: unknown type 'Missing'` pointing into lib.sx; exit 1.
|
||||
// Regression (stdlib E3).
|
||||
#import "modules/std.sx";
|
||||
#import "0761-modules-imported-generic-undeclared-field/lib.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
b : Bad(s32) = .{ x = 1, y = 2 };
|
||||
print("{}\n", b.x);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Flat-imported generic struct with a genuinely-undeclared field type
|
||||
// (`y: Missing`). 0759 covers a NON-generic imported struct; a generic one is
|
||||
// instantiated cross-module, so before the fix its field nodes were resolved in
|
||||
// the (main-file) instantiation context — the source-aware leaf saw "main",
|
||||
// trusted it to the `UnknownTypeChecker` (which never walks imports), and
|
||||
// silently fabricated a 0-field stub. `instantiateGenericStruct` now resolves
|
||||
// the template's fields in THIS module's source context, so the undeclared name
|
||||
// surfaces at the reference.
|
||||
Bad :: struct($T: Type) {
|
||||
x: T;
|
||||
y: Missing;
|
||||
}
|
||||
@@ -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/0760-modules-imported-generic-value-param-as-field-type/lib.sx:9:8
|
||||
|
|
||||
9 | x: N;
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: unknown type 'Missing'
|
||||
--> examples/0761-modules-imported-generic-undeclared-field/lib.sx:11:8
|
||||
|
|
||||
11 | y: Missing;
|
||||
| ^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -698,7 +698,7 @@ pub const Lowering = struct {
|
||||
self.program_index.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {};
|
||||
self.lowerFunction(&cd.value.data.fn_decl, cd.name, is_imported);
|
||||
} else if (cd.value.data == .struct_decl) {
|
||||
self.registerStructDecl(&cd.value.data.struct_decl);
|
||||
self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file);
|
||||
} else if (cd.value.data == .enum_decl) {
|
||||
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
} else if (cd.value.data == .union_decl) {
|
||||
@@ -711,7 +711,7 @@ pub const Lowering = struct {
|
||||
self.lowerComptimeSideEffect(ct.expr);
|
||||
},
|
||||
.struct_decl => {
|
||||
self.registerStructDecl(&decl.data.struct_decl);
|
||||
self.registerStructDecl(&decl.data.struct_decl, decl.source_file);
|
||||
},
|
||||
.enum_decl => {
|
||||
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
@@ -935,7 +935,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
self.declareFunction(&cd.value.data.fn_decl, cd.name);
|
||||
} else if (cd.value.data == .struct_decl) {
|
||||
self.registerStructDecl(&cd.value.data.struct_decl);
|
||||
self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file);
|
||||
} else if (cd.value.data == .enum_decl) {
|
||||
// Register enum/tagged-union types in the type table
|
||||
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
@@ -1114,7 +1114,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
},
|
||||
.struct_decl => {
|
||||
self.registerStructDecl(&decl.data.struct_decl);
|
||||
self.registerStructDecl(&decl.data.struct_decl, decl.source_file);
|
||||
},
|
||||
.enum_decl => {
|
||||
// Register enum/tagged-union types in the type table
|
||||
@@ -3024,7 +3024,7 @@ pub const Lowering = struct {
|
||||
// Block-local type declarations
|
||||
.struct_decl => |sd| {
|
||||
self.recordLocalTypeName(sd.name);
|
||||
self.registerStructDecl(&node.data.struct_decl);
|
||||
self.registerStructDecl(&node.data.struct_decl, node.source_file orelse self.current_source_file);
|
||||
},
|
||||
.enum_decl, .union_decl => {
|
||||
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
||||
@@ -3198,7 +3198,7 @@ pub const Lowering = struct {
|
||||
// Handle local type declarations: MyType :: struct/union/enum { ... }
|
||||
if (cd.value.data == .struct_decl) {
|
||||
self.recordLocalTypeName(cd.name);
|
||||
self.registerStructDecl(&cd.value.data.struct_decl);
|
||||
self.registerStructDecl(&cd.value.data.struct_decl, self.current_source_file);
|
||||
return;
|
||||
}
|
||||
if (cd.value.data == .enum_decl or cd.value.data == .union_decl) {
|
||||
@@ -13582,6 +13582,27 @@ pub const Lowering = struct {
|
||||
// Return `.unresolved` so callers (e.g. lambda return-type inference,
|
||||
// call-site `$R` inference) treat it as not-yet-known.
|
||||
if (node.data == .type_expr and node.data.type_expr.is_generic) {
|
||||
// A VALUE param (`$N: u32`) named in a TYPE position (`x: N`) is bound
|
||||
// to a compile-time integer, not a type, so `resolveBinding` above
|
||||
// found no TYPE binding and it lands here. In the MAIN file the
|
||||
// `UnknownTypeChecker` owns this diagnostic (and halts before codegen);
|
||||
// an imported template's fields are resolved in the template's source
|
||||
// context (see `instantiateGenericStruct`) and are checker-trusted, so
|
||||
// this leaf is the sole guard — emit the tailored hint, mirroring the
|
||||
// imported `.undeclared` leaf. A genuinely-unbound type param (`$R`,
|
||||
// no value binding) stays a silent `.unresolved`.
|
||||
const nm = node.data.type_expr.name;
|
||||
const bound_value = if (self.comptime_value_bindings) |cvb| cvb.contains(nm) else false;
|
||||
if (bound_value) {
|
||||
const is_main = if (self.main_file) |mf|
|
||||
(if (self.current_source_file) |csf| std.mem.eql(u8, csf, mf) else true)
|
||||
else
|
||||
true;
|
||||
if (!is_main) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ nm, nm });
|
||||
}
|
||||
}
|
||||
return .unresolved;
|
||||
}
|
||||
// Bare type names resolve through the source-aware `selectNominalLeaf`
|
||||
@@ -13901,6 +13922,22 @@ pub const Lowering = struct {
|
||||
self.pack_bindings = pb;
|
||||
self.pack_arg_types = pb;
|
||||
|
||||
// Resolve the field type nodes in the TEMPLATE's source context, not the
|
||||
// (possibly cross-module) instantiation site. A field naming a type
|
||||
// visible only in the template's module then resolves correctly, and the
|
||||
// source-aware nominal leaf classifies main vs imported by the TEMPLATE's
|
||||
// file — so an undeclared field type (`y: Missing`) or a value param used
|
||||
// as a type (`x: N` for `$N: u32`) is diagnosed at the right authority
|
||||
// (the leaf for an imported template, the `UnknownTypeChecker` for a
|
||||
// main-file one) instead of silently fabricating a stub / poisoning with
|
||||
// `.unresolved` that panics at LLVM emission.
|
||||
const saved_src = self.current_source_file;
|
||||
const saved_diag_src = if (self.diagnostics) |d| d.current_source_file else null;
|
||||
if (tmpl.source_file) |sf| {
|
||||
self.current_source_file = sf;
|
||||
if (self.diagnostics) |d| d.current_source_file = sf;
|
||||
}
|
||||
|
||||
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
||||
for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| {
|
||||
const field_ty = self.resolveTypeWithBindings(ftype_node);
|
||||
@@ -13910,6 +13947,9 @@ pub const Lowering = struct {
|
||||
}) catch unreachable;
|
||||
}
|
||||
|
||||
self.current_source_file = saved_src;
|
||||
if (self.diagnostics) |d| d.current_source_file = saved_diag_src;
|
||||
|
||||
// Restore bindings
|
||||
self.type_bindings = saved_type_bindings;
|
||||
self.comptime_value_bindings = saved_value_bindings;
|
||||
@@ -14342,7 +14382,7 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void {
|
||||
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void {
|
||||
const table = &self.module.types;
|
||||
const name_id = table.internString(sd.name);
|
||||
|
||||
@@ -14391,6 +14431,7 @@ pub const Lowering = struct {
|
||||
.type_params = tps,
|
||||
.field_names = fnames,
|
||||
.field_type_nodes = ftype_nodes,
|
||||
.source_file = source_file,
|
||||
}) catch {};
|
||||
|
||||
// Register methods under "TemplateName.method" in fn_ast_map
|
||||
|
||||
@@ -15,6 +15,15 @@ pub const StructTemplate = struct {
|
||||
type_params: []const TemplateParam,
|
||||
field_names: []const []const u8,
|
||||
field_type_nodes: []const *const Node, // raw AST pointers — must be copied from heap nodes
|
||||
// The module that DECLARED this template. Instantiation resolves the
|
||||
// field type nodes in THIS source context, not the (possibly cross-module)
|
||||
// instantiation site — so a field naming a type visible only in the
|
||||
// template's module resolves correctly, and the source-aware nominal leaf
|
||||
// classifies main vs imported by the TEMPLATE's file (an undeclared field
|
||||
// type or a value param used as a type is diagnosed at the right authority,
|
||||
// never silently stubbed). Null only when the decl carried no source file
|
||||
// (synthesized / comptime registration).
|
||||
source_file: ?[]const u8 = null,
|
||||
};
|
||||
pub const TemplateParam = struct {
|
||||
name: []const u8,
|
||||
|
||||
Reference in New Issue
Block a user