Skip to content

move TypeOutlives normalization out of region checking#152270

Open
JohnTitor wants to merge 11 commits intorust-lang:mainfrom
JohnTitor:normalize-before-region-obl
Open

move TypeOutlives normalization out of region checking#152270
JohnTitor wants to merge 11 commits intorust-lang:mainfrom
JohnTitor:normalize-before-region-obl

Conversation

@JohnTitor
Copy link
Member

This adds two more normalization steps for outlives before region checking, as mentioned in the issue.

r? @lcnr
Fixes #151461 and fixes rust-lang/trait-system-refactor-initiative#260

@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. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Feb 7, 2026
Comment on lines +90 to +94
for obligation in infcx.take_registered_region_obligations() {
if seen_outputs.insert((obligation.sup_type, obligation.sub_region)) {
normalized_obligations.push(obligation);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

we can't guarantee that the newly registered obligations are normalized.

What I would like to do instead is to normalize in fn compute_type_outlives_goal, rn we register the goal right away, but we could deeply normalize the type first. This means that the trait solver handles overflow for us then.

We don't have a folder to deeply normalize in the solver, so either duplicate the existing impl used by deeply_normalize (which can only be used outside of the solver) ,and change it to use an ecx, or make it generic over the normalization procedure

Copy link
Contributor

Choose a reason for hiding this comment

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

we should entirely remove the fn resolve_regions_with_normalize and instead expect the obligations to already be normalized

Copy link
Member Author

@JohnTitor JohnTitor Feb 14, 2026

Choose a reason for hiding this comment

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

I see removed the normalization steps (I couldn't remove function entirely as it's used in rustc_trait_selection) in region checking and tried to implement it on next solver: 3f9571c

I don't think I've implemented normalizer on next solver completely but would like to receive your review.

(Seems ICE'd but I'd like to confirm I'm going to the right way)

@JohnTitor JohnTitor force-pushed the normalize-before-region-obl branch from af4e816 to 3f9571c Compare February 14, 2026 09:54
@rustbot
Copy link
Collaborator

rustbot commented Feb 14, 2026

Some changes occurred to the core trait solver

cc @rust-lang/initiative-trait-system-refactor

@rustbot

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

assert!(!self.in_snapshot(), "cannot process registered region obligations in a snapshot");

// Must loop since the process of normalizing may itself register region obligations.
// We loop to handle the case that processing one obligation ends up registering another.
Copy link
Contributor

Choose a reason for hiding this comment

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

this should now never happen. Afaik we only added the loop in the PR doing normalization. Probably useful to look at the git blame here to revert pretty much all the unnecessary changes here

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 see, addressed: be997ff

Comment on lines +120 to +122
/// With the next solver, `TypeOutlives` goals are normalized while they are
/// evaluated (and before being registered as region obligations), so region
/// resolution does not need to normalize them.
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 the framing of this comment only makes sense while doing the change in this PR and is otherwise confusing.

I think if we want to talk about normalization at all here, we should mention that type outlives obligations are expected to already be normalized at this point.

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 was almost for myself, removed: 794d33d (this PR)

@@ -0,0 +1,15 @@
//@ compile-flags: -Znext-solver=globally

Copy link
Contributor

Choose a reason for hiding this comment

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

please add comment explaining what we're testing here and why it ICE'd

Copy link
Member Author

Choose a reason for hiding this comment

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

Added with link: c7b2252

&ObligationCause::dummy_with_span(span),
);
Some(Certainty::Yes)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

try removing the fast path. I think it's not necessary for performance and easy to get wrong. We currently incorrectly register things if they have inference variables in them. These inference variables could then get instantiated with something that references an (higher-ranked) alias we have to normalize

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed: 2f8c93e

// obligations so that later processing does not have to structurally process aliases.
let ty = self.resolve_vars_if_possible(ty);
let ty = if ty.has_aliases() {
self.deeply_normalize_for_outlives(goal.param_env, ty)
Copy link
Contributor

Choose a reason for hiding this comment

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

do the has_aliases check in the function instead of before calling it 🤔 while moving these sorts of checks out functions is generally good, here this check is an optimization of this function, so it should stay inside of it

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, addressed: 471f298

return Ok(ct);
}

let ty::ConstKind::Unevaluated(..) = ct.kind() else {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: please use an if let instead of an early return

Copy link
Member Author

Choose a reason for hiding this comment

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

Addresed in 471f298

return Ok(ty);
}

let ty::Alias(..) = ty.kind() else { return ty.try_super_fold_with(self) };
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

Copy link
Member Author

Choose a reason for hiding this comment

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

Addresed in 471f298

),
);

