Skip to content

Conversation

ada4a
Copy link
Contributor

@ada4a ada4a commented Aug 20, 2025

this is based #15519, but mainly to avoid gnarly rebase conflicts later

changelog: none

@rustbot
Copy link
Collaborator

rustbot commented Aug 20, 2025

r? @Alexendoo

rustbot has assigned @Alexendoo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Aug 20, 2025
@ada4a
Copy link
Contributor Author

ada4a commented Aug 20, 2025

r? clippy

@rustbot rustbot assigned blyxyas and unassigned Alexendoo Aug 20, 2025
@ada4a ada4a force-pushed the non_canonical_impls branch 2 times, most recently from c0dd48b to e10a7a2 Compare August 22, 2025 13:32
@rustbot

This comment has been minimized.

@ada4a ada4a force-pushed the non_canonical_impls branch from e10a7a2 to e93a9fa Compare August 24, 2025 06:16
@rustbot

This comment has been minimized.

@blyxyas
Copy link
Member

blyxyas commented Aug 24, 2025

:0 triagebot was updated with new functionality. Neat!

@blyxyas
Copy link
Member

blyxyas commented Sep 5, 2025

Profiling results: 0.5% performance improvement found on tokio for this pull request.

@ada4a
Copy link
Contributor Author

ada4a commented Sep 5, 2025

That's nice, but also a bit unexpected -- this PR (as far as I can recollect from looking at the diff lol) only moves some stuff around. My only explanation would be that reducing the size of the check function allowed LLVM to optimize it better? Or does a 0.5% change lie within noice threshold @blyxyas?

@blyxyas
Copy link
Member

blyxyas commented Sep 5, 2025

Hi Ada, sorry to announce that my previous comment was not accurate. I just forgot to propagate the -Wclippy::non_canonical* rust flag to master. The actual numbers are not in the noticeable range.

@ada4a
Copy link
Contributor Author

ada4a commented Sep 5, 2025

Ah, thanks for letting me know. But yeah, I wasn't expecting them to be, so all good:)

&& !block.span.in_external_macro(cx.sess().source_map())
&& !is_from_proc_macro(cx, impl_item)
{
let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id);
Copy link
Member

Choose a reason for hiding this comment

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

Move this check into a fairly up position with matches!(trait_name, Some(sym::Clone) | Some(sym::PartialOrd)), as it's a fairly cheap check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The link points to the source code of get_diagnostic_item, not get_diagnostic_name? And I've always heard the opposite -- that get_diagnostic_name is fairly expensive, and one should at least avoid calling it twice. But if you say so...

I'm also not exactly sure how you see this being implemented, is it something like this?

diff --git i/clippy_lints/src/non_canonical_impls.rs w/clippy_lints/src/non_canonical_impls.rs
index c385f03ba..56422909d 100644
--- i/clippy_lints/src/non_canonical_impls.rs
+++ w/clippy_lints/src/non_canonical_impls.rs
@@ -115,6 +115,9 @@ impl LateLintPass<'_> for NonCanonicalImpls {
     fn check_impl_item<'tcx>(&mut self, cx: &LateContext<'tcx>, impl_item: &ImplItem<'tcx>) {
         if let Node::Item(item) = cx.tcx.parent_hir_node(impl_item.hir_id())
             && let Some(trait_impl) = cx.tcx.impl_trait_ref(item.owner_id).map(EarlyBinder::skip_binder)
+            && let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id)
+            // NOTE: check this early to avoid expensive checks that come after this one
+            && matches!(trait_name, Some(sym::Clone | sym::PartialOrd))
             && !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())
             && let ImplItemKind::Fn(_, impl_item_id) = impl_item.kind
             && let body = cx.tcx.hir_body(impl_item_id)
@@ -122,7 +125,6 @@ impl LateLintPass<'_> for NonCanonicalImpls {
             && !block.span.in_external_macro(cx.sess().source_map())
             && !is_from_proc_macro(cx, impl_item)
         {
-            let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id);
             if trait_name == Some(sym::Clone)
                 && let Some(copy_def_id) = cx.tcx.get_diagnostic_item(sym::Copy)
                 && implements_trait(cx, trait_impl.self_ty(), copy_def_id, &[])

