docs: file issue 0189 (non-type expr in type position fabricates empty struct)
This commit is contained in:
@@ -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`.)
|
||||
Reference in New Issue
Block a user