Files
sx/issues/0064-nondollar-type-param-silent-empty-struct.md
agra 1c14383495 ERR/E5.1: verify generic failable composition (sub-feature 8); resolve 0062, file 0064
Generic value-carrying failable composition works with the documented
$T: Type generic form (catch / destructure / failure-propagation / a
second monomorphization at a different T). Issue 0062 was an invalid-repro
report — it used the non-generic T: type form, which is a plain Type-valued
param, not a generic type parameter. Marked 0062 resolved (not a bug).

The only real residual: a non-$ T: Type function param used as a type
silently resolves to an empty {} (renders T{}) instead of erroring. Filed
as 0064 (deferred, orthogonal to ERR — the $T idiom works).

Regression: 1044-errors-generic-failable-composition.sx.
2026-06-01 22:35:02 +03:00

2.2 KiB

0064 — non-$ T: Type function param used as a type silently yields {}

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.

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, 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.