From e8d873286dd00571069178a1d51419765850551b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Van=C4=9Bk?= Date: Thu, 6 Aug 2026 21:07:06 +0200 Subject: [PATCH] exclude `noreturn`, `undefined` and `@compileError` peers in branching type resolution Fixes #3210 --- src/analysis.zig | 58 +++++++++++++++++++------ tests/analysis/either.zig | 11 +++-- tests/analysis/peer_type_resolution.zig | 8 ++++ tests/analysis/switch.zig | 37 ++++++++++++++++ 4 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/analysis.zig b/src/analysis.zig index 7b526719d7..bdd2c0642f 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -3881,9 +3881,49 @@ pub const Type = struct { if (entries.len == 1) return entries[0].type; + // Mirror Zig's peer type resolution by ignoring branches that never + // produce a value (`noreturn`, `@compileError`) or that coerce to + // any of the other peers (`undefined`). + var filtered: std.ArrayList(TypeWithDescriptor) = try .initCapacity(arena, entries.len); + for (entries) |entry| { + switch (entry.type.data) { + .compile_error => continue, + .ip_index => |payload| switch (payload.type) { + .noreturn_type, .undefined_type => continue, + else => {}, + }, + else => {}, + } + filtered.appendAssumeCapacity(entry); + } + const remaining = filtered.items; + + if (remaining.len == 0) { + // Every branch is `noreturn`, `undefined` or `@compileError`. + // An `undefined` branch determines the result; otherwise prefer + // `noreturn` over `@compileError` so that the result does not + // depend on branch order. + for (entries) |entry| { + switch (entry.type.data) { + .ip_index => |payload| if (payload.type == .undefined_type) return entry.type, + else => {}, + } + } + for (entries) |entry| { + switch (entry.type.data) { + .ip_index => |payload| if (payload.type == .noreturn_type) return entry.type, + else => {}, + } + } + return entries[0].type; + } + + if (remaining.len == 1) + return remaining[0].type; + peer_type_resolution: { - var chosen = entries[0].type; - for (entries[1..]) |entry| { + var chosen = remaining[0].type; + for (remaining[1..]) |entry| { const candidate = entry.type; chosen = try analyser.resolvePeerTypes(chosen, candidate) orelse break :peer_type_resolution; } @@ -3913,29 +3953,21 @@ pub const Type = struct { var deduplicator: Deduplicator = .empty; defer deduplicator.deinit(arena); - const has_type_val = for (entries) |entry| { - if (entry.type.data == .compile_error) { - continue; - } - break entry.type.is_type_val; - } else entries[0].type.is_type_val; + const has_type_val = remaining[0].type.is_type_val; - for (entries) |entry| { + for (remaining) |entry| { try deduplicator.put( arena, .{ .type_data = entry.type.data, .descriptor = entry.descriptor }, {}, ); - if (entry.type.data == .compile_error) { - continue; - } if (entry.type.is_type_val != has_type_val) { return null; } } if (deduplicator.count() == 1) - return entries[0].type; + return remaining[0].type; return .{ .data = .{ .either = try arena.dupe(Type.Data.EitherEntry, deduplicator.keys()) }, diff --git a/tests/analysis/either.zig b/tests/analysis/either.zig index 7969cae7aa..8a6cf70aea 100644 --- a/tests/analysis/either.zig +++ b/tests/analysis/either.zig @@ -2,15 +2,20 @@ const UnknownType: type = undefined; const unknown_value: UnknownType = undefined; const non_type_and_type: if (true) undefined else i32 = unknown_value; -// ^^^^^^^^^^^^^^^^^ (unknown)() +// ^^^^^^^^^^^^^^^^^ (i32)() const type_and_non_type: if (true) i32 else undefined = unknown_value; -// ^^^^^^^^^^^^^^^^^ (unknown)() +// ^^^^^^^^^^^^^^^^^ (i32)() const compile_error_and_type: if (true) @compileError("Foo") else i32 = 1; -// ^^^^^^^^^^^^^^^^^^^^^^ (either type)() +// ^^^^^^^^^^^^^^^^^^^^^^ (i32)() const type_and_compile_error: if (true) i32 else @compileError("Foo") = 1; +// ^^^^^^^^^^^^^^^^^^^^^^ (i32)() + +// The literal type values `noreturn`/`undefined` are valid peers and must +// not be mistaken for value-level branches that never produce a value. +const type_and_noreturn_type: if (true) u8 else noreturn = unknown_value; // ^^^^^^^^^^^^^^^^^^^^^^ (either type)() const StructU32 = struct { diff --git a/tests/analysis/peer_type_resolution.zig b/tests/analysis/peer_type_resolution.zig index 86b6850fce..e19ec08cf5 100644 --- a/tests/analysis/peer_type_resolution.zig +++ b/tests/analysis/peer_type_resolution.zig @@ -170,6 +170,14 @@ pub fn main() !void { _ = noreturn_1; // ^^^^^^^^^^ (S)() + const undefined_0 = if (runtime_bool) s else undefined; + _ = undefined_0; + // ^^^^^^^^^^^ (S)() + + const undefined_1 = if (runtime_bool) undefined else s; + _ = undefined_1; + // ^^^^^^^^^^^ (S)() + // Use @compileLog to verify the expected type with the compiler: // @compileLog(error_union_0); diff --git a/tests/analysis/switch.zig b/tests/analysis/switch.zig index c718f2ed72..b3ae6c744b 100644 --- a/tests/analysis/switch.zig +++ b/tests/analysis/switch.zig @@ -138,4 +138,41 @@ const labeled_switch_2 = blk: switch (undefined) { 2 => B{}, else => unreachable, }; + +// https://github.com/zigtools/zls/issues/3210 +const switch_with_unreachable = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^^^^ (type)(A) + 1 => A, + else => unreachable, +}; + +const switch_with_undefined = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^^ (type)(A) + 1 => A, + else => undefined, +}; + +const switch_with_compile_error = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^^^^^^ (type)(A) + 1 => A, + else => @compileError(""), +}; + +const switch_only_noreturn = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^ (noreturn)() + 1 => unreachable, + else => @compileError(""), +}; + +const switch_only_noreturn_2 = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^^^ (noreturn)() + 1 => @compileError(""), + else => unreachable, +}; + +const switch_undefined_and_noreturn = switch (some_u8) { +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (@TypeOf(undefined))(undefined) + 1 => unreachable, + else => undefined, +}; // zig fmt: on