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:
89
examples/optionals/0915-optional-chain-array-field-index.sx
Normal file
89
examples/optionals/0915-optional-chain-array-field-index.sx
Normal 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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
47
issues/0183-index-non-indexable-type-panics.md
Normal file
47
issues/0183-index-non-indexable-type-panics.md
Normal 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.
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user