fix(ir): vector lane store resolves lane element type [F0.5]

Writing a Vector lane (`v.x = …`, `.y/.z/.w` + colour aliases) panicked
with "unresolved type reached LLVM emission". The store path had no
vector branch: a `.field_access` target on a Vector fell through to
struct-field lookup, matched nothing, left `field_ty = .unresolved`, and
built a `ptrTo(.unresolved)` that tripped the LLVM emission guard. The
read path resolved the lane fine — the two had diverged (issue-0083
two-resolver class).

Extract a shared `Lowering.vectorLaneIndex` resolver and route BOTH paths
through it. The read path (`lowerFieldAccessOnType`) delegates to it,
dropping its silent `else 0` fallback. A new vector branch in
`lowerAssignment` GEPs a typed pointer to the lane (`structGepTyped`) and
stores via `storeOrCompound` (plain + compound). `emitStructGep` now
addresses a vector base type with a `[0, lane]` GEP. A non-lane field now
reports field-not-found on both paths instead of silent-lane-0 / panic.

Regression: examples/1506-vectors-lane-store.sx (panicked pre-fix, now
reads back written values) + a vectorLaneIndex unit test. Resolves issue
0086; spec documents element assignment.
This commit is contained in:
agra
2026-06-05 01:32:35 +03:00
parent 3f279dea6b
commit a7ee179577
9 changed files with 168 additions and 4 deletions

View File

@@ -1264,7 +1264,18 @@ pub const Ops = struct {
else
self.e.resolveGepStructType(fa.base, instruction);
const st_kind = c.LLVMGetTypeKind(struct_llvm_ty);
if (st_kind == c.LLVMStructTypeKind or st_kind == c.LLVMArrayTypeKind) {
if (st_kind == c.LLVMVectorTypeKind or st_kind == c.LLVMScalableVectorTypeKind) {
// Vector lane address: GEP [0, lane] into the in-memory vector,
// yielding a pointer to the lane element for a scalar store
// (vector lane assignment — issue 0086). Mirrors how the read
// path extracts a lane; here we address it for a store.
var indices = [_]c.LLVMValueRef{
c.LLVMConstInt(self.e.cached_i64, 0, 0),
c.LLVMConstInt(self.e.cached_i64, @intCast(fa.field_index), 0),
};
const result = c.LLVMBuildGEP2(self.e.builder, struct_llvm_ty, base_ptr, &indices, 2, "vgep");
self.e.mapRef(result);
} else if (st_kind == c.LLVMStructTypeKind or st_kind == c.LLVMArrayTypeKind) {
const result = c.LLVMBuildStructGEP2(self.e.builder, struct_llvm_ty, base_ptr, @intCast(fa.field_index), "gep");
self.e.mapRef(result);
} else {

View File

@@ -1053,3 +1053,23 @@ test "conversions: optionalOfFlattened wraps once, flattening a nested optional"
try std.testing.expect(module.types.optionalOf(opt_s64) != opt_s64);
}
test "lower: vectorLaneIndex maps swizzle components, colour aliases, rejects non-lanes" {
// Positional swizzle components → lanes 0..3.
try std.testing.expectEqual(@as(?u32, 0), Lowering.vectorLaneIndex("x"));
try std.testing.expectEqual(@as(?u32, 1), Lowering.vectorLaneIndex("y"));
try std.testing.expectEqual(@as(?u32, 2), Lowering.vectorLaneIndex("z"));
try std.testing.expectEqual(@as(?u32, 3), Lowering.vectorLaneIndex("w"));
// Colour aliases share the same lane indices.
try std.testing.expectEqual(@as(?u32, 0), Lowering.vectorLaneIndex("r"));
try std.testing.expectEqual(@as(?u32, 1), Lowering.vectorLaneIndex("g"));
try std.testing.expectEqual(@as(?u32, 2), Lowering.vectorLaneIndex("b"));
try std.testing.expectEqual(@as(?u32, 3), Lowering.vectorLaneIndex("a"));
// Any non-lane field is rejected (null) so the read and write paths share
// one rule — a non-lane store no longer falls through to an .unresolved
// pointee that panics at LLVM emission (issue 0086).
try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("q"));
try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("xy"));
try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("len"));
try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex(""));
}

View File

@@ -2354,6 +2354,30 @@ pub const Lowering = struct {
}
}
// Vector lane assignment: `v.x = val` (also .y/.z/.w and the
// colour aliases .r/.g/.b/.a). GEP a pointer to the lane and
// store with the vector element type — mirrors the read path
// (lowerFieldAccessOnType) and shares vectorLaneIndex so the
// two never diverge. Without this the field falls through to
// the struct-field lookup below, where no field matches,
// leaving field_ty = .unresolved and a pointer-to-.unresolved
// that panics at LLVM emission (issue 0086).
if (!obj_ty.isBuiltin()) {
const vinfo = self.module.types.get(obj_ty);
if (vinfo == .vector) {
const vidx = Lowering.vectorLaneIndex(fa.field) orelse {
_ = self.emitFieldError(obj_ty, fa.field, asgn.target.span);
return;
};
const elem_ty = vinfo.vector.element;
const gep = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty);
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, elem_ty);
self.storeOrCompound(gep, coerced, asgn.op, elem_ty);
return;
}
}
const struct_fields = self.getStructFields(obj_ty);
var field_idx: u32 = 0;
var field_ty: TypeId = .unresolved;
@@ -5100,6 +5124,19 @@ pub const Lowering = struct {
}
/// 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
/// other field name so the read path (`lowerFieldAccessOnType`) and the
/// write path (`lowerAssignment`) share one resolver and reject a
/// non-lane field identically (issue 0086).
pub fn vectorLaneIndex(field: []const u8) ?u32 {
if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0;
if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1;
if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2;
if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3;
return null;
}
fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
const field_name_id = self.module.types.internString(field);
@@ -5159,12 +5196,16 @@ pub const Lowering = struct {
}
}
// Vector field access: .x/.y/.z/.w → index 0/1/2/3
// Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) →
// lane 0/1/2/3. Shares lane-index resolution with the write path
// (lowerAssignment) via vectorLaneIndex; a non-lane field falls
// through to the field-not-found error below.
if (!obj_ty.isBuiltin()) {
const vinfo = self.module.types.get(obj_ty);
if (vinfo == .vector) {
const vidx: u32 = if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) 0 else if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) 1 else if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) 2 else if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) 3 else 0;
return self.builder.structGet(obj, vidx, vinfo.vector.element);
if (Lowering.vectorLaneIndex(field)) |vidx| {
return self.builder.structGet(obj, vidx, vinfo.vector.element);
}
}
}