Files
sx/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md
agra 4ca466fa96 fix: optional-chain index opt?.xs[i] over array/ptr-array field (issue 0181)
opt?.xs[i] typed and lowered the index over the optional CONTAINER
(?[N]T); getElementType returned .unresolved, so index_get reached LLVM
with an unresolved element type and panicked. Mirroring the 0101
!-unwrap fix: add lowerOptionalChainIndex (optional_has_value -> some:
unwrap + index (index_gep+load for ?*[N]T, else index_get) +
optional_wrap; none: const_null; merge -> ?ElemType, element-optional
flattened). The typer + dispatch guard compute the element via
ptrToArrayElem(child) orelse getElementType(child), so value-arrays,
slices, many-pointers, AND pointer-to-array (?*[N]T) children resolve.
Null receivers short-circuit (no null deref).

Regression: examples/optionals/0915-optional-chain-array-field-index.sx.
Verified by 3 adversarial reviews, suite 794/0. Filed broader pre-existing
gap 0183 (indexing a non-indexable type panics instead of diagnosing).
2026-06-23 12:29:29 +03:00

4.9 KiB

0181 — ?.-chain (and ?-postfix) on an optional whose child struct contains an ARRAY field panics unresolved type reached LLVM emission

RESOLVED. opt?.xs[i] typed and lowered the index over the optional CONTAINER (?[N]T) — getElementType returned .unresolved, so index_get reached LLVM with an unresolved element type and panicked. Mirroring the issue-0101 !-unwrap fix: added lowerOptionalChainIndex (src/ir/lower/expr.zig) — optional_has_value → some: unwrap + index (index_gep+load for ?*[N]T, else index_get) + optional_wrap; none: const_null; merge → ?ElemType (element-optional flattened, so [N]?T?T). The typer (src/ir/expr_typer.zig) and the dispatch guard both compute the element via ptrToArrayElem(child) orelse getElementType(child) so value-arrays, slices, many-pointers, AND pointer-to-array (?*[N]T) children resolve. Null receivers short-circuit (no null deref, verified in IR). Regression: examples/optionals/0915-optional-chain-array-field-index.sx. Verified by 3 adversarial reviews; suite 794/0. (Broader pre-existing gap found + filed: 0183 — indexing a non-indexable type *T/*[]T/struct panics instead of a diagnostic, reproduces without optionals.)

Symptom

A ?. optional-chain access (or the ? optional-test postfix used in a member-access chain) on a value of type ?S, where S is a struct that contains an array field, panics:

thread … panic: unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted (exit 134).

The same chain on ?S where S has no array field works fine, and the ! force-unwrap chain (opt!.field) on the same array-containing ?S works fine. The defect is specific to the ?/?. operator's receiver-type inference when the optional's child struct contains an array field — that receiver types as .unresolved and reaches LLVM. This is a pure runtime lowering bug: no #run/comptime is involved.

Observed vs expected:

  • Observed: SIGABRT panic (exit 134) at src/backend/llvm/types.zig:196 (toLLVMTypeInfo .unresolved arm), reached from declareFunction's param.ty lowering of a synthesized accessor.
  • Expected: the chain evaluates (prints the field), exactly as the !-unwrap and the non-array ?. forms already do.

Reproduction

Pure runtime, no #run — panics:

#import "modules/std.sx";
Arr3 :: struct { xs: [3]i64; }
mk :: () -> ?Arr3 { r : Arr3 = .{ xs = .[1,2,3] }; return r; }
main :: () { print("{}\n", mk()?.xs[0] ?? 99); }   // PANIC exit 134

Control A — same chain, child struct has NO array field — WORKS, prints 7:

#import "modules/std.sx";
Pt :: struct { x: i64; }
mk :: () -> ?Pt { return Pt.{ x = 7 }; }
main :: () { print("{}\n", mk()?.x ?? 99); }

Control B — same array-containing ?Arr3, but ! force-unwrap — WORKS, prints 1:

#import "modules/std.sx";
Arr3 :: struct { xs: [3]i64; }
mk :: () -> ?Arr3 { r : Arr3 = .{ xs = .[1,2,3] }; return r; }
main :: () { print("{}\n", mk()!.xs[0]); }

(The issue 0167 (E) repro A?.xs[0] hit this same bug — it used ? where ! was meant; with ! the comptime #run ?Arr3 case evaluates. So this is the residual defect that 0167's (E) repro tripped, distinct from 0167 (C)/(E), both of which are fixed.)

Investigation prompt

The ? optional-chaining / optional-test path synthesizes an accessor whose receiver (the unwrapped child) types as .unresolved specifically when the child is a struct containing an array field — mirroring the already-fixed issue-0101 !-unwrap bug (inferExprType had no force_unwrap arm → receiver typed .unresolved). The ! path was fixed (see examples/optionals/0905-optionals-unwrap-field-chain.sx); the ?/?. path has an analogous gap that only surfaces for an array-containing child (a plain-scalar/string child happens to resolve).

Suspected area: src/ir/lower.zig inferExprType (grep for the optional-chain / ? postfix / safe_nav handling) and/or src/ir/lower/ accessor-chain lowering — find where the ?-chain receiver type is computed and why an array-containing struct child yields .unresolved. Compare against the working !-unwrap arm (issue 0101 fix) and apply the same receiver-type flow.

Verification: the first repro above prints 1 and exits 0; controls A and B still pass; add a regression under examples/optionals/ covering ?.-chain on an array-containing ?S (field read + ?? default). Confirm examples/comptime/0644-comptime-run-array-aggregate.sx (issue 0167) still passes.

Provenance

Discovered while implementing issue 0167 (C: comptime reg→value array-in- aggregate bridge; E: clean-abort on comptime-init failure). 0167 (C) and (E) are FIXED and verified; the ?Arr3 access form in 0167's (E) repro tripped this SEPARATE, pre-existing runtime lowering bug (confirmed reproducible on clean HEAD with no #run).