Skip to content

Sema: allow @ptrCast of slices changing the length #22706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/std/debug.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub fn FullPanic(comptime panicFn: fn ([]const u8, ?usize) noreturn) type {
@tagName(accessed), @tagName(active),
});
}
pub fn sliceCastLenRemainder(src_len: usize) noreturn {
@branchHint(.cold);
std.debug.panicExtra(@returnAddress(), "slice length '{d}' does not divide exactly into destination elements", .{src_len});
}
pub fn reachedUnreachable() noreturn {
@branchHint(.cold);
call("reached unreachable code", @returnAddress());
Expand Down
5 changes: 5 additions & 0 deletions lib/std/debug/no_panic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub fn inactiveUnionField(_: anytype, _: anytype) noreturn {
@trap();
}

pub fn sliceCastLenRemainder(_: usize) noreturn {
@branchHint(.cold);
@trap();
}

pub fn reachedUnreachable() noreturn {
@branchHint(.cold);
@trap();
Expand Down
5 changes: 5 additions & 0 deletions lib/std/debug/simple_panic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ pub fn inactiveUnionField(active: anytype, accessed: @TypeOf(active)) noreturn {
call("access of inactive union field", null);
}

pub fn sliceCastLenRemainder(src_len: usize) noreturn {
_ = src_len;
call("slice length does not divide exactly into destination elements", null);
}

pub fn reachedUnreachable() noreturn {
call("reached unreachable code", null);
}
Expand Down
412 changes: 287 additions & 125 deletions src/Sema.zig

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Zcu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ pub const BuiltinDecl = enum {
@"panic.outOfBounds",
@"panic.startGreaterThanEnd",
@"panic.inactiveUnionField",
@"panic.sliceCastLenRemainder",
@"panic.reachedUnreachable",
@"panic.unwrapNull",
@"panic.castToNull",
Expand Down Expand Up @@ -352,6 +353,7 @@ pub const BuiltinDecl = enum {
.@"panic.outOfBounds",
.@"panic.startGreaterThanEnd",
.@"panic.inactiveUnionField",
.@"panic.sliceCastLenRemainder",
.@"panic.reachedUnreachable",
.@"panic.unwrapNull",
.@"panic.castToNull",
Expand Down
156 changes: 156 additions & 0 deletions test/behavior/ptrcast.zig
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,159 @@ test "@ptrCast restructures sliced comptime-only array" {
comptime assert(sub[2] == 5);
comptime assert(sub[3] == 6);
}

test "@ptrCast slice multiplying length" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const S = struct {
fn doTheTest(zero: u32) !void {
const in: []const u32 = &.{ zero, zero };
const out: []const u8 = @ptrCast(in);
try expect(out.len == 8);
try expect(@as([*]const u8, @ptrCast(in.ptr)) == out.ptr);
}
};
try S.doTheTest(0);
try comptime S.doTheTest(0);
}

test "@ptrCast array pointer to slice multiplying length" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const S = struct {
fn doTheTest(zero: u32) !void {
const in: *const [2]u32 = &.{ zero, zero };
const out: []const u8 = @ptrCast(in);
try expect(out.len == 8);
try expect(out.ptr == @as([*]const u8, @ptrCast(in.ptr)));
}
};
try S.doTheTest(0);
try comptime S.doTheTest(0);
}

test "@ptrCast slice dividing length" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const S = struct {
fn doTheTest(zero: u8) !void {
const in: []const u8 = &.{ zero, zero, zero, zero, zero, zero, zero, zero };
const out: []align(1) const u32 = @ptrCast(in);
try expect(out.len == 2);
try expect(out.ptr == @as([*]align(1) const u32, @ptrCast(in.ptr)));
}
};
try S.doTheTest(0);
try comptime S.doTheTest(0);
}

test "@ptrCast array pointer to slice dividing length" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const S = struct {
fn doTheTest(zero: u8) !void {
const in: *const [8]u8 = &.{ zero, zero, zero, zero, zero, zero, zero, zero };
const out: []align(1) const u32 = @ptrCast(in);
try expect(out.len == 2);
try expect(out.ptr == @as([*]align(1) const u32, @ptrCast(in.ptr)));
}
};
try S.doTheTest(0);
try comptime S.doTheTest(0);
}

