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:
46
examples/1506-vectors-lane-store.sx
Normal file
46
examples/1506-vectors-lane-store.sx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Writing a `Vector` lane — `v.x = …` (and `.y`/`.z`/`.w`, plus the colour
|
||||
// aliases `.r`/`.g`/`.b`/`.a`) — stores through a pointer to that lane and
|
||||
// reads back the written value. Covers a `.[…]`-init write, a `= ---`
|
||||
// (undefined) init write, writes to every lane of a 4-lane vector, the colour
|
||||
// aliases, and a compound lane assignment (`+= / *=`).
|
||||
//
|
||||
// Regression (issue 0086): the lane STORE path fell through to struct-field
|
||||
// lookup, where no field matched a vector lane, leaving the lane pointer's
|
||||
// pointee as the `.unresolved` sentinel — which panicked at LLVM emission
|
||||
// ("unresolved type reached LLVM emission"). The store path now resolves the
|
||||
// lane index via the same shared `vectorLaneIndex` resolver the read path uses
|
||||
// and GEPs a typed pointer to the lane, so the two paths never diverge.
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
// `.[…]`-init, then write each lane of a 4-lane vector.
|
||||
v : Vector(4, f32) = .[0.0, 0.0, 0.0, 0.0];
|
||||
v.x = 1.0;
|
||||
v.y = 2.0;
|
||||
v.z = 3.0;
|
||||
v.w = 4.0;
|
||||
print("x={}\n", v.x);
|
||||
print("y={}\n", v.y);
|
||||
print("z={}\n", v.z);
|
||||
print("w={}\n", v.w);
|
||||
|
||||
// `= ---` (undefined) init, then write every lane.
|
||||
u : Vector(4, f32) = ---;
|
||||
u.x = 10.0;
|
||||
u.y = 20.0;
|
||||
u.z = 30.0;
|
||||
u.w = 40.0;
|
||||
print("u={} {} {} {}\n", u.x, u.y, u.z, u.w);
|
||||
|
||||
// Colour aliases on a 3-lane vector.
|
||||
col : Vector(3, f32) = .[0.0, 0.0, 0.0];
|
||||
col.r = 0.5;
|
||||
col.g = 0.25;
|
||||
col.b = 0.125;
|
||||
print("col={} {} {}\n", col.r, col.g, col.b);
|
||||
|
||||
// Compound lane assignment.
|
||||
col.r += 0.5;
|
||||
col.g *= 4.0;
|
||||
print("col2={} {}\n", col.r, col.g);
|
||||
}
|
||||
1
examples/expected/1506-vectors-lane-store.exit
Normal file
1
examples/expected/1506-vectors-lane-store.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/1506-vectors-lane-store.stderr
Normal file
1
examples/expected/1506-vectors-lane-store.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
7
examples/expected/1506-vectors-lane-store.stdout
Normal file
7
examples/expected/1506-vectors-lane-store.stdout
Normal file
@@ -0,0 +1,7 @@
|
||||
x=1.000000
|
||||
y=2.000000
|
||||
z=3.000000
|
||||
w=4.000000
|
||||
u=10.000000 20.000000 30.000000 40.000000
|
||||
col=0.500000 0.250000 0.125000
|
||||
col2=1.000000 1.000000
|
||||
@@ -1,5 +1,35 @@
|
||||
# 0086 — writing to a Vector lane (`v.x = …`) panics with "unresolved type reached LLVM emission"
|
||||
|
||||
> **RESOLVED** (F0.5).
|
||||
>
|
||||
> **Root cause.** The vector-lane STORE path had no vector branch. In
|
||||
> `Lowering.lowerAssignment` (`src/ir/lower.zig`) a `.field_access` target on a
|
||||
> `Vector` fell through to the struct-field lookup, where no field matched a
|
||||
> lane name, so `field_ty` stayed `.unresolved`. The store then built a
|
||||
> `ptrTo(.unresolved)` whose pointee reached LLVM in `emitStore`
|
||||
> (`src/backend/llvm/ops.zig`) → `toLLVMTypeInfo` `.unresolved` tripwire panic.
|
||||
> The READ path resolved the lane fine; the two paths had diverged (issue-0083
|
||||
> two-resolver class).
|
||||
>
|
||||
> **Fix (per file).**
|
||||
> - `src/ir/lower.zig` — extracted a shared `Lowering.vectorLaneIndex(field)`
|
||||
> resolver (`.x/.y/.z/.w` + colour aliases `.r/.g/.b/.a` → lane 0..3, `null`
|
||||
> otherwise). The READ path (`lowerFieldAccessOnType`) now delegates to it
|
||||
> (dropping its silent `else 0` fallback), and a new vector branch in
|
||||
> `lowerAssignment` uses the SAME resolver to `structGepTyped` a typed pointer
|
||||
> to the lane and `storeOrCompound` with the vector element type (plain and
|
||||
> compound assignment). A non-lane field now reports a field-not-found error on
|
||||
> both paths instead of silently reading lane 0 / panicking.
|
||||
> - `src/backend/llvm/ops.zig` — `emitStructGep` now addresses a vector
|
||||
> `base_type` with a `[0, lane]` `GEP2`, yielding a pointer to the lane element
|
||||
> for the scalar store.
|
||||
>
|
||||
> **Regression test.** `examples/1506-vectors-lane-store.sx` — `.[…]`-init and
|
||||
> `= ---`-init writes, every lane of a 4-lane vector, colour aliases, and a
|
||||
> compound lane assignment, reading each value back. Unit test
|
||||
> `src/ir/lower.test.zig` pins the `vectorLaneIndex` contract.
|
||||
|
||||
|
||||
## Symptom
|
||||
Assigning to a component of a `Vector` local — `v.x = 1.0` (also `.y` / `.z` /
|
||||
`.w`) — aborts the compiler with the internal panic:
|
||||
|
||||
7
specs.md
7
specs.md
@@ -1170,6 +1170,13 @@ v.x // first element
|
||||
v.z // third element
|
||||
```
|
||||
|
||||
**Element assignment**: the same lane names are assignable l-values; plain and
|
||||
compound assignment write a single component in place.
|
||||
```sx
|
||||
v.x = 1.0; // write the first lane
|
||||
v.y += 2.0; // compound assignment to a lane
|
||||
```
|
||||
|
||||
**Index access**: `v[i]` extracts by index.
|
||||
```sx
|
||||
v[0] // first element
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user