diff --git a/conformance/results/mypy/overloads_evaluation.toml b/conformance/results/mypy/overloads_evaluation.toml index 61c5af6a8..660060666 100644 --- a/conformance/results/mypy/overloads_evaluation.toml +++ b/conformance/results/mypy/overloads_evaluation.toml @@ -3,8 +3,14 @@ notes = """ Does not expand boolean arguments to `Literal[True]` and `Literal[False]`. Does not expand enum arguments to literal variants. Does not expand tuple arguments to possible combinations. +<<<<<<< HEAD +Does not handle unpacked arguments when checking for parameter type equivalence. +Does not evaluate Any in some cases where overload is ambiguous. +Evaluates Any in some cases where overload is not ambiguous. +======= Does not evaluate `Any` in some cases where overload is ambiguous. Evaluates `Any` in some cases where overload is not ambiguous. +>>>>>>> main """ conformance_automated = "Fail" errors_diff = """ @@ -14,10 +20,11 @@ Line 161: Unexpected errors ['overloads_evaluation.py:161: error: No overload va Line 162: Unexpected errors ['overloads_evaluation.py:162: error: Expression is of type "Any", not "Literal[0, 1]" [assert-type]'] Line 205: Unexpected errors ['overloads_evaluation.py:205: error: Argument 1 to "expand_tuple" has incompatible type "tuple[int, int | str]"; expected "tuple[int, int]" [arg-type]'] Line 206: Unexpected errors ['overloads_evaluation.py:206: error: Expression is of type "int", not "int | str" [assert-type]'] -Line 265: Unexpected errors ['overloads_evaluation.py:265: error: Expression is of type "list[Any]", not "Any" [assert-type]'] -Line 281: Unexpected errors ['overloads_evaluation.py:281: error: Expression is of type "list[Any]", not "Any" [assert-type]'] -Line 303: Unexpected errors ['overloads_evaluation.py:303: error: Expression is of type "Any", not "float" [assert-type]'] -Line 347: Unexpected errors ['overloads_evaluation.py:347: error: Expression is of type "list[Any]", not "Any" [assert-type]'] +Line 268: Unexpected errors ['overloads_evaluation.py:268: error: Expression is of type "list[Any]", not "Any" [assert-type]'] +Line 284: Unexpected errors ['overloads_evaluation.py:284: error: Expression is of type "list[Any]", not "Any" [assert-type]'] +Line 306: Unexpected errors ['overloads_evaluation.py:306: error: Expression is of type "Any", not "float" [assert-type]'] +Line 350: Unexpected errors ['overloads_evaluation.py:350: error: Expression is of type "list[Any]", not "Any" [assert-type]'] +Line 439: Unexpected errors ['overloads_evaluation.py:439: error: Expression is of type "Any", not "bool" [assert-type]'] """ output = """ overloads_evaluation.py:38: error: All overload variants of "example1_1" require at least one argument [call-overload] @@ -46,8 +53,9 @@ overloads_evaluation.py:161: note: def expand_enum(x: Literal[Color.BLUE]) - overloads_evaluation.py:162: error: Expression is of type "Any", not "Literal[0, 1]" [assert-type] overloads_evaluation.py:205: error: Argument 1 to "expand_tuple" has incompatible type "tuple[int, int | str]"; expected "tuple[int, int]" [arg-type] overloads_evaluation.py:206: error: Expression is of type "int", not "int | str" [assert-type] -overloads_evaluation.py:265: error: Expression is of type "list[Any]", not "Any" [assert-type] -overloads_evaluation.py:281: error: Expression is of type "list[Any]", not "Any" [assert-type] -overloads_evaluation.py:303: error: Expression is of type "Any", not "float" [assert-type] -overloads_evaluation.py:347: error: Expression is of type "list[Any]", not "Any" [assert-type] +overloads_evaluation.py:268: error: Expression is of type "list[Any]", not "Any" [assert-type] +overloads_evaluation.py:284: error: Expression is of type "list[Any]", not "Any" [assert-type] +overloads_evaluation.py:306: error: Expression is of type "Any", not "float" [assert-type] +overloads_evaluation.py:350: error: Expression is of type "list[Any]", not "Any" [assert-type] +overloads_evaluation.py:439: error: Expression is of type "Any", not "bool" [assert-type] """ diff --git a/conformance/results/pycroscope/overloads_evaluation.toml b/conformance/results/pycroscope/overloads_evaluation.toml index f04831ec5..3381b94cc 100644 --- a/conformance/results/pycroscope/overloads_evaluation.toml +++ b/conformance/results/pycroscope/overloads_evaluation.toml @@ -1,9 +1,21 @@ -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 371: Unexpected errors ['./overloads_evaluation.py:371:16: Any[multiple_overload_matches] is not equivalent to bool'] +Line 400: Unexpected errors ['./overloads_evaluation.py:400:16: Any[multiple_overload_matches] is not equivalent to bool'] +Line 420: Unexpected errors ['./overloads_evaluation.py:420:16: Any[multiple_overload_matches] is not equivalent to bool'] +Line 439: Unexpected errors ['./overloads_evaluation.py:439:16: Any[multiple_overload_matches] is not equivalent to bool'] +Line 441: Unexpected errors ['./overloads_evaluation.py:441:16: Any[multiple_overload_matches] is not equivalent to bool'] +Line 468: Unexpected errors ['./overloads_evaluation.py:468:16: Any[multiple_overload_matches] is not equivalent to ./overloads_evaluation.py.A[Any[explicit]]'] """ output = """ ./overloads_evaluation.py:38:0: Cannot call overloaded function [incompatible_call] ./overloads_evaluation.py:46:0: Cannot call overloaded function [incompatible_argument] ./overloads_evaluation.py:51:0: Cannot call overloaded function [incompatible_argument] ./overloads_evaluation.py:116:4: Cannot call overloaded function [incompatible_argument] +./overloads_evaluation.py:371:16: Any[multiple_overload_matches] is not equivalent to bool +./overloads_evaluation.py:400:16: Any[multiple_overload_matches] is not equivalent to bool +./overloads_evaluation.py:420:16: Any[multiple_overload_matches] is not equivalent to bool +./overloads_evaluation.py:439:16: Any[multiple_overload_matches] is not equivalent to bool +./overloads_evaluation.py:441:16: Any[multiple_overload_matches] is not equivalent to bool +./overloads_evaluation.py:468:16: Any[multiple_overload_matches] is not equivalent to ./overloads_evaluation.py.A[Any[explicit]] """ diff --git a/conformance/results/pyrefly/overloads_evaluation.toml b/conformance/results/pyrefly/overloads_evaluation.toml index 6b60e3f4a..d6362efdc 100644 --- a/conformance/results/pyrefly/overloads_evaluation.toml +++ b/conformance/results/pyrefly/overloads_evaluation.toml @@ -1,10 +1,12 @@ conformant = "Pass" -conformance_automated = "Pass" +conformance_automated = "Fail" errors_diff = """ +Line 395: Unexpected errors ['assert_type(int, Any) failed [assert-type]'] """ output = """ ERROR overloads_evaluation.py:38:11-13: No matching overload found for function `example1_1` called with arguments: () [no-matching-overload] ERROR overloads_evaluation.py:46:15-16: Argument `Literal[1]` is not assignable to parameter `y` with type `str` in function `example1_1` [bad-argument-type] ERROR overloads_evaluation.py:51:12-13: Argument `Literal[1]` is not assignable to parameter `x` with type `str` in function `example1_1` [bad-argument-type] ERROR overloads_evaluation.py:116:13-22: No matching overload found for function `example2` called with arguments: (int | str, int | str, Literal[1]) [no-matching-overload] +ERROR overloads_evaluation.py:395:16-27: assert_type(int, Any) failed [assert-type] """ diff --git a/conformance/results/pyright/overloads_evaluation.toml b/conformance/results/pyright/overloads_evaluation.toml index cb63679bf..7dbb8c78a 100644 --- a/conformance/results/pyright/overloads_evaluation.toml +++ b/conformance/results/pyright/overloads_evaluation.toml @@ -1,10 +1,17 @@ conformant = "Partial" notes = """ +<<<<<<< HEAD +Does not evaluate Any in some cases where overload is ambiguous. +Picks first overload instead of most general return type in some cases where overload is ambiguous. +======= Does not evaluate `Any` in some cases where overload is ambiguous. +>>>>>>> main """ conformance_automated = "Fail" errors_diff = """ -Line 281: Unexpected errors ['overloads_evaluation.py:281:17 - error: "assert_type" mismatch: expected "Any" but received "list[int]" (reportAssertTypeFailure)'] +Line 284: Unexpected errors ['overloads_evaluation.py:284:17 - error: "assert_type" mismatch: expected "Any" but received "list[int]" (reportAssertTypeFailure)'] +Line 395: Unexpected errors ['overloads_evaluation.py:395:17 - error: "assert_type" mismatch: expected "Any" but received "int" (reportAssertTypeFailure)'] +Line 468: Unexpected errors ['overloads_evaluation.py:468:17 - error: "assert_type" mismatch: expected "A[Any]" but received "A[None]" (reportAssertTypeFailure)'] """ output = """ overloads_evaluation.py:38:1 - error: No overloads for "example1_1" match the provided arguments @@ -20,5 +27,7 @@ overloads_evaluation.py:116:14 - error: Argument of type "int | str" cannot be a overloads_evaluation.py:116:17 - error: Argument of type "int | str" cannot be assigned to parameter "y" of type "int" in function "example2"   Type "int | str" is not assignable to type "int"     "str" is not assignable to "int" (reportArgumentType) -overloads_evaluation.py:281:17 - error: "assert_type" mismatch: expected "Any" but received "list[int]" (reportAssertTypeFailure) +overloads_evaluation.py:284:17 - error: "assert_type" mismatch: expected "Any" but received "list[int]" (reportAssertTypeFailure) +overloads_evaluation.py:395:17 - error: "assert_type" mismatch: expected "Any" but received "int" (reportAssertTypeFailure) +overloads_evaluation.py:468:17 - error: "assert_type" mismatch: expected "A[Any]" but received "A[None]" (reportAssertTypeFailure) """ diff --git a/conformance/results/results.html b/conformance/results/results.html index 7915147fd..2612522a6 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -1725,29 +1725,79 @@

Python Type System Conformance Test Results

  • Does not expand boolean arguments to Literal[True] and Literal[False].
  • Does not expand enum arguments to literal variants.
  • Does not expand tuple arguments to possible combinations.
  • +
  • <<<<<<< HEAD
  • +
  • Does not handle unpacked arguments when checking for parameter type equivalence.
  • +
  • Does not evaluate Any in some cases where overload is ambiguous.
  • +
  • Evaluates Any in some cases where overload is not ambiguous.
  • +
  • =======
  • Does not evaluate Any in some cases where overload is ambiguous.
  • Evaluates Any in some cases where overload is not ambiguous.
  • +
  • +
    +
    +
    +
    +
    +
    +

    main

    +
    +
    +
    +
    +
    +
    +
  • - Pass + Unknown Pass Partial + + + Partial + + + + Partial + - Pass - Pass 3.5 / 5 • 70.0% - 3.5 / 5 • 70.0% + 2.5 / 5 • 50.0% 5 / 5 • 100.0% 4.5 / 5 • 90.0% - 5 / 5 • 100.0% - 5 / 5 • 100.0% + 4.5 / 5 • 90.0% + 4.5 / 5 • 90.0% @@ -2608,11 +2658,11 @@

    Python Type System Conformance Test Results

    108.5 / 145 • 74.8% - 138 / 145 • 95.2% + 137 / 145 • 94.5% 140.5 / 145 • 96.9% 135.5 / 145 • 93.4% - 132 / 145 • 91.0% - 144.5 / 145 • 99.7% + 131.5 / 145 • 90.7% + 144 / 145 • 99.3% diff --git a/conformance/results/ty/overloads_evaluation.toml b/conformance/results/ty/overloads_evaluation.toml index 3a9a963c2..38638ad26 100644 --- a/conformance/results/ty/overloads_evaluation.toml +++ b/conformance/results/ty/overloads_evaluation.toml @@ -1,9 +1,15 @@ -conformance_automated = "Pass" +conformant = "Partial" +notes = """ +Returns Any instead of most general return type for ambiguous calls. +""" +conformance_automated = "Fail" errors_diff = """ +Line 468: Unexpected errors ['overloads_evaluation.py:468:5: error[type-assertion-failure] Type `Unknown` does not match asserted type `A[Any]`'] """ output = """ overloads_evaluation.py:38:1: error[no-matching-overload] No overload of function `example1_1` matches arguments overloads_evaluation.py:46:15: error[invalid-argument-type] Argument to function `example1_1` is incorrect: Expected `str`, found `Literal[1]` overloads_evaluation.py:51:12: error[invalid-argument-type] Argument to function `example1_1` is incorrect: Expected `str`, found `Literal[1]` overloads_evaluation.py:116:5: error[no-matching-overload] No overload of function `example2` matches arguments +overloads_evaluation.py:468:5: error[type-assertion-failure] Type `Unknown` does not match asserted type `A[Any]` """ diff --git a/conformance/results/zuban/overloads_evaluation.toml b/conformance/results/zuban/overloads_evaluation.toml index 0e3b48cf8..a27018d53 100644 --- a/conformance/results/zuban/overloads_evaluation.toml +++ b/conformance/results/zuban/overloads_evaluation.toml @@ -1,5 +1,12 @@ -conformance_automated = "Pass" +conformant = "Partial" +notes = """ +Does not handle unpacked arguments when checking for parameter type equivalence. +Returns Any instead of most general return type for ambiguous calls. +""" +conformance_automated = "Fail" errors_diff = """ +Line 439: Unexpected errors ['overloads_evaluation.py:439: error: Expression is of type "Any", not "bool" [misc]'] +Line 468: Unexpected errors ['overloads_evaluation.py:468: error: Expression is of type "Any", not "A[Any]" [misc]'] """ output = """ overloads_evaluation.py:38: error: All overload variants of "example1_1" require at least one argument [call-overload] @@ -16,4 +23,6 @@ overloads_evaluation.py:51: note: def example1_1(x: int, y: str) -> int overloads_evaluation.py:51: note: def example1_1(x: str) -> str overloads_evaluation.py:116: error: Argument 1 to "example2" has incompatible type "int | str"; expected "int" [arg-type] overloads_evaluation.py:116: error: Argument 2 to "example2" has incompatible type "int | str"; expected "str" [arg-type] +overloads_evaluation.py:439: error: Expression is of type "Any", not "bool" [misc] +overloads_evaluation.py:468: error: Expression is of type "Any", not "A[Any]" [misc] """ diff --git a/conformance/tests/overloads_evaluation.py b/conformance/tests/overloads_evaluation.py index c89f9d0ba..dea5b5a22 100644 --- a/conformance/tests/overloads_evaluation.py +++ b/conformance/tests/overloads_evaluation.py @@ -235,10 +235,13 @@ def check_variadic(v: list[int]) -> None: assert_type(ret1, int) -# > Step 5: For all arguments, determine whether all possible -# > :term:`materializations ` of the argument's type are assignable to -# > the corresponding parameter type for each of the remaining overloads. If so, -# > eliminate all of the subsequent remaining overloads. +# > Step 5: For each of the remaining overloads, determine whether all +# > arguments satisfy at least one of the following conditions: +# > - All possible :term:`materializations ` of the argument's type are +# > assignable to the corresponding parameter type, or +# > - The parameter types corresponding to this argument in all of the remaining overloads +# > are :term:`equivalent`. +# > If so, eliminate all of the subsequent remaining overloads. @overload @@ -345,3 +348,121 @@ def check_example7(v1: list[Any], v2: Any) -> None: ret3 = example7(v1, v2) assert_type(ret3, Any) + + +@overload +def example8(x: str, y: Literal['o1']) -> bool: ... + + +@overload +def example8(x: str, y: str) -> int: ... + + +def example8(x: str, y: str) -> bool | int: + return True + + +def check_example8(x: Any): + # The parameter type corresponding to argument `x` is `str` in both + # overloads, and all materializations of argument `y`'s type of + # `Literal['o1']` match the first overload, so the second overload can be + # eliminated. + ret = example8(x, 'o1') + assert_type(ret, bool) + + +@overload +def example9(x: str, y: Literal['o1']) -> bool: ... + + +@overload +def example9(x: bytes, y: Literal['o1', 'o2']) -> bool: ... + + +@overload +def example9(x: bytes, y: str) -> int: ... + + +def example9(x: str | bytes, y: str) -> bool | int: + return True + + +def check_example9(x: Any): + # All three overloads are candidates. The parameter types corresponding to + # argument `x` are `str` and `bytes`, which are not equivalent, so none of + # the overloads can be eliminated. We fall back to `Any`. + ret1 = example9(x, 'o1') + assert_type(ret1, Any) + # The second and third overload are candidates. The parameter type + # corresponding to argument `x` is `bytes` in both candidates, so we can + # eliminate the third overload. + ret2 = example9(x, 'o2') + assert_type(ret2, bool) + + +@overload +def example10(x: int) -> bool: ... + + +@overload +def example10(*args: int) -> int: ... + + +def example10(*args: int, **kwargs: int) -> int: + return 0 + + +def check_example10(x: Any): + # The parameters corresponding to argument `x` (`x` in the first overload + # and `*args` in the second) both have type `int`, so the second overload + # can be eliminated. + ret = example10(x) + assert_type(ret, bool) + + +@overload +def example11(x: Literal['o1'], y: int, z: str) -> bool: ... + + +@overload +def example11(x: str, y: int, z: str) -> int: ... + + +def example11(x: str, y: int, z: str) -> bool | int: + return True + + +def check_example11(x: Any): + # `*x` maps to `(y: int, z: str)` in both overloads, so the second overload + # can be eliminated. + ret1 = example11('o1', *x) + assert_type(ret1, bool) + ret2 = example11('o1', x, x) + assert_type(ret2, bool) + + +class A[T]: + x: T + + def f(self) -> T: + return self.x + + +@overload +def example12(x: A[None]) -> A[None]: ... + + +@overload +def example12(x: A[Any]) -> A[Any]: ... + + +def example12(x: A[Any]) -> A[Any]: + return x + + +def check_example12(x: Any): + # Step 5 eliminates the first overload because there exists a + # materialization of `A[Any]` that is not assignable to `A[None]`. Step 6 + # picks the second overload. + ret = example12(x) + assert_type(ret, A[Any]) diff --git a/docs/spec/overload.rst b/docs/spec/overload.rst index 7d9ef1bc1..fe244eee6 100644 --- a/docs/spec/overload.rst +++ b/docs/spec/overload.rst @@ -268,39 +268,66 @@ If so, eliminate overloads that do not have a variadic parameter. Step 5 ~~~~~~ -For all arguments, determine whether all possible -:term:`materializations ` of the argument's type are assignable to -the corresponding parameter type for each of the remaining overloads. If so, -eliminate all of the subsequent remaining overloads. +For each of the candidate overloads, determine whether all arguments satisfy at +least one of the following conditions: -Consider the following example:: +- All possible :term:`materializations ` of the argument's type + are assignable to the corresponding parameter type, or +- The parameter types corresponding to this argument in all of the candidate + overloads are :term:`equivalent`. For an unpacked argument, this condition is + satisfied if the argument maps to the same number of parameters in all + candidate overloads, and for each parameter, the types in all candidate + overloads are equivalent. + +If so, eliminate all of the subsequent candidate overloads. + +Consider the following examples:: @overload - def example(x: list[int]) -> int: ... + def example1(x: list[int]) -> int: ... @overload - def example(x: list[Any]) -> str: ... + def example1(x: list[Any]) -> str: ... @overload - def example(x: Any) -> Any: ... + def example1(x: Any) -> Any: ... def test(a: list[Any]): # All materializations of list[Any] will match either the first or # second overload, so the third overload can be eliminated. - example(a) + example1(a) + +and:: + + @overload + def example2(x: str, y: Literal['o1']) -> bool: ... + @overload + def example2(x: str, y: str) -> int: ... + + def test(x: Any): + # The parameter type corresponding to argument `x` is `str` in both + # overloads, and all materializations of argument `y`'s type of + # `Literal['o1']` match the first overload, so the second overload can be + # eliminated. + example2(x, 'o1') This rule eliminates overloads that will never be chosen even if the caller eliminates types that include ``Any``. -If the call involves more than one argument, all possible materializations of -every argument type must be assignable to its corresponding parameter type. -If this condition exists, all subsequent remaining overloads should be eliminated. - Once this filtering process is applied for all arguments, examine the return -types of the remaining overloads. If these return types include type variables, -they should be replaced with their solved types. If the resulting return types -for all remaining overloads are :term:`equivalent`, proceed to step 6. +types of the remaining overloads. If these return types include type variables +that are scoped to the called function, they should be replaced with their +solved types. -If the return types are not equivalent, overload matching is ambiguous. In -this case, assume a return type of ``Any`` and stop. +Next, keep only the overloads for which both of the following are true: +* All :term:`materializations ` of every other candidate + overload's return type are assignable to this overload's return type, and +* This overload's return type is assignable to every other candidate overload's + return type. + +This rule picks the most general return type, if one exists. + +- If no candidate overloads remain, overload matching is ambiguous. In this + case, assume a return type of ``Any`` and stop. +- If one or more candidate overloads remain, proceed to step 6. Step 6 ~~~~~~ @@ -397,6 +424,24 @@ Example 4:: r2 = example4(v2, 1) reveal_type(r2) # Should reveal Any +Example 5:: + + class A[T]: + x: T + def f(self) -> T: + return self.x + + @overload + def example5(x: A[None]) -> A[None]: ... + @overload + def example5(x: A[Any]) -> A[Any]: ... + + def test(x: Any): + # Step 5 eliminates the first overload because there exists a + # materialization of `A[Any]` that is not assignable to `A[None]`. Step 6 + # picks the second overload. + reveal_type(example5(x)) # Should reveal `A[Any]` + .. _argument-type-expansion: