Files
Brian Ferri 25cafe0b34 spirv: fix vector casts, packed field pointers and debug names of comptime-known values (#37040)
I'm writing a small numeric library whose kernels run on the CPU, on CUDA and on Vulkan through the self-hosted SPIR-V backend, and I was making them generic over the float type. `f64` needs its own `exp` and `log` on the GPU, so I wrote them as polynomial series, and the `f64` Vulkan module stopped validating. This fixes the three backend bugs underneath that, and a crash I ran into after merging master.

The series take floats apart with `@bitCast` on vectors. Between vectors of the same length this reaches the Vulkan/OpenGL fallback in `bitCast`, which stores the source into a function variable of the destination type:

```zig
export fn main() callconv(.{ .spirv_kernel = .{ .x = 1, .y = 1, .z = 1 } }) void {
    var x: @Vector(2, f32) = .{ 1, 2 };
    _ = &x;
    const bits: @Vector(2, i32) = @bitCast(x);
    _ = bits;
}
```

```
$ zig build-exe -fno-llvm -target spirv64-vulkan -mcpu vulkan_v1_2 bitcast.zig -femit-bin=bitcast.spv
$ spirv-val --target-env vulkan1.2 bitcast.spv
error: line 45: OpStore Pointer <id> '36[%36]'s type does not match Object <id> '29[%29]'s type.
```

An elementwise vector bitcast is now a scalar `bitCast` per lane, the way `buildConvert` and friends already unroll vectors. Going through the scalar path also normalizes lanes of strange integers: a single `OpBitcast` on the whole vector validates for `@Vector(n, u3)` to `@Vector(n, i3)`, but leaves lanes that don't compare equal to the negative values they should hold.

The series also convert between integer and float lanes. A vector of one lane, or of more than four without `vector16`, is an array in SPIR-V, and `airFloatFromInt` and `airIntFromFloat` emitted a single `OpConvert*` with that array as its result type:

```zig
export fn main() callconv(.{ .spirv_kernel = .{ .x = 1, .y = 1, .z = 1 } }) void {
    var x: @Vector(1, i32) = .{1};
    _ = &x;
    const y: @Vector(1, f32) = @floatFromInt(x);
    _ = y;
}
```

```
$ spirv-val --target-env vulkan1.2 convert.spv
error: line 40: Expected float scalar or vector type as Result Type: ConvertSToF
  %y = OpConvertSToF %_arr_f32_u32_1 %26
```

`@floatCast` and `@intCast` already go through `buildConvert`, which unrolls the vector; these two now do as well, with `buildConvert` picking the int/float conversion opcodes.

The coefficients sit in a comptime array that the series take as a comptime slice, and that alone fails to compile, although nothing reads the slice at runtime:

```zig
const weights = [_]f32{ 1, 2, 3 };

inline fn first(comptime w: []const f32) f32 {
    return w[0];
}

export fn main() callconv(.{ .spirv_kernel = .{ .x = 1, .y = 1, .z = 1 } }) void {
    var x: f32 = first(&weights);
    _ = &x;
}
```

```
$ zig build-exe -fno-llvm -target spirv64-vulkan -mcpu vulkan_v1_2 dbgvar.zig -femit-bin=dbgvar.spv
dbgvar.zig:7:8: error: TODO (SPIR-V): pointer to constant 'dbgvar.weights'
```

It appears `airDbgVar` lowers the operand only to attach an `OpName` to it, and a pointer to a non-extern constant is still a TODO. A comptime-known value has no runtime variable of its own to name, so `airDbgVar` now leaves interned operands alone. The same thing held back 58 of the behavior tests skipped on SPIR-V, and the test commit un-skips them.

After merging master, four of those tests crashed the compiler in `structFieldPtr`, which has `unreachable` for packed containers since ebb8683ef1 ([#36996](https://codeberg.org/ziglang/zig/pulls/36996)):

```zig
const Flags = packed struct { a: bool, b: bool };

export fn main() callconv(.{ .spirv_kernel = .{ .x = 1, .y = 1, .z = 1 } }) void {
    var flags: Flags = .{ .a = true, .b = false };
    const b = &flags.b;
    b.* = true;
}
```

```
thread 3961624 panic: reached unreachable code
src/codegen/spirv/CodeGen.zig:6906:27: 0x4767157 in structFieldPtr (main.zig)
            .@"packed" => unreachable,
```

My reading is that the `unreachable` holds for the pointer derivations it was written for, since `constantPtr` now turns packed pointers into pointers to their host integer first, but `airStructFieldPtr` and `airStructFieldPtrIndex` still pass packed structs through. A pointer to a packed field is a bit-pointer into the struct's own host integer, so it is the struct pointer: that is what the old code computed, and what the C backend now does with `.begin`. Packed unions get back the TODO error they had before. I may be missing a plan for packed field pointers here, and I'm happy to change it if so. The commit also un-skips three packed tests that crash on master the same way. I suppose nothing caught this because those tests are skipped on SPIR-V and the `spirv64-vulkan` behavior target is commented out in `test/tests.zig`.

I ran the behavior tests through `tools/unskip_spirv_tests.zig` and [zig-spirv-test-executor](https://github.com/Snektron/zig-spirv-test-executor) on a Radeon 680M, and the new and un-skipped tests on an RTX 4060 as well. Every fix has tests that fail on master and pass with it; the vector ones compare lane by lane, since `expectEqual` on vectors doesn't compile for SPIR-V yet. Running each behavior file whole, nothing that passes on master fails. Five more skips already pass on master without these changes, so I left them out, but I can add them. The new vector tests copy the aarch64, arm, sparc64 and riscv64 skips of the tests around them, which I couldn't check.

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/37040
Reviewed-by: Ali Cheraghi <alichraghi@noreply.codeberg.org>
2026-10-04 11:50:49 +02:00

565 lines
15 KiB
Zig

const builtin = @import("builtin");
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const mem = std.mem;
test "continue in for loop" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const array = [_]i32{ 1, 2, 3, 4, 5 };
var sum: i32 = 0;
for (array) |x| {
sum += x;
if (x < 3) {
continue;
}
break;
}
if (sum != 6) unreachable;
}
test "break from outer for loop" {
try testBreakOuter();
try comptime testBreakOuter();
}
fn testBreakOuter() !void {
const array = "aoeu";
var count: usize = 0;
outer: for (array) |_| {
for (array) |_| {
count += 1;
break :outer;
}
}
try expect(count == 1);
}
test "continue outer for loop" {
try testContinueOuter();
try comptime testContinueOuter();
}
fn testContinueOuter() !void {
const array = "aoeu";
var counter: usize = 0;
outer: for (array) |_| {
for (array) |_| {
counter += 1;
continue :outer;
}
}
try expect(counter == array.len);
}
test "ignore lval with underscore (for loop)" {
for ([_]void{}, 0..) |_, i| {
_ = i;
for ([_]void{}, 0..) |_, j| {
_ = j;
break;
}
break;
}
}
test "basic for loop" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const expected_result: [24]u8 = .{
9, 8, 7, 6, 0, 1, 2, 3,
9, 8, 7, 6, 0, 1, 2, 3,
9, 8, 7, 6, 0, 1, 2, 3,
};
var buffer: [expected_result.len]u8 = undefined;
var buf_index: usize = 0;
const array = [_]u8{ 9, 8, 7, 6 };
for (array) |item| {
buffer[buf_index] = item;
buf_index += 1;
}
for (array, 0..) |item, index| {
_ = item;
buffer[buf_index] = @as(u8, @intCast(index));
buf_index += 1;
}
const array_ptr = &array;
for (array_ptr) |item| {
buffer[buf_index] = item;
buf_index += 1;
}
for (array_ptr, 0..) |item, index| {
_ = item;
buffer[buf_index] = @as(u8, @intCast(index));
buf_index += 1;
}
const unknown_size: []const u8 = &array;
for (unknown_size) |item| {
buffer[buf_index] = item;
buf_index += 1;
}
for (unknown_size, 0..) |_, index| {
buffer[buf_index] = @as(u8, @intCast(index));
buf_index += 1;
}
try expect(mem.eql(u8, buffer[0..buf_index], &expected_result));
}
test "for with null and T peer types and inferred result location type" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const S = struct {
fn doTheTest(slice: []const u8) !void {
if (for (slice) |item| {
if (item == 10) {
break item;
}
} else null) |v| {
_ = v;
@panic("fail");
}
}
};
try S.doTheTest(&[_]u8{ 1, 2 });
try comptime S.doTheTest(&[_]u8{ 1, 2 });
}
test "2 break statements and an else" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn entry(t: bool, f: bool) !void {
var buf: [10]u8 = undefined;
var ok = false;
ok = for (&buf) |*item| {
_ = item;
if (f) break false;
if (t) break true;
} else false;
try expect(ok);
}
};
try S.entry(true, false);
try comptime S.entry(true, false);
}
test "for loop with pointer elem var" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const source = "abcdefg";
var target: [source.len]u8 = undefined;
@memcpy(target[0..], source);
mangleString(target[0..]);
try expect(mem.eql(u8, &target, "bcdefgh"));
for (source, 0..) |*c, i| {
_ = i;
try expect(@TypeOf(c) == *const u8);
}
for (&target, 0..) |*c, i| {
_ = i;
try expect(@TypeOf(c) == *u8);
}
}
fn mangleString(s: []u8) void {
for (s) |*c| {
c.* += 1;
}
}
test "for copies its payload" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest() !void {
var x = [_]usize{ 1, 2, 3 };
for (x, 0..) |value, i| {
// Modify the original array
x[i] += 99;
try expect(value == i + 1);
}
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "for on slice with allowzero ptr" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const S = struct {
fn doTheTest(slice: []const u8) !void {
const ptr = @as([*]allowzero const u8, @ptrCast(slice.ptr))[0..slice.len];
for (ptr, 0..) |x, i| try expect(x == i + 1);
for (ptr, 0..) |*x, i| try expect(x.* == i + 1);
}
};
try S.doTheTest(&[_]u8{ 1, 2, 3, 4 });
try comptime S.doTheTest(&[_]u8{ 1, 2, 3, 4 });
}
test "else continue outer for" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var i: usize = 6;
var buf: [5]u8 = undefined;
while (true) {
i -= 1;
for (buf[i..5]) |_| {
return;
} else continue;
}
}
test "for loop with else branch" {
{
var x = [_]u32{ 1, 2 };
_ = &x;
const q = for (x) |y| {
if ((y & 1) != 0) continue;
break y * 2;
} else @as(u32, 1);
try expect(q == 4);
}
{
var x = [_]u32{ 1, 2 };
_ = &x;
const q = for (x) |y| {
if ((y & 1) != 0) continue;
break y * 2;
} else @panic("");
try expect(q == 4);
}
}
test "count over fixed range" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
var sum: usize = 0;
for (0..6) |i| {
sum += i;
}
try expect(sum == 15);
}
test "two counters" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
var sum: usize = 0;
for (0..10, 10..20) |i, j| {
sum += 1;
try expect(i + 10 == j);
}
try expect(sum == 10);
}
test "1-based counter and ptr to array" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
var ok: usize = 0;
for (1..6, "hello") |i, b| {
if (i == 1) {
try expect(b == 'h');
ok += 1;
}
if (i == 2) {
try expect(b == 'e');
ok += 1;
}
if (i == 3) {
try expect(b == 'l');
ok += 1;
}
if (i == 4) {
try expect(b == 'l');
ok += 1;
}
if (i == 5) {
try expect(b == 'o');
ok += 1;
}
}
try expect(ok == 5);
}
test "slice and two counters, one is offset and one is runtime" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const slice: []const u8 = "blah";
var start: usize = 0;
_ = &start;
for (slice, start..4, 1..5) |a, b, c| {
if (a == 'b') {
try expect(b == 0);
try expect(c == 1);
}
if (a == 'l') {
try expect(b == 1);
try expect(c == 2);
}
if (a == 'a') {
try expect(b == 2);
try expect(c == 3);
}
if (a == 'h') {
try expect(b == 3);
try expect(c == 4);
}
}
}
test "two slices, one captured by-ref" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var buf: [10]u8 = undefined;
const slice1: []const u8 = "blah";
const slice2: []u8 = buf[0..4];
for (slice1, slice2) |a, *b| {
b.* = a;
}
try expect(slice2[0] == 'b');
try expect(slice2[1] == 'l');
try expect(slice2[2] == 'a');
try expect(slice2[3] == 'h');
}
test "raw pointer and slice" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var buf: [10]u8 = undefined;
const slice: []const u8 = "blah";
const ptr: [*]u8 = buf[0..4];
for (ptr, slice) |*a, b| {
a.* = b;
}
try expect(buf[0] == 'b');
try expect(buf[1] == 'l');
try expect(buf[2] == 'a');
try expect(buf[3] == 'h');
}
test "raw pointer and counter" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var buf: [10]u8 = undefined;
const ptr: [*]u8 = &buf;
for (ptr, 0..4) |*a, b| {
a.* = @as(u8, @intCast('A' + b));
}
try expect(buf[0] == 'A');
try expect(buf[1] == 'B');
try expect(buf[2] == 'C');
try expect(buf[3] == 'D');
}
test "inline for with slice as the comptime-known" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const comptime_slice = "hello";
var runtime_i: usize = 3;
_ = &runtime_i;
const S = struct {
var ok: usize = 0;
fn check(comptime a: u8, b: usize) !void {
if (a == 'l') {
try expect(b == 3);
ok += 1;
} else if (a == 'o') {
try expect(b == 4);
ok += 1;
} else {
@compileError("fail");
}
}
};
inline for (comptime_slice[3..5], runtime_i..5) |a, b| {
try S.check(a, b);
}
try expect(S.ok == 2);
}
test "inline for with counter as the comptime-known" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var runtime_slice = "hello";
var runtime_i: usize = 3;
_ = &runtime_i;
const S = struct {
var ok: usize = 0;
fn check(a: u8, comptime b: usize) !void {
if (b == 3) {
try expect(a == 'l');
ok += 1;
} else if (b == 4) {
try expect(a == 'o');
ok += 1;
} else {
@compileError("fail");
}
}
};
inline for (runtime_slice[runtime_i..5], 3..5) |a, b| {
try S.check(a, b);
}
try expect(S.ok == 2);
}
test "inline for on tuple pointer" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
const S = struct { u32, u32, u32 };
var s: S = .{ 100, 200, 300 };
inline for (&s, 0..) |*x, i| {
x.* = i;
}
try expectEqual(S{ 0, 1, 2 }, s);
}
test "ref counter that starts at zero" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
for ([_]usize{ 0, 1, 2 }, 0..) |i, j| {
try expectEqual(i, j);
try expectEqual((&i).*, (&j).*);
}
inline for (.{ 0, 1, 2 }, 0..) |i, j| {
try expectEqual(i, j);
try expectEqual((&i).*, (&j).*);
}
}
test "inferred alloc ptr of for loop" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
{
var cond = false;
_ = &cond;
const opt = for (0..1) |_| {
if (cond) break cond;
} else null;
try expectEqual(@as(?bool, null), opt);
}
{
var cond = true;
_ = &cond;
const opt = for (0..1) |_| {
if (cond) break cond;
} else null;
try expectEqual(@as(?bool, true), opt);
}
}
test "for loop results in a bool" {
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
try std.testing.expect(for ([1]u8{0}) |x| {
if (x == 0) break true;
} else false);
}
test "return from inline for" {
const S = struct {
fn do() bool {
inline for (.{"a"}) |_| {
if (true) return false;
}
return true;
}
};
try std.testing.expect(!S.do());
}
test "for loop 0 length range" {
const map: []const u8 = &.{};
for (map, 0..map.len) |i, j| {
_ = i;
_ = j;
comptime unreachable;
}
}
test "labeled break from else" {
const S = struct {
fn doTheTest(x: u32) !void {
var y: u32 = 0;
const ok = label: while (y < x) : (y += 1) {
if (y == 10) break :label false;
} else {
break :label true;
};
try expect(ok);
}
};
try S.doTheTest(5);
try comptime S.doTheTest(5);
}
test "value break from inline for" {
const S = struct {
fn doTheTest() !void {
const x = inline for (0..2) |_| {
if (true) {
var idx: u32 = 0;
idx += 1;
break idx;
}
};
try expect(x == 1);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}