fix(ir): missing struct field assignment errors cleanly, no LLVM panic [F0.10]
Assigning to a nonexistent struct field (`p.q = 2` where Point has no `q`) aborted the compiler with the `.unresolved` LLVM tripwire instead of a source diagnostic (issue 0094). The lvalue field lookup never diagnosed a miss: - `lowerAssignment`'s `.field_access` target left `field_ty = .unresolved` when no struct field matched, then built `ptrTo(field_ty)` and stored — so a pointer-to-`.unresolved` reached LLVM emission and tripped the panic. - `lowerExprAsPtr`'s `.field_access` fallback returned `structGepTyped(obj_ptr, 0, .s64, obj_ty)` on a miss — a silent field-0/`.s64` default that mislowered the lvalue. Both sites now reuse the read path's `emitFieldError` (the exact facility `lowerFieldAccessOnType` uses), so read and write reject identically with `field 'q' not found on type 'Point'`. `lowerExprAsPtr` also resolves union/tagged-union fields via `union_gep` (the old `.s64` fallback was silently standing in for union field access — e.g. `u.a[0] = v`), so that path is fixed, not just made loud. The `types.zig` tripwire is untouched: the fix is to never produce `.unresolved` for a missing-field store. Regression tests: - examples/1145-diagnostics-missing-struct-field-assign.sx — negative, both sites error, exit 1. - examples/0165-types-nested-struct-field-assign.sx — positive, nested struct field write + address-of a matched field still work. - src/ir/lower.test.zig — lowering unit test asserting the field-not-found diagnostic for a missing-field assignment.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# 0094 — assigning to a missing struct field panics with "unresolved type reached LLVM emission"
|
||||
|
||||
> **RESOLVED** (F0.10). **Root cause:** the lvalue field lookup never diagnosed a
|
||||
> missing field. In `Lowering.lowerAssignment`'s `.field_access` target path
|
||||
> (`src/ir/lower.zig`), `field_ty` started as `.unresolved`; when no struct field
|
||||
> matched, the code still built `ptrTo(field_ty)` / `structGepTyped` and stored —
|
||||
> so a pointer-to-`.unresolved` reached LLVM emission and tripped the
|
||||
> `src/backend/llvm/types.zig` tripwire. The nested lvalue-pointer path
|
||||
> (`Lowering.lowerExprAsPtr`'s `.field_access` fallback) had the sibling defect:
|
||||
> on a miss it returned `structGepTyped(obj_ptr, 0, .s64, obj_ty)` — a silent
|
||||
> field-0/`.s64` default.
|
||||
>
|
||||
> **Fix (`src/ir/lower.zig`):**
|
||||
> 1. `lowerAssignment` `.field_access` target — track a `found` flag over the
|
||||
> struct-field loop; on a miss, emit the read path's field-not-found
|
||||
> diagnostic (`emitFieldError`) and bail, never constructing
|
||||
> `ptrTo(.unresolved)`.
|
||||
> 2. `lowerExprAsPtr` `.field_access` — resolve union/tagged-union fields via
|
||||
> `union_gep` (mirroring the write path; the old `.s64` fallback was silently
|
||||
> standing in for union field access), then the struct-field loop, then
|
||||
> `emitFieldError` on a genuine miss. The `.s64` sentinel is gone.
|
||||
>
|
||||
> Both sites now reuse `emitFieldError` (the exact facility the read path
|
||||
> `lowerFieldAccessOnType` uses), so the read and write paths reject identically.
|
||||
> The `types.zig` tripwire is untouched — the fix is to never produce
|
||||
> `.unresolved` for a missing-field store.
|
||||
>
|
||||
> **Regression tests:** `examples/1145-diagnostics-missing-struct-field-assign.sx`
|
||||
> (negative — both sites error, exit 1), `examples/0165-types-nested-struct-field-assign.sx`
|
||||
> (positive — nested struct field write + address-of a matched field still work),
|
||||
> and a lowering unit test in `src/ir/lower.test.zig`
|
||||
> ("assigning to a missing struct field emits field-not-found, no panic").
|
||||
|
||||
## Symptom
|
||||
Assigning to a nonexistent struct field (`p.q = ...`) panics during LLVM emission instead of reporting a source diagnostic.
|
||||
|
||||
Observed: the compiler reaches the `.unresolved` LLVM tripwire in `src/backend/llvm/types.zig:175` via `emitStore`.
|
||||
Expected: a normal compile error like `field 'q' not found on type 'Point'`, matching the read-field diagnostic path.
|
||||
|
||||
## Reproduction
|
||||
```sx
|
||||
Point :: struct { x: s64; }
|
||||
|
||||
main :: () {
|
||||
p := Point.{ x = 1 };
|
||||
p.q = 2;
|
||||
}
|
||||
```
|
||||
|
||||
Running `./zig-out/bin/sx run repro.sx` currently panics with:
|
||||
```text
|
||||
panic: unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted
|
||||
```
|
||||
|
||||
## Investigation prompt
|
||||
Fix issue 0094 in the sx compiler: assigning to a missing struct field (`p.q = 2`) panics with `.unresolved` reaching LLVM emission instead of emitting a field-not-found diagnostic.
|
||||
|
||||
Suspected area: `src/ir/lower.zig`, especially `Lowering.lowerAssignment`'s `.field_access` target path around the struct-field lookup (`field_ty` starts as `.unresolved`, no matched field diagnoses, then `ptrTo(field_ty)` is stored) and the related `Lowering.lowerExprAsPtr` field-access fallback that returns `structGepTyped(obj_ptr, 0, .s64, obj_ty)` on lookup failure. The fix should make failed lvalue field lookup loud, reusing `emitFieldError(obj_ty, field, span)` or equivalent, and should not use `.s64`, `.void`, or any real type as a sentinel.
|
||||
|
||||
Verification: run the repro and expect exit 1 with a source diagnostic `field 'q' not found on type 'Point'`; no LLVM panic. Then run `zig build`, `zig build test`, and `bash tests/run_examples.sh`.
|
||||
Reference in New Issue
Block a user