Skip to content

TypeRelationError#19580

Closed
MatthewMckee4 wants to merge 23 commits intoastral-sh:mainfrom
MatthewMckee4:type-relation-error
Closed

TypeRelationError#19580
MatthewMckee4 wants to merge 23 commits intoastral-sh:mainfrom
MatthewMckee4:type-relation-error

Conversation

@MatthewMckee4
Copy link
Contributor

@MatthewMckee4 MatthewMckee4 commented Jul 27, 2025

Summary

Towards astral-sh/ty#163

Test Plan

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jul 27, 2025
@MatthewMckee4
Copy link
Contributor Author

MatthewMckee4 commented Jul 27, 2025

One thing I just can't catch is messing everything up, will go over it tmr evening. If anyone has a time for a quick scan it should be in types.rs, but im not too sure

self,
db: &'db dyn Db,
target: Type<'db>,
) -> Result<(), TypeRelationError> {
Copy link
Member

@MichaReiser MichaReiser Jul 28, 2025

Choose a reason for hiding this comment

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

Nit: It feels a bit off to me to use a Result here. I would expect an Err only if two types can't be compared but that's not how Result is used here.

I think I'd use a custom enum over Result with a Yes and No(reason) variants. We can mark the type as #[must_use] if we're worried about callers not handling the error but it seems not handling the error actually seems to be the default?

What's unclear to me is how we plan on aggregating TypeRelationErrors. But maybe that's just because it's not yet clear to me what variants TypeRelationError will have

Copy link
Member

Choose a reason for hiding this comment

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

I think using Result might have some advantages further down the line: methods like map_err could be useful for bubbling up a low-level assignability failure of a wrapped type into a high-level assignability failure for the wrapping type. But I agree that we need a more ergonomic solution for the default case: I was going to suggest having low-level methods try_type_relation, try_subtype_relation and try_assignability_relation (which you can use if you need to know why the relation doesn't apply between two types), but to also have high-level has_relation_to, is_subtype_of and is_assignable_to methods like we have now:

impl<'db> Type<'db> {
    fn has_relation_to(db: &'db dyn Db, other: Type<'db>, relation: TypeRelation) -> bool {
        self.try_type_relation(db, other, relation).is_ok()
    }

    fn is_subtype_of(db: &'db dyn Db, other: Type<'db>) -> bool {
        self.has_relation_to(db, other, TypeRelation::Subtyping)
    }

    fn is_assignable_to(db: &'db dyn Db, other: Type<'db>) -> bool {
        self.has_relation_to(db, other, TypeRelation::Assignability)
    }
}

it's a bit hard to tell what the best design is, however, until we try to start trying to use (in diagnostics) the extra information now available to us.

Copy link
Member

Choose a reason for hiding this comment

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

We can always implement map_err etc. on the custom enum. I just find is_ok to be too implicit and has_relation_to(db, other, relation) == Ok(()) seems to verbose (and also confusing).

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong opinion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

try_... seems like a good structure and simplifies this PR a lot

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creating our own enum also means we can implement from bool. Which also simplifies a lot of code

@github-actions
Copy link
Contributor

github-actions bot commented Jul 28, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Jul 28, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@codspeed-hq
Copy link

codspeed-hq bot commented Jul 28, 2025

CodSpeed Performance Report

Merging #19580 will not alter performance

Comparing MatthewMckee4:type-relation-error (e26ec50) with main (81867ea)

Summary

✅ 8 untouched

@codspeed-hq
Copy link

codspeed-hq bot commented Jul 28, 2025

CodSpeed WallTime Performance Report

Merging #19580 will not alter performance

Comparing MatthewMckee4:type-relation-error (e26ec50) with main (81867ea)

Summary

✅ 7 untouched benchmarks

@MatthewMckee4
Copy link
Contributor Author

Will mark as ready for some initial feedback

@MatthewMckee4
Copy link
Contributor Author

Not sure as of now what to do about the regressions

@MichaReiser
Copy link
Member

One thing you could try is ot use thin_vec. I suspect that the performance regression is mainly because the result of the type operations no longer fit into a single register. But it might also be that we return false in many places and tracking the information is anything but free.

But I think I'll need a better understanding for what we want to use this new information and if it's even worth tracking all possible errors.

It might be good to start with: What exact information do we need and, from there, backtrack on what information we need to propagate in these relation methods.

It might also be that we want separate relation methods to avoid the overhead in the most common path.

@MatthewMckee4
Copy link
Contributor Author

It's probably also useful if I turn TypeRelationError into an enum? All of the todo fails right now will create a new vec, but for the most part most vec will have one item

@AlexWaygood
Copy link
Member

But I think I'll need a better understanding for what we want to use this new information and if it's even worth tracking all possible errors.

It might be good to start with: What exact information do we need and, from there, backtrack on what information we need to propagate in these relation methods.

astral-sh/ty#163 (comment) and astral-sh/ty#163 (comment) show examples of why we need to track this information

},
}
})
.collect::<Vec<_>>();
Copy link
Member

Choose a reason for hiding this comment

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

this is a lot more expensive than what we do on main because it means that we now have to iterate through the entire MRO (and allocate a Vec) whereas previously we could short-circuit as soon as we spotted that one class was a subclass of the other. We should avoid .collect() calls in this and similar cases wherever possible, and continue to short-circuit. I think in most cases, it's probably okay if we only provide a single reason why one type is not a subtype of another, even if there are multiple underlying reasons why the relation cannot be satisfied.

Protocols might be an exception to this, but we can deal with that later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I realised this, will address later

@MichaReiser
Copy link
Member

astral-sh/ty#163 (comment) and astral-sh/ty#163 (comment) show examples of why we need to track this information

What's not clear to me from this if this is something we need to integrate into the is_ methods directly or if we should have a separate method that visits a type again and then diagnoses why it isn't assignable (by re-testing each union element to find the one that isn't assignable).

@AlexWaygood
Copy link
Member

What's not clear to me from this if this is something we need to integrate into the is_ methods directly or if we should have a separate method that visits a type again and then diagnoses why it isn't assignable (by re-testing each union element to find the one that isn't assignable).

Yeah, that's a good question. It would be interesting to experiment with some different designs here.

The nice thing about the current approach in this PR is that it does not increase the maintenance burden of the type-relational methods. I would definitely not want to go back to a situation similar to what we had before #18430: having separate Type::is_assignable_to and Type::is_subtype_of methods was a maintenance nightmare that led to many subtle bugs across our code. I'm concerned that your suggestion might similarly lead to us trying to have duplicated implementations of Type::has_relation_to which would be very hard to keep in sync (this method is very subtle, and very complex!).

There might also be ways of implementing your suggestion that would not significantly increase the maintenance burden. On the other hand, I also think there are some obvious ways of optimising this PR branch, as I commented above

@MatthewMckee4
Copy link
Contributor Author

Nice, thats a bit better

@MatthewMckee4
Copy link
Contributor Author

nice!!

@MatthewMckee4
Copy link
Contributor Author

There's maybe an argument to rename TypeRelationErrorKind to TypeRelationErrorReason?

I'm also not sure if we should have a base (NoReason) case, it seems like most of those cases would be a simple DoesNotInheritFrom or similar.

@MatthewMckee4
Copy link
Contributor Author

Is there any interest in taking this further?

@AlexWaygood
Copy link
Member

Is there any interest in taking this further?

Yes, definitely! I think we absolutely need something like this.

Sorry we haven't got back to you, that's on me :-( There's a lot of things to juggle right now.

@MatthewMckee4
Copy link
Contributor Author

All good, when you want to revisit let me know and I can fix the merge conflicts

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

Comments