Skip to content

Be a bit more careful around exotic cycles in in the inliner #143704

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

compiler-errors
Copy link
Member

@compiler-errors compiler-errors commented Jul 9, 2025

Copied from the comment here: #143700 (comment)


#![feature(fn_traits)]

#[inline]
pub fn a() {
    FnOnce::call_once(a, ());
    FnOnce::call_once(b, ());
}

#[inline]
pub fn b() {
    FnOnce::call_once(b, ());
    FnOnce::call_once(a, ());
}

This should demonstrate the issue. For ease of discussion, I'm gonna call the two fn-def types {a} and {b}.

When collecting the cyclic local callees in mir_callgraph_cyclic for a, we first check the first call terminator in a. We end up calling process on <{a} as FnOnce>::call_once, which ends up visiting a's instance again. This is cyclical. However, we don't end up marking FnOnce::call_once as a cyclical def id because it's a foreign item. That's fine.

When visiting the second call terminator in a, which is <{b} as FnOnce>::call_once, we end up recursing into b. We check the first terminator, which is <{b} as FnOnce>::call_once, but although that is its own mini cycle, it doesn't consider itself a cycle for the purpose of this query because it doesn't involve the root. However, when we visit the second terminator in b, which is <{a} as FnOnce>::call_once, we end up erroneously not considering that call to be cyclical since we've already inserted it into our set of seen instances, and as a consequence we don't recurse into it. This means that we never collect b as recursive.

Do this in the flipped case too, and we end up having two functions which mututally do not consider each other to be recursive participants. This leads to a query cycle.


I ended up also renaming some variables so I could more clearly understand their responsibilities in this code. Let me know if the renames are not welcome.

Fixes #143700
r? @cjgillot

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 9, 2025
@rustbot
Copy link
Collaborator

rustbot commented Jul 9, 2025

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

