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

@@ -0,0 +1,89 @@
// Optional-chain `?.` reaching an indexable field, then indexing it:
// `opt?.xs[i]`. The `?.` makes the whole expression optional — it short-
// circuits to null when the receiver is null, else unwraps and indexes —
// so `opt?.xs[i]` is `?ElemType`, coalesced/unwrapped at the use site.
//
// Regression (issue 0181): `opt?.xs[i]` typed its element through
// `getElementType(?[N]T)` as `.unresolved`, and an `index_get` on the
// optional value reached LLVM emission (panic: "unresolved type reached
// LLVM emission", exit 134). This is the `?.`-chain analogue of issue 0101
// (the `!`-unwrap form, fixed earlier).
//
// Also covers the `?*[N]T` shape — a `?.` field that is a POINTER to an
// array. Indexing auto-derefs (GEP the pointee array element through the
// unwrapped pointer), so `opt?.p[i]` is `?ElemType` too. `getElementType`
// has no pointer arm, so the dispatch guard / typer consult
// `ptrToArrayElem` first (else this branch was unreachable dead code and
// the `?*[N]T` case fell through to the same exit-134 panic).
#import "modules/std.sx";
Arr3 :: struct { xs: [3]i64; }
Pt :: struct { x: i64; y: i64; }
Holder :: struct { ps: [2]Pt; sl: []i64; }
PArr :: struct { p: *[3]i64; } // pointer-to-array field
PU8 :: struct { p: *[4]u8; } // sub-word element
g_i64 : [3]i64 = ---;
g_u8 : [4]u8 = ---;
mk_parr :: (present: bool) -> ?PArr {
if present { return PArr.{ p = @g_i64 }; }
return null;
}
mk_pu8 :: () -> ?PU8 { return PU8.{ p = @g_u8 }; }
mk_arr :: (present: bool) -> ?Arr3 {
if present {
r : Arr3 = ---;
r.xs[0] = 1; r.xs[1] = 2; r.xs[2] = 3;
return r;
}
return null;
}
mk_holder :: (data: []i64) -> ?Holder {
r : Holder = ---;
r.ps[0].x = 10; r.ps[0].y = 11;
r.ps[1].x = 20; r.ps[1].y = 21;
r.sl = data;
return r;
}
main :: () {
// Present receiver: unwraps + indexes.
print("present[0]: {}\n", mk_arr(true)?.xs[0] ?? 99); // 1
print("present[2]: {}\n", mk_arr(true)?.xs[2] ?? 99); // 3
// Null receiver: short-circuits to null, coalesced to the default.
print("null: {}\n", mk_arr(false)?.xs[0] ?? 99); // 99
// Result is `?ElemType` — bind it, then unwrap.
x := mk_arr(true)?.xs[1]; // ?i64
print("bound: {}\n", x ?? 0); // 2
print("unwrap: {}\n", mk_arr(true)?.xs[2]!); // 3
// Struct-element array `[2]Pt`: indexing yields `?Pt`.
p := mk_holder(.[])?.ps[1]; // ?Pt
print("struct.x: {}\n", p?.x ?? -1); // 20
print("struct.y: {}\n", p?.y ?? -1); // 21
// Slice field through the chain.
arr : [3]i64 = .[5, 6, 7];
print("slice[1]: {}\n", mk_holder(arr[..])?.sl[1] ?? -1); // 6
// Pointer-to-array field `?*[N]T`: indexing auto-derefs through the
// unwrapped pointer (GEP the pointee array element), multi-element.
g_i64[0] = 10; g_i64[1] = 20; g_i64[2] = 30;
print("ptr[0]: {}\n", mk_parr(true)?.p[0] ?? 99); // 10
print("ptr[1]: {}\n", mk_parr(true)?.p[1] ?? 99); // 20
print("ptr[2]: {}\n", mk_parr(true)?.p[2] ?? 99); // 30
// Null receiver short-circuits — no deref of the null pointer.
print("ptr null: {}\n", mk_parr(false)?.p[0] ?? 99); // 99
// Sub-word element `?*[4]u8` — adjacent lanes not clobbered.
g_u8[0] = 7; g_u8[1] = 8; g_u8[2] = 9; g_u8[3] = 200;
print("u8[0]: {}\n", mk_pu8()?.p[0] ?? 0); // 7
print("u8[3]: {}\n", mk_pu8()?.p[3] ?? 0); // 200
}

View File

@@ -0,0 +1,14 @@
present[0]: 1
present[2]: 3
null: 99
bound: 2
unwrap: 3
struct.x: 20
struct.y: 21
slice[1]: 6
ptr[0]: 10
ptr[1]: 20
ptr[2]: 30
ptr null: 99
u8[0]: 7
u8[3]: 200

View File

@@ -1,5 +1,20 @@
# 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

View File

@@ -0,0 +1,47 @@
# 0183 — indexing a non-indexable type (`*T`, `*[]T`, struct, …) panics instead of a clean diagnostic
## Symptom
`expr[i]` where `expr`'s type is not array / slice / many-pointer / string —
e.g. a single-element pointer `*T`, a pointer-to-slice `*[]T`, or a struct — does
NOT emit a type error. It falls through `lowerIndexExpr` to an `index_get` with an
`.unresolved` element type and reaches codegen, panicking `unresolved type
reached LLVM emission` (exit 134). Pure runtime, no optionals/comptime. (`[*]T`
many-pointers and `[N]T`/`[]T`/`string` ARE indexable and unaffected.)
Found during adversarial review of issue 0181 (the optional-chain index fix); the
same fall-through underlies the `?*[]T`/`?*T`/`?struct` chain-index panics, but it
reproduces identically WITHOUT optional chaining, so it is a separate, broader
gap in the index lowering.
## Reproduction
```sx
#import "modules/std.sx";
main :: () {
x := 5;
p : *i64 = @x;
print("{}\n", p[0]); // panic: unresolved type reached LLVM emission, exit 134
}
```
Also panics: indexing a `*[]i64` (pointer-to-slice), indexing a plain struct
value. Expected: a located diagnostic, e.g. `error: cannot index a value of type
'*i64' (use a many-pointer '[*]T', or dereference first)`, exit 1.
## Investigation prompt
`src/ir/lower/expr.zig` `lowerIndexExpr`: after the array / slice / many-pointer /
string / optional-chain dispatch arms, the fall-through emits `index_get` with
`getElementType(obj_ty)` even when that is `.unresolved`. Add a final guard: if
the object type is not indexable (element type resolves to `.unresolved` and the
type isn't a recognized indexable shape), emit
`self.diagnostics.addFmt(.err, span, "cannot index a value of type '{s}'", .{...})`
and return a placeholder — never emit an `index_get` with an unresolved element
type. Mirror the located-diagnostic + placeholder pattern used elsewhere in the
lowering. The static typer (`src/ir/expr_typer.zig` `index_expr`) should likewise
yield `.unresolved` (already does) so this is the single choke point. Follow the
no-silent-fallback rule (here it's a loud PANIC, which must become a clean
diagnostic). Verify: the repro exits 1 with the diagnostic; `[*]T`/`[]T`/`[N]T`/
`string`/optional-chain indexing all still work. Add an
`examples/diagnostics/12xx-index-non-indexable.sx` negative regression.

View File

@@ -393,6 +393,19 @@ pub const ExprTyper = struct {
return self.l.inferExprType(arg_node);
}
const obj_ty = self.l.inferExprType(ie.object);
// Optional-chain index `opt?.xs[i]`: the object types as an
// optional container (`?[N]T` / `?[]T` / `?[*]T`), so the whole
// index expression is `?ElemType` (flattened if the element is
// itself optional) — mirrors lowerOptionalChainIndex (issue 0181).
if (!obj_ty.isBuiltin() and self.l.module.types.get(obj_ty) == .optional) {
const child = self.l.module.types.get(obj_ty).optional.child;
// `?*[N]T` is indexable: element is the pointee array's
// element. `getElementType` has no pointer arm, so consult
// `ptrToArrayElem` first (mirrors lowerIndexExpr's guard) —
// otherwise `?*[N]T` typed as `.unresolved` (issue 0181).
const elem = self.l.ptrToArrayElem(child) orelse self.l.getElementType(child);
if (elem != .unresolved) return self.l.optionalOfFlattened(elem);
}
if (self.l.ptrToArrayElem(obj_ty)) |elem| return elem;
return self.l.getElementType(obj_ty);
},

View File

@@ -1982,6 +1982,7 @@ pub const Lowering = struct {
pub const lowerNumericLimit = lower_expr.lowerNumericLimit;
pub const lowerStructConstant = lower_expr.lowerStructConstant;
pub const lowerOptionalChain = lower_expr.lowerOptionalChain;
pub const lowerOptionalChainIndex = lower_expr.lowerOptionalChainIndex;
pub const vectorLaneIndex = lower_expr.vectorLaneIndex;
pub const lowerFieldAccessOnType = lower_expr.lowerFieldAccessOnType;
pub const lowerEnumLiteral = lower_expr.lowerEnumLiteral;

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