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).
This commit is contained in:
agra
2026-06-23 12:29:29 +03:00
parent fa7c07faf8
commit 4ca466fa96
9 changed files with 242 additions and 0 deletions

View File

@@ -1021,6 +1021,49 @@ pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess,
return self.builder.blockParam(merge_bb, 0, result_ty);
}
/// Lower an indexed optional-chain access: `opt?.xs[i]` where the `?.` field is
/// an array / slice / many-pointer. Mirrors `lowerOptionalChain`'s short-circuit
/// — the index applies in the some-branch, producing `?ElemType` (null when the
/// receiver was null). `child` is the unwrapped container type, `elem_ty` the
/// indexed element type.
pub fn lowerOptionalChainIndex(self: *Lowering, ie: *const ast.IndexExpr, child: TypeId, elem_ty: TypeId) Ref {
// The chained `?.` field access produced the optional value; lower it.
const opt_val = self.lowerExpr(ie.object);
// If the element is itself optional, indexing flattens (no double-wrap),
// matching the field-chain `?.` flattening rule.
const elem_is_optional = !elem_ty.isBuiltin() and self.module.types.get(elem_ty) == .optional;
const result_ty = if (elem_is_optional) elem_ty else self.module.types.optionalOf(elem_ty);
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
const some_bb = self.freshBlock("chain.some");
const none_bb = self.freshBlock("chain.none");
const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty});
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
// Some: unwrap the container, index it. A `?*[N]T` unwraps to the pointer
// (GEP through it); a value container (`?[N]T` / `?[]T`) unwraps to the
// aggregate value and `index_get`s the element.
self.builder.switchToBlock(some_bb);
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, child);
const idx = self.lowerExpr(ie.index);
const elem_val = if (self.ptrToArrayElem(child)) |pelem| blk: {
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = unwrapped, .rhs = idx } }, self.module.types.ptrTo(pelem));
break :blk self.builder.load(gep, pelem);
} else self.builder.emit(.{ .index_get = .{ .lhs = unwrapped, .rhs = idx } }, elem_ty);
const some_result = if (elem_is_optional) elem_val else self.builder.emit(.{ .optional_wrap = .{ .operand = elem_val } }, result_ty);
self.builder.br(merge_bb, &.{some_result});
// None: null optional.
self.builder.switchToBlock(none_bb);
const none_result = self.builder.constNull(result_ty);
self.builder.br(merge_bb, &.{none_result});
self.builder.switchToBlock(merge_bb);
return self.builder.blockParam(merge_bb, 0, result_ty);
}
/// Field access on a known type (shared by regular field access and optional chaining)
/// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour
/// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any
@@ -1755,6 +1798,24 @@ pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref {
}
// Infer element type from the object's slice/array type
const obj_ty = self.inferExprType(ie.object);
// Optional-chain index: `opt?.xs[i]`. The `?.` makes the object an
// optional whose child is the (array/slice/many-ptr) field — so the index
// applies inside the chain's some-branch and the whole expression is
// `?ElemType` (null if the receiver was null). Without this the element
// type resolved through `getElementType(?[N]T)` was `.unresolved` and an
// `index_get` on the optional value reached LLVM emission (issue 0181).
if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .optional) {
const child = self.module.types.get(obj_ty).optional.child;
// A pointer-to-array child (`?*[N]T`) is indexable too: its element is
// the pointee array's element. `getElementType` has no pointer arm, so
// ask `ptrToArrayElem` first (mirrors the non-optional `*[N]T` path
// below) — otherwise the `?*[N]T` case fell through to a plain
// `index_get` with an `.unresolved` element type (issue 0181).
const elem_ty = self.ptrToArrayElem(child) orelse self.getElementType(child);
if (elem_ty != .unresolved) {
return self.lowerOptionalChainIndex(ie, child, elem_ty);
}
}
// Array with addressable storage: GEP the element in place + load,
// never `index_get` on the loaded array VALUE — that realizes as
// copy-whole-array-to-temp per read (the general-expression sibling