Change:
<ins> // run-pass // This file checks that fn ptrs are considered structurally matchable. // See also rust-lang/rust#63479. fn main() { let mut count = 0; // A type which is not structurally matchable: struct NotSM; // And one that is: #[derive(PartialEq, Eq)] struct SM; fn trivial() {} fn sm_to(_: SM) {} fn not_sm_to(_: NotSM) {} fn to_sm() -> SM { SM } fn to_not_sm() -> NotSM { NotSM } // To recreate the scenario of interest in #63479, we need to add // a ref-level-of-indirection so that we descend into the type. fn r_sm_to(_: &SM) {} fn r_not_sm_to(_: &NotSM) {} fn r_to_r_sm(_: &()) -> &SM { &SM } fn r_to_r_not_sm(_: &()) -> &NotSM { &NotSM } #[derive(PartialEq, Eq)] struct Wrap<T>(T); // In the code below, we put the match input into a local so that // we can assign it an explicit type that is an fn ptr instead of // a singleton type of the fn itself that the type inference would // otherwise assign. // Check that fn() is #[structural_match] const CFN1: Wrap<fn()> = Wrap(trivial); let input: Wrap<fn()> = Wrap(trivial); match Wrap(input) { Wrap(CFN1) => count += 1, Wrap(_) => {} }; // Check that fn(T) is #[structural_match] when T is too. const CFN2: Wrap<fn(SM)> = Wrap(sm_to); let input: Wrap<fn(SM)> = Wrap(sm_to); match Wrap(input) { Wrap(CFN2) => count += 1, Wrap(_) => {} }; // Check that fn() -> T is #[structural_match] when T is too. const CFN3: Wrap<fn() -> SM> = Wrap(to_sm); let input: Wrap<fn() -> SM> = Wrap(to_sm); match Wrap(input) { Wrap(CFN3) => count += 1, Wrap(_) => {} }; // Check that fn(T) is #[structural_match] even if T is not. const CFN4: Wrap<fn(NotSM)> = Wrap(not_sm_to); let input: Wrap<fn(NotSM)> = Wrap(not_sm_to); match Wrap(input) { Wrap(CFN4) => count += 1, Wrap(_) => {} }; // Check that fn() -> T is #[structural_match] even if T is not. const CFN5: Wrap<fn() -> NotSM> = Wrap(to_not_sm); let input: Wrap<fn() -> NotSM> = Wrap(to_not_sm); match Wrap(input) { Wrap(CFN5) => count += 1, Wrap(_) => {} }; // Check that fn(&T) is #[structural_match] when T is too. const CFN6: Wrap<fn(&SM)> = Wrap(r_sm_to); let input: Wrap<fn(&SM)> = Wrap(r_sm_to); match Wrap(input) { Wrap(CFN6) => count += 1, Wrap(_) => {} }; // Check that fn() -> &T is #[structural_match] when T is too. const CFN7: Wrap<fn(&()) -> &SM> = Wrap(r_to_r_sm); let input: Wrap<fn(&()) -> &SM> = Wrap(r_to_r_sm); match Wrap(input) { Wrap(CFN7) => count += 1, Wrap(_) => {} }; // Check that fn(T) is #[structural_match] even if T is not. const CFN8: Wrap<fn(&NotSM)> = Wrap(r_not_sm_to); let input: Wrap<fn(&NotSM)> = Wrap(r_not_sm_to); match Wrap(input) { Wrap(CFN8) => count += 1, Wrap(_) => {} }; // Check that fn() -> T is #[structural_match] even if T is not. const CFN9: Wrap<fn(&()) -> &NotSM> = Wrap(r_to_r_not_sm); let input: Wrap<fn(&()) -> &NotSM> = Wrap(r_to_r_not_sm); match Wrap(input) { Wrap(CFN9) => count += 1, Wrap(_) => {} }; // Check that a type which has fn ptrs is `#[structural_match]`. #[derive(PartialEq, Eq)] struct Foo { alpha: fn(NotSM), beta: fn() -> NotSM, gamma: fn(SM), delta: fn() -> SM, } const CFOO: Foo = Foo { alpha: not_sm_to, beta: to_not_sm, gamma: sm_to, delta: to_sm, }; let input = Foo { alpha: not_sm_to, beta: to_not_sm, gamma: sm_to, delta: to_sm }; match input { CFOO => count += 1, Foo { .. } => {} }; // Final count must be 10 now if all assert_eq!(count, 10); } </ins>

Discussion:
A contrived example: This code will build on stable and beta, but not on nightly. It looks like it's related to possibly?
Compiler team triage: Definitely related to . Going to assign to for follow-up, although they are on leave until early September. It seems like this falls in a grey area: it was being accepted due to bugs in the existing code-base, however it is also a case that we likely could accept regardless of what rules we ultimately adopted. (Because the constant itself is a fn pointer.) is this regression affecting a real project?
I would say this is definitely a bug and the old behavior was correct: we're comparing function pointers so it shouldn't matter whether params derive or implement PartialEq, since no concrete parameter will be compared, but the functions themselves. Real projects being affected: I spotted the regression when updating my erlang VM implementation to latest nightly (and latest tokyo async/await): I define an Export as an enum that can either be a native function or an erlang function: Then in the instruction engine I cheat and override some of the exports to define them as a instruction (for example apply/2, apply/3 both need to modify the flow to jump to a different instruction): First I define a constant pointing to the function I want to override: Then I match on it and override: So the reason the code fails here is because is defined as , and has some custom code defined for . It's a bit of a hack, but this is more or less how erlang itself implements these specific instructions.
Reduced:
Changing to a raw pointer makes it compile successfully:
check-in from compiler triage: is back next week. A few weeks back, reported a real project affected by this regression (sorry!). As I wrote last week, from a lang-team perspective this falls into a grey zone, though I personally am inclined to try and accept the code, both for backwards compat reasons and because I don't think there's a compelling reason not to (though that depends on what complications result, I suppose). Overall, we need to settle our "constants in match patterns" story.
I'm going to leave this one nominated to bring it to felix's attention.
For the time being I've worked around the problem by taking the function pointers, casting them to usize then comparing that:
triage: P-high. Assigning to self.

Lets see if I can come up with something to fix this before this regression leaks out to stable.
(okay, after looking at the code, I think this should be pretty easy to fix and backport.)