Copy link
Member

@blyxyas blyxyas Sep 22, 2025

Choose a reason for hiding this comment

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

get_diagnostic_name, get_diagnostic_item, etc... all call to all_diagnostic_items(()) which calls todiagnostic_items(krate_id). This function is querified via the query system (that's why the definition on rustdoc goes to a macro).

Queries cache their results, so just running them the first time is expensive. And we call these functions many times throughout the compiler (every time we want to mention a diagnostic item, so not few). And apart from that, an unconditional all_diagnostic_items(()) is executed here

So the cost of computing diagnostic items doesn't really incur much, just the cost of retrieving the result =^w^=

NOTE: Yep that's more or less how I'd implement it

Copy link
Contributor Author

@ada4a ada4a Sep 22, 2025

Choose a reason for hiding this comment

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

[...] all call to all_diagnostic_items(()) which calls to diagnostic_items(krate_id). This function is querified via the query system (that's why the definition on rustdoc goes to a macro).

That is indeed what I would have expected, weird that I was hearing people say the opposite.. Thank you again for clearing this up:)

Copy link
Member

Choose a reason for hiding this comment

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

The link points to the source code of get_diagnostic_item, not get_diagnostic_name? And I've always heard the opposite -- that get_diagnostic_name is fairly expensive, and one should at least avoid calling it twice.

Not doing the job twice is always better. My recommendation is usually to avoid calling is_diagnostic_item(A) || is_diagnostic_item(B) when you can call matches!(get_diagnostic_name(), Some(A | B)), that prevents a double-lookup.

Copy link
Member

Choose a reason for hiding this comment

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

And also, such a lookup is also more expensive (even if cheap) than a structural matching against Expr::kind for example.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Sep 21, 2025
@ada4a
Copy link
Contributor Author

ada4a commented Sep 21, 2025

@rustbot ready

I put the incorporation of the review feedback into its own commit, because all the comments were about the pre-existing code.. Do let me know if you'd prefer something else

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Sep 21, 2025
Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! ❤️

View changes since this review

Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

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

Reverting the approval just for being safe, I was testing yesterday if diagnostic_items actually caches its results or not, as it has the eval_always. (No changes are actually requested, just blocking the PR from merging by another maintainer)

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Sep 23, 2025
Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

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

Just checked, and indeed queries with eval_always only mean that they are not taken into account between incremental compilations. I'll make a PR to r-l/r because it's a really confusing name TwT.

Just one more nit and I'll merge this

View changes since this review

Comment on lines 118 to 122
&& let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id)
// NOTE: check this early to avoid expensive checks that come after this one
&& matches!(trait_name, Some(sym::Clone | sym::PartialOrd))
&& !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())
&& let ImplItemKind::Fn(_, impl_item_id) = impl_item.kind
Copy link
Member

Choose a reason for hiding this comment

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

Could you move this statement above?

Suggested change
&& let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id)
// NOTE: check this early to avoid expensive checks that come after this one
&& matches!(trait_name, Some(sym::Clone | sym::PartialOrd))
&& !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())
&& let ImplItemKind::Fn(_, impl_item_id) = impl_item.kind
&& let trait_name = cx.tcx.get_diagnostic_name(trait_impl.def_id)
&& let ImplItemKind::Fn(_, impl_item_id) = impl_item.kind
// NOTE: check this early to avoid expensive checks that come after this one
&& matches!(trait_name, Some(sym::Clone | sym::PartialOrd))
&& !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the motivation is that this is a cheap check that could filter out many cases, then I'd maybe put it as the very first one?

A bit off-topic: staring at the let-chain a bit more, why do we even start from an ImplItem, anyway? It looks like we:

  1. start from the fn clone/eq
  2. go up to the impl
  3. check if it's impl Clone/PartialEq for S
  4. check whether S implements the other trait
  5. check whether the fn clone/eq we started with is the canonical implementation

