Skip to content

Conversation

@dianne
Copy link
Contributor

@dianne dianne commented Dec 31, 2025

As of #138499, trying to evaluate a const block in anything depended on by borrow-checking will result in a query cycle. Since that could happen in constant promotion, this PR adds a check for const blocks there to stop them from being evaluated.

Admittedly, this is a hack. See #124328 for discussion of a more principled fix: removing cases like this from constant promotion altogether. To simplify the conditions under which promotion can occur, we probably shouldn't be implicitly promoting division or array indexing at all if possible. That would likely require a FCW and migration period, so I figure we may as well patch up the cycle now and simplify later.

Fixes #150464

I'll also lang-nominate this for visibility. I'm not sure there's much to discuss about this PR specifically, but it does represent a change in semantics. In Rust 1.87, the code below compiled. In Rust 1.88, it became a query cycle error. After this PR, it fails to borrow-check because the temporaries can no longer be promoted.

let (x, y, z);
// We only promote array indexing if the index is known to be in-bounds.
x = &([0][const { 0 }] & 0);
// We only promote integer division if the divisor is known not to be zero.
y = &(1 / const { 1 });
// Furthermore, if the divisor is `-1`, we only promote if the dividend is
// known not to be `int::MIN`.
z = &(const { 1 } / -1);
// The borrowed temporaries can't be promoted, so they were dropped at the ends
// of their respective statements.
(x, y, z);

@rustbot
Copy link
Collaborator

rustbot commented Dec 31, 2025

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

@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 Dec 31, 2025
@rustbot
Copy link
Collaborator

rustbot commented Dec 31, 2025

r? @lcnr

rustbot has assigned @lcnr.
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

@dianne dianne added T-lang Relevant to the language team I-lang-nominated Nominated for discussion during a lang team meeting. labels Dec 31, 2025
@dianne
Copy link
Contributor Author

dianne commented Dec 31, 2025

Since this adds some extra query calls, a perf run may be warranted. @bors try @rust-timer queue

