Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions compiler/rustc_resolve/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,13 +1094,12 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
ignore_import: Option<Import<'ra>>,
) -> Result<Decl<'ra>, ControlFlow<Determinacy, Determinacy>> {
let key = BindingKey::new(ident, ns);
// `try_borrow_mut` is required to ensure exclusive access, even if the resulting binding
// doesn't need to be mutable. It will fail when there is a cycle of imports, and without
// the exclusive access infinite recursion will crash the compiler with stack overflow.
let resolution = &*self
.resolution_or_default(module, key, orig_ident_span)
.try_borrow_mut_unchecked()
.map_err(|_| ControlFlow::Continue(Determined))?;
let resolution = self.resolution_or_default(module, key, orig_ident_span);
// We need to detect import cycles to avoid infinite recursion. The guard ensures
// the resolution is removed when this resolve call ends.
let _cycle_guard =
self.enter_cycle_detector(resolution).map_err(|_| ControlFlow::Continue(Determined))?;
let resolution = resolution.borrow();

let binding = resolution.non_glob_decl.filter(|b| Some(*b) != ignore_decl);

Expand Down Expand Up @@ -1161,13 +1160,12 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
ignore_import: Option<Import<'ra>>,
) -> Result<Decl<'ra>, ControlFlow<Determinacy, Determinacy>> {
let key = BindingKey::new(ident, ns);
// `try_borrow_mut` is required to ensure exclusive access, even if the resulting binding
// doesn't need to be mutable. It will fail when there is a cycle of imports, and without
// the exclusive access infinite recursion will crash the compiler with stack overflow.
let resolution = &*self
.resolution_or_default(module.to_module(), key, orig_ident_span)
.try_borrow_mut_unchecked()
.map_err(|_| ControlFlow::Continue(Determined))?;
let resolution = self.resolution_or_default(module.to_module(), key, orig_ident_span);
// We need to detect import cycles to avoid infinite recursion. The guard ensures
// the resolution is removed when this resolve call ends.
let _cycle_guard =
self.enter_cycle_detector(resolution).map_err(|_| ControlFlow::Continue(Determined))?;
let resolution = resolution.borrow();

let binding = resolution.glob_decl.filter(|b| Some(*b) != ignore_decl);

Expand Down
54 changes: 54 additions & 0 deletions compiler/rustc_resolve/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,60 @@ impl<'ra> NameResolution<'ra> {
}
}

// module to keep the TLS private and only accessible through the `Resolver`.
pub(crate) mod cycle_detection {
use std::collections::BTreeSet;

use crate::imports::NameResolution;
use crate::ref_mut::CmRefCell;
use crate::{CacheRefCell, Resolver};

thread_local! {
/// During import resolution, recursive imports can form cycles.
/// This set stores the active resolution stack for the current thread.
/// So it's essentially a recursion stack.
///
/// The key is the address of a `NameResolution`; we only need identity,
/// not access to the value.
static ACTIVE_RESOLUTIONS: CacheRefCell<BTreeSet<*const ()>> = Default::default();
}
Comment on lines +361 to +369

@LorrensP-2158466 LorrensP-2158466 Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So I already did it using TLS to show how I was thinking of a solution for parallel import resolution.

Can be easily translated to Rc<RefCell<...>> in the Resolver itself.

View changes since the review


/// We need a way to remove the resolution from the stack whenever we return, this type
/// holds that resolution key and removes it on drop.
pub(crate) struct ImportCycleGuard {
key: *const (),
}

impl Drop for ImportCycleGuard {
fn drop(&mut self) {
ACTIVE_RESOLUTIONS.with_borrow_mut(|active| {
active.remove(&self.key);
});
}
}

@LorrensP-2158466 LorrensP-2158466 Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

DropGuard is possible (unstable feature though). But I don't like how I need to fill in the types:

/// We need a way to remove the resolution from the stack whenever we return, this type
/// holds that resolution key and removes it on drop.
pub(crate) type ImportCycleGuard<D: FnOnce(*const ())> = DropGuard<*const (), D>;

pub(crate) fn enter_cycle_detector(
    &self,
    resolution: &'ra CmRefCell<NameResolution<'ra>>,
) -> Result<ImportCycleGuard<impl FnOnce(*const ())>, ()> {

View changes since the review


impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
/// Enters the `resolution` into the active import resolution stack.
///
/// Returns `Err(())` if `res` is already active, which means an import cycle
/// was detected.
pub(crate) fn enter_cycle_detector(
&self,
resolution: &'ra CmRefCell<NameResolution<'ra>>,
) -> Result<ImportCycleGuard, ()> {
let key = std::ptr::from_ref(resolution).cast::<()>();

ACTIVE_RESOLUTIONS.with_borrow_mut(|active| {
if !active.insert(key) {
return Err(());
}

Ok(ImportCycleGuard { key })
})
}
}
}

/// An error that may be transformed into a diagnostic later. Used to combine multiple unresolved
/// import errors within the same use tree into a single diagnostic.
#[derive(Debug, Clone)]
Expand Down
4 changes: 0 additions & 4 deletions compiler/rustc_resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2944,10 +2944,6 @@ mod ref_mut {
self.0.borrow_mut()
}

pub(crate) fn try_borrow_mut_unchecked(&self) -> Result<RefMut<'_, T>, BorrowMutError> {
self.0.try_borrow_mut()
}

#[track_caller]
pub(crate) fn try_borrow_mut<'ra, 'tcx>(
&self,
Expand Down
Loading