Skip to content

Conversation

@BoxyUwU
Copy link
Member

@BoxyUwU BoxyUwU commented Nov 6, 2025

r? lcnr

"remove normalize call"

Fixes #132765

If the normalization fails we would sometimes get a TypeError containing inference variables created inside of the probe used by coercion. These would then get leaked out causing ICEs in diagnostics logic

"leak check and lub for closure<->closure coerce-lubs of same defids"

Fixes rust-lang/trait-system-refactor-initiative#233

fn peculiar() -> impl Fn(u8) -> u8 {
    return |x| x + 1
}

the |x| x + 1 expr has a type of Closure(?31t) which we wind up inferring the RPIT to. The CoerceMany ret_coercion for the whole peculiar typeck has an expected type of RPIT (unnormalized). When we type check the return |x| x + 1 expr we go from the never type to Closure(?31t) which then participates in the ret_coercion giving us a coerce-lub(RPIT, Closure(?31t)).

Normalizing RPIT gives us some Closure(?50t) where ?31t and ?50t have been unified with ?31t as the root var. resolve_vars_if_possible doesn't resolve infer vars to their roots so these wind up with different structural identities so the fast path doesn't apply and we fall back to coercing to a fn ptr. cc #147193 which also fixes this

New solver probably just gets more inference variables here because canonicalization + generally different approach to normalization of opaques. Idk :3

FCP worthy stuffy

there are some other FCP worthy things but they're in my FCP comment which also contains some analysis of the breaking nature of the previously listed changes in this PR: #148602 (comment)

- leak checking the lub for fndef<->fndef coerce-lubs
- start lubbing closure<->closure coerce-lubs and leak check it
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Nov 6, 2025
@BoxyUwU BoxyUwU changed the title non-breaking parts of #147565 misc coercion cleanups and handle safety correctly Nov 6, 2025
@rust-log-analyzer

This comment has been minimized.

@BoxyUwU BoxyUwU force-pushed the coercion_cleanup_uncontroversial branch from bc29ba9 to d3e3eaa Compare November 7, 2025 15:53
@BoxyUwU BoxyUwU added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. A-coercions Area: implicit and explicit `expr as Type` coercions T-types Relevant to the types team, which will review and decide on the PR/issue. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-clippy Relevant to the Clippy team. labels Nov 7, 2025
@BoxyUwU BoxyUwU marked this pull request as ready for review November 7, 2025 16:22
@rustbot
Copy link
Collaborator

rustbot commented Nov 7, 2025

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri

Some changes occurred in compiler/rustc_codegen_cranelift

cc @bjorn3

This PR changes rustc_public

cc @oli-obk, @celinval, @ouz-a

This PR changes a file inside tests/crashes. If a crash was fixed, please move into the corresponding ui subdir and add 'Fixes #' to the PR description to autoclose the issue upon merge.

Some changes occurred to constck

cc @fee1-dead

@BoxyUwU
Copy link
Member Author

BoxyUwU commented Nov 7, 2025

Hi @rust-lang/types. I've recently gone over our implementation of coercions and discovered a few things which I'd like to change which need to go through an FCP. I'm not really sure if it makes sense to split this into three separate FCPs or not so I've just kept it all together. Let me know if you'd rather me split them apart :)

I am not including lang on this FCP as this is either clear bug fixes that make our behaviour more consistent and agree with what is already in the reference, or its just incredibly minor type checking stuff :)

@rfcbot merge

Leak Checks in coerce-lub to fn-pointers

Background Context

For misc context on the leak checking see a previous FCP by lcnr: #119820. Also note that due to not having implied bounds on binders the leak check can incorrectly produce errors that would be satisfied if we took implied bounds into account.

leak check fndef lub fndef

When performing a coerce-lub between two FnDefs we first attempt to see if they have some common supertype (infcx.lub). If there is a common supertype we return that instead of coercing the FnDefs to fn-pointers.

macro_rules! lub {
    ($lhs:expr, $rhs:expr) => {
        if true { $lhs } else { $rhs }
    };
}

fn foo<T: ?Sized>() {}

// This performs a `coerce-lub(foo<?lhs>, foo<?rhs)` operation, which
// will infer `?lubbed_ty = foo<?lhs>, ?lhs=?rhs`.
let r: /* ?lubbed_ty */ = lub!(foo::<_ /* ?lhs */>, foo::<_ /* ?rhs */>);

