fix: coerce array/vector literal elements to element type (issue 0168)

[N]?T arrays were corrupted: a positional literal .{ null, 7 } stored
bare T/null elements into {T,i1} optional slots because array elements
were never coerced (getStructFields is empty for an array, so the
i<struct_fields.len field-coercion gate never fired). A present element
then read back as absent and direct indexing segfaulted.

lowerStructLiteral's positional branch now computes array_elem_ty for
array/vector targets and coerces each element to it; lowerArrayLiteral
generalizes its slice-only coercion to coerce every element via
coerceToType (layout-aware: scalar->{T,i1}, pointer-sentinel->one-word,
array->slice, concrete->protocol). Verified by 3 adversarial reviews,
suite 780/0.

Regression: examples/optionals/0913-optionals-array-of-optionals.sx.
Filed adjacent pre-existing bugs: 0173 (typed .[null,..] element), 0174
(tuple positional-element coercion), 0175 (positional struct literal
variable element zeroed).
This commit is contained in:
agra
2026-06-22 22:50:20 +03:00
parent 2ea25e84ec
commit 5a436eddb1
9 changed files with 253 additions and 17 deletions

View File

@@ -207,16 +207,39 @@ pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: a
return result;
}
// Positional literal: use source order
// Positional literal: use source order.
//
// For an ARRAY / VECTOR target the literal `.{ a, b, ... }` has no named
// fields — `getStructFields` returns empty for these, so the per-field
// coercion below (`i < struct_fields.len`) never fires. Each positional
// element must still be coerced to the homogeneous element type, or a
// scalar element flowing into an aggregate element slot stores the wrong
// shape. Concretely `[N]?T` would store a bare `T`/`null` into a `{T,i1}`
// slot — corrupting the array (a present element reads back as absent;
// indexing it segfaults). Issue 0168. `coerceToType` is a no-op when the
// element already matches (the common `[N]i64`/`[N]Struct` case).
const array_elem_ty: TypeId = if (!ty.isBuiltin()) switch (self.module.types.get(ty)) {
.array, .vector => self.getElementType(ty),
else => .unresolved,
} else .unresolved;
var fields = std.ArrayList(Ref).empty;
defer fields.deinit(self.alloc);
for (sl.field_inits, 0..) |fi, i| {
const saved_tt = self.target_type;
if (array_elem_ty != .unresolved) self.target_type = array_elem_ty;
var val = self.lowerExpr(fi.value);
self.target_type = saved_tt;
// Coerce field value to match struct field type
if (i < struct_fields.len) {
const src_ty = self.inferExprType(fi.value);
val = self.coerceToType(val, src_ty, struct_fields[i].ty);
} else if (array_elem_ty != .unresolved) {
const src_ty = self.builder.getRefType(val);
if (src_ty != array_elem_ty) {
val = self.coerceToType(val, src_ty, array_elem_ty);
}
}
fields.append(self.alloc, val) catch unreachable;
}
@@ -1529,22 +1552,23 @@ pub fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref {
self.target_type = elem_ty;
var val = self.lowerExpr(elem);
self.target_type = old_tt;
// A nested `.[...]` element at a slice element type lowers to an
// aggregate array `[N]U` (lowerArrayLiteral always yields an array
// value); materialize it into a `[]U` slice so the element is a real
// {ptr,len} header rather than a raw array the callee would read its
// header off of. This per-element coercion recurses with
// the literal nesting, so `[][]T` and deeper coerce at every level.
if (!elem_ty.isBuiltin()) {
const ei = self.module.types.get(elem_ty);
if (ei == .slice) {
const val_ty = self.builder.getRefType(val);
if (!val_ty.isBuiltin()) {
const vi = self.module.types.get(val_ty);
if (vi == .array and vi.array.element == ei.slice.element) {
val = self.coerceToType(val, val_ty, elem_ty);
}
}
// Coerce each element to the declared element type. Setting
// `target_type` above steers literal lowering, but the actual
// wrap/erase (scalar → optional `{T,i1}`, array → slice header,
// concrete → protocol, etc.) lives in `coerceToType`. Without this,
// an `[N]?T` literal stores bare `T`/`null` elements into a slot whose
// stride is the optional's `{T,i1}` size — corrupting the aggregate
// (a present element reads back as absent; indexing segfaults).
// Issue 0168.
//
// `coerceToType` classifies a same-type element as `.no_op`/`.none`
// and returns `val` unchanged, so this is a no-op for elements already
// at `elem_ty` (the common `[N]i64`/`[N]Struct` case). The earlier
// slice special-case is now subsumed by this general coercion.
if (elem_ty != .unresolved) {
const val_ty = self.builder.getRefType(val);
if (val_ty != elem_ty) {
val = self.coerceToType(val, val_ty, elem_ty);
}
}
elems.append(self.alloc, val) catch unreachable;