Files
sx/issues/0163-untagged-union-payload-binding-match-panics.md
agra ff9e448f8c fix: optional-chain getter/field correctness from 0160 adversarial review
Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the
touched optional-chain / optional-coercion code; all fixed here:

1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never
   called directly first) panicked with "unresolved type reached LLVM emission":
   a cold instance method is absent from resolveFuncByName, so the getter's
   return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain
   and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered)
   before querying its return type. (The 0907 test passed only by luck — List(i64)
   is warmed by stdlib use; 0907 now also exercises a cold user generic.)

2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted
   the pointer bits as the field (silent garbage) — the some-branch real-field
   path didn't load through the pointer. It now derefs `?*T` before the field
   access. (Pre-existing — the else-branch predates 0160 — but it's the same
   function and a silent miscompile, so fixed here.)

3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the
   lowerVarDecl optional arm wrapped the raw array. It now coerces the value to
   the optional's child type (array→slice) before wrapping.

Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING
bugs the reviews surfaced in untouched subsystems are filed as issues 0161
(struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163
(untagged-union payload-binding match).
2026-06-22 18:55:41 +03:00

1.9 KiB

0163 — payload-binding match on a plain untagged union panics instead of diagnosing

Symptom

A match-style if x == { case .variant: (v) { ... } } with a PAYLOAD BINDING (v) on a value of a plain UNTAGGED union type panics in the LLVM backend instead of producing a diagnostic. An untagged union has no discriminant, so a case-payload binding is not a valid construct and should be rejected at typecheck.

  • Observed: thread panic: unresolved type reached LLVM emission at src/backend/llvm/types.zig:196, reached via emit_llvm.zig:1289 declareFunctiontoLLVMType(param.ty) (exit 134).
  • Expected: a clean diagnostic (e.g. "cannot bind a payload from an untagged union — use a tagged enum/union with a discriminant").

Surfaced during the issue-0160 review (blast-radius probing). NOT caused by 0160 — the panic path is union-match lowering → declareFunction, none of which the 0160 fix touches. Removing the (v) binding, or using a tagged enum instead, both work.

Reproduction

#import "modules/std.sx";
Shape :: union { circle: i64; rect: i64; }   // plain untagged union (no discriminant)
main :: () {
    s : Shape = .{ circle = 5 };
    r := if s == { case .circle: (v) { v } case .rect: (v) { v * 2 } };  // panic
    print("{}\n", r);
}

Investigation prompt

The match/case lowering binds a case payload (v) whose type it resolves against the union variant — but a plain .@"union" (untagged) has no per-variant discriminant, so the binding's type leaks out as .unresolved and reaches declareFunction. In the match-arm lowering (grep the case/match_arm path in src/ir/lower/), reject a payload-binding case when the scrutinee type is an untagged .@"union" (only .tagged_union / .@"enum" payloads are bindable): emit a diagnostic and bail, before any .unresolved type is produced. Verify with the repro (expect a clean error, not a panic). Add a diagnostics example.