Commit 149031204c

mlugg <mlugg@mlugg.co.uk>
2025-02-01 10:47:01
Sema: skip aliasing check and runtime operation for `@memcpy` of zero-bit type
This check isn't valid in such cases, because the source and destination pointers both refer to zero bits of memory, meaning they effectively never alias. Resolves: #21655
1 parent d97441d
Changed files (3)
src
test
behavior
src/Sema.zig
@@ -25928,6 +25928,22 @@ fn zirMemcpy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
         }
     }
 
+    zero_bit: {
+        const src_comptime = try src_elem_ty.comptimeOnlySema(pt);
+        const dest_comptime = try dest_elem_ty.comptimeOnlySema(pt);
+        assert(src_comptime == dest_comptime); // IMC
+        if (src_comptime) break :zero_bit;
+
+        const src_has_bits = try src_elem_ty.hasRuntimeBitsIgnoreComptimeSema(pt);
+        const dest_has_bits = try dest_elem_ty.hasRuntimeBitsIgnoreComptimeSema(pt);
+        assert(src_has_bits == dest_has_bits); // IMC
+        if (src_has_bits) break :zero_bit;
+
+        // The element type is zero-bit. We've done all validation (aside from the aliasing check,
+        // which we must skip) so we're done.
+        return;
+    }
+
     const runtime_src = rs: {
         const dest_ptr_val = try sema.resolveDefinedValue(block, dest_src, dest_ptr) orelse break :rs dest_src;
         const src_ptr_val = try sema.resolveDefinedValue(block, src_src, src_ptr) orelse break :rs src_src;
src/Type.zig
@@ -452,6 +452,13 @@ pub fn hasRuntimeBitsIgnoreComptime(ty: Type, zcu: *const Zcu) bool {
     return hasRuntimeBitsInner(ty, true, .eager, zcu, {}) catch unreachable;
 }
 
+pub fn hasRuntimeBitsIgnoreComptimeSema(ty: Type, pt: Zcu.PerThread) SemaError!bool {
+    return hasRuntimeBitsInner(ty, true, .sema, pt.zcu, pt.tid) catch |err| switch (err) {
+        error.NeedLazy => unreachable, // this would require a resolve strat of lazy
+        else => |e| return e,
+    };
+}
+
 /// true if and only if the type takes up space in memory at runtime.
 /// There are two reasons a type will return false:
 /// * the type is a comptime-only type. For example, the type `type` itself.
test/behavior/memcpy.zig
@@ -1,6 +1,7 @@
 const std = @import("std");
 const builtin = @import("builtin");
 const expect = std.testing.expect;
+const assert = std.debug.assert;
 
 test "memcpy and memset intrinsics" {
     if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
@@ -99,3 +100,31 @@ comptime {
     s.set("hello");
     if (!std.mem.eql(u8, s.buffer[0..5], "hello")) @compileError("bad");
 }
+
+test "@memcpy comptime-only type" {
+    const in: [4]type = .{ u8, u16, u32, u64 };
+    comptime var out: [4]type = undefined;
+    @memcpy(&out, &in);
+
+    comptime assert(out[0] == u8);
+    comptime assert(out[1] == u16);
+    comptime assert(out[2] == u32);
+    comptime assert(out[3] == u64);
+}
+
+test "@memcpy zero-bit type with aliasing" {
+    const S = struct {
+        fn doTheTest() void {
+            var buf: [3]void = @splat({});
+            const slice: []void = &buf;
+            // These two pointers are the same, but it's still not considered aliasing because
+            // the input and output slices both correspond to zero bits of memory.
+            @memcpy(slice, slice);
+            comptime assert(buf[0] == {});
+            comptime assert(buf[1] == {});
+            comptime assert(buf[2] == {});
+        }
+    };
+    S.doTheTest();
+    comptime S.doTheTest();
+}