Files
sx/issues/0094-missing-struct-field-assignment-unresolved-panic.md
agra c98bebc4e3 fix(ir): missing-field multi-assign + promoted-union-member lvalue [F0.10]
Completes the issue-0094 fix. attempt-1 made single-assign and address-of
diagnose a missing struct field; the stress-review found two remaining defects
in that change:

1. lowerMultiAssign's `.field_access` target kept the pre-fix shape — a
   struct-only loop that defaulted `field_idx 0` / `field_ty .unresolved` on a
   miss, then built the GEP and stored unconditionally. A missing field
   (`p.q, y = 2, 3`) silently wrote field 0 (printed `x=2 y=3`, no diagnostic),
   and a valid promoted-union / tuple member at a non-zero offset corrupted
   field 0 instead of its own slot.

2. attempt-1's new union branch in lowerExprAsPtr resolved only DIRECT union
   field names, so `@v.x` on a promoted anonymous-struct member reported
   "field 'x' not found on type 'Vec2'" even though `v.x = 41` worked.

Both lvalue-pointer sites and the multi-assign store now route through one
shared resolver, `fieldLvaluePtr`, that handles struct fields, union direct
fields, promoted anonymous-struct union members, and tuple elements, and
returns null (no field-0 / `.unresolved` default) on a genuine miss. Each
caller emits the read path's `emitFieldError` on null. This collapses the
three previously-divergent field-lvalue walks into one, fixing the
multi-assign missing-field corruption, the promoted-member over-rejection,
and (as a side effect of correct resolution) non-zero-offset promoted-union
and tuple multi-assign stores. The types.zig tripwire is untouched.

Regression tests:
- examples/1145 extended: multi-assign missing field (`p.r, y`) errors, exit 1.
- examples/0166 (new): promoted union member written and address-of'd,
  including a non-zero-offset member (`@v.y`), compiles and runs.
- src/ir/lower.test.zig: multi-assign missing-field field-not-found unit test.
2026-06-05 14:00:24 +03:00

4.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): all three lvalue field-store sites — single assignment, address-of, and multi-target assignment — route field resolution through one shared helper, fieldLvaluePtr(obj_ptr, obj_ty, field), which resolves struct fields, union/tagged-union direct fields, promoted anonymous-struct union members, and tuple elements, and returns null (no field 0 / .unresolved default) when nothing matches. Each caller emits the read path's field-not-found diagnostic (emitFieldError) on a null result:

  1. lowerAssignment .field_access target — found-flag bail on the struct loop, with union direct + promoted handled before it.
  2. lowerExprAsPtr .field_access — delegates to fieldLvaluePtr, so the address-of path now resolves promoted union members (@v.x) — not only direct union fields — and a genuine miss errors. The .s64 sentinel is gone.
  3. lowerMultiAssign .field_access target — replaced its struct-only loop (which defaulted field_idx 0 / field_ty .unresolved on a miss, silently storing into field 0 — p.q, y = 2, 3 printed x=2 y=3) with the shared fieldLvaluePtr; a missing field now errors, and a valid promoted-union / tuple member at a non-zero offset stores into its own slot, not field 0.

All sites reuse emitFieldError (the exact facility the read path lowerFieldAccessOnType uses), so the read and write paths reject identically. The src/backend/llvm/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 — single-assign, address-of, AND multi-assign missing-field all error, exit 1), examples/0165-types-nested-struct-field-assign.sx (positive — nested struct field write + address-of a matched field), examples/0166-types-union-promoted-member-lvalue.sx (positive — promoted union member written and address-of'd, including a non-zero offset member), and two lowering unit tests in src/ir/lower.test.zig (single- and multi-assign missing-field field-not-found).

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.