// assert that `r` is a ZST/`FnDef`
unsafe { std::mem::transmute::<_, ()>(r) }

On stable we do not leak check after this infcx.lub operation. This can lead us to incorrectly believe two FnDefs have a mutual supertype and avoid coercing to fn-pointers.

This FCP proposes that we start leak checking there. See this test for a code example that will go from error->compile with this change: tests/ui/coercion/leak_check_fndef_lub.rs

This is theoretically breaking. There could exist deadcode which performs a coerce-lub of two unequal-by-binders FnDefs and then only typechecks due to the lubbed type being a FnDef.

With this change we would now (correctly) coerce those FnDefs to fn-pointers resulting in an error. See this test for a code example that will go from compile->error with this change: tests/ui/coercion/leak_check_fndef_lub_deadcode_breakage.rs

I believe this theoeretical breakage to be acceptable. I do not expect this to be encountered in practice let alone often, crater results agree with this showing no regressions due to this change.

If someone does wind up encountering this I still think this is acceptable. This feels like a clear bug fix to me and certainly falls under "allowable" inference breakage.

perform closure lub closure

When performing a coerce-lub between two closures we now handle it the same way we do FnDefs (including the proposed leak check addition). We first try to determine if the two closure types have a mutual supertype and if so we return that instead of coercing both to a fn-pointer.

I don't believe this is observable on stable but can be observed on nightly via type_alias_impl_trait hacks under the new trait solver: tests/ui/coercion/lub_closures_before_fnptr_coercion.rs

Safety and Target Features in coerce-lub

Background Context

See the reference header describing target features for an explanation of how target features interact with the safety of the function: r-attributes.codegen.target_feature

safe target feature fns are fn not unsafe fn

On stable when performing a coerce-lub operation between closures and FnDefs we do not treat the FnDef's signature as being safe if it has target features enabled. This differs from normal coerce operations.

This differs from normal coerce operations where when coercing an FnDef to a fn-pointer we will produce a safe fn-pointer if the target features of the FnDef are also enabled in the current body:

#[target_feature(enable = "avx512")]
fn my_fn() {}

#[target_feature(enable = "avx512")]
fn test() {
    let a: fn() = my_fn;
}

It also differs from other coerce-lub operations when coercing between a FnDef and a fn-pointer, which does correctly allow the FnDef to coerce to a safe fn-pointer.

This FCP proposes that when acquiring the signature of a FnDef during a coerce-lub operation, we give a safe signature if all of the target features of the FnDef are enabled in the current body.

See this test for examples of the proposed behaviour: tests/ui/coercion/lub_coercion_handles_safety.rs

This is theoretically a breaking change as there could be code that relies on us incorrectly coercing safe target feature functions to unsafe fnpointers when we will now coerce them to safe fn-pointers.

I expect this to be quite unlikely as safe target features are a relatively new addition to the language (1.86.0) and also a somewhat niche feature.

The crater runs confirm this showing no breakage due to this change.

If someone does wind up encountering this it still feels acceptable to me as it is a clear bug fix and certainly falls under "allowable" inference breakage.

fn->unsafe fn in coerce-lub of fndefs and closures

On stable when performing a coerce-lub operation between closures and FnDefs we do not allow coercing closures or FnDefs of safe functons to unsafe fn-pointers directly.

This differs from normal coerce operations where when coercing n safe FnDef/closure to an unsafe fn-pointer we will produce an unsafe fn-pointer:

fn my_fn() {}

fn test() {
    let a: unsafe fn() = my_fn;
}

It also differs from other coerce-lub operations when coercing between a safe FnDef/closure and a unsafe fn-pointer, which does correctly allow the FnDef/closure to coerce to an unsafe fn-pointer.

This FCP proposes that after acquiring the signatures of both sides of the coerce-lub operation, if they have mismatched safeties we coerce the safe signature to an unsafe signature.

See this test for examples of the proposed behaviour: tests/ui/coercion/lub_coercion_handles_safety.rs

I don't expect this to be able to break code as any code involving coerce-lubs of signatures with differing safeties is currently an error on stable.

Removing an unnecessary normalize call

