fix(types): reject unknown type names instead of silent empty struct (issue 0064)

An identifier used in a type position that resolved to nothing fell through
to `type_bridge.resolveTypeName`'s empty-struct-stub fallback, silently
interning a 0-field struct named after the identifier. A value parameter
mistakenly used as a type (`(T: Type, ...) -> T`, missing the `$`) or a
typo'd type name therefore compiled and ran, rendering as `T{}`.

New post-scan diagnostic pass `checkUnknownTypeNames` (lower.zig Pass 1f)
walks every main-file function signature and non-generic struct field type
and rejects any leaf name that is not a primitive, an in-scope generic param
(`$T` / `type_params`), a declared type, or a real (non-stub) registered
type. The load-bearing empty-struct stub is left intact — forward references
and foreign-class opaque types still depend on it during the scan — and the
pass runs before body lowering, so `hasErrors()` halts the build before any
stub reaches codegen.

A value param used as a type gets a tailored hint to write `$T: Type`; a
genuine unknown gets "unknown type 'X'". Imported concrete types are
recognized via the type table, and inline compound spellings (`[:0]u8`),
arbitrary-width ints (`u1`/`u2`), and `$`-introduced generics (`-> $R`) are
exempted to avoid false positives.

Regressions: examples/1111 (tailored hint) + 1112 (typo'd field type).
This commit is contained in:
agra
2026-06-02 10:24:30 +03:00
parent 9214eefba1
commit c490ffcfe9
10 changed files with 249 additions and 0 deletions

View File

@@ -1,5 +1,26 @@
# 0064 — non-`$` `T: Type` function param used as a type silently yields `{}`
> **✅ RESOLVED (2026-06-02).** Root cause as diagnosed: an identifier in a type
> position that resolved to nothing fell through to `type_bridge.resolveTypeName`'s
> empty-struct stub, silently interning a 0-field struct under the name. **Fix
> (option 2, surfaced as a diagnostic):** a new post-scan pass
> `checkUnknownTypeNames` ([src/ir/lower.zig], Pass 1f) walks every main-file
> function signature and non-generic struct field type and rejects any leaf name
> that is not a primitive, an in-scope generic param (`$T` / `type_params`), a
> declared type, or a real (non-stub) registered type. The load-bearing
> empty-struct stub is left intact (forward references + foreign-class opaque
> types still rely on it during the scan); the pass runs after scanning and before
> body lowering, so `core.zig`'s `hasErrors()` halts the build before any stub
> reaches codegen. A value param used as a type gets the tailored hint
> *"'T' is a value parameter, not a type; introduce a generic type parameter with
> `$T: Type`"*; a genuine unknown name gets *"unknown type 'X'"*. Imported concrete
> types are recognized via the type table (`findByName`), so cross-module
> references aren't false-flagged; inline compound spellings (`[:0]u8`), arbitrary-
> width ints (`u1`/`u2`), and `$`-introduced generics (`-> $R`) are all exempted.
> Regression tests: `examples/1111-diagnostics-nondollar-type-param-rejected.sx`
> (tailored hint, exit 1) and `examples/1112-diagnostics-unknown-type-name-rejected.sx`
> (typo'd field type, exit 1). Suite: 347 passed, 0 failed.
## Symptom
A function parameter declared `T: Type` (or lowercase `T: type`) — i.e. WITHOUT