fix(diag): undeclared type in a main-file generic struct field → diagnostic (no silent stub) [stdlib E3 attempt-2]

Closes the main-file carveout left by attempt-1 (4072689): a genuinely-
undeclared type used as a field type inside a MAIN-file GENERIC struct still
fell through the type leaf's empty-struct stub and silently compiled —
`Box :: struct($T: Type) { good: T; bad: MissingType; }` with `b : Box(s64)`
exited 0 and printed a value instead of reporting `unknown type 'MissingType'`.

Root cause: `UnknownTypeChecker` is the main-file diagnostic authority (the
type leaf defers to it for `.undeclared` names there), but
`checkStructFieldTypes` SKIPPED every generic struct outright ("its fields
reference `$T`, resolved at instantiation"), so the undeclared name was never
examined. The sibling `walkBodyTypes` `.struct_decl` arm skipped body-local
generic structs the same way.

Fix (semantic_diagnostics.zig, checker only — no leaf change):
- `checkStructFieldTypes`: stop skipping generic structs; walk the field
  types with the struct's OWN type params (`$T`, `$N`, `..$Ts`) passed as the
  in-scope set. A param name resolves; any OTHER bare name that is neither
  declared nor a generic param is reported. Value-param positions (a `Vector`
  lane count, a `$N: u32` arg) are still skipped inside
  `checkTypeNodeForUnknown` / `isValueParamPosition`.
- `walkBodyTypes` `.struct_decl`: same close for body-local structs — the
  local struct's own type params join the enclosing scope's in-scope params
  (so it can name both the outer fn's `$T` and its own), any other bare field
  type is still flagged.

The `..$Ts` pack field `(..$Ts)` parses to a `spread_expr` inside the tuple,
which hits `checkTypeNodeForUnknown`'s `else` arm — never walked — so the pack
examples (0538-0543, 0414) stay green. The checker walks only MAIN-file decls,
so library generic structs (List, Map) are untouched.

Regression: examples/0171-types-undeclared-type-in-generic-struct-field — the
reviewer's exact shape; `unknown type 'MissingType'` at the field, exit 1.
Fail-before on 4072689 (prints 7, exit 0), pass-after.

Gate: zig build; zig build test (423/423 + LSP corpus sweep 514); run_examples
498 passed / 0 failed (prior 497 byte-identical); m3te ios-sim build exit 0.
This commit is contained in:
agra
2026-06-08 08:36:45 +03:00
parent 4072689afe
commit a4906975bd
5 changed files with 53 additions and 5 deletions

View File

@@ -0,0 +1,29 @@
// A genuinely-undeclared type name used as a field type inside a MAIN-file
// GENERIC struct must emit a clean "unknown type" diagnostic, not silently
// compile.
//
// The `UnknownTypeChecker` used to SKIP generic structs entirely ("their field
// types reference the struct's own `$T`, resolved at instantiation"). That skip
// was too broad: a field type like `bad: MissingType` — which is NOT a type
// param and names no declared type — fell through the type leaf's empty-struct
// stub and the struct silently compiled, mis-sizing every downstream load.
//
// The checker now walks generic-struct fields with the struct's own type params
// (`$T`) in scope: `good: T` resolves (it IS a param) while `bad: MissingType`
// is reported. A value-param position (a `Vector` lane count, a `$N: u32` arg)
// is still skipped, so a valid generic struct keeps compiling unchanged.
//
// Expected: `error: unknown type 'MissingType'` pointing at the field; exit 1.
// Regression (stdlib E3).
#import "modules/std.sx";
Box :: struct($T: Type) {
good: T;
bad: MissingType;
}
main :: () -> s32 {
b : Box(s64) = .{ good = 7, bad = 0 };
print("{}\n", b.good);
return 0;
}

View File

@@ -0,0 +1,5 @@
error: unknown type 'MissingType'
--> examples/0171-types-undeclared-type-in-generic-struct-field.sx:22:10
|
22 | bad: MissingType;
| ^^^^^^^^^^^

View File

@@ -490,10 +490,14 @@ pub const UnknownTypeChecker = struct {
}
fn checkStructFieldTypes(self: UnknownTypeChecker, sd: *const ast.StructDecl, declared: *std.StringHashMap(void)) void {
// Generic struct fields reference the struct's own type params ($T) —
// resolved at instantiation, not here.
if (sd.type_params.len != 0) return;
for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, &.{}, &.{});
// A generic struct's field types may reference its own type params
// (`$T`, `$N`, the `..$Ts` pack) — those are IN SCOPE here, so pass them
// through rather than skipping the whole decl. Skipping silently let a
// genuinely-undeclared field type (`bad: MissingType`) fall through the
// type leaf's empty-struct stub and compile (stdlib E3). A value-param
// position (a `Vector` lane count, a `$N: u32` arg) is still skipped
// inside `checkTypeNodeForUnknown` / `isValueParamPosition`.
for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, sd.type_params, &.{});
}
fn checkFnSignatureTypes(self: UnknownTypeChecker, fd: *const ast.FnDecl, declared: *std.StringHashMap(void)) void {
@@ -592,7 +596,15 @@ pub const UnknownTypeChecker = struct {
if (cd.type_annotation) |ta| self.checkTypeNodeForUnknown(ta, declared, in_scope.items, type_vals.items);
self.walkBodyTypes(cd.value, declared, in_scope, type_vals);
},
.struct_decl => |sd| if (sd.type_params.len == 0) {
.struct_decl => |sd| {
// A body-local struct's own type params (`$T`) join the enclosing
// scope's in-scope params (so a local generic struct can name both
// the outer fn's `$T` and its own); any OTHER bare field type is
// still a genuinely-undeclared type. Mirrors the top-level
// `checkStructFieldTypes` close of the generic-struct carveout.
const save = in_scope.items.len;
defer in_scope.shrinkRetainingCapacity(save);
for (sd.type_params) |tp| in_scope.append(self.alloc, tp) catch {};
for (sd.field_types) |ft| self.checkTypeNodeForUnknown(ft, declared, in_scope.items, type_vals.items);
},
.call => |c| {