We currently have a redundant normalize call inside of coercion logic to re-normalize an already normalized type. This was mostly a code tidyup but happend to fix an ICE.

This is theoretically breaking as re-normalizing already normalized types is not necessarily a no-op under the old solver due to ambiguous aliases inside of binders. I don't expect this to really be encountered in practice and this is backed up by the lack of crater regressions. If there are regressions we can always just re-add a normalize call somewhere.

Crater Regressions

There were no crater regressions detected for this set of changes. The crater run can be found here: #147565#issuecomment-3492623733. This crater run had an additional change part of it which does cause regressions and is not included in this PR. An analysis of those regressions can be found here: #147565#issuecomment-3486416652, which should hopefully show that those regressions are not a problem without the additional changes that PR has.

@rust-rfcbot
Copy link
Collaborator

rust-rfcbot commented Nov 7, 2025

Team member @BoxyUwU has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Nov 7, 2025
Comment on lines +12 to +14
/// Go from a fn-item type to a fn pointer or an unsafe fn pointer.
/// It cannot convert an unsafe fn-item to a safe fn pointer.
ReifyFnPointer(hir::Safety),
Copy link
Member Author

Choose a reason for hiding this comment

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

I've gone and allowed ReifyFnPointer coercions to coerce safe fndefs to unsafe fn pointers directly instead of needing to compose two coercions. This made handling safety in coerce-lub simpler/nicer.

@rust-log-analyzer
Copy link
Collaborator

The job x86_64-gnu-tools failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
 Documenting proc_macro_test v0.1.0 (/checkout/tests/rustdoc-gui/src/proc_macro_test)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.79s
   Generated /checkout/obj/build/x86_64-unknown-linux-gnu/test/rustdoc-gui/doc/proc_macro_test/index.html
npm WARN deprecated [email protected]: < 24.10.2 is no longer supported
npm ERR! code 127
npm ERR! git dep preparation failed
npm ERR! command /node/bin/node /node/lib/node_modules/npm/bin/npm-cli.js install --force --cache=/home/user/.npm --prefer-offline=false --prefer-online=false --offline=false --no-progress --no-save --no-audit --include=dev --include=peer --include=optional --no-package-lock-only --no-dry-run
npm ERR! npm WARN using --force Recommended protections disabled.
npm ERR! npm WARN deprecated [email protected]: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm ERR! npm WARN deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm ERR! npm WARN deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm ERR! npm WARN deprecated [email protected]: This functionality has been moved to @npmcli/fs
npm ERR! npm WARN deprecated [email protected]: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
npm ERR! npm WARN deprecated [email protected]: This package is no longer supported. Please use @npmcli/package-json instead.
npm ERR! npm WARN deprecated [email protected]: This package is no longer supported.
npm ERR! npm ERR! code 127
npm ERR! npm ERR! path /home/user/.npm/_cacache/tmp/git-cloneXXXXXXspIiXs/node_modules/rollup
npm ERR! npm ERR! command failed
npm ERR! npm ERR! command sh -c patch-package
npm ERR! npm ERR! sh: 1: patch-package: not found
npm ERR! 
npm ERR! npm ERR! A complete log of this run can be found in: /home/user/.npm/_logs/2025-11-07T17_05_25_027Z-debug-0.log

npm ERR! A complete log of this run can be found in: /home/user/.npm/_logs/2025-11-07T17_05_17_591Z-debug-0.log
npm install did not exit successfully

thread 'main' (61389) panicked at src/tools/rustdoc-gui-test/src/main.rs:63:10:
unable to install browser-ui-test: Custom { kind: Other, error: "npm install returned exit code exit status: 127" }
stack backtrace:
   0: __rustc::rust_begin_unwind
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/std/src/panicking.rs:698:5
   1: core::panicking::panic_fmt
             at /rustc/3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f/library/core/src/panicking.rs:80:14

@lcnr
Copy link
Contributor

lcnr commented Nov 16, 2025

@rfcbot reviewed

@rust-rfcbot rust-rfcbot added the final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. label Nov 17, 2025
@rust-rfcbot rust-rfcbot removed the proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. label Nov 17, 2025
@rust-rfcbot
Copy link
Collaborator

