Skip to content

trait_selection: Preserve eager normalization failures - #162618

Draft
Dnreikronos wants to merge 3 commits into
rust-lang:mainfrom
Dnreikronos:trait_solver_preserve_normalization_failure
Draft

trait_selection: Preserve eager normalization failures#162618
Dnreikronos wants to merge 3 commits into
rust-lang:mainfrom
Dnreikronos:trait_solver_preserve_normalization_failure

Conversation

@Dnreikronos

@Dnreikronos Dnreikronos commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Draft experiment following the discussion on #161407 and Zulip about preserving eager normalization failures.

The nested inherent associated type case fails during eager normalization, but the fallback can accept it. Eager normalization reduces the inner alias to &'b (), then tries to equate Foo<for<'b> fn(&'b ())> with the impl's Foo<fn(&'a ())>. The impl lifetime sits outside the for<'b> binder, so that relation fails the leak check. The fallback retries the original nested aliases. That turns the lifetime equalities into constraints returned by nested goals, which instantiate_and_apply_query_response marks VisibleForLeakCheck::No. The parent leak check misses them and the fallback succeeds. Resolving the resulting inference variable at the end hides the rejected relation.

I changed normalize_with_universes to return a Result carrying the failed obligation. Deep normalization returns that error immediately. The final resolve_vars_if_possible workaround proposed in #161407 is absent here. The general policy for nested constraints stays as it is.

For the infallible normalize entry point, I kept recovery inside the function for now. It catches the error, puts the failed obligation in the list, then runs ReplaceAliasWithInfer over the original value. The visitor still creates inference variables and obligations for aliases without escaping bound vars. For aliases with escaping bound vars, it leaves the alias in place and no longer creates placeholder obligations. Keeping the original failure means a successful fallback can't silently discard it.

My thinking was to get deep normalization to report the failure while keeping the existing normalize callers working. I think making normalization fallible throughout is the better direction, with HIR typeck reporting the failure and replacing aliases with TyKind::Error. I haven't made those caller changes here. That's the part I'd like to settle before taking this further.

The fallback normalization path can build a value from fresh inference
variables and return their obligations separately. Fulfillment may
later constrain those variables without rebuilding the folded value.

Resolve the value after fulfillment so deep normalization does not
return inference nodes that the context has already constrained.
The failure is only observable through a debug assertion because the
caller resolves the value before returning. Run the test only with
compiler debug assertions enabled.

Use a const function and an intentionally invalid body to reach the MIR
normalization path in metadata-only UI tests.
@rustbot rustbot added 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. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Sep 10, 2026
@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

The job test-aarch64-gnu-llvm-21-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
---- [ui] tests/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice.rs stdout ----
Saved the actual stderr to `/checkout/obj/build/aarch64-unknown-linux-gnu/test/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice/implied_outlives_bounds_not_resolving_vars_ice.stderr`
diff of stderr:

- error[E0478]: lifetime bound not satisfied
+ error[E0271]: type mismatch resolving `Foo<fn(&())>::Assoc == &()`
2   --> $DIR/implied_outlives_bounds_not_resolving_vars_ice.rs:13:11
3    |
4 LL | fn bar(_: fn(Foo<for<'b> fn(Foo<fn(&'b ())>::Assoc)>::Assoc)) {}

-    |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ
6 
- error[E0478]: lifetime bound not satisfied
+ error[E0271]: type mismatch resolving `Foo<fn(&())>::Assoc == &()`
8   --> $DIR/implied_outlives_bounds_not_resolving_vars_ice.rs:13:11
9    |
10 LL | fn bar(_: fn(Foo<for<'b> fn(Foo<fn(&'b ())>::Assoc)>::Assoc)) {}

-    |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ
12    |
13    = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`
14 

- error: higher-ranked subtype error
-   --> $DIR/implied_outlives_bounds_not_resolving_vars_ice.rs:13:8
-    |
- LL | fn bar(_: fn(Foo<for<'b> fn(Foo<fn(&'b ())>::Assoc)>::Assoc)) {}
-    |        ^
+ error: aborting due to 2 previous errors
20 
- error: aborting due to 3 previous errors
- 
---
To only update this specific test, also pass `--test-args traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice.rs`

error: 1 errors occurred comparing output.
status: exit status: 1
command: env -u RUSTC_LOG_COLOR RUSTC_ICE="0" RUST_BACKTRACE="short" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2/bin/rustc" "/checkout/tests/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice.rs" "-Zsimulate-remapped-rust-src-base=/rustc/FAKE_PREFIX" "-Ztranslate-remapped-path-to-local-path=no" "-Z" "ignore-directory-in-diagnostics-source-blocks=/cargo" "-Z" "ignore-directory-in-diagnostics-source-blocks=/checkout/vendor" "--sysroot" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2" "--target=aarch64-unknown-linux-gnu" "--check-cfg" "cfg(test,FALSE)" "--error-format" "json" "--json" "future-incompat" "-Ccodegen-units=1" "-Zui-testing" "-Zdeduplicate-diagnostics=no" "-Zwrite-long-types-to-disk=no" "-Cstrip=debuginfo" "--emit" "metadata" "-C" "prefer-dynamic" "--out-dir" "/checkout/obj/build/aarch64-unknown-linux-gnu/test/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice" "-Znext-solver=coherence" "-A" "unused" "-W" "unused_attributes" "-A" "internal_features" "-A" "incomplete_features" "-A" "unused_parens" "-A" "unused_braces" "-Crpath" "-Cdebuginfo=0" "-Lnative=/checkout/obj/build/aarch64-unknown-linux-gnu/native/rust-test-helpers" "-Znext-solver"
stdout: none
--- stderr -------------------------------
error[E0271]: type mismatch resolving `Foo<fn(&())>::Assoc == &()`
##[error]  --> /checkout/tests/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice.rs:13:11
   |
LL | fn bar(_: fn(Foo<for<'b> fn(Foo<fn(&'b ())>::Assoc)>::Assoc)) {}
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ

error[E0271]: type mismatch resolving `Foo<fn(&())>::Assoc == &()`
##[error]  --> /checkout/tests/ui/traits/next-solver/implied_outlives_bounds_not_resolving_vars_ice.rs:13:11
   |
LL | fn bar(_: fn(Foo<for<'b> fn(Foo<fn(&'b ())>::Assoc)>::Assoc)) {}
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ
   |
   = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`

error: aborting due to 2 previous errors

@lcnr

lcnr commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

sgtm, this is now 2 unrelated changes, is it?

  • we change ReplaceAliasesWithInfer to keep hr aliases around
  • we change the inner normalize to return Err

I think we should do both. Generally try to only do changes in the same PR if they build on each other

@lcnr

lcnr commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

ah, with the ReplaceAliasesWithInfer. I think we still want to emit a Projection obligation, just not return an infer var 🤔

@Dnreikronos

Copy link
Copy Markdown
Contributor Author

sgtm, this is now 2 unrelated changes, is it?

  • we change ReplaceAliasesWithInfer to keep hr aliases around
  • we change the inner normalize to return Err

I think we should do both. Generally try to only do changes in the same PR if they build on each other

Yeah, I can split them. My thinking was that once we kept the eager failure, we already had an error to report, so I dropped the extra obligations for higher-ranked aliases in the fallback. That's why I bundled the two changes. I'd prefer to keep this one focused on normalize_with_universes returning Err and work through the visitor behavior separately.

ah, with the ReplaceAliasesWithInfer. I think we still want to emit a Projection obligation, just not return an infer var 🤔

yep, I removed too much there. I figured keeping the eager failure gave us the error we needed, so I dropped the higher-ranked projection obligations during recovery ;/. But that first failure doesn't cover every alias the visitor walks. I'll keep emitting the Projection obligation while leaving the alias in the returned type. I think handling that separately will make it easier to reason about, so I'll keep this PR focused on normalize_with_universes returning Err.
Sounds good for you?

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. 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.

4 participants