test "@ptrCast slice with complex length increase" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const TwoBytes = [2]u8;
const ThreeBytes = [3]u8;

const S = struct {
fn doTheTest(zero: ThreeBytes) !void {
const in: []const ThreeBytes = &.{ zero, zero };
const out: []const TwoBytes = @ptrCast(in);
try expect(out.len == 3);
try expect(out.ptr == @as([*]const TwoBytes, @ptrCast(in.ptr)));
}
};
try S.doTheTest(@splat(0));
try comptime S.doTheTest(@splat(0));
}

test "@ptrCast array pointer to slice with complex length increase" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const TwoBytes = [2]u8;
const ThreeBytes = [3]u8;

const S = struct {
fn doTheTest(zero: ThreeBytes) !void {
const in: *const [2]ThreeBytes = &.{ zero, zero };
const out: []const TwoBytes = @ptrCast(in);
try expect(out.len == 3);
try expect(out.ptr == @as([*]const TwoBytes, @ptrCast(in.ptr)));
}
};
try S.doTheTest(@splat(0));
try comptime S.doTheTest(@splat(0));
}

test "@ptrCast slice with complex length decrease" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const TwoBytes = [2]u8;
const ThreeBytes = [3]u8;

const S = struct {
fn doTheTest(zero: TwoBytes) !void {
const in: []const TwoBytes = &.{ zero, zero, zero };
const out: []const ThreeBytes = @ptrCast(in);
try expect(out.len == 2);
try expect(out.ptr == @as([*]const ThreeBytes, @ptrCast(in.ptr)));
}
};
try S.doTheTest(@splat(0));
try comptime S.doTheTest(@splat(0));
}

test "@ptrCast array pointer to slice with complex length decrease" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;

const TwoBytes = [2]u8;
const ThreeBytes = [3]u8;

const S = struct {
fn doTheTest(zero: TwoBytes) !void {
const in: *const [3]TwoBytes = &.{ zero, zero, zero };
const out: []const ThreeBytes = @ptrCast(in);
try expect(out.len == 2);
try expect(out.ptr == @as([*]const ThreeBytes, @ptrCast(in.ptr)));
}
};
try S.doTheTest(@splat(0));
try comptime S.doTheTest(@splat(0));
}
57 changes: 57 additions & 0 deletions test/cases/compile_errors/slice_cast_change_len.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
comptime {
const in: []const comptime_int = &.{0};
const out: []const type = @ptrCast(in);
_ = out;
}

const One = u8;
const Two = [2]u8;
const Three = [3]u8;
const Four = [4]u8;
const Five = [5]u8;

// []One -> []Two (small to big, divides neatly)
comptime {
const in: []const One = &.{ 1, 0, 0 };
const out: []const Two = @ptrCast(in);
_ = out;
}
comptime {
const in: *const [3]One = &.{ 1, 0, 0 };
const out: []const Two = @ptrCast(in);
_ = out;
}

// []Four -> []Five (small to big, does not divide)
comptime {
const in: []const Four = &.{.{ 0, 0, 0, 0 }};
const out: []const Five = @ptrCast(in);
_ = out;
}
comptime {
const in: *const [1]Four = &.{.{ 0, 0, 0, 0 }};
const out: []const Five = @ptrCast(in);
_ = out;
}

// []Three -> []Two (big to small, does not divide)
comptime {
const in: []const Three = &.{.{ 0, 0, 0 }};
const out: []const Two = @ptrCast(in);
_ = out;
}
comptime {
const in: *const [1]Three = &.{.{ 0, 0, 0 }};
const out: []const Two = @ptrCast(in);
_ = out;
}

// error
//
// :3:31: error: cannot infer length of slice of 'type' from slice of 'comptime_int'
// :16:30: error: slice length '3' does not divide exactly into destination elements
// :21:30: error: slice length '3' does not divide exactly into destination elements
// :28:31: error: slice length '1' does not divide exactly into destination elements
// :33:31: error: slice length '1' does not divide exactly into destination elements
// :40:30: error: slice length '1' does not divide exactly into destination elements
// :45:30: error: slice length '1' does not divide exactly into destination elements
26 changes: 26 additions & 0 deletions test/cases/safety/slice_cast_change_len_0.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! []One -> []Two (small to big, divides neatly)

const One = u8;
const Two = [2]u8;

/// A runtime-known value to prevent these safety panics from being compile errors.
var rt: u8 = 0;

pub fn main() void {
const in: []const One = &.{ 1, 0, rt };
const out: []const Two = @ptrCast(in);
_ = out;
std.process.exit(1);
}

pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
if (std.mem.eql(u8, message, "slice length '3' does not divide exactly into destination elements")) {
std.process.exit(0);
}
std.process.exit(1);
}

const std = @import("std");

// run
// backend=llvm
26 changes: 26 additions & 0 deletions test/cases/safety/slice_cast_change_len_1.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! []Four -> []Five (small to big, does not divide)

const Four = [4]u8;
const Five = [5]u8;

/// A runtime-known value to prevent these safety panics from being compile errors.
var rt: u8 = 0;

pub fn main() void {
const in: []const Four = &.{.{ 0, 0, 0, rt }};
const out: []const Five = @ptrCast(in);
_ = out;
std.process.exit(1);
}

pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
if (std.mem.eql(u8, message, "slice length '1' does not divide exactly into destination elements")) {
std.process.exit(0);
}
std.process.exit(1);
}

const std = @import("std");

// run
// backend=llvm
26 changes: 26 additions & 0 deletions test/cases/safety/slice_cast_change_len_2.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! []Three -> []Two (big to small, does not divide)

const Two = [2]u8;
const Three = [3]u8;

/// A runtime-known value to prevent these safety panics from being compile errors.
var rt: u8 = 0;

pub fn main() void {
const in: []const Three = &.{.{ 0, 0, rt }};
const out: []const Two = @ptrCast(in);
_ = out;
std.process.exit(1);
}

pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
if (std.mem.eql(u8, message, "slice length '1' does not divide exactly into destination elements")) {
std.process.exit(0);
}
std.process.exit(1);
}

const std = @import("std");

// run
// backend=llvm
4 changes: 3 additions & 1 deletion test/incremental/change_panic_handler_explicit
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub const panic = struct {
pub const outOfBounds = no_panic.outOfBounds;
pub const startGreaterThanEnd = no_panic.startGreaterThanEnd;
pub const inactiveUnionField = no_panic.inactiveUnionField;
pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder;
pub const reachedUnreachable = no_panic.reachedUnreachable;
pub const unwrapNull = no_panic.unwrapNull;
pub const castToNull = no_panic.castToNull;
Expand Down Expand Up @@ -66,6 +67,7 @@ pub const panic = struct {
pub const outOfBounds = no_panic.outOfBounds;
pub const startGreaterThanEnd = no_panic.startGreaterThanEnd;
pub const inactiveUnionField = no_panic.inactiveUnionField;
pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder;
pub const reachedUnreachable = no_panic.reachedUnreachable;
pub const unwrapNull = no_panic.unwrapNull;
pub const castToNull = no_panic.castToNull;
Expand Down Expand Up @@ -112,7 +114,7 @@ pub const panic = struct {
pub const outOfBounds = std.debug.no_panic.outOfBounds;
pub const startGreaterThanEnd = std.debug.no_panic.startGreaterThanEnd;
pub const inactiveUnionField = std.debug.no_panic.inactiveUnionField;
pub const messages = std.debug.no_panic.messages;
pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder;
pub const reachedUnreachable = no_panic.reachedUnreachable;
pub const unwrapNull = no_panic.unwrapNull;
pub const castToNull = no_panic.castToNull;
Expand Down
Loading