Files
zig/test/behavior/ptrfromint.zig
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

51 lines
1.7 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const expectEqual = std.testing.expectEqual;
test "casting integer address to function pointer" {
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
addressToFunction();
comptime addressToFunction();
}
fn addressToFunction() void {
var addr: usize = 0xdeadbee0;
_ = &addr;
_ = @as(*const fn () void, @ptrFromInt(addr));
}
test "mutate through ptr initialized with constant ptrFromInt value" {
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;
forceCompilerAnalyzeBranchHardCodedPtrDereference(false);
}
fn forceCompilerAnalyzeBranchHardCodedPtrDereference(x: bool) void {
const hardCodedP = @as(*volatile u8, @ptrFromInt(0xdeadbeef));
if (x) {
hardCodedP.* = hardCodedP.* | 10;
} else {
return;
}
}
test "@ptrFromInt creates null pointer" {
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 ptr = @as(?*u32, @ptrFromInt(0));
try expectEqual(@as(?*u32, null), ptr);
}
test "@ptrFromInt creates allowzero zero pointer" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
const ptr = @as(*allowzero u32, @ptrFromInt(0));
try expectEqual(@as(usize, 0), @intFromPtr(ptr));
}