let result = match self.ecx.try_evaluate_goal(GoalSource::TypeRelating, goal) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why try_evaluate_goal?

Thinking about how to implement this, I'd take a look at the FindParamInClause visitor

I expect this function to do:

  • call structurally_normalize_term instead of manually evaluating
  • if you encounter an infer var, return Certainty::Maybe (change the error ty of the type folder to be Result<Certainty, NoSolution>)
  • if it errors, return an error
  • if you hit the recursion limit, return overflow (i think FindParamInClause also needs to check the recursion limit and can currently cause stack overflows)

With this the returned type should be fully normalized

when encountering binders, you need to map the bound vars to placeholders, and then map placeholders back to bound vars on success (same as the deeply_normalize folder

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in 471f298 but I'm still not sure if it follows your intention completely. I had to tweak some checks in compute_type_outlives_goal otherwise it'd emit spammy recursion limit errors for may test cases on next solver.

self.probe(|_| inspect::ProbeKind::NormalizedSelfTyAssembly)
.enter(|ecx| ecx.deeply_normalize_for_outlives(goal.param_env, ty))
.ok()
.unwrap_or(ty)
Copy link
Contributor

Choose a reason for hiding this comment

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

what do you mean with "this avoids spurious overflows"?

I would expect us to always deeply_normalize and if we returb Err(Ok(certainty)) we don't register a type outlives but instead call evaluate_added_goals_and_make_canonical_response with that certainty. Actually, please change deeply_normalize_for_outlvies to return a MaybeCause instead as we should never return Certainty::Yes

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahhh, I see, that's why other compiler error tests emit recursion limit errors on that change.
Changed the response type and corrected the checks with MaybeCause: 1d8274e

@rust-log-analyzer

This comment has been minimized.

@JohnTitor JohnTitor force-pushed the normalize-before-region-obl branch from 471f298 to 1d8274e Compare February 16, 2026 13:20
@rustbot

This comment has been minimized.

Comment on lines +339 to +348
let ty = self.shallow_resolve(ty);
if !ty.has_aliases() {
return Ok(ty);
}
if ty.has_non_region_infer() {
return Err(Ok(MaybeCause::Ambiguity));
}
if ty.has_opaque_types() || ty.has_non_region_param() || ty.has_non_region_placeholders() {
return Ok(ty);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems odd, why?

why return if it has opaques or placeholders

We do need to normalize if the ty has no aliases but infer vars

Copy link
Member Author

Choose a reason for hiding this comment

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

Otherwise tests/ui/traits/next-solver/cycles/cycle-modulo-ambig-aliases.rs starts to emit overflow errors, is it ok here?

Copy link
Contributor

Choose a reason for hiding this comment

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

this test already emits overflow errors, so this seems fine 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in fd760f2.
But it now returns a lot of recursion errors, maybe we could stop emitting by comparing alias ty with arg ty?
And I've also seen a lot of unwanted (I think) errors/ICEs. I've struggled with this for a while but no clue, I'd like to ask what's I'm wrong here.

|| ty.has_non_region_placeholders()
{
return Err(Ok(MaybeCause::Ambiguity));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

match on ty. if infer, return ambiguity, if alias ,normalize, otherwise, recur

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in fd760f2 (but I'm struggled, see above).

}

/// The inverse of [`BoundVarReplacer`]: replaces placeholders with the bound vars from which
/// they came.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove the version outside of rustc_next_trait-solver and use this one in rustc_trait_selection

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah sorry it exists, addressed in ce67440.

@rust-log-analyzer

This comment has been minimized.

opaque_types_jank: OpaqueTypesJank::AllGood,
});
}
Err(Err(NoSolution)) => ty,
Copy link
Contributor

Choose a reason for hiding this comment

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

propagate the Err(NoSolution) to the caller. The TypeOutlives goal doesn't hold if normalization failed

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in 2bc1fa0!

@JohnTitor JohnTitor force-pushed the normalize-before-region-obl branch from 1d8274e to 2bc1fa0 Compare March 4, 2026 12:48
@rustbot
Copy link
Collaborator

rustbot commented Mar 4, 2026

This PR was rebased onto a different main 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.

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

Labels

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. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ICE]: unexpected overflowed when processing region obligations move TypeOutlives normalization out of region checking again

4 participants