# 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. > The pass also walks function **bodies** (`checkBodyTypes` + `collectBodyDeclNames`): > local `var` / `const` type annotations — including inside `if` / loop / `match` / > `push` / `defer` / `onfail` blocks and decl-value blocks — are checked with the > enclosing function's generic params in scope, and body-local `T :: struct/enum/ > union` declarations are collected so they aren't false-flagged. This closes the > silent body-level hole where `v: Coordnate = 5` (a non-existent type) compiled and > ran with the value dropped. Nested function / closure bodies are their own scope > and are not descended (safe under-coverage); explicit `cast(T)` already has its > own `unresolved` diagnostic and is left to it. > The walk descends into **nested closure / function bodies** too (`walkBodyTypes` > + `checkScope`): each scope accumulates its generic params onto the parent's, so > a closure body still sees the enclosing function's `$T`, and a type annotation in > any nesting depth is checked. `harvestScopeDecls` collects type-decl names across > the whole body (including nested scopes) so locals aren't false-flagged. Cast > targets are handled too: `cast(T)` where `T` is a value-`Type` param (the > otherwise-silent cast case) gets the tailored hint, while an unknown *literal* > cast target is left to the existing value-resolution `unresolved` diagnostic (no > double-report). The only remaining under-coverage is benign (annotations buried in > AST positions the walker doesn't descend stay unchecked — never a false positive). > Regression tests: `examples/1111` (tailored hint, signature), `1112` (typo'd field > type), `1113` (body-level local annotation), `1114` (nested-closure annotation), > `1115` (`cast` value param) — all exit 1. Suite: 350 passed, 0 failed. ## Symptom A function parameter declared `T: Type` (or lowercase `T: type`) — i.e. WITHOUT the `$` generic-type-parameter sigil — that is then referenced in a type position (`-> T`, `Closure() -> T`, etc.) silently resolves `T` to a fabricated empty struct `{}` instead of the caller's argument type. The function "runs" but produces garbage (the value renders as `T{}`), with no diagnostic. ```sx idwrap :: (T: Type, f: Closure() -> T) -> T { return f(); } main :: () -> s32 { print("{}\n", idwrap(s32, closure(() -> s32 { return 7; }))); // prints "T{}", want 7 return 0; } ``` The correct, working form is the generic `$T: Type` (the `$` introduces the generic type parameter — see `specs.md` §"`$` generic type parameter introduction"). With `$T`, the binding is established and the result is `7`. So this is not a miscompile of a valid program — it's a **missing diagnostic** for a misuse: a non-generic `Type`-typed value param can't be used as a type, and should be rejected (or the `$` requirement explained), not silently turned into an empty struct. ## Reproduction See above. Compare `idwrap :: ($T: Type, …)` (works, prints 7) vs `idwrap :: (T: Type, …)` (prints `T{}`). ## Investigation prompt In [src/ir/lower.zig](../src/ir/lower.zig), `resolveTypeWithBindings` resolves a `.type_expr` named `T` by checking `type_bindings` (works for `$T`, which `buildTypeBindings` registers). For a non-`$` `T: Type` param there is no binding, so resolution falls through to `type_bridge.resolveAstType`, which fabricates an empty-struct stub for the unknown name `T` — the classic "silent empty struct" the CLAUDE.md REJECTED-PATTERNS warn about. Fix options: (1) at scan/sema time, reject referencing a non-`$` `Type`-typed param in a type position with a diagnostic ("type parameter must be introduced with `$` — write `$T: Type`"); or (2) make `resolveAstType` return `.unresolved` + a diagnostic for an unknown bare type name in a generic-eligible position, instead of stubbing `{}`. Deferred — orthogonal to ERR; the working `$T` idiom exists. Low priority but should not stay silent.