But I think it would be less confusing to:

  1. start from an impl
  2. check if it's impl Clone/PartialEq for S
  3. check whether S implements the other trait
  4. descend to the method(s) and check those

Copy link
Member

Choose a reason for hiding this comment

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

Great thinking outside the box. If you want, now it's the moment to implement it. It has a +1 from me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great thinking outside the box. If you want, now it's the moment to implement it. It has a +1 from me

Thank you^^ But in all honesty I'm not completely sure which of the two points you are referring to, so I'll implement the former in this PR, and probably create a second PR for the latter?

Copy link
Contributor

@Jarcho Jarcho Sep 23, 2025

Choose a reason for hiding this comment

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

If you're going to start at the impl you'll want something like:

enum Trait {
    Clone,
    PartialOrd
}
impl Trait {
    fn from_def_id(lang_items: &LanguageItems, did: DefId) -> Option<Self> {
        if Some(trait_id) == lang_items.clone_trait() {
            Some(Self::Clone)
        } else if Some(did) == lang_items.partial_ord_trait() {
            Some(Self::PartialOrd)
        } else {
            None
        }
    }
}

fn check_item(&mut self,cx: &LateContext<'_>,item: &'_ rustc_hir::Item<'_>) {
    if let ItemKind::Impl(impl_) = item.kind
        && let Some(trait_) = impl_.of_trait
        && impl_.items.len() <= 2
        && let Some(trait_id) = trait_.trait_ref.path.res.opt_def_id()
        && let Some(trait_) = Trait::from_def_id(cx.tcx.lang_items(), trait_id)
        && !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())
    {
        match trait_ {
            Trait::Clone => match *impl_.items {
                [assoc, _] if let assoc = cx.tcx.hir_impl_item(assoc)
                        && assoc.name == sym::clone
                => check_clone(assoc),
                [assoc] | [_, assoc] => check_clone(cx.tcx.hir_impl_item(assoc)),
                _ => return,
            }
            Trait::PartialOrd
                if let Some(assoc) = impl_.items.iter().find_map(|&id| {
                    let assoc = cx.tcx.hir_impl_item(assoc)
                    (assoc.name == sym::partial_cmp).then_some(assoc)
                }) => check_partial_ord(assoc),
            _ => return,
        }
    }
}

This will minimize the number of impl blocks that need to call queries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you very much @Jarcho! Though check_clone does check clone_from as well, so I'll hopefully be able to simplify the first match arm a bit:)

Copy link
Contributor

Choose a reason for hiding this comment

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

Just edited to handle the extra methods on PartialOrd. Forgot those existed.

too many lines no more!
imo it's a bit more clear than checking for the same thing (`expr.kind`)
in multiple branches
@ada4a ada4a force-pushed the non_canonical_impls branch from 577e22c to 21a5948 Compare September 23, 2025 14:37
@rustbot
Copy link
Collaborator

rustbot commented Sep 23, 2025

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@ada4a
Copy link
Contributor Author

ada4a commented Sep 23, 2025

Done^^ Also rebased onto the latest master, because the build-on-save would cause the old rust version to be used, completely wrecking (AFAICT) the incremental compilation cache

@blyxyas
Copy link
Member

blyxyas commented Sep 24, 2025

Are you sure it's done? I'm not seeing a change similar to what Jason suggested...

@ada4a
Copy link
Contributor Author

ada4a commented Sep 24, 2025

I opened a separate PR for it (#15749), since it's a rather major change to the lint structure (and seems to have a bug currently, need to look into that). But I can pull it into this PR as well, if you prefer^^

Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! ❤️

View changes since this review

@blyxyas blyxyas added this pull request to the merge queue Sep 24, 2025
Merged via the queue into rust-lang:master with commit 3e0590c Sep 24, 2025
11 checks passed
@ada4a ada4a deleted the non_canonical_impls branch September 24, 2025 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants