Files
sx/issues/0133-union-member-struct-literal-assign-unresolved-panic.md
agra 45befed698 docs(issues): correct 0132 root cause; file 0133 and 0134
- 0132: rewrite to the verified root cause -- protocol method signature
  registration resolves type names via flat findByName and picks the wrong
  same-name author. Original payload-field hypothesis kept as superseded;
  repro switched to canonical `impl ... for` syntax. Still open (the
  protocol path is unchanged).
- 0133: assigning a struct literal to a union member panics ("unresolved
  type reached LLVM emission"); pre-existing, surfaced while testing.
- 0134: a same-name `error` set collapses into a namespaced import's set --
  error-set declarations lack per-decl nominal identity (E6a gap); this is
  what keeps the 0132-class error-ref resolution dormant.
2026-06-13 13:41:30 +03:00

5.3 KiB

0133 — assigning a struct LITERAL to a union member panics ("unresolved type reached LLVM emission")

Symptom

One-line: u.b = .{ ... } where b is a NAMED-struct member of a plain union compiles to an .unresolved-typed struct_init and trips the LLVM-emission tripwire. The RHS struct literal never receives its target type (the union member's type), so it lowers as .unresolved.

  • Observed: thread … panic: unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted (src/backend/llvm/types.zig:176), reached from emitStructInit (src/backend/llvm/ops.zig:1211) because the struct_init instruction's ty is .unresolved.
  • Expected: the literal types itself as the union member's struct type (here S) and stores into the member — exactly as it already does when the left-hand side is a STRUCT field.

This is PRE-EXISTING (reproduces on master / before any issue-0132 work) and ORTHOGONAL to type-name resolution: it reproduces with a unique, non-colliding type name. Surfaced while testing issue 0132's broader-latent fix (making enum/union payload registration visibility-aware) — that fix makes a colliding-name union member resolve to the correct type, at which point this separate codegen bug is what blocks the end-to-end union case.

Reproduction

Minimal, standalone (only modules/std.sx):

#import "modules/std.sx";

S :: struct { code: i64; }
U :: union { a: i64; b: S; }

main :: () {
    u : U = ---;
    u.b = .{ code = 9 };          // <-- panics: struct literal has no target type
    print("code={}\n", u.b.code);
}

Run: ./zig-out/bin/sx run issues/0133-union-member-struct-literal-assign-unresolved-panic.sx → panics today; the fix should make it print code=9, exit 0.

Bisection (what does / does not trigger it)

Variant Result
u.b = .{ code = 9 } (union member ← struct LITERAL) PANICS
o.b = .{ code = 9 } where o : Outer = struct { a; b: S } (STRUCT member ← literal) OK
s : S = .{ code = 9 }; u.b = s (union member ← pre-made value) OK
u : U = --- then only read (no literal assign) OK

So the trigger is exactly the conjunction (LHS is a union member) AND (RHS is a struct literal). A struct-field LHS propagates the target type to the literal; a pre-made value needs no target type. Only the union-member-lvalue + literal-RHS combination drops it.

Investigation prompt

Assigning a struct literal to a NAMED-struct member of a plain union panics with "unresolved type reached LLVM emission". Repro: issues/0133-union-member-struct-literal-assign-unresolved-panic.sx (expect a panic today; the fix should make it print code=9, exit 0).

The struct_init instruction for the RHS literal .{ code = 9 } has ty == .unresolved — the literal was lowered without a target type, so it could not resolve to the union member's struct type S. The panic is the codegen tripwire in src/backend/llvm/types.zig:176 (toLLVMTypeInfo), reached from emitStructInit (src/backend/llvm/ops.zig:1211).

Root area: assignment lowering in src/ir/lower.ziglowerAssignment's .field_access target path. Issue 0094 already routes the lvalue POINTER through the shared fieldLvaluePtr (which correctly resolves union/tagged-union direct members — that's why a pre-made value stores fine). The gap is the RHS TARGET TYPE: for a STRUCT-field LHS the code sets self.target_type to the field's type before lowering the RHS (so a struct literal types itself), but for a UNION-member LHS that target-type propagation is missing, so the literal lowers under a null/unresolved target → struct_init.ty == .unresolved.

Suspected fix: before lowering the RHS expression in lowerAssignment's field-access path, compute the LHS member's type for union / tagged-union members too (reuse the same member-type lookup fieldLvaluePtr already performs — ideally have it RETURN the resolved field type, or factor a fieldLvalueType helper, so the lvalue-pointer path and the target-type path cannot diverge — the two-resolver defect class this codebase keeps burning on) and set self.target_type to it for the RHS lowering. Do NOT paper over with an .unresolved→default; per CLAUDE.md, resolve the real member type or emit a diagnostic.

Verification: the repro prints code=9 exit 0; then zig build && zig build test green. Add positive coverage (a union member written via struct literal, then read back) — extend examples/0166-types-union-promoted-member-lvalue.sx or add a new examples/01xx-types-union-member-struct-literal-assign.sx. When resolved, also note in issue 0132 that the broader-latent union case is now demonstrable end-to-end.

Notes

  • Tripwire site (symptom): src/backend/llvm/types.zig:176 (toLLVMTypeInfo, .unresolved arm) via emitStructInit (src/backend/llvm/ops.zig:1211).
  • Root area (cause): Lowering.lowerAssignment .field_access target path in src/ir/lower.zig — RHS target-type not set for union/ tagged-union members.
  • Related but distinct: issue 0094 (RESOLVED) fixed the lvalue-POINTER field resolution (missing-field panic + .i64/field-0 defaults). This issue is the RHS-literal TARGET-TYPE path, which 0094 did not touch.