🔔 This is now entering its final comment period, as per the review above. 🔔

}
ty::Ref(r_b, _, mutbl_b) => {
return self.coerce_borrowed_pointer(a, b, r_b, mutbl_b);
return self.coerce_to_ref(a, b, mutbl_b, r_b);
Copy link
Contributor

Choose a reason for hiding this comment

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

why switch args? 🤔 feels like the previous order matched the order used in the ty itself?

Comment on lines +293 to 298
let target_ty = self.use_lub.then(|| self.next_ty_var(self.cause.span)).unwrap_or(b);

push_coerce_obligation(a, target_ty);
if self.use_lub {
push_coerce_obligation(b, target_ty);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

let target_ty = if self.use_lub {
    // When computing the lub, we create a new target
    // and coerce both `a` and `b` to it.
    let target_ty = self.next_ty_var(self.cause.span);
    push_coerce_obligation(a, target_ty);
    push_coerce_obligation(b, target_ty);
    target_ty
} else {
    // When subtyping, we don't need to create a new target
    // as we only coerce `a` to `b`.
    push_coerce_obligation(a, b);
    b
};

your code felt a bit unclear to me initially

Comment on lines +442 to 444
let ty::Ref(..) = coerced_a.kind() else {
span_bug!(self.cause.span, "expected a ref type, got {:?}", coerced_a);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

convert this to an assert_matches!/assert!(matches!(..))?

Comment on lines +890 to +891
// FIXME: we shouldn't be normalizing here as coercion is inside of
// a probe. This can probably cause ICEs.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand that FIXME. Normalizing inside of a probe is totally fine as long as we don't leak the infer vars. This happens for errors of commit_if_ok but I don't think not normalizing is the solution here.

More specifically, we have to normalize here as the fn-sig is the unnnormalized one from a function definition

// We would then see that `FnDef(f)` can't be coerced to `Box<fn(?1)>`
// and return a `TypeError` referencing this new variable `?1`. This
// then caused ICEs as diagnostics would encounter inferences variables
// from the result of normalization inside of the probe used be coercion.
Copy link
Contributor

Choose a reason for hiding this comment

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

add comment that regression test for #132765?


enum LeakCheck {
Yes,
Default,
Copy link
Contributor

Choose a reason for hiding this comment

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

Default seems weird. Maybe

/// Whether to explicitly leak check in `Coerce::unify_raw`.
///
/// FIMXE: We may want to change type relations to always leak-check
/// after exiting a binder, at which point we will always do so and
/// no longer need to handle this explicitly
enum ExplicitLeakCheck {
   Yes,
   No,
}

Comment on lines 1220 to 1225
// Don't coerce pairs of fndefs or pairs of closures to fn ptrs
// if they can just be lubbed.
//
// See #88097 or `lub_closures_before_fnptr_coercion.rs` for where
// we would erroneously coerce closures to fnptrs when attempting to
// coerce a closure to itself.
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe move this comment above let lubbed_tys. Right now this is kinda confusing. It not immediately clear why we have a closure without arguments here.

Or alternatively, maybe explicitly pass in prev_ty and new_ty to it 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

resolved in a later commit

Comment on lines +12 to +14
// These don't currently lub but could in theory one day.
// If that happens this test should be adjusted to use
// fn ptrs that can't be lub'd.
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm what? 🤔

  • "fn ptrs that can't be lub'd" u mean fn defs?
  • why can they in theory be lubbed at some point? their arg is invariant so there's no way to lub them, is there?

// of an opaque with a hidden type of `Closure<C1, C2>`. This then allows
// us to substitute `C1` and `C2` for arbitrary types in the parent scope.
//
// See: <https://github.com/lcnr/random-rust-snippets/issues/13>
Copy link
Contributor

Choose a reason for hiding this comment

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

i think this test is cool given the amount of jank necessary to get it to work and it felt really good to get this to work.

I don't think it's useful for our UI suite however and will likely be annoying to handle if its behavior ever changes (which feels likely) 🤔

I would prefer to just have that snippet somewhere else, e.g. add it to lcnr/random-rust-snippets#13 or sth, instead of keeping it in our ui test suite

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

Labels

A-coercions Area: implicit and explicit `expr as Type` coercions disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-types Relevant to the types team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FulfillmentErrorCode::Project ICE for opaques [ICE]: index out of bounds

5 participants