3.0 KiB
0189 — non-type expression in type position silently fabricates an empty struct
Status: OPEN
Symptom
A non-type expression used in type position — e.g. a field_access
like g.a — is silently accepted and resolved to a bogus zero-field
struct {} instead of being rejected with a diagnostic.
- Observed:
x : g.a = ---;compiles with exit 0, emitting LLVMalloca {}(an empty struct) forx.Tuple(i32, g.a)likewise yields{ i32, {} }with astore ... zeroinitializer. - Expected: a user-facing "not a type" diagnostic at the offending expression and a clean non-zero exit (never a fabricated empty struct reaching codegen).
This is the classic silent-fallback-default failure mode: a lookup that
should fail returns a "reasonable-looking" value ({}) and ships
invisibly.
Reproduction
S :: struct { a: i32; }
g : S = .{ a = 1 };
main :: () -> i32 {
x : g.a = ---; // `g.a` is a runtime VALUE, not a type
0
}
Run: ./zig-out/bin/sx run repro.sx → exits 0 (should error). Inspect
./zig-out/bin/sx ir repro.sx → x is alloca {}.
The tuple form x : Tuple(i32, g.a) = ---; reproduces the same
fabrication for the second element. (The bug is not tuple-specific —
it predates and is independent of the Tuple(...) syntax cutover; the
plain g.a case above has no tuple at all.)
Investigation prompt
The fabrication lives in the type-resolution bridge, not in the tuple
code. In src/ir/type_bridge.zig, resolveAstType's handling of a
field_access (and likely any non-type-shaped expression) in type
position falls through to building a zero-field stub struct rather than
returning the .unresolved sentinel + a diagnostic.
The tuple-element validation arm in src/ir/lower/generic.zig (and the
literal screen near src/ir/lower.zig:960) only rejects the five literal
tags (int/float/string/bool/null_literal); it leans on
resolveCompound's .unresolved propagation to catch everything else —
which works only when the element actually resolves to .unresolved. A
field_access resolves to the fabricated {} stub instead, so it slips
through.
Likely fix (pick one, verify against the repro):
- In
type_bridge.resolveAstType, thefield_access-in-type-position arm (and any non-type-shaped expression) should emit a diagnostic viaself.diagnostics.addFmt(.err, span, "...")and return.unresolved, never a fabricated empty struct. - Broaden the type-element screen to reject any non-type-shaped
element node up front using
type_bridge.isTypeShapedAstNode(already used byresolveTupleLiteralTypeArg), instead of the explicit literal-tag allowlist.
Verification: the repro above must error with a clear "not a type"
diagnostic and a non-zero exit; Tuple(i32, g.a) must reject too; the
existing examples/diagnostics/1116 (literal non-type element) must keep
passing. Add a regression example for the field_access case.
(Found by adversarial review during the tuple-syntax cutover, commit
989e18b7.)