involved: &mut FxHashSet<LocalDefId>,
recursion_limiter: &mut FxHashMap<DefId, usize>,
recursion_limit: Limit,
) -> bool {
trace!(%caller);
let mut cycle_found = false;
let mut reaches_root = false;
Copy link
Member Author

@compiler-errors compiler-errors Jul 9, 2025

Choose a reason for hiding this comment

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

This is more like "may reach root" given that we bail out with a possibly false-positive when hitting the recursion limit, but that was a mouthful.


for &(callee, args) in tcx.mir_inliner_callees(caller.def) {
for &(callee_def_id, args) in tcx.mir_inliner_callees(caller.def) {
Copy link
Member Author

Choose a reason for hiding this comment

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

with the name callee, DefId ends up shadowing Instance ends up shadowing DefId again 😆

// end up reaching that same recursive callee through some *other* cycle.
c
} else {
seen.insert(callee, false);
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 think it's safe to consider this to be false initially.

cycle_found = true;
reaches_root = true;
seen.insert(callee, true);
continue;
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 don't think it's necessary to recurse on callees here, so I just continued.

StorageLive(_1);
StorageLive(_2);
_2 = ();
- _1 = <fn() {b} as FnOnce<()>>::call_once(b, move _2) -> [return: bb1, unwind continue];
Copy link
Member Author

Choose a reason for hiding this comment

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

It's nice to see we didn't break inlining at least for the FnOnce call. It's kind of a shame we can't inline a into b or vice versa, but it's obviously not clear which one to inline into the other without adding arbitrary ordering which is bad.

@cjgillot
Copy link
Contributor

Thanks a lot for fixing this!
@bors r+

@bors
Copy link
Collaborator

bors commented Jul 12, 2025

📌 Commit c39a187 has been approved by cjgillot

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 12, 2025
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Jul 12, 2025
…gillot

Be a bit more careful around exotic cycles in in the inliner

Copied from the comment here: rust-lang#143700 (comment)

---

```rust
#![feature(fn_traits)]

#[inline]
pub fn a() {
    FnOnce::call_once(a, ());
    FnOnce::call_once(b, ());
}

#[inline]
pub fn b() {
    FnOnce::call_once(b, ());
    FnOnce::call_once(a, ());
}
```

This should demonstrate the issue. For ease of discussion, I'm gonna call the two fn-def types `{a}` and `{b}`.

When collecting the cyclic local callees in `mir_callgraph_cyclic` for `a`, we first check the first call terminator in `a`. We end up calling process on `<{a} as FnOnce>::call_once`, which ends up visiting `a`'s instance again. This is cyclical. However, we don't end up marking `FnOnce::call_once` as a cyclical def id because it's a foreign item. That's fine.

When visiting the second call terminator in `a`, which is `<{b} as FnOnce>::call_once`, we end up recursing into `b`. We check the first terminator, which is `<{b} as FnOnce>::call_once`, but although that is its own mini cycle, it doesn't consider itself a cycle for the purpose of this query because it doesn't involve the *root*. However, when we visit the *second* terminator in `b`, which is `<{a} as FnOnce>::call_once`, we end up **erroneously** *not* considering that call to be cyclical since we've already inserted it into our set of seen instances, and as a consequence we don't recurse into it. This means that we never collect `b` as recursive.

Do this in the flipped case too, and we end up having two functions which mututally do not consider each other to be recursive participants. This leads to a query cycle.

---

I ended up also renaming some variables so I could more clearly understand their responsibilities in this code. Let me know if the renames are not welcome.

Fixes rust-lang#143700
r? `@cjgillot`
bors added a commit that referenced this pull request Jul 12, 2025
Rollup of 9 pull requests

Successful merges:

 - #143213 (de-duplicate condition scoping logic between AST→HIR lowering and `ScopeTree` construction)
 - #143461 (make `cfg_select` a builtin macro)
 - #143519 (Check assoc consts and tys later like assoc fns)
 - #143554 (slice: Mark `rotate_left`, `rotate_right` unstably const)
 - #143704 (Be a bit more careful around exotic cycles in in the inliner)
 - #143774 (constify `From` and `Into`)
 - #143786 (Fix fallback for CI_JOB_NAME)
 - #143796 (Fix ICE for parsed attributes with longer path not handled by CheckAttribute)
 - #143798 (Remove format short command trait)

r? `@ghost`
`@rustbot` modify labels: rollup
fmease added a commit to fmease/rust that referenced this pull request Jul 13, 2025
…gillot

Be a bit more careful around exotic cycles in in the inliner

Copied from the comment here: rust-lang#143700 (comment)

---

```rust
#![feature(fn_traits)]

#[inline]
pub fn a() {
    FnOnce::call_once(a, ());
    FnOnce::call_once(b, ());
}

#[inline]
pub fn b() {
    FnOnce::call_once(b, ());
    FnOnce::call_once(a, ());
}
```

This should demonstrate the issue. For ease of discussion, I'm gonna call the two fn-def types `{a}` and `{b}`.

When collecting the cyclic local callees in `mir_callgraph_cyclic` for `a`, we first check the first call terminator in `a`. We end up calling process on `<{a} as FnOnce>::call_once`, which ends up visiting `a`'s instance again. This is cyclical. However, we don't end up marking `FnOnce::call_once` as a cyclical def id because it's a foreign item. That's fine.

When visiting the second call terminator in `a`, which is `<{b} as FnOnce>::call_once`, we end up recursing into `b`. We check the first terminator, which is `<{b} as FnOnce>::call_once`, but although that is its own mini cycle, it doesn't consider itself a cycle for the purpose of this query because it doesn't involve the *root*. However, when we visit the *second* terminator in `b`, which is `<{a} as FnOnce>::call_once`, we end up **erroneously** *not* considering that call to be cyclical since we've already inserted it into our set of seen instances, and as a consequence we don't recurse into it. This means that we never collect `b` as recursive.

Do this in the flipped case too, and we end up having two functions which mututally do not consider each other to be recursive participants. This leads to a query cycle.

---

I ended up also renaming some variables so I could more clearly understand their responsibilities in this code. Let me know if the renames are not welcome.

Fixes rust-lang#143700
r? ``@cjgillot``
bors added a commit that referenced this pull request Jul 13, 2025
Rollup of 14 pull requests

Successful merges:

 - #143301 (`tests/ui`: A New Order [26/N])
 - #143461 (make `cfg_select` a builtin macro)
 - #143519 (Check assoc consts and tys later like assoc fns)
 - #143554 (slice: Mark `rotate_left`, `rotate_right` unstably const)
 - #143634 (interpret/allocation: expose init + write_wildcards on a range)
 - #143679 (Preserve the .debug_gdb_scripts section)
 - #143685 (Resolve: merge `source_bindings` and `target_bindings` into `bindings`)
 - #143704 (Be a bit more careful around exotic cycles in in the inliner)
 - #143734 (Refactor resolve resolution bindings)
 - #143774 (constify `From` and `Into`)
 - #143785 (Add --compile-time-deps argument for x check)
 - #143786 (Fix fallback for CI_JOB_NAME)
 - #143825 (clippy: fix test filtering when TESTNAME is empty)
 - #143826 (Fix command trace)

r? `@ghost`
`@rustbot` modify labels: rollup
@matthiaskrgr
Copy link
Member

@bors r-
#143866 (comment)

@bors bors added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Jul 13, 2025
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 (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

cycle detected when optimizing MIR
6 participants