This can be mitigated by reordering the logic to only try const-evaluating after we do other checks (e.g. to make sure we can promote the array we're indexing into, for array indexing), but since this fixes a beta regression, I figure I'll start with a small patch in case there's appetite for a backport.

@rust-timer

This comment has been minimized.

@rust-bors

This comment has been minimized.

rust-bors bot added a commit that referenced this pull request Dec 31, 2025
Don't try to evaluate const blocks during constant promotion
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Dec 31, 2025
@rust-log-analyzer

This comment has been minimized.

@dianne dianne force-pushed the no-const-block-eval-in-promotion branch from c4492d6 to 304c5af Compare December 31, 2025 22:54
@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 1, 2026

☀️ Try build successful (CI)
Build commit: e44c792 (e44c792db0a3a17223067eaa688596a57d30423f, parent: 8d670b93d40737e1b320fd892c6f169ffa35e49e)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (e44c792): comparison URL.

Overall result: no relevant changes - no action needed

Benchmarking this pull request means it may be perf-sensitive – we'll automatically label it not fit for rolling up. You can override this, but we strongly advise not to, due to possible changes in compiler perf.

@bors rollup=never
@rustbot label: -S-waiting-on-perf -perf-regression

Instruction count

This benchmark run did not return any relevant results for this metric.

Max RSS (memory usage)

This benchmark run did not return any relevant results for this metric.

Cycles

Results (primary 2.2%, secondary 0.6%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.2% [2.2%, 2.2%] 1
Regressions ❌
(secondary)
2.8% [2.8%, 2.8%] 1
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-1.5% [-1.5%, -1.5%] 1
All ❌✅ (primary) 2.2% [2.2%, 2.2%] 1

Binary size

This benchmark run did not return any relevant results for this metric.

Bootstrap: 477.934s -> 476.786s (-0.24%)
Artifact size: 390.83 MiB -> 390.85 MiB (0.01%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Jan 1, 2026
@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels Jan 1, 2026
@cuviper
Copy link
Member

cuviper commented Jan 2, 2026

Since #150464 is a beta regression from crater:

@rustbot label beta-nominated

However, there was only one failure of this sort in the whole crater run, and further minimizations also failed back to 1.88, so it's probably not a big deal if we let it ride.

@rustbot rustbot added the beta-nominated Nominated for backporting to the compiler in the beta channel. label Jan 2, 2026
Comment on lines 694 to 695
Const::Ty(..) | Const::Val(..) => true,
Const::Unevaluated(uc, _) => self.tcx.def_kind(uc.def) != DefKind::InlineConst,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Const::Ty(..) | Const::Val(..) => true,
Const::Unevaluated(uc, _) => self.tcx.def_kind(uc.def) != DefKind::InlineConst,
// `Const::Ty` is always a `ConstKind::Param` right now and that can never be turned
// into a mir value for promotion
// FIXME(mgca): do we want uses of type_const to be normalized during promotion?
Const::Ty(..) => false,
Const::Val(..) => true,
// Other kinds of unevaluated's should be able to cause query cycles too, but
// inline consts *always* cause query cycles
Const::Unevaluated(uc, _) => self.tcx.def_kind(uc.def) != DefKind::InlineConst,

On stable a Const::Ty is always a ConstKind::Param which you can't do anything meaningful with.

Am I correct in assuming non inline consts can also cause a query cycle here? The following example seems to demonstrate this:

const fn foo() -> &'static u32 {
    &(10 / <()>::ASSOC)
}

trait Trait {
    const ASSOC: u32;
}

impl Trait for () {
    const ASSOC: u32 = *foo();
}

I think this is the kind of cycle we're talking about here?

Copy link
Contributor Author

@dianne dianne Jan 2, 2026

Choose a reason for hiding this comment

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

The cycle there has the same shape, but I find it conceptually a bit different, since it arises from a cyclic dependency in user code: by definition, CTFE of <()>::ASSOC requires CTFE of foo, which requires CTFE of <()>::ASSOC. Even if we weren't trying to evaluate <()>::ASSOC during constant promotion, it and foo are both unusable. Since the cycle is unavoidable, I don't think we need to catch it in constant promotion.

The cycle for inline consts is due to how they're borrow-checked along with their typeck root. If borrow-checking the containing body requires evaluating the constant, this gives rise to a cycle, since evaluating the constant requires borrow-checking it, which again means borrow-checking the containing body. However, using the inline const is perfectly fine as long as we don't evaluate it until after borrow-checking.

I'll tack a note onto to the "other kinds of unevaluated's" comment to clarify why the inline const cycle in particular needs to be avoided.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the suggested change with further elaboration in the comment: (diff)

Comment on lines 21 to 23
// At the time #150464 was reported, the index as evaluated before checking whether the indexed
// expression and index expression could themselves be promoted. This can't be promoted since
// it references a local, but it still resulted in a query cycle.
Copy link
Member

@BoxyUwU BoxyUwU Jan 2, 2026

Choose a reason for hiding this comment

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

Does that mean this PR makes that code go from query cycle -> pass? I feel like it'd be nice to be more clear/explicit about which specific code is fixed by this PR. Afaict any code which doesn't actually need promotion to take place but had a const block there will now pass whereas was previously broken by #138499?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's right, yeah. I've made the wording more explicit and added some additional examples to hopefully make the tests flow better conceptually (and catch if changes to constant promotion would make it become out-of-date): (diff)

@jackh726
Copy link
Member

jackh726 commented Jan 3, 2026

I'll also lang-nominate this for visibility. I'm not sure there's much to discuss about this PR specifically, but it does represent a change in semantics. In Rust 1.87, the code below compiled. In Rust 1.88, it became a query cycle error. After this PR, it fails to borrow-check because the temporaries can no longer be promoted.

let (x, y, z);
// We only promote array indexing if the index is known to be in-bounds.
x = &([0][const { 0 }] & 0);
// We only promote integer division if the divisor is known not to be zero.
y = &(1 / const { 1 });
// Furthermore, if the divisor is `-1`, we only promote if the dividend is
// known not to be `int::MIN`.
z = &(const { 1 } / -1);
// The borrowed temporaries can't be promoted, so they were dropped at the ends
// of their respective statements.
(x, y, z);

This feels not-correct. I would expect that a const block would be "transparent" to static promotion - I would expect that all of these would continue to compile. Changing this to not compiling just to fix an implementation-detail (query cycle) seems wrong.

@dianne
Copy link
Contributor Author

dianne commented Jan 3, 2026

This is something that'd need a language team decision, but I'd argue long term we shouldn't be implicitly promoting integer division or array accesses at all (per #124328). Having a bunch of special cases for things we can promote makes Rust harder to reason about (and harder to write lints/diagnostics for, IME). In this light, my feeling is that we shouldn't complicate the compiler implementation to make &(1 / const { 1 }) promote.

@dianne
Copy link
Contributor Author

dianne commented Jan 3, 2026

A more targeted alternative to this PR could be to reorder the logic for checking if array indexing can be used in promoted constants, so that it recursively checks the array and index before evaluating the index to see if it's in-bounds. That'd fix the regression reported in #150464 (since it'd bail when finding that the array can't be used in a promoted constant) but would leave in the query cycles for cases where whether we promote something depends on CTFE of an inline constant (like &([0][const { 0 }] & 0))

@dianne dianne force-pushed the no-const-block-eval-in-promotion branch from 304c5af to d758793 Compare January 3, 2026 09:51
@RalfJung
Copy link
Member

RalfJung commented Jan 7, 2026

This is something that'd need a language team decision, but I'd argue long term we shouldn't be implicitly promoting integer division or array accesses at all (per #124328). Having a bunch of special cases for things we can promote makes Rust harder to reason about (and harder to write lints/diagnostics for, IME). In this light, my feeling is that we shouldn't complicate the compiler implementation to make &(1 / const { 1 }) promote.

Also to be clear, there is a clear migration path for affected code: explicit promotion, expressed as const { &(1 / 1) }.

@dianne
Copy link
Contributor Author

dianne commented Jan 7, 2026

the code that ran into this in the crater run had the form: assert_eq!(a, b[offset_of!(Type, field)] & c);, which doesn't have an obvious place to put the const block. Although you could still assign it in a variable instead.

The b[...] & c there doesn't need to be promoted to compile (and for many ways of writing b and c, it can't be promoted anyway). If it did need to be promoted though, you could put the entire b[...] & c expression in a const block. Possibly a more contrived macro could result in something that couldn't straightforwardly be migrated, but I'd be surprised if implicit promotion was relied on in such a case.

@RalfJung
Copy link
Member

RalfJung commented Jan 7, 2026

Yeah I was talking about code that needs promotion, i.e. code that worked before #138499 but is still broken after this PR. assert_eq!(a, b[offset_of!(Type, field)] & c); should be code that works again unchanged after this PR.

(Confusingly @theemathas now deleted their comment. Not a good idea to do that after notifications have already been sent. :)

@traviscross traviscross added P-lang-drag-0 Lang team prioritization drag level 0.https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang. and removed P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels Jan 14, 2026
@scottmcm
Copy link
Member

scottmcm commented Jan 14, 2026

Also to be clear, there is a clear migration path for affected code: explicit promotion, expressed as const { &(1 / 1) }.

Clarifying question: would &const { 1 / 1 } also continue to work here? Or does the & have to be inside?

EDIT: from @dianne in the lang meeting, yes, that's still fine because the const { … } is always promotable -- the error is in calculating the const block, not in deciding whether the expression is promotion-eligible.


Also, if this allows more code to compile, it does need a lang FCP because reverting it would break that code again.

@traviscross
Copy link
Contributor

traviscross commented Jan 14, 2026

We talked about this in the lang call. Thanks @dianne for joining us to talk about it. This sounds right.

@rfcbot fcp merge lang

@rust-rfcbot

This comment was marked as duplicate.

@traviscross

This comment was marked as duplicate.

@rust-rfcbot
Copy link
Collaborator

rust-rfcbot commented Jan 14, 2026

Team member @traviscross 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!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
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 Jan 14, 2026
@traviscross
Copy link
Contributor

traviscross commented Jan 14, 2026

The idea, as I'm given to understand, is that query cycles were holding space for what we'd otherwise be breaking here. So we're expecting this to only allow new things to compile. E.g., this will work now:

fn main() {
    let y = &(1 / const { 1 });
}

As @dianne says:

As long as the referent of y isn't required to live past the current block, this works. Instead of being promoted, this results in a lifetime-extended temporary.

To err on the safe side, let's check this hypothesis that query cycles were holding space for us in all cases with a crater run.

@bors try

@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Jan 14, 2026
Don't try to evaluate const blocks during constant promotion
@tmandry
Copy link
Member

tmandry commented Jan 14, 2026

@rfcbot reviewed

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

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

@nikomatsakis
Copy link
Contributor

@rfcbot reviewed

@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 14, 2026

☀️ Try build successful (CI)
Build commit: a7b168a (a7b168ac4977e8e93a43448b44847adaa305d5a9, parent: 8c52f735abd1af9a73941b78fe7ed2ab08b9c0dd)

@traviscross
Copy link
Contributor

@craterbot check

@craterbot
Copy link
Collaborator

👌 Experiment pr-150557 created and queued.
🤖 Automatically detected try build a7b168a
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot added S-waiting-on-crater Status: Waiting on a crater run to be completed. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beta-nominated Nominated for backporting to the compiler in the beta channel. 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-nominated Nominated for discussion during a lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-0 Lang team prioritization drag level 0.https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang. S-waiting-on-crater Status: Waiting on a crater run to be completed. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

regression: "cycle detected when borrow-checking" in offset_of!