diff options
| author | Matthias Krüger <476013+matthiaskrgr@users.noreply.github.com> | 2025-03-12 17:59:06 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-12 17:59:06 +0100 |
| commit | d55e2e43338c2aa2612f6ee7c868cd49849add6c (patch) | |
| tree | c01d71fc5f6ae6ec79bee0c53c3ac063b29a4273 /compiler | |
| parent | 0998d4095b0f11061f78a3f9c77a87838a4c1cb7 (diff) | |
| parent | 18809a2b123752d904a9487117665115b404a513 (diff) | |
| download | rust-d55e2e43338c2aa2612f6ee7c868cd49849add6c.tar.gz rust-d55e2e43338c2aa2612f6ee7c868cd49849add6c.zip | |
Rollup merge of #137314 - lcnr:cycles-with-unknown-kind, r=compiler-errors
change definitely unproductive cycles to error builds on top of #136824 by adding a third variant to `PathKind` for paths which may change to be coinductive in the future but must not be so right now. Most notably, impl where-clauses of not yet coinductive traits. With this, we can change cycles which are definitely unproductive to a proper error. This fixes https://github.com/rust-lang/trait-system-refactor-initiative/issues/114. This does not affect stable as we keep these cycles as ambiguous during coherence. r? ````````@compiler-errors```````` ````````@nikomatsakis````````
Diffstat (limited to 'compiler')
8 files changed, 194 insertions, 82 deletions
diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs index b0bbec48947..e48ee71c858 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs @@ -271,12 +271,39 @@ where /// and will need to clearly document it in the rustc-dev-guide before /// stabilization. pub(super) fn step_kind_for_source(&self, source: GoalSource) -> PathKind { - match (self.current_goal_kind, source) { - (_, GoalSource::NormalizeGoal(step_kind)) => step_kind, - (CurrentGoalKind::CoinductiveTrait, GoalSource::ImplWhereBound) => { - PathKind::Coinductive + match source { + // We treat these goals as unknown for now. It is likely that most miscellaneous + // nested goals will be converted to an inductive variant in the future. + // + // Having unknown cycles is always the safer option, as changing that to either + // succeed or hard error is backwards compatible. If we incorrectly treat a cycle + // as inductive even though it should not be, it may be unsound during coherence and + // fixing it may cause inference breakage or introduce ambiguity. + GoalSource::Misc => PathKind::Unknown, + GoalSource::NormalizeGoal(path_kind) => path_kind, + GoalSource::ImplWhereBound => { + // We currently only consider a cycle coinductive if it steps + // into a where-clause of a coinductive trait. + // + // We probably want to make all traits coinductive in the future, + // so we treat cycles involving their where-clauses as ambiguous. + if let CurrentGoalKind::CoinductiveTrait = self.current_goal_kind { + PathKind::Coinductive + } else { + PathKind::Unknown + } } - _ => PathKind::Inductive, + // Relating types is always unproductive. If we were to map proof trees to + // corecursive functions as explained in #136824, relating types never + // introduces a constructor which could cause the recursion to be guarded. + GoalSource::TypeRelating => PathKind::Inductive, + // Instantiating a higher ranked goal can never cause the recursion to be + // guarded and is therefore unproductive. + GoalSource::InstantiateHigherRanked => PathKind::Inductive, + // These goal sources are likely unproductive and can be changed to + // `PathKind::Inductive`. Keeping them as unknown until we're confident + // about this and have an example where it is necessary. + GoalSource::AliasBoundConstCondition | GoalSource::AliasWellFormed => PathKind::Unknown, } } @@ -606,7 +633,7 @@ where let (NestedNormalizationGoals(nested_goals), _, certainty) = self.evaluate_goal_raw( GoalEvaluationKind::Nested, - GoalSource::Misc, + GoalSource::TypeRelating, unconstrained_goal, )?; // Add the nested goals from normalization to our own nested goals. @@ -683,7 +710,7 @@ where pub(super) fn add_normalizes_to_goal(&mut self, mut goal: Goal<I, ty::NormalizesTo<I>>) { goal.predicate = goal.predicate.fold_with(&mut ReplaceAliasWithInfer::new( self, - GoalSource::Misc, + GoalSource::TypeRelating, goal.param_env, )); self.inspect.add_normalizes_to_goal(self.delegate, self.max_input_universe, goal); @@ -939,7 +966,15 @@ where rhs: T, ) -> Result<(), NoSolution> { let goals = self.delegate.relate(param_env, lhs, variance, rhs, self.origin_span)?; - self.add_goals(GoalSource::Misc, goals); + if cfg!(debug_assertions) { + for g in goals.iter() { + match g.predicate.kind().skip_binder() { + ty::PredicateKind::Subtype { .. } | ty::PredicateKind::AliasRelate(..) => {} + p => unreachable!("unexpected nested goal in `relate`: {p:?}"), + } + } + } + self.add_goals(GoalSource::TypeRelating, goals); Ok(()) } diff --git a/compiler/rustc_next_trait_solver/src/solve/inspect/build.rs b/compiler/rustc_next_trait_solver/src/solve/inspect/build.rs index 1607fbb1b6a..6a8e0790f7c 100644 --- a/compiler/rustc_next_trait_solver/src/solve/inspect/build.rs +++ b/compiler/rustc_next_trait_solver/src/solve/inspect/build.rs @@ -421,7 +421,7 @@ impl<D: SolverDelegate<Interner = I>, I: Interner> ProofTreeBuilder<D> { self.add_goal( delegate, max_input_universe, - GoalSource::Misc, + GoalSource::TypeRelating, goal.with(delegate.cx(), goal.predicate), ); } diff --git a/compiler/rustc_next_trait_solver/src/solve/mod.rs b/compiler/rustc_next_trait_solver/src/solve/mod.rs index 1fa35b60304..199f0c7512e 100644 --- a/compiler/rustc_next_trait_solver/src/solve/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/mod.rs @@ -313,7 +313,9 @@ where ty::AliasRelationDirection::Equate, ), ); - self.add_goal(GoalSource::Misc, alias_relate_goal); + // We normalize the self type to be able to relate it with + // types from candidates. + self.add_goal(GoalSource::TypeRelating, alias_relate_goal); self.try_evaluate_added_goals()?; Ok(self.resolve_vars_if_possible(normalized_term)) } else { diff --git a/compiler/rustc_next_trait_solver/src/solve/project_goals.rs b/compiler/rustc_next_trait_solver/src/solve/project_goals.rs index d9452880071..944d5f0e042 100644 --- a/compiler/rustc_next_trait_solver/src/solve/project_goals.rs +++ b/compiler/rustc_next_trait_solver/src/solve/project_goals.rs @@ -24,7 +24,8 @@ where ty::AliasRelationDirection::Equate, ), ); - self.add_goal(GoalSource::Misc, goal); + // A projection goal holds if the alias is equal to the expected term. + self.add_goal(GoalSource::TypeRelating, goal); self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes) } } diff --git a/compiler/rustc_next_trait_solver/src/solve/search_graph.rs b/compiler/rustc_next_trait_solver/src/solve/search_graph.rs index 67eb442d2cc..eba496fa226 100644 --- a/compiler/rustc_next_trait_solver/src/solve/search_graph.rs +++ b/compiler/rustc_next_trait_solver/src/solve/search_graph.rs @@ -1,9 +1,9 @@ use std::convert::Infallible; use std::marker::PhantomData; -use rustc_type_ir::Interner; use rustc_type_ir::search_graph::{self, PathKind}; -use rustc_type_ir::solve::{CanonicalInput, Certainty, QueryResult}; +use rustc_type_ir::solve::{CanonicalInput, Certainty, NoSolution, QueryResult}; +use rustc_type_ir::{Interner, TypingMode}; use super::inspect::ProofTreeBuilder; use super::{FIXPOINT_STEP_LIMIT, has_no_inference_or_external_constraints}; @@ -47,7 +47,24 @@ where ) -> QueryResult<I> { match kind { PathKind::Coinductive => response_no_constraints(cx, input, Certainty::Yes), - PathKind::Inductive => response_no_constraints(cx, input, Certainty::overflow(false)), + PathKind::Unknown => response_no_constraints(cx, input, Certainty::overflow(false)), + // Even though we know these cycles to be unproductive, we still return + // overflow during coherence. This is both as we are not 100% confident in + // the implementation yet and any incorrect errors would be unsound there. + // The affected cases are also fairly artificial and not necessarily desirable + // so keeping this as ambiguity is fine for now. + // + // See `tests/ui/traits/next-solver/cycles/unproductive-in-coherence.rs` for an + // example where this would matter. We likely should change these cycles to `NoSolution` + // even in coherence once this is a bit more settled. + PathKind::Inductive => match input.typing_mode { + TypingMode::Coherence => { + response_no_constraints(cx, input, Certainty::overflow(false)) + } + TypingMode::Analysis { .. } + | TypingMode::PostBorrowckAnalysis { .. } + | TypingMode::PostAnalysis => Err(NoSolution), + }, } } @@ -57,12 +74,7 @@ where input: CanonicalInput<I>, result: QueryResult<I>, ) -> bool { - match kind { - PathKind::Coinductive => response_no_constraints(cx, input, Certainty::Yes) == result, - PathKind::Inductive => { - response_no_constraints(cx, input, Certainty::overflow(false)) == result - } - } + Self::initial_provisional_result(cx, kind, input) == result } fn on_stack_overflow( diff --git a/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs b/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs index 4f177df89e2..352ac7c1a4e 100644 --- a/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs +++ b/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs @@ -440,7 +440,7 @@ impl<'tcx> ProofTreeVisitor<'tcx> for BestObligation<'tcx> { match (child_mode, nested_goal.source()) { ( ChildMode::Trait(_) | ChildMode::Host(_), - GoalSource::Misc | GoalSource::NormalizeGoal(_), + GoalSource::Misc | GoalSource::TypeRelating | GoalSource::NormalizeGoal(_), ) => { continue; } diff --git a/compiler/rustc_type_ir/src/search_graph/mod.rs b/compiler/rustc_type_ir/src/search_graph/mod.rs index 18e84db5d68..9ae83928057 100644 --- a/compiler/rustc_type_ir/src/search_graph/mod.rs +++ b/compiler/rustc_type_ir/src/search_graph/mod.rs @@ -13,6 +13,7 @@ /// behavior as long as the resulting behavior is still correct. use std::cmp::Ordering; use std::collections::BTreeMap; +use std::collections::hash_map::Entry; use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; @@ -20,7 +21,7 @@ use std::marker::PhantomData; use derive_where::derive_where; use rustc_index::{Idx, IndexVec}; #[cfg(feature = "nightly")] -use rustc_macros::HashStable_NoContext; +use rustc_macros::{HashStable_NoContext, TyDecodable, TyEncodable}; use tracing::debug; use crate::data_structures::HashMap; @@ -111,21 +112,35 @@ pub trait Delegate { /// In the initial iteration of a cycle, we do not yet have a provisional /// result. In the case we return an initial provisional result depending /// on the kind of cycle. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "nightly", derive(HashStable_NoContext))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "nightly", derive(TyDecodable, TyEncodable, HashStable_NoContext))] pub enum PathKind { - Coinductive, + /// A path consisting of only inductive/unproductive steps. Their initial + /// provisional result is `Err(NoSolution)`. We currently treat them as + /// `PathKind::Unknown` during coherence until we're fully confident in + /// our approach. Inductive, + /// A path which is not be coinductive right now but we may want + /// to change of them to be so in the future. We return an ambiguous + /// result in this case to prevent people from relying on this. + Unknown, + /// A path with at least one coinductive step. Such cycles hold. + Coinductive, } + impl PathKind { /// Returns the path kind when merging `self` with `rest`. /// /// Given an inductive path `self` and a coinductive path `rest`, /// the path `self -> rest` would be coinductive. + /// + /// This operation represents an ordering and would be equivalent + /// to `max(self, rest)`. fn extend(self, rest: PathKind) -> PathKind { - match self { - PathKind::Coinductive => PathKind::Coinductive, - PathKind::Inductive => rest, + match (self, rest) { + (PathKind::Coinductive, _) | (_, PathKind::Coinductive) => PathKind::Coinductive, + (PathKind::Unknown, _) | (_, PathKind::Unknown) => PathKind::Unknown, + (PathKind::Inductive, PathKind::Inductive) => PathKind::Inductive, } } } @@ -159,9 +174,6 @@ impl UsageKind { } } } - fn and_merge(&mut self, other: impl Into<Self>) { - *self = self.merge(other); - } } /// For each goal we track whether the paths from this goal @@ -297,7 +309,7 @@ impl CycleHeads { let path_from_entry = match step_kind { PathKind::Coinductive => AllPathsToHeadCoinductive::Yes, - PathKind::Inductive => path_from_entry, + PathKind::Unknown | PathKind::Inductive => path_from_entry, }; self.insert(head, path_from_entry); @@ -305,6 +317,63 @@ impl CycleHeads { } } +bitflags::bitflags! { + /// Tracks how nested goals have been accessed. This is necessary to disable + /// global cache entries if computing them would otherwise result in a cycle or + /// access a provisional cache entry. + #[derive(Debug, Clone, Copy)] + pub struct PathsToNested: u8 { + /// The initial value when adding a goal to its own nested goals. + const EMPTY = 1 << 0; + const INDUCTIVE = 1 << 1; + const UNKNOWN = 1 << 2; + const COINDUCTIVE = 1 << 3; + } +} +impl From<PathKind> for PathsToNested { + fn from(path: PathKind) -> PathsToNested { + match path { + PathKind::Inductive => PathsToNested::INDUCTIVE, + PathKind::Unknown => PathsToNested::UNKNOWN, + PathKind::Coinductive => PathsToNested::COINDUCTIVE, + } + } +} +impl PathsToNested { + /// The implementation of this function is kind of ugly. We check whether + /// there currently exist 'weaker' paths in the set, if so we upgrade these + /// paths to at least `path`. + #[must_use] + fn extend_with(mut self, path: PathKind) -> Self { + match path { + PathKind::Inductive => { + if self.intersects(PathsToNested::EMPTY) { + self.remove(PathsToNested::EMPTY); + self.insert(PathsToNested::INDUCTIVE); + } + } + PathKind::Unknown => { + if self.intersects(PathsToNested::EMPTY | PathsToNested::INDUCTIVE) { + self.remove(PathsToNested::EMPTY | PathsToNested::INDUCTIVE); + self.insert(PathsToNested::UNKNOWN); + } + } + PathKind::Coinductive => { + if self.intersects( + PathsToNested::EMPTY | PathsToNested::INDUCTIVE | PathsToNested::UNKNOWN, + ) { + self.remove( + PathsToNested::EMPTY | PathsToNested::INDUCTIVE | PathsToNested::UNKNOWN, + ); + self.insert(PathsToNested::COINDUCTIVE); + } + } + } + + self + } +} + /// The nested goals of each stack entry and the path from the /// stack entry to that nested goal. /// @@ -322,15 +391,18 @@ impl CycleHeads { /// results from a the cycle BAB depending on the cycle root. #[derive_where(Debug, Default, Clone; X: Cx)] struct NestedGoals<X: Cx> { - nested_goals: HashMap<X::Input, UsageKind>, + nested_goals: HashMap<X::Input, PathsToNested>, } impl<X: Cx> NestedGoals<X> { fn is_empty(&self) -> bool { self.nested_goals.is_empty() } - fn insert(&mut self, input: X::Input, path_from_entry: UsageKind) { - self.nested_goals.entry(input).or_insert(path_from_entry).and_merge(path_from_entry); + fn insert(&mut self, input: X::Input, paths_to_nested: PathsToNested) { + match self.nested_goals.entry(input) { + Entry::Occupied(mut entry) => *entry.get_mut() |= paths_to_nested, + Entry::Vacant(entry) => drop(entry.insert(paths_to_nested)), + } } /// Adds the nested goals of a nested goal, given that the path `step_kind` from this goal @@ -341,18 +413,15 @@ impl<X: Cx> NestedGoals<X> { /// the same as for the child. fn extend_from_child(&mut self, step_kind: PathKind, nested_goals: &NestedGoals<X>) { #[allow(rustc::potential_query_instability)] - for (input, path_from_entry) in nested_goals.iter() { - let path_from_entry = match step_kind { - PathKind::Coinductive => UsageKind::Single(PathKind::Coinductive), - PathKind::Inductive => path_from_entry, - }; - self.insert(input, path_from_entry); + for (input, paths_to_nested) in nested_goals.iter() { + let paths_to_nested = paths_to_nested.extend_with(step_kind); + self.insert(input, paths_to_nested); } } #[cfg_attr(feature = "nightly", rustc_lint_query_instability)] #[allow(rustc::potential_query_instability)] - fn iter(&self) -> impl Iterator<Item = (X::Input, UsageKind)> { + fn iter(&self) -> impl Iterator<Item = (X::Input, PathsToNested)> + '_ { self.nested_goals.iter().map(|(i, p)| (*i, *p)) } @@ -490,7 +559,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { // goals as this change may cause them to now depend on additional // goals, resulting in new cycles. See the dev-guide for examples. if parent_depends_on_cycle { - parent.nested_goals.insert(parent.input, UsageKind::Single(PathKind::Inductive)) + parent.nested_goals.insert(parent.input, PathsToNested::EMPTY); } } } @@ -666,7 +735,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { // // We must therefore not use the global cache entry for `B` in that case. // See tests/ui/traits/next-solver/cycles/hidden-by-overflow.rs - last.nested_goals.insert(last.input, UsageKind::Single(PathKind::Inductive)); + last.nested_goals.insert(last.input, PathsToNested::EMPTY); } debug!("encountered stack overflow"); @@ -749,16 +818,11 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { // We now care about the path from the next highest cycle head to the // provisional cache entry. - match path_from_head { - PathKind::Coinductive => {} - PathKind::Inductive => { - *path_from_head = Self::cycle_path_kind( - &self.stack, - stack_entry.step_kind_from_parent, - head, - ) - } - } + *path_from_head = path_from_head.extend(Self::cycle_path_kind( + &self.stack, + stack_entry.step_kind_from_parent, + head, + )); // Mutate the result of the provisional cache entry in case we did // not reach a fixpoint. *result = mutate_result(input, *result); @@ -858,7 +922,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { for &ProvisionalCacheEntry { encountered_overflow, ref heads, - path_from_head, + path_from_head: head_to_provisional, result: _, } in entries.iter() { @@ -870,24 +934,19 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { // A provisional cache entry only applies if the path from its highest head // matches the path when encountering the goal. + // + // We check if any of the paths taken while computing the global goal + // would end up with an applicable provisional cache entry. let head = heads.highest_cycle_head(); - let full_path = match Self::cycle_path_kind(stack, step_kind_from_parent, head) { - PathKind::Coinductive => UsageKind::Single(PathKind::Coinductive), - PathKind::Inductive => path_from_global_entry, - }; - - match (full_path, path_from_head) { - (UsageKind::Mixed, _) - | (UsageKind::Single(PathKind::Coinductive), PathKind::Coinductive) - | (UsageKind::Single(PathKind::Inductive), PathKind::Inductive) => { - debug!( - ?full_path, - ?path_from_head, - "cache entry not applicable due to matching paths" - ); - return false; - } - _ => debug!(?full_path, ?path_from_head, "paths don't match"), + let head_to_curr = Self::cycle_path_kind(stack, step_kind_from_parent, head); + let full_paths = path_from_global_entry.extend_with(head_to_curr); + if full_paths.contains(head_to_provisional.into()) { + debug!( + ?full_paths, + ?head_to_provisional, + "cache entry not applicable due to matching paths" + ); + return false; } } } @@ -986,8 +1045,8 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> { let last = &mut self.stack[last_index]; last.reached_depth = last.reached_depth.max(next_index); - last.nested_goals.insert(input, UsageKind::Single(step_kind_from_parent)); - last.nested_goals.insert(last.input, UsageKind::Single(PathKind::Inductive)); + last.nested_goals.insert(input, step_kind_from_parent.into()); + last.nested_goals.insert(last.input, PathsToNested::EMPTY); if last_index != head { last.heads.insert(head, step_kind_from_parent); } diff --git a/compiler/rustc_type_ir/src/solve/mod.rs b/compiler/rustc_type_ir/src/solve/mod.rs index 6f37db1cb85..de2e0f2c0ec 100644 --- a/compiler/rustc_type_ir/src/solve/mod.rs +++ b/compiler/rustc_type_ir/src/solve/mod.rs @@ -58,20 +58,24 @@ impl<I: Interner, P> Goal<I, P> { /// Why a specific goal has to be proven. /// /// This is necessary as we treat nested goals different depending on -/// their source. This is currently mostly used by proof tree visitors -/// but will be used by cycle handling in the future. +/// their source. This is used to decide whether a cycle is coinductive. +/// See the documentation of `EvalCtxt::step_kind_for_source` for more details +/// about this. +/// +/// It is also used by proof tree visitors, e.g. for diagnostics purposes. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "nightly", derive(HashStable_NoContext))] pub enum GoalSource { Misc, - /// We're proving a where-bound of an impl. + /// A nested goal required to prove that types are equal/subtypes. + /// This is always an unproductive step. /// - /// FIXME(-Znext-solver=coinductive): Explain how and why this - /// changes whether cycles are coinductive. + /// This is also used for all `NormalizesTo` goals as we they are used + /// to relate types in `AliasRelate`. + TypeRelating, + /// We're proving a where-bound of an impl. ImplWhereBound, /// Const conditions that need to hold for `~const` alias bounds to hold. - /// - /// FIXME(-Znext-solver=coinductive): Are these even coinductive? AliasBoundConstCondition, /// Instantiating a higher-ranked goal and re-proving it. InstantiateHigherRanked, @@ -79,7 +83,6 @@ pub enum GoalSource { /// This is used in two places: projecting to an opaque whose hidden type /// is already registered in the opaque type storage, and for rigid projections. AliasWellFormed, - /// In case normalizing aliases in nested goals cycles, eagerly normalizing these /// aliases in the context of the parent may incorrectly change the cycle kind. /// Normalizing aliases in goals therefore tracks the original path kind for this |
