Skip to content

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Apr 29, 2025

Summary

@sharkdp and I realised in our 1:1 this morning that our control flow for assert statements isn't quite accurate at the moment. Namely, for something like this:

def _(x: int | None):
    assert x is None, reveal_type(x)

we currently reveal None for x here, but this is incorrect. In actual fact, the msg expression of an assert statement (the expression after the comma) will only be evaluated if the test (x is None) evaluates to False. As such, we should be adding a constraint of ~None to x in the msg expression, which should simplify the inferred type of x to int in that context ((int | None) & ~None -> int).

Test Plan

Mdtests added.

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Apr 29, 2025
@AlexWaygood AlexWaygood force-pushed the alex/assert-narrowing branch from 72af944 to b28723b Compare April 29, 2025 10:55
@github-actions
Copy link
Contributor

github-actions bot commented Apr 29, 2025

mypy_primer results

Changes were detected when running on open source projects
PyWinCtl (https://github.com/Kalmat/PyWinCtl)
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:21:8: Cannot resolve import `AppKit`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:22:8: Cannot resolve import `Quartz`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:25:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:25:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:25:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_macos.py:25:6: Cannot resolve import `pywinbox`
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:28:13: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:29:12: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:68:35: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:120:29: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:168:109: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:379:60: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:379:90: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:396:62: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:396:96: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_macos.py:421:31: Variable of type `Never` is not allowed in a type expression
- warning[lint:possibly-unresolved-reference] src/pywinctl/_pywinctl_macos.py:1546:54: Name `hSubMenu` used when possibly not defined
- error[lint:unresolved-import] src/pywinctl/_pywinctl_win.py:28:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_win.py:28:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_win.py:28:6: Cannot resolve import `pywinbox`
- error[lint:unresolved-import] src/pywinctl/_pywinctl_win.py:28:6: Cannot resolve import `pywinbox`
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:48:35: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:74:29: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:109:146: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:286:42: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:301:48: Variable of type `Never` is not allowed in a type expression
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:314:16: Type `<module 'ctypes'>` has no attribute `windll`
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:366:9: Type `<module 'ctypes'>` has no attribute `windll`
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:371:9: Type `<module 'ctypes'>` has no attribute `windll`
- error[lint:invalid-type-form] src/pywinctl/_pywinctl_win.py:453:69: Variable of type `Never` is not allowed in a type expression
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:461:9: Type `<module 'ctypes'>` has no attribute `windll`
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:521:27: Type `<module 'ctypes'>` has no attribute `windll`
- error[lint:unresolved-attribute] src/pywinctl/_pywinctl_win.py:522:27: Type `<module 'ctypes'>` has no attribute `windll`
- Found 83 diagnostics
+ Found 50 diagnostics

graphql-core (https://github.com/graphql-python/graphql-core)
- warning[lint:possibly-unresolved-reference] tests/execution/test_map_async_iterable.py:274:19: Name `anext` used when possibly not defined
- Found 734 diagnostics
+ Found 733 diagnostics

optuna (https://github.com/optuna/optuna)
- error[lint:invalid-type-form] tests/pruners_tests/test_successive_halving.py:116:26: Variable of type `Never` is not allowed in a type expression
- Found 2432 diagnostics
+ Found 2431 diagnostics

apprise (https://github.com/caronc/apprise)
- error[lint:unresolved-attribute] test/test_apprise_config.py:467:20: Type `Literal[ConfigBase]` has no attribute `parse_url`
- Found 3597 diagnostics
+ Found 3596 diagnostics

rotki (https://github.com/rotki/rotki)
- warning[lint:unresolved-reference] rotkehlchen/tests/unit/test_ethereum_inquirer.py:425:9: Name `etherscan_tx_or_tx_receipt_calls` used when not defined
- Found 3396 diagnostics
+ Found 3395 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:30:20: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:31:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:34:30: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:42:42: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:156:24: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:383:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:399:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:485:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/bytecode_injection/core.py:590:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_10.py:15:32: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_10.py:15:48: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:37:57: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:55:31: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:55:49: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:68:53: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:122:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:193:33: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:207:47: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:251:32: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_11.py:251:48: Variable of type `Never` is not allowed in a type expression
- warning[lint:possibly-unresolved-reference] ddtrace/internal/coverage/instrumentation_py3_11.py:445:30: Name `ext_instr` used when possibly not defined
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:27:32: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:27:48: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:40:31: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:40:55: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:59:11: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_12.py:59:27: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_13.py:21:32: Variable of type `Never` is not allowed in a type expression
- error[lint:invalid-type-form] ddtrace/internal/coverage/instrumentation_py3_13.py:21:48: Variable of type `Never` is not allowed in a type expression
- Found 7030 diagnostics
+ Found 7001 diagnostics

@AlexWaygood AlexWaygood force-pushed the alex/assert-narrowing branch from b28723b to 22ffa02 Compare April 29, 2025 11:00
Copy link
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@AlexWaygood AlexWaygood reopened this Apr 29, 2025
@carljm carljm removed their request for review April 29, 2025 13:55
@AlexWaygood AlexWaygood enabled auto-merge (squash) April 29, 2025 16:59
@AlexWaygood AlexWaygood disabled auto-merge April 29, 2025 16:59
@AlexWaygood
Copy link
Member Author

Oh, it looks like 56fa086 introduced lots of new mypy_primer hits @sharkdp

@carljm
Copy link
Contributor

carljm commented Apr 29, 2025

Just spot-checking the very first error (from parso), it looks to me like this PR is now wrongly suggesting that there is a possible control flow path from the assertion message to the following code, where in fact, the assertion message control flow path is always terminal.

@sharkdp
Copy link
Contributor

sharkdp commented Apr 29, 2025

Should not have pushed just before leaving. I'll fix that, of course.

@sharkdp sharkdp force-pushed the alex/assert-narrowing branch from 112b9d6 to 2b8cbbd Compare April 29, 2025 19:21
@sharkdp sharkdp closed this Apr 29, 2025
@sharkdp sharkdp reopened this Apr 29, 2025
@sharkdp
Copy link
Contributor

sharkdp commented Apr 29, 2025

Ok, this looks much better. The three new diagnostics on attrs are extremely weird — looking into it.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me (pending investigation of the new attrs diagnostics.)

The new psycopg2 diagnostics look related at least in part to not understanding cyclic control flow, though it's not clear to me why this PR surfaces them.

@sharkdp
Copy link
Contributor

sharkdp commented Apr 29, 2025

The new psycopg2 diagnostics look related at least in part to not understanding cyclic control flow, though it's not clear to me why this PR surfaces them.

I think it's related to #17723. I will look into that tomorrow.

@sharkdp sharkdp force-pushed the alex/assert-narrowing branch from 2b8cbbd to 08db457 Compare April 30, 2025 07:32
@sharkdp
Copy link
Contributor

sharkdp commented Apr 30, 2025

@AlexWaygood The new fuzz check claims it found a new panic on this branch. The following snippet panics indeed:

class name_3[name_2: name_0]:
    pass
name_3.name_4
assert name_3[name_0]

but it also panics on main. It's my understanding that we still detect a lot of panics with the fuzzer in general, so I'm going to ignore that "additional" failure — but let me know if you think that it should be reported separately.

If it's just the fuzzer test that's being flaky, maybe that's something to be aware of / to look into.

@sharkdp
Copy link
Contributor

sharkdp commented Apr 30, 2025

Ok, this looks much better. The three new diagnostics on attrs are extremely weird — looking into it.

Turns out this was also related to #17723. After fixing this, these new diagnostics are gone again.

The new psycopg2 diagnostics look related at least in part to not understanding cyclic control flow, though it's not clear to me why this PR surfaces them.

It's true that we don't completely understand that code. But in the absence of cyclic control flow, we should not emit an error, because the else branch is "unreachable":

assert# some assertion that affected reachability constraints below (without the fix for #17723)

first = True
for params in params_seq:
    if first:  # always true, in the absence of cyclic control flow
        pgq =first = False
    else:
        use(pgq)  # unreachable, in the absence of cyclic control flow

After fixing #17723, we now "correctly" identify that else branch as unreachable again, and no diagnostics are emitted.

@sharkdp sharkdp merged commit 8a6787b into main Apr 30, 2025
34 of 35 checks passed
@sharkdp sharkdp deleted the alex/assert-narrowing branch April 30, 2025 07:57
@AlexWaygood AlexWaygood restored the alex/assert-narrowing branch April 30, 2025 11:11
@AlexWaygood
Copy link
Member Author

@AlexWaygood The new fuzz check claims it found a new panic on this branch. The following snippet panics indeed:

class name_3[name_2: name_0]:
    pass
name_3.name_4
assert name_3[name_0]

It's weird to me that it's highlighting this snippet specifically. It looks like something that could well be affected by this branch, since it includes an assert statement on the final line. But I agree with you -- it looks like it panics in exactly the same way on the commit prior to this one.

but it also panics on main. It's my understanding that we still detect a lot of panics with the fuzzer in general, so I'm going to ignore that "additional" failure — but let me know if you think that it should be reported separately.

If it's just the fuzzer test that's being flaky, maybe that's something to be aware of / to look into.

Yes, this seems to be flakiness in the fuzzer test. Passing the --only-new-bugs flag (which is what we're doing in the CI job) should mean that it ignores pre-existing panics, and only reports new panics relative to the main-branch baseline. I'm looking into it.

@AlexWaygood AlexWaygood deleted the alex/assert-narrowing branch April 30, 2025 11:21
@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 30, 2025

@sharkdp there is a bug in the fuzzer script here... but it's not quite what it seemed.

There is indeed code that involves asserts which now panics following this PR, but did not panic prior to this PR. Try running red-knot on https://gist.github.com/AlexWaygood/0925516f7b39516cf02f5dac6fca541e for a repro.

What happened here is the following:

  1. The fuzzer script ran the new version of red-knot on a large file of randomly generated code (the gist I linked to above), and spotted that red-knot panicked.
  2. The fuzzer script then ran the baseline version of red-knot on the same file, noticed that the old version of red-knot did not panic, and deduced that the panic constituted a "new red-knot bug".
  3. The fuzzer script then used a different Python library to minimize the large file of randomly generated code to a snippet of code that was as small as possible but still reproduced a panic from red-knot. It accidentally "overly minimized" the snippet: the minimal repro is now something that the baseline version of red-knot also panics on.

I'll update the minimization logic in the fuzzer script to be more sophisticated about this in the future, so that it doesn't produce such confusing results. I don't know if it's worth looking into the panic from https://gist.github.com/AlexWaygood/0925516f7b39516cf02f5dac6fca541e right now or not. As you say, we have plenty of pre-existing cases involving asserts and/or PEP-695 classes that produce panics. I'll leave that decision to you :-)

@sharkdp
Copy link
Contributor

sharkdp commented Apr 30, 2025

The fuzzer script then used a different Python library to minimize the large file of randomly generated code to a snippet of code that was as small as possible but still reproduced a panic from red-knot. It accidentally "overly minimized" the snippet: the minimal repro is now something that the baseline version of red-knot also panics on.

Ahh — that makes sense. Thank you for the analysis. I think I'm going to skip analyzing this further for now.

dcreager added a commit that referenced this pull request Apr 30, 2025
* main:
  [red-knot] Use 'full' salsa backtrace output that includes durability and revisions (#17735)
  [red-knot] Initial support for protocol types (#17682)
  [red-knot] Computing a type ordering for two non-normalized types is meaningless (#17734)
  [red-knot] Include salsa backtrace in check and mdtest panic messages (#17732)
  [red-knot] Fix control flow for `assert` statements (#17702)
  [red-knot] Fix recording of negative visibility constraints (#17731)
  [red-knot] Update salsa (#17730)
  [red-knot] Support overloads for callable equivalence (#17698)
  [red-knot] Run py-fuzzer in CI to check for new panics (#17719)
  Upload red-knot binaries in CI on completion of linux tests (#17720)
  [`flake8-use-pathlib`] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call (#17705)
@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 30, 2025

Here is a minimized repro of something that did not panic prior to this PR, but does now (courtesy of a fixed version of the fuzzer script that I have locally):

class name_3[name_2: name_0]:
    pass
name_3.name_4
assert name_3[name_0]
import name_0

@AlexWaygood
Copy link
Member Author

I think it is probably still the case that this PR just exposed a pre-existing issue rather than actually introducing a new issue. So I think it's still okay to just leave this for now.

@sharkdp
Copy link
Contributor

sharkdp commented Apr 30, 2025

I also think so. The following snippet also panics (introduces the same reachability constraints than the assertion).

class name_3[name_2: name_0]:
    pass
name_3.name_4
if name_3[name_0]:
    import name_0

@AlexWaygood
Copy link
Member Author

And here's a fix for the fuzzer issue: #17739

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants