diff --git a/examples/optionals/0915-optional-chain-array-field-index.sx b/examples/optionals/0915-optional-chain-array-field-index.sx new file mode 100644 index 00000000..6e8bcbb1 --- /dev/null +++ b/examples/optionals/0915-optional-chain-array-field-index.sx @@ -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 +} diff --git a/examples/optionals/expected/0915-optional-chain-array-field-index.exit b/examples/optionals/expected/0915-optional-chain-array-field-index.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/optionals/expected/0915-optional-chain-array-field-index.exit @@ -0,0 +1 @@ +0 diff --git a/examples/optionals/expected/0915-optional-chain-array-field-index.stderr b/examples/optionals/expected/0915-optional-chain-array-field-index.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/optionals/expected/0915-optional-chain-array-field-index.stderr @@ -0,0 +1 @@ + diff --git a/examples/optionals/expected/0915-optional-chain-array-field-index.stdout b/examples/optionals/expected/0915-optional-chain-array-field-index.stdout new file mode 100644 index 00000000..8cdf29a5 --- /dev/null +++ b/examples/optionals/expected/0915-optional-chain-array-field-index.stdout @@ -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 diff --git a/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md b/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md index 6555f716..04b411cf 100644 --- a/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md +++ b/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md @@ -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 diff --git a/issues/0183-index-non-indexable-type-panics.md b/issues/0183-index-non-indexable-type-panics.md new file mode 100644 index 00000000..48f63c68 --- /dev/null +++ b/issues/0183-index-non-indexable-type-panics.md @@ -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. diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 542421e0..567646c5 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -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); }, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0773f08c..2a3a9b46 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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; diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 3d194345..26719ca8 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -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