From a4906975bd93f7256513ac33b56714ef83640e10 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 08:36:45 +0300 Subject: [PATCH] =?UTF-8?q?fix(diag):=20undeclared=20type=20in=20a=20main-?= =?UTF-8?q?file=20generic=20struct=20field=20=E2=86=92=20diagnostic=20(no?= =?UTF-8?q?=20silent=20stub)=20[stdlib=20E3=20attempt-2]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...undeclared-type-in-generic-struct-field.sx | 29 +++++++++++++++++++ ...declared-type-in-generic-struct-field.exit | 1 + ...clared-type-in-generic-struct-field.stderr | 5 ++++ ...clared-type-in-generic-struct-field.stdout | 1 + src/ir/semantic_diagnostics.zig | 22 ++++++++++---- 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 examples/0171-types-undeclared-type-in-generic-struct-field.sx create mode 100644 examples/expected/0171-types-undeclared-type-in-generic-struct-field.exit create mode 100644 examples/expected/0171-types-undeclared-type-in-generic-struct-field.stderr create mode 100644 examples/expected/0171-types-undeclared-type-in-generic-struct-field.stdout diff --git a/examples/0171-types-undeclared-type-in-generic-struct-field.sx b/examples/0171-types-undeclared-type-in-generic-struct-field.sx new file mode 100644 index 0000000..82a9ad7 --- /dev/null +++ b/examples/0171-types-undeclared-type-in-generic-struct-field.sx @@ -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; +} diff --git a/examples/expected/0171-types-undeclared-type-in-generic-struct-field.exit b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stderr b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stderr new file mode 100644 index 0000000..6685e51 --- /dev/null +++ b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stderr @@ -0,0 +1,5 @@ +error: unknown type 'MissingType' + --> examples/0171-types-undeclared-type-in-generic-struct-field.sx:22:10 + | +22 | bad: MissingType; + | ^^^^^^^^^^^ diff --git a/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stdout b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0171-types-undeclared-type-in-generic-struct-field.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 9dacdb4..fb936bb 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -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| {