Skip to content

refactor(resolve): splitting glob and non-glob name bindings #112659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 2 additions & 1 deletion compiler/rustc_resolve/src/build_reduced_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use rustc_ast::visit::{self, AssocCtxt, Visitor};
use rustc_ast::{self as ast, AssocItem, AssocItemKind, MetaItemKind, StmtKind};
use rustc_ast::{Block, Fn, ForeignItem, ForeignItemKind, Impl, Item, ItemKind, NodeId};
use rustc_attr as attr;
use rustc_data_structures::intern::Interned;
use rustc_data_structures::sync::Lrc;
use rustc_errors::{struct_span_err, Applicability};
use rustc_expand::expand::AstFragment;
Expand Down Expand Up @@ -378,7 +379,7 @@ impl<'a, 'b, 'tcx> BuildReducedGraphVisitor<'a, 'b, 'tcx> {
if !type_ns_only || ns == TypeNS {
let key = BindingKey::new(target, ns);
let mut resolution = this.resolution(current_module, key).borrow_mut();
resolution.add_single_import(import);
resolution.single_imports.insert(Interned::new_unchecked(import));
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: unrelated change, could be moved to a separate cleanup commit.

}
});
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_resolve/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
ctxt: Option<SyntaxContext>,
) {
for (key, resolution) in self.resolutions(module).borrow().iter() {
if let Some(binding) = resolution.borrow().binding {
if let Some(binding) = resolution.borrow().available_binding() {
let res = binding.res();
if filter_fn(res) && ctxt.map_or(true, |ctxt| ctxt == key.ident.span.ctxt()) {
names.push(TypoSuggestion::typo_from_ident(key.ident, res));
Expand Down
17 changes: 6 additions & 11 deletions compiler/rustc_resolve/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,16 +870,11 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
let resolution =
self.resolution(module, key).try_borrow_mut().map_err(|_| (Determined, Weak::No))?; // This happens when there is a cycle of imports.

// If the primary binding is unusable, search further and return the shadowed glob
// binding if it exists. What we really want here is having two separate scopes in
// a module - one for non-globs and one for globs, but until that's done use this
// hack to avoid inconsistent resolution ICEs during import validation.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you keep this comment?
This PR doesn't introduce two separate scopes yet.

let binding =
[resolution.binding, resolution.shadowed_glob].into_iter().find_map(|binding| {
match (binding, ignore_binding) {
(Some(binding), Some(ignored)) if ptr::eq(binding, ignored) => None,
_ => binding,
}
let binding = [resolution.non_glob_binding(), resolution.glob_binding()]
.into_iter()
.find_map(|binding| match (binding, ignore_binding) {
(Some(binding), Some(ignored)) if ptr::eq(binding, ignored) => None,
_ => binding,
});

if let Some(Finalize { path_span, report_private, .. }) = finalize {
Expand All @@ -900,7 +895,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
}

// Forbid expanded shadowing to avoid time travel.
if let Some(shadowed_glob) = resolution.shadowed_glob
if let Some(shadowed_glob) = resolution.glob_binding()
Copy link
Contributor

Choose a reason for hiding this comment

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

This line alone looks like a change in semantics - the binding is still glob now, but not necessarily shadowed.
But the binding.res() != shadowed_glob.res() condition below should ensure the old behavior, right?

&& restricted_shadowing
&& binding.expansion != LocalExpnId::ROOT
&& binding.res() != shadowed_glob.res()
Expand Down
170 changes: 73 additions & 97 deletions compiler/rustc_resolve/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
Resolver, Segment,
};
use crate::{Finalize, Module, ModuleOrUniformRoot, ParentScope, PerNS, ScopeSet};
use crate::{NameBinding, NameBindingKind, PathResult};
use crate::{NameBinding, NameBindingKind, PathResult, Res};

use rustc_ast::NodeId;
use rustc_data_structures::fx::FxHashSet;
Expand All @@ -34,8 +34,6 @@ use smallvec::SmallVec;
use std::cell::Cell;
use std::{mem, ptr};

type Res = def::Res<NodeId>;

/// Contains data for specific kinds of imports.
#[derive(Clone)]
pub(crate) enum ImportKind<'a> {
Expand Down Expand Up @@ -212,25 +210,32 @@ pub(crate) struct NameResolution<'a> {
/// Single imports that may define the name in the namespace.
/// Imports are arena-allocated, so it's ok to use pointers as keys.
pub single_imports: FxHashSet<Interned<'a, Import<'a>>>,
/// The least shadowable known binding for this name, or None if there are no known bindings.
pub binding: Option<&'a NameBinding<'a>>,
pub shadowed_glob: Option<&'a NameBinding<'a>>,
non_glob_binding: Option<&'a NameBinding<'a>>,
glob_binding: Option<&'a NameBinding<'a>>,
}

impl<'a> NameResolution<'a> {
/// Returns the binding for the name if it is known or None if it not known.
pub(crate) fn binding(&self) -> Option<&'a NameBinding<'a>> {
self.binding.and_then(|binding| {
if !binding.is_glob_import() || self.single_imports.is_empty() {
Some(binding)
} else {
None
}
self.non_glob_binding()
.or_else(|| if self.single_imports.is_empty() { self.glob_binding() } else { None })
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic is likely incorrect and is a source of issues like #56593 (because it doesn't account for macros), but let's keep it as is in this PR.

I'd just want to rename it to something more explanatory, like determinate_binding, keep the removed comment and add a FIXME.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think that issue #56593 is interesting and could be the next task to work on.

}

pub(crate) fn non_glob_binding(&self) -> Option<&'a NameBinding<'a>> {
self.non_glob_binding.and_then(|binding| {
Copy link
Contributor

Choose a reason for hiding this comment

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

.and_then(|...| Some(x) ) is .map(|...| x)

assert!(!binding.is_glob_import());
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure these asserts are useful, there's literally one place where these fields are assigned and the glob-ness is checked there.
(I guess they could be kept during development and removed before merging.)

Some(binding)
})
}

pub(crate) fn glob_binding(&self) -> Option<&'a NameBinding<'a>> {
self.glob_binding.and_then(|binding| {
assert!(binding.is_glob_import());
Some(binding)
})
}

pub(crate) fn add_single_import(&mut self, import: &'a Import<'a>) {
self.single_imports.insert(Interned::new_unchecked(import));
pub(crate) fn available_binding(&self) -> Option<&'a NameBinding<'a>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub(crate) fn available_binding(&self) -> Option<&'a NameBinding<'a>> {
pub(crate) fn binding(&self) -> Option<&'a NameBinding<'a>> {

Assuming the other method is renamed to determinate_binding.

self.non_glob_binding().or(self.glob_binding())
}
}

Expand Down Expand Up @@ -305,65 +310,41 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
self.check_reserved_macro_name(key.ident, res);
self.set_binding_parent_module(binding, module);
self.update_resolution(module, key, |this, resolution| {
if let Some(old_binding) = resolution.binding {
if res == Res::Err && old_binding.res() != Res::Err {
// Do not override real bindings with `Res::Err`s from error recovery.
return Ok(());
}
match (old_binding.is_glob_import(), binding.is_glob_import()) {
(true, true) => {
if res != old_binding.res() {
resolution.binding = Some(this.ambiguity(
AmbiguityKind::GlobVsGlob,
old_binding,
binding,
));
} else if !old_binding.vis.is_at_least(binding.vis, this.tcx) {
// We are glob-importing the same item but with greater visibility.
resolution.binding = Some(binding);
}
}
(old_glob @ true, false) | (old_glob @ false, true) => {
let (glob_binding, nonglob_binding) =
if old_glob { (old_binding, binding) } else { (binding, old_binding) };
if glob_binding.res() != nonglob_binding.res()
&& key.ns == MacroNS
&& nonglob_binding.expansion != LocalExpnId::ROOT
{
resolution.binding = Some(this.ambiguity(
AmbiguityKind::GlobVsExpanded,
nonglob_binding,
glob_binding,
));
} else {
resolution.binding = Some(nonglob_binding);
}

if let Some(old_binding) = resolution.shadowed_glob {
assert!(old_binding.is_glob_import());
if glob_binding.res() != old_binding.res() {
resolution.shadowed_glob = Some(this.ambiguity(
AmbiguityKind::GlobVsGlob,
old_binding,
glob_binding,
));
} else if !old_binding.vis.is_at_least(binding.vis, this.tcx) {
resolution.shadowed_glob = Some(glob_binding);
}
} else {
resolution.shadowed_glob = Some(glob_binding);
}
}
(false, false) => {
return Err(old_binding);
}
if let Some(old_binding) = resolution.available_binding() && res == Res::Err && old_binding.res() != Res::Err {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you keep the old code structure here when possible?
Otherwise it's hard to see what changes here.

// Do not override real bindings with `Res::Err`s from error recovery.
Ok(())
} else if binding.is_glob_import() {
if let Some(old_binding) = resolution.glob_binding() {
if binding.res() != old_binding.res() {
resolution.glob_binding = Some(this.ambiguity(
AmbiguityKind::GlobVsGlob,
old_binding,
binding,
));
} else if !old_binding.vis.is_at_least(binding.vis, this.tcx) {
resolution.glob_binding = Some(binding);
}
} else {
resolution.binding = Some(binding);
resolution.glob_binding = Some(binding);
}

if let Some(old_binding) = resolution.non_glob_binding() {
if binding.res() != old_binding.res() && key.ns == MacroNS && old_binding.expansion != LocalExpnId::ROOT {
resolution.non_glob_binding = Some(this.ambiguity(
AmbiguityKind::GlobVsExpanded,
old_binding,
binding,
));
}
}
Ok(())
})
} else if let Some(old_binding) = resolution.non_glob_binding() {
Err(old_binding)
} else {
resolution.non_glob_binding = Some(binding);
Ok(())
}
})
}

fn ambiguity(
Expand Down Expand Up @@ -549,11 +530,11 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
for (key, resolution) in self.resolutions(module).borrow().iter() {
let resolution = resolution.borrow();

if let Some(binding) = resolution.binding {
if let NameBindingKind::Import { import, .. } = binding.kind
&& let Some((amb_binding, _)) = binding.ambiguity
&& binding.res() != Res::Err
&& exported_ambiguities.contains(&Interned::new_unchecked(binding))
if let Some(glob_binding) = resolution.glob_binding() {
if let NameBindingKind::Import { import, .. } = glob_binding.kind
&& let Some((amb_binding, _)) = glob_binding.ambiguity
&& glob_binding.res() != Res::Err
&& exported_ambiguities.contains(&Interned::new_unchecked(glob_binding))
{
self.lint_buffer.buffer_lint_with_diagnostic(
AMBIGUOUS_GLOB_REEXPORTS,
Expand All @@ -569,7 +550,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
);
}

if let Some(glob_binding) = resolution.shadowed_glob {
if let Some(binding) = resolution.non_glob_binding() {
let binding_id = match binding.kind {
NameBindingKind::Res(res) => {
Some(self.def_id_to_node_id[res.def_id().expect_local()])
Expand Down Expand Up @@ -769,9 +750,8 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
.emit();
}
let key = BindingKey::new(target, ns);
this.update_resolution(parent, key, |_, resolution| {
resolution.single_imports.remove(&Interned::new_unchecked(import));
});
let mut resolution = this.resolution(parent, key).borrow_mut();
resolution.single_imports.remove(&Interned::new_unchecked(import));
Copy link
Contributor

Choose a reason for hiding this comment

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

update_resolution has some extra logic related to glob_importers.
Does it never run in this case? Or it runs but it's irrelevant?

Copy link
Contributor Author

@bvanjoi bvanjoi Jun 16, 2023

Choose a reason for hiding this comment

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

It is a cleanup, because it seems will never meet other logic in update_resolution because the binding is not updated, and it will hit following code always:

 match resolution.binding() {
      _ if old_binding.is_some() => return t,
      None => return t,
      // xxxxxx
}

Of course, It should revert it back because it is irrelevant.

}
}
}
Expand Down Expand Up @@ -1025,29 +1005,25 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
let resolutions = resolutions.as_ref().into_iter().flat_map(|r| r.iter());
let names = resolutions
.filter_map(|(BindingKey { ident: i, .. }, resolution)| {
let resolution = resolution.borrow();
if i.name == ident.name {
return None;
} // Never suggest the same name
match *resolution.borrow() {
NameResolution { binding: Some(name_binding), .. } => {
match name_binding.kind {
NameBindingKind::Import { binding, .. } => {
match binding.kind {
// Never suggest the name that has binding error
// i.e., the name that cannot be previously resolved
NameBindingKind::Res(Res::Err) => None,
_ => Some(i.name),
}
None
} else if let Some(name_binding) = resolution.available_binding() {
match name_binding.kind {
NameBindingKind::Import { binding, .. } => {
match binding.kind {
// Never suggest the name that has binding error
// i.e., the name that cannot be previously resolved
NameBindingKind::Res(Res::Err) => None,
_ => Some(i.name),
}
_ => Some(i.name),
}
_ => Some(i.name),
}
NameResolution { ref single_imports, .. }
if single_imports.is_empty() =>
{
None
}
_ => Some(i.name),
} else if resolution.single_imports.is_empty() {
None
} else {
Some(i.name)
}
})
.collect::<Vec<Symbol>>();
Expand Down
10 changes: 8 additions & 2 deletions compiler/rustc_resolve/src/late.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2969,7 +2969,8 @@ impl<'a: 'ast, 'b, 'ast, 'tcx> LateResolutionVisitor<'a, 'b, 'ast, 'tcx> {
let Some((module, _)) = &self.current_trait_ref else { return; };
ident.span.normalize_to_macros_2_0_and_adjust(module.expansion);
let key = BindingKey::new(ident, ns);
let mut binding = self.r.resolution(module, key).try_borrow().ok().and_then(|r| r.binding);
let mut binding =
self.r.resolution(module, key).try_borrow().ok().and_then(|r| r.available_binding());
debug!(?binding);
if binding.is_none() {
// We could not find the trait item in the correct namespace.
Expand All @@ -2980,7 +2981,12 @@ impl<'a: 'ast, 'b, 'ast, 'tcx> LateResolutionVisitor<'a, 'b, 'ast, 'tcx> {
_ => ns,
};
let key = BindingKey::new(ident, ns);
binding = self.r.resolution(module, key).try_borrow().ok().and_then(|r| r.binding);
binding = self
.r
.resolution(module, key)
.try_borrow()
.ok()
.and_then(|r| r.available_binding());
debug!(?binding);
}
let Some(binding) = binding else {
Expand Down
23 changes: 13 additions & 10 deletions compiler/rustc_resolve/src/late/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,15 +1012,16 @@ impl<'a: 'ast, 'ast, 'tcx> LateResolutionVisitor<'a, '_, 'ast, 'tcx> {
self.resolve_path(mod_path, None, None)
{
let resolutions = self.r.resolutions(module).borrow();
let targets: Vec<_> =
resolutions
.iter()
.filter_map(|(key, resolution)| {
resolution.borrow().binding.map(|binding| binding.res()).and_then(
|res| if filter_fn(res) { Some((key, res)) } else { None },
)
})
.collect();
let targets: Vec<_> = resolutions
.iter()
.filter_map(|(key, resolution)| {
resolution
.borrow()
.available_binding()
.map(|binding| binding.res())
.and_then(|res| if filter_fn(res) { Some((key, res)) } else { None })
})
.collect();
if targets.len() == 1 {
let target = targets[0];
return Some(TypoSuggestion::single_item_from_ident(target.0.ident, target.1));
Expand Down Expand Up @@ -1543,7 +1544,9 @@ impl<'a: 'ast, 'ast, 'tcx> LateResolutionVisitor<'a, '_, 'ast, 'tcx> {
let targets = resolutions
.borrow()
.iter()
.filter_map(|(key, res)| res.borrow().binding.map(|binding| (key, binding.res())))
.filter_map(|(key, res)| {
res.borrow().available_binding().map(|binding| (key, binding.res()))
})
.filter(|(_, res)| match (kind, res) {
(AssocItemKind::Const(..), Res::Def(DefKind::AssocConst, _)) => true,
(AssocItemKind::Fn(_), Res::Def(DefKind::AssocFn, _)) => true,
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ impl<'a> ModuleData<'a> {
F: FnMut(&mut R, Ident, Namespace, &'a NameBinding<'a>),
{
for (key, name_resolution) in resolver.as_mut().resolutions(self).borrow().iter() {
if let Some(binding) = name_resolution.borrow().binding {
if let Some(binding) = name_resolution.borrow().available_binding() {
f(resolver, key.ident, key.ns, binding);
}
}
Expand Down
21 changes: 21 additions & 0 deletions tests/ui/resolve/non-glob-first.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// check-pass

mod a {
pub trait P {}
}
pub use a::*;

mod b {
#[derive(Clone)]
pub enum P {
A
}
}
pub use b::P;

mod c {
use crate::*;
pub struct _S(Vec<P>);
}

fn main() {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there some specific issue for this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No specific problem, it just tests/ui passed after refactoring but block by this comment. So I reduced it and added it into tests/ui after fixed.

btw, It is had some relative with #56593