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`
|
# 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
|
## Symptom
|
||||||
|
|
||||||
A `?.` optional-chain access (or the `?` optional-test postfix used in a
|
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);
|
return self.l.inferExprType(arg_node);
|
||||||
}
|
}
|
||||||
const obj_ty = self.l.inferExprType(ie.object);
|
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;
|
if (self.l.ptrToArrayElem(obj_ty)) |elem| return elem;
|
||||||
return self.l.getElementType(obj_ty);
|
return self.l.getElementType(obj_ty);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1982,6 +1982,7 @@ pub const Lowering = struct {
|
|||||||
pub const lowerNumericLimit = lower_expr.lowerNumericLimit;
|
pub const lowerNumericLimit = lower_expr.lowerNumericLimit;
|
||||||
pub const lowerStructConstant = lower_expr.lowerStructConstant;
|
pub const lowerStructConstant = lower_expr.lowerStructConstant;
|
||||||
pub const lowerOptionalChain = lower_expr.lowerOptionalChain;
|
pub const lowerOptionalChain = lower_expr.lowerOptionalChain;
|
||||||
|
pub const lowerOptionalChainIndex = lower_expr.lowerOptionalChainIndex;
|
||||||
pub const vectorLaneIndex = lower_expr.vectorLaneIndex;
|
pub const vectorLaneIndex = lower_expr.vectorLaneIndex;
|
||||||
pub const lowerFieldAccessOnType = lower_expr.lowerFieldAccessOnType;
|
pub const lowerFieldAccessOnType = lower_expr.lowerFieldAccessOnType;
|
||||||
pub const lowerEnumLiteral = lower_expr.lowerEnumLiteral;
|
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);
|
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)
|
/// 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
|
/// 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
|
/// 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
|
// Infer element type from the object's slice/array type
|
||||||
const obj_ty = self.inferExprType(ie.object);
|
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,
|
// Array with addressable storage: GEP the element in place + load,
|
||||||
// never `index_get` on the loaded array VALUE — that realizes as
|
// never `index_get` on the loaded array VALUE — that realizes as
|
||||||
// copy-whole-array-to-temp per read (the general-expression sibling
|
// copy-whole-array-to-temp per read (the general-expression sibling
|
||||||
|
|||||||
Reference in New Issue
Block a user