From 5f74e4f3f8b909835ef794253a98a77868b3880e Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 21 Jul 2026 11:02:29 +0100 Subject: [PATCH] Sema: make loading uninstantiable types a runtime safety panic For generic code, it is far more useful to consider this equivalent to `unreachable`. Meanwhile, it is difficult to accidentally write concrete code which performs this operation, so very little is actually lost from not having a compile error. If we accept that this operation does not trigger a compile error, then it already has the potential to invoke Illegal Behavior today, because uninstantiable types have an unspecified size in memory, so the dereference always potentially exceeds the pointer's provenance. (That said, this only means it is *possible* for the operation to invoke IB, so the langspec should nonetheless explicitly specify that dereferencing a pointer to an uninstantiable type is itself Illegal Behavior.) Storing uninstantiable types into memory does not require any specific language rules, because that operation can never be reached anyway due to it requiring an operand (the RHS of `a = b`) whose type is the store type, which is (by definition) impossible for uninstantiable types. Resolves: https://codeberg.org/ziglang/zig/issues/36247 --- lib/std/debug.zig | 4 ++ lib/std/debug/no_panic.zig | 5 ++ lib/std/debug/simple_panic.zig | 4 ++ src/Sema.zig | 63 ++++++++++++++++++- src/Zcu.zig | 4 ++ .../compile_errors/initialize_empty_union.zig | 31 --------- .../cases/safety/load_uninstantiable_enum.zig | 20 ++++++ .../load_uninstantiable_enum_from_slice.zig | 21 +++++++ .../safety/load_uninstantiable_union.zig | 23 +++++++ .../load_uninstantiable_union_from_slice.zig | 24 +++++++ .../incremental/change_panic_handler_explicit | 3 + 11 files changed, 169 insertions(+), 33 deletions(-) create mode 100644 test/cases/safety/load_uninstantiable_enum.zig create mode 100644 test/cases/safety/load_uninstantiable_enum_from_slice.zig create mode 100644 test/cases/safety/load_uninstantiable_union.zig create mode 100644 test/cases/safety/load_uninstantiable_union_from_slice.zig diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 898ae314d9084e35fed670b03854959ec06beead..faeed5f667c9a8dc4ec0f38ef2a829b5f42dff3c 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -207,6 +207,10 @@ pub fn FullPanic(comptime panicFn: fn ([]const u8, ?usize) noreturn) type { @branchHint(.cold); call("'noreturn' function returned", @returnAddress()); } + pub fn loadUninstantiableType() noreturn { + @branchHint(.cold); + call("attempt to load uninstantiable type", @returnAddress()); + } }; } diff --git a/lib/std/debug/no_panic.zig b/lib/std/debug/no_panic.zig index f24317b9b7d66ae9076df3d4790cfa88d47e1a26..d47c9799a990e0d53cdf61e735b008deb355fd44 100644 --- a/lib/std/debug/no_panic.zig +++ b/lib/std/debug/no_panic.zig @@ -134,3 +134,8 @@ pub fn noreturnReturned() noreturn { @branchHint(.cold); @trap(); } + +pub fn loadUninstantiableType() noreturn { + @branchHint(.cold); + @trap(); +} diff --git a/lib/std/debug/simple_panic.zig b/lib/std/debug/simple_panic.zig index a5a09fa1162e75cf9aad5a69a1bfcd3a3e1845d9..af7231251aa0e9bd94ef1eef36280d110b4d2019 100644 --- a/lib/std/debug/simple_panic.zig +++ b/lib/std/debug/simple_panic.zig @@ -126,3 +126,7 @@ pub fn memcpyAlias() noreturn { pub fn noreturnReturned() noreturn { call("'noreturn' function returned", null); } + +pub fn loadUninstantiableType() noreturn { + call("attempt to load uninstantiable type", null); +} diff --git a/src/Sema.zig b/src/Sema.zig index 1b81e7653ea9d8586e8c887978078c9a97ba6925..96bb93f02bcd78185e14c04f40ab832fc09868b4 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -31040,7 +31040,18 @@ fn analyzeLoad( const comptime_only = switch (elem_ty.classify(zcu)) { .no_possible_value => switch (elem_ty.zigTypeTag(zcu)) { .@"opaque" => return sema.fail(block, src, "cannot load opaque type '{f}'", .{elem_ty.fmt(pt)}), - else => return sema.fail(block, src, "cannot load uninstantiable type '{f}'", .{elem_ty.fmt(pt)}), + else => { + // Loading an uninstantiable type always invokes Illegal Behavior. + if (block.isComptime()) { + return sema.fail(block, src, "cannot load uninstantiable type '{f}'", .{elem_ty.fmt(pt)}); + } else if (block.wantSafety()) { + try sema.safetyPanic(block, src, .load_uninstantiable_type); + return .unreachable_value; + } else { + _ = try block.addNoOp(.unreach); + return .unreachable_value; + } + }, }, .one_possible_value => return .fromValue((try elem_ty.onePossibleValue(pt)).?), .runtime => false, @@ -35059,12 +35070,60 @@ fn getExpectedBuiltinFnType(sema: *Sema, decl: Zcu.StdLangDecl) CompileError!Typ .@"panic.copyLenMismatch", .@"panic.memcpyAlias", .@"panic.noreturnReturned", + .@"panic.loadUninstantiableType", => try pt.funcType(.{ .param_types = &.{}, .return_type = .noreturn_type, }), - else => unreachable, + .StackTrace, + .CallingConvention, + .SourceLocation, + .Signedness, + .AddressSpace, + .VaList, + .CallModifier, + .AtomicOrder, + .AtomicRmwOp, + .ReduceOp, + .FloatMode, + .PrefetchOptions, + .ExportOptions, + .ExternOptions, + .BranchHint, + .assembly, + .@"assembly.Clobbers", + .Type, + .@"Type.Fn", + .@"Type.Fn.ParamAttributes", + .@"Type.Fn.Attributes", + .@"Type.Int", + .@"Type.Float", + .@"Type.Pointer", + .@"Type.Pointer.Size", + .@"Type.Pointer.Attributes", + .@"Type.Array", + .@"Type.Vector", + .@"Type.Optional", + .@"Type.ErrorUnion", + .@"Type.ErrorSet", + .@"Type.Enum", + .@"Type.Enum.Mode", + .@"Type.Union", + .@"Type.Union.FieldAttributes", + .@"Type.Struct", + .@"Type.Struct.FieldAttributes", + .@"Type.ContainerLayout", + .@"Type.Opaque", + .@"Type.Spirv", + .@"Type.Spirv.Image", + .@"Type.Spirv.Image.Usage", + .@"Type.Spirv.Image.Format", + .@"Type.Spirv.Image.Dimensionality", + .@"Type.Spirv.Image.Depth", + .@"Type.Spirv.Image.Access", + .panic, + => unreachable, // not a function (`decl.kind() != .func`) }; } diff --git a/src/Zcu.zig b/src/Zcu.zig index 465506eb7b41dcbcb16620c7ef84dc8b5f406ea4..0c231862cb0720fec4419115e08b712eacbdcf6f 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -517,6 +517,7 @@ pub const StdLangDecl = enum { @"panic.copyLenMismatch", @"panic.memcpyAlias", @"panic.noreturnReturned", + @"panic.loadUninstantiableType", VaList, @@ -606,6 +607,7 @@ pub const StdLangDecl = enum { .@"panic.copyLenMismatch", .@"panic.memcpyAlias", .@"panic.noreturnReturned", + .@"panic.loadUninstantiableType", => .func, }; } @@ -679,6 +681,7 @@ pub const SimplePanicId = enum { copy_len_mismatch, memcpy_alias, noreturn_returned, + load_uninstantiable_type, pub fn toStdLangDecl(id: SimplePanicId) StdLangDecl { return switch (id) { @@ -702,6 +705,7 @@ pub const SimplePanicId = enum { .copy_len_mismatch => .@"panic.copyLenMismatch", .memcpy_alias => .@"panic.memcpyAlias", .noreturn_returned => .@"panic.noreturnReturned", + .load_uninstantiable_type => .@"panic.loadUninstantiableType", // zig fmt: on }; } diff --git a/test/cases/compile_errors/initialize_empty_union.zig b/test/cases/compile_errors/initialize_empty_union.zig index a7945a105b6d973411f6d2e31810ae6e5f7e595e..fe8203c1ed1873c2c4634eae13ab177ea079474d 100644 --- a/test/cases/compile_errors/initialize_empty_union.zig +++ b/test/cases/compile_errors/initialize_empty_union.zig @@ -28,25 +28,6 @@ export fn init5() void { _ = @as(U5, undefined); } -export fn deref0(ptr: *const U0) void { - _ = ptr.*; -} -export fn deref1(ptr: *const U1) void { - _ = ptr.*; -} -export fn deref2(ptr: *const U2) void { - _ = ptr.*; -} -export fn deref3(ptr: *const U3) void { - _ = ptr.*; -} -export fn deref4(ptr: *const U4) void { - _ = ptr.*; -} -export fn deref5(ptr: *const U5) void { - _ = ptr.*; -} - // error // // :13:17: error: expected type 'tmp.U0', found '@TypeOf(undefined)' @@ -67,15 +48,3 @@ export fn deref5(ptr: *const U5) void { // :28:17: error: expected type 'tmp.U5', found '@TypeOf(undefined)' // :28:17: note: cannot coerce to uninstantiable type 'tmp.U5' // :10:12: note: union declared here -// :32:12: error: cannot load uninstantiable type 'tmp.U0' -// :5:12: note: union declared here -// :35:12: error: cannot load uninstantiable type 'tmp.U1' -// :6:12: note: union declared here -// :38:12: error: cannot load uninstantiable type 'tmp.U2' -// :7:12: note: union declared here -// :41:12: error: cannot load uninstantiable type 'tmp.U3' -// :8:12: note: union declared here -// :44:12: error: cannot load uninstantiable type 'tmp.U4' -// :9:12: note: union declared here -// :47:12: error: cannot load uninstantiable type 'tmp.U5' -// :10:12: note: union declared here diff --git a/test/cases/safety/load_uninstantiable_enum.zig b/test/cases/safety/load_uninstantiable_enum.zig new file mode 100644 index 0000000000000000000000000000000000000000..91a26417b689a0f06ae1ed1462bffdab5366cd30 --- /dev/null +++ b/test/cases/safety/load_uninstantiable_enum.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn { + _ = stack_trace; + if (std.mem.eql(u8, message, "attempt to load uninstantiable type")) { + std.process.exit(0); + } + std.process.exit(1); +} + +const E = enum {}; +pub fn main() error{TestFailed}!void { + const bytes: [32]u8 = @splat(0); + const ptr: *const E = @ptrCast(&bytes); + _ = ptr.*; + return error.TestFailed; +} +// run +// backend=selfhosted,llvm +// target=x86_64-linux,aarch64-linux,wasm32-wasi diff --git a/test/cases/safety/load_uninstantiable_enum_from_slice.zig b/test/cases/safety/load_uninstantiable_enum_from_slice.zig new file mode 100644 index 0000000000000000000000000000000000000000..57c308f8f6b15ca4f61eb36a3da0c246b6831dd8 --- /dev/null +++ b/test/cases/safety/load_uninstantiable_enum_from_slice.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn { + _ = stack_trace; + if (std.mem.eql(u8, message, "attempt to load uninstantiable type")) { + std.process.exit(0); + } + std.process.exit(1); +} + +const E = enum {}; +pub fn main() error{TestFailed}!void { + const bytes: [32]u8 = @splat(0); + const ptr: *const [1]E = @ptrCast(&bytes); + const slice: []const E = ptr; + _ = slice[0]; + return error.TestFailed; +} +// run +// backend=selfhosted,llvm +// target=x86_64-linux,aarch64-linux,wasm32-wasi diff --git a/test/cases/safety/load_uninstantiable_union.zig b/test/cases/safety/load_uninstantiable_union.zig new file mode 100644 index 0000000000000000000000000000000000000000..87f84e07c7f624104d797b94037fc6148491265b --- /dev/null +++ b/test/cases/safety/load_uninstantiable_union.zig @@ -0,0 +1,23 @@ +const std = @import("std"); + +pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn { + _ = stack_trace; + if (std.mem.eql(u8, message, "attempt to load uninstantiable type")) { + std.process.exit(0); + } + std.process.exit(1); +} + +const U = union { + foo: struct { a: u8, b: noreturn, }, + bar: enum {}, +}; +pub fn main() error{TestFailed}!void { + const bytes: [32]u8 = @splat(0); + const ptr: *const U = @ptrCast(&bytes); + _ = ptr.*; + return error.TestFailed; +} +// run +// backend=selfhosted,llvm +// target=x86_64-linux,aarch64-linux,wasm32-wasi diff --git a/test/cases/safety/load_uninstantiable_union_from_slice.zig b/test/cases/safety/load_uninstantiable_union_from_slice.zig new file mode 100644 index 0000000000000000000000000000000000000000..844bfd0b7d766bfb9e5eb0f911061eda7bace0d6 --- /dev/null +++ b/test/cases/safety/load_uninstantiable_union_from_slice.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn { + _ = stack_trace; + if (std.mem.eql(u8, message, "attempt to load uninstantiable type")) { + std.process.exit(0); + } + std.process.exit(1); +} + +const U = union { + foo: struct { a: u8, b: noreturn, }, + bar: enum {}, +}; +pub fn main() error{TestFailed}!void { + const bytes: [32]u8 = @splat(0); + const ptr: *const [1]U = @ptrCast(&bytes); + const slice: []const U = ptr; + _ = slice[0]; + return error.TestFailed; +} +// run +// backend=selfhosted,llvm +// target=x86_64-linux,aarch64-linux,wasm32-wasi diff --git a/test/incremental/change_panic_handler_explicit b/test/incremental/change_panic_handler_explicit index 662d13847f9800553b41fca4e9545e3bc07dbb17..f748a57afc43e7d18e5458549a18c173d21dc56c 100644 --- a/test/incremental/change_panic_handler_explicit +++ b/test/incremental/change_panic_handler_explicit @@ -36,6 +36,7 @@ pub const panic = struct { pub const copyLenMismatch = no_panic.copyLenMismatch; pub const memcpyAlias = no_panic.memcpyAlias; pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; }; fn myPanic(msg: []const u8, _: ?usize) noreturn { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); @@ -84,6 +85,7 @@ pub const panic = struct { pub const copyLenMismatch = no_panic.copyLenMismatch; pub const memcpyAlias = no_panic.memcpyAlias; pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; }; fn myPanic(msg: []const u8, _: ?usize) noreturn { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); @@ -132,6 +134,7 @@ pub const panic = struct { pub const copyLenMismatch = no_panic.copyLenMismatch; pub const memcpyAlias = no_panic.memcpyAlias; pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; }; fn myPanicNew(msg: []const u8, _: ?usize) noreturn { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); -- 2.54.0