Files
sx/issues/0094-missing-struct-field-assignment-unresolved-panic.md
agra e13518e8aa 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.
2026-06-05 13:24:15 +03:00

3.5 KiB

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

Point :: struct { x: s64; }

main :: () {
    p := Point.{ x = 1 };
    p.q = 2;
}

Running ./zig-out/bin/sx run repro.sx currently panics with:

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.