Zuban Incorrectly Marks Match Case As Unreachable
Hey guys! Let's dive into a peculiar issue I stumbled upon while updating the typing conformance results. It seems Zuban, a static type checker for Python, is incorrectly flagging a match case as unreachable. This is especially interesting because it contradicts the expected behavior based on the provided code example. I'll walk you through the problem, the code, and the unexpected results, and why this happens. Hopefully, we can figure out what is happening and how to fix it!
The Unexpected Behavior of Zuban
So, the core problem? Zuban doesn't seem to throw an error on the assert_type calls within a specific test case. These calls are supposed to verify the type compatibility. The specific code in question is from the typing module's conformance tests, specifically in tuples_type_compat.py. The key point here is that Zuban should be catching potential type mismatches, but it's not. This is a head-scratcher!
To make things clearer, I extracted the problematic test case into a standalone file, which allowed me to isolate the issue. When I ran zuban check on this simplified version, I got a warning that the code was unreachable. This is not what we want! This means that Zuban believes some of the case blocks in the match statement will never execute. But is that true? Let’s examine the code and what it is doing, so you can tell if this is a problem or not.
Code Snippet: The Problematic Code
Here’s a simplified version of the code that highlights the issue. Understanding this code is key to understanding the problem. The core is the match statement and how Zuban interprets it. The Func6Input type alias defines a union of tuple types. The func6 function then uses a match statement to handle different cases based on the input tuple's structure. The assert_type calls within each case are designed to verify the type of val at that point. I think the comments in the code will give you some context on the situation.
from typing import TypeAlias, assert_type
Func6Input: TypeAlias = tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]
def func6(val: Func6Input):
match val:
case (x,):
# Type may be narrowed to tuple[int].
assert_type(val, tuple[int]) # E[func6_1]
assert_type(val, Func6Input) # E[func6_1]
case (x, y):
# Type may be narrowed to tuple[str, str] | tuple[int, int].
assert_type(val, tuple[str, str] | tuple[int, int]) # E[func6_2]
assert_type(val, Func6Input) # E[func6_2]
case (x, y, z):
# Type may be narrowed to tuple[int, str, int].
assert_type(val, tuple[int, str, int]) # E[func6_3]
assert_type(val, Func6Input) # E[func6_3]
Zuban's Output: The Unreachable Warning
Let’s take a look at the output generated by Zuban. It's crucial for understanding the problem. I ran zuban check foo.py and got the following output. The errors show that the type checker is struggling to correctly analyze the code:
foo.py:11: error: Expression is of type "tuple[int]", not "tuple[int] | tuple[str, str] | tuple[int, Unpack[Tuple[str, ...]], int]" [misc]
foo.py:16: error: Expression is of type "tuple[str, str] | tuple[int, int]", not "tuple[int] | tuple[str, str] | tuple[int, Unpack[Tuple[str, ...]], int]" [misc]
foo.py:20: error: Statement is unreachable [unreachable]
Found 3 errors in 1 file (checked 1 source file)
Notice that there's an error: Statement is unreachable message. This is the crux of the issue. Zuban thinks the last case block will never be executed. This is incorrect. While the type checker correctly identifies type mismatches in the first two assert_type calls, the unreachable warning indicates a deeper problem with how Zuban handles pattern matching and type narrowing within match statements.
Deep Dive: Analyzing the Errors and Warnings
Let’s break down the errors and warnings in more detail to understand what's happening. We need to understand why Zuban is behaving in this way. Here’s a closer look at each error and how it relates to the overall problem.
Error 1 and 2: Type Mismatches
The first two errors (foo.py:11 and foo.py:16) indicate that Zuban is detecting type mismatches in the assert_type calls. This is expected behavior. However, the types are not completely matching. This behavior is correct because it correctly identifies that val has been narrowed to a more specific type within each case. In the first case, val is narrowed to tuple[int]. In the second, it narrows to either tuple[str, str] or tuple[int, int]. These errors themselves aren't the primary concern.
The Unreachable Warning: The Core Issue
The foo.py:20: error: Statement is unreachable warning is the real problem. This tells us that Zuban believes the third case (x, y, z): will never be executed. This suggests that Zuban is not correctly accounting for the possible types of Func6Input. The type Func6Input is a union of three tuple types. The third case (x, y, z) is a valid pattern when val is of type tuple[int, *tuple[str, ...], int]. Therefore, this case should be reachable.
Possible Causes: Why is Zuban Misbehaving?
So, what's going on? Why is Zuban incorrectly marking the match case as unreachable? Here are some possible causes:
- Type Narrowing Issues: Zuban may have problems accurately tracking how the type of
valnarrows within thematchstatement. Type narrowing is essential formatchstatements to work correctly with unions. Zuban might not be correctly identifying the possibilities. - Pattern Matching Logic: The logic that Zuban uses to analyze the patterns in the
matchstatement could be flawed. It's possible that the pattern matching algorithm isn't correctly handling the different tuple structures defined inFunc6Input. - Union Type Handling: Handling union types, like
Func6Input, can be complex for type checkers. Zuban may have difficulties tracking the possibilities of union types. The complex combination of union types and tuple unpacking makes analysis difficult.
These potential problems could be happening individually or even in combination. Identifying the precise cause requires deeper investigation into Zuban's internal workings.
Troubleshooting and Potential Solutions
Let's brainstorm how to address this issue. Fixing this requires understanding the source of the problem. Here’s how we might approach the problem:
- Reviewing Zuban's Source Code: The most direct way to understand the issue is to dive into Zuban's codebase. Analyzing the code responsible for type checking
matchstatements and handling union types would reveal the problem. - Creating a Minimal Reproducible Example: I already did this, but we could make it even simpler. If we can create an even simpler, self-contained example that triggers the same error, it makes it easier to test and isolate the problem.
- Reporting the Bug: The best course of action is to report the bug to the Zuban developers. Providing a clear, concise example and a detailed description will help them understand and fix the issue.
Conclusion: The Unreachable Case
In conclusion, Zuban incorrectly marks a match case as unreachable, which is a significant problem. While Zuban is correctly detecting type mismatches, the