diff --git a/issues/0189-field-access-in-type-position-fabricates-empty-struct.md b/issues/0189-field-access-in-type-position-fabricates-empty-struct.md new file mode 100644 index 00000000..bcd43d89 --- /dev/null +++ b/issues/0189-field-access-in-type-position-fabricates-empty-struct.md @@ -0,0 +1,74 @@ +# 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 LLVM + `alloca {}` (an empty struct) for `x`. `Tuple(i32, g.a)` likewise + yields `{ i32, {} }` with a `store ... 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 + +```sx +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): +1. In `type_bridge.resolveAstType`, the `field_access`-in-type-position + arm (and any non-type-shaped expression) should emit a diagnostic via + `self.diagnostics.addFmt(.err, span, "...")` and return `.unresolved`, + never a fabricated empty struct. +2. Broaden the type-element screen to reject **any** non-type-shaped + element node up front using `type_bridge.isTypeShapedAstNode` (already + used by `resolveTupleLiteralTypeArg`), 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`.)