Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] (Gradual) intersection types are not handled in assignability #14899

Open
sharkdp opened this issue Dec 10, 2024 · 7 comments
Open
Labels
help wanted Contributions especially welcome red-knot Multi-file analysis & type inference

Comments

@sharkdp
Copy link
Contributor

sharkdp commented Dec 10, 2024

The is_assignable_to-is-reflexive property test, which makes sure that we can assign any type to itself, is currently failing with counter-examples like Any & T, where T is an arbitrary type (that doesn't let the intersection collapse).

The reason for this is that we can't rely on equivalence for assignability of gradual types anymore ever since #14758. We do have some special casing for gradual types like Any and Unknown, as well as for unions and tuples of these, but we do lack handling of intersections.

This is not completely trivial. We can patch this with some superficial handling like

--- a/crates/red_knot_python_semantic/src/types.rs
+++ b/crates/red_knot_python_semantic/src/types.rs
@@ -748,6 +748,14 @@ impl<'db> Type<'db> {
                 .elements(db)
                 .iter()
                 .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
+            (Type::Intersection(intersection), ty) => intersection
+                .positive(db)
+                .iter()
+                .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
+            (ty, Type::Intersection(intersection)) => intersection
+                .positive(db)
+                .iter()
+                .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
             (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
                 let self_elements = self_tuple.elements(db);
                 let target_elements = target_tuple.elements(db);

which also works for things like T & ~Any, because we always add Any/Unknown as positive contributions to intersections.

But then quickcheck finds types like ~tuple[Any, T], for which the simple handling above breaks.

@sharkdp sharkdp added the red-knot Multi-file analysis & type inference label Dec 10, 2024
@carljm carljm added the help wanted Contributions especially welcome label Dec 14, 2024
@carljm
Copy link
Contributor

carljm commented Dec 14, 2024

Moving this out of Backlog and into Ready, and also marking help-wanted in case a contributor is interested in it, because I think having the quickcheck tests stable and green on main is quite valuable. Just now I was making another change to type relations, ran the quickcheck tests, and hit this failure, which diverted me for a while until I realized it was failing also on main and found this issue.

sharkdp pushed a commit that referenced this issue Dec 14, 2024
## Summary

Teach red-knot that `type[...]` is always disjoint from `None` and from
`LiteralString`. Fixes #14925.

This should properly be generalized to "all instances of final types
which are not subclasses of `type`", but until we support finality,
hardcoding `None` (which is known to be final) allows us to fix the
subtype transitivity property test.

## Test Plan

Existing tests pass, added new unit tests for `is_disjoint_from` and
`is_subtype_of`.

`QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable` fails only the "assignability
is reflexive" test, which is known to fail on `main` (#14899).

The same command, with `property_tests.rs` edited to prevent generating
intersection tests (the cause of #14899), passes all quickcheck tests.
carljm pushed a commit that referenced this issue Dec 14, 2024
…<metaclass of T>)` (#14970)

## Summary

A class is an instance of its metaclass, so `ClassLiteral("ABC")` is not
disjoint from `Instance("ABCMeta")`. However, we erroneously consider
the two types disjoint on the `main` branch. This PR fixes that.

This bug was uncovered by adding some more core types to the property
tests that provide coverage for classes that have custom metaclasses.
The additions to the property tests are included in this PR.

## Test Plan

New unit tests and property tests added. Tested with:
- `cargo test -p red_knot_python_semantic`
- `QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable`

The assignability property test fails on this branch, but that's a known
issue that exists on `main`, due to
#14899.
AlexWaygood added a commit that referenced this issue Jan 3, 2025
## Summary

We understand `sys.version_info` branches now! As such, I _believe_ this
branch is no longer required; all tests pass without it. I also ran
`QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable`, and no tests failed except for
the known issue with `Type::is_assignable_to()`
(#14899)

## Test Plan

See above
@rtpg
Copy link
Contributor

rtpg commented Jan 6, 2025

which also works for things like T & ~Any, because we always add Any/Unknown as positive contributions to intersections.

Regarding this, is Ty meant to mirror exactly the sort of structures that we'd end up with using Type<'db>? Like we would never have T & ~Any with Type, and we wouldn't have an intersection of unions (since we normalize to unions of intersections).

I think the arbitrary generator currently in the property tests will gladly generate non-normalized intersections in particular, which might be generating false positives in these tests.

@rtpg
Copy link
Contributor

rtpg commented Jan 6, 2025

After poking at this a bit more, I now feel like the main problem with the existing Ty generation is mainly that a test failure shows the Ty intead of a representation of the actual Type used. So you don't see the shape of the actual type tested, just the pre-transformed Ty smart constructor.

@AlexWaygood
Copy link
Member

After poking at this a bit more, I now feel like the main problem with the existing Ty generation is mainly that a test failure shows the Ty intead of a representation of the actual Type used. So you don't see the shape of the actual type tested, just the pre-transformed Ty smart constructor.

Yeah, agreed that this would be a big usability improvement!

@rtpg
Copy link
Contributor

rtpg commented Jan 6, 2025

The idle thought I had here is that it would be cool if quickcheck ran shrunk failures one last time after finding minimal repros, and had a "verbose log" mechanism that would let me put in "expensive" logs or TestResult error message generation, without actually causing slowdowns most of the time. Or really any sort of way to run something on the minimum repro right before stopping.

For now I might try to just add something into the test harness and see how much slower it is. Computers still are fast after all.

sharkdp pushed a commit that referenced this issue Jan 7, 2025
…#15297)

## Summary

While looking at #14899, I looked at seeing if I could get shrinking on
the examples. It turned out to be straightforward, with a couple of
caveats.

I'm calling `clone` a lot during shrinking. Since by the shrink step
we're already looking at a test failure this feels fine? Unless I
misunderstood `quickcheck`'s core loop

When shrinking `Intersection`s, in order to just rely on `quickcheck`'s
`Vec` shrinking without thinking about it too much, the shrinking
strategy is:
- try to shrink the negative side (keeping the positive side the same)
- try to shrink the positive side (keeping the negative side the same)

This means that you can't shrink from `(A & B & ~C & ~D)` directly to
`(A & ~C)`! You would first need an intermediate failure at `(A & B &
~C)` or `(A & ~C & ~D)`. This feels good enough. Shrinking the negative
side first also has the benefit of trying to strip down negative
elements in these intersections.

## Test Plan
`cargo test -p red_knot_python_semantic -- --ignored
types::property_tests::stable` still fails as it current does on `main`,
but now the errors seem more minimal.
@sharkdp
Copy link
Contributor Author

sharkdp commented Jan 8, 2025

This issue is actually a bit more general than the title/description make it sound like.

We currently don't have any logic for handling intersection types in the is_assignable_to relation. We do have special handling in is_equivalent_to and is_subtype_of, so for fully-static types, we're probably good(?). But for gradual types, it looks like we need some special handling. For example, we currently don't allow Unknown & int to be assigned to int, so something like this fails:

def f(x) -> None:
    if isinstance(x, int):
        reveal_type(x)  # Unknown & int
        y: int = x  # currently fails with invalid-assignment

We could probably write a property test like forall T, S. is_assignable_to(T & S, T) that would catch this?

@sharkdp sharkdp changed the title [red-knot] is_assignable_to is not reflexive [red-knot] (Gradual) intersection types are not handled in assignability Jan 8, 2025
@sharkdp
Copy link
Contributor Author

sharkdp commented Jan 8, 2025

This bug now has some failing unit tests here:

# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Any, int], int))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))

sharkdp added a commit that referenced this issue Jan 8, 2025
## Summary

- Add a workflow to run property tests on a daily basis (based on
`daily_fuzz.yaml`)
- Mark `assignable_to_is_reflexive` as flaky (related to #14899)
- Add new (failing) `intersection_assignable_to_both` test (also related
to #14899)

## Test Plan

Ran:

```bash
export QUICKCHECK_TESTS=100000
while cargo test --release -p red_knot_python_semantic -- \
  --ignored types::property_tests::stable; do :; done
```

Observed successful property_tests CI run
@carljm carljm added this to the Red Knot Q1 2025 milestone Jan 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Contributions especially welcome red-knot Multi-file analysis & type inference
Projects
None yet
Development

No branches or pull requests

4 participants