Rustc Name Resolution: Research Report
This report documents how name resolution works in rustc, with focus on Rust 2024 edition semantics. It covers namespace structure, macro resolution (derives, attribute macros, bang macros), cross-crate concerns, expansion ordering, globs, and preludes.
Source base: ~/dev/rust/compiler/
1. Namespace Structure
Source: rustc_hir/src/def.rs:687-700
Rust has exactly three namespaces:
#![allow(unused)] fn main() { pub enum Namespace { TypeNS, // structs, enums, unions, traits, mods, type aliases ValueNS, // fns, consts, statics, local variables, enum variant ctors MacroNS, // all macros (bang, attr, derive) + non-macro attributes } }
All macros — regardless of kind — live in MacroNS. This includes macro_rules!
items, pub macro items, proc-macro bang/attr/derive macros, and even inert
attributes like #[inline].
2. Sub-Namespace Splitting Within MacroNS
Source: rustc_resolve/src/macros.rs:85-102
Although all macros share MacroNS, the resolver applies a sub-namespace filter:
#![allow(unused)] fn main() { /// Macro namespace is separated into two sub-namespaces, one for bang macros and /// one for attribute-like macros (attributes, derives). /// We ignore resolutions from one sub-namespace when searching names in scope for another. pub(crate) fn sub_namespace_match( candidate: Option<MacroKinds>, requirement: Option<MacroKind>, ) -> bool { let (Some(candidate), Some(requirement)) = (candidate, requirement) else { return true; }; match requirement { MacroKind::Bang => candidate.contains(MacroKinds::BANG), MacroKind::Attr | MacroKind::Derive => { candidate.intersects(MacroKinds::ATTR | MacroKinds::DERIVE) } } } }
Key insight: Attr macros and Derive macros share one sub-namespace; Bang macros have their own. A derive macro will not shadow a bang macro of the same name, and vice versa. However, an attr macro and a derive macro of the same name can shadow each other.
The MacroKinds bitflags (rustc_hir/src/def.rs:38-44):
#![allow(unused)] fn main() { pub struct MacroKinds(u8); bitflags! { impl MacroKinds: u8 { const BANG = 1 << 0; const ATTR = 1 << 1; const DERIVE = 1 << 2; } } }
A single definition can support multiple kinds simultaneously — e.g., a
macro_rules! macro can be usable as both a bang and an attr macro. The DefKind
stores Macro(MacroKinds) with the full bitflag set.
3. The MacroKind Enum
Source: rustc_span/src/hygiene.rs:1145-1176
#![allow(unused)] fn main() { pub enum MacroKind { Bang, // foo!() Attr, // #[foo] Derive, // #[derive(Foo)] } }
This is used as the "requirement" when resolving a macro invocation — it determines which sub-namespace filter is applied.
4. How macro_rules! Macros Are Resolved
Textual (Lexical) Scoping
Source: rustc_resolve/src/macros.rs:50-83
macro_rules! macros without #[macro_export] live in a textual scope chain,
not in the module's MacroNS bindings:
#![allow(unused)] fn main() { pub(crate) enum MacroRulesScope<'ra> { Empty, Def(&'ra MacroRulesDecl<'ra>), Invocation(LocalExpnId), } }
When a macro_rules! definition is encountered, it creates a
MacroRulesScope::Def linking back to the prior scope. This covers everything
textually after the definition within the same block/module.
Source: rustc_resolve/src/build_reduced_graph.rs:1322-1375
For non-exported macro_rules!:
- The macro is NOT placed into the module's
MacroNSbindings. - Instead, it's placed into the textual
MacroRulesScopechain. - It uses
SemiOpaquetransparency.
Module Scoping via #[macro_export]
Source: rustc_resolve/src/build_reduced_graph.rs:1339-1360
When #[macro_export] is present:
- A
MacroExportimport is created with the crate root as the parent module. - The macro becomes accessible via
use crate::macro_name;from any edition.
Rust 2024 Changes
The OUT_OF_SCOPE_MACRO_CALLS lint (deny-by-default, future-incompatible) prevents
calling macro_rules! macros that are textually in scope via the MacroRulesScope
chain when they are inside a module that contains an inert attribute
(macros.rs:1159-1217). This tightens scoping rules in Rust 2024.
5. How pub macro (Declarative Macros 2.0) Are Resolved
Source: rustc_resolve/src/build_reduced_graph.rs:1376-1393
pub macro foo items:
- Placed directly into the parent module's
MacroNSviadefine_local. - Subject to normal visibility rules.
- Resolved like any other named item in the module.
- Use
Opaquetransparency (definition-site hygiene). - Can be imported via
usenormally.
6. How Proc-Macro Bang Macros Are Resolved
Source: rustc_resolve/src/build_reduced_graph.rs:1261-1278
#[proc_macro] fn my_macro(...) creates a DefKind::Macro(MacroKinds::BANG) item
in the defining crate's MacroNS. From external crates, it's discovered via crate
metadata and made accessible through use crate_name::my_macro;.
Resolution path for my_macro!(...) (macros.rs:267-282):
- Single-segment: uses
ScopeSet::Macro(MacroKind::Bang), walking the full scope chain with the bang sub-namespace filter. - Multi-segment (
crate::my_macro!): uses standard path resolution inMacroNS.
7. How Derive Macros Are Resolved
Source: rustc_resolve/src/macros.rs:386-472
When the compiler encounters #[derive(MyDerive)]:
resolve_derivesis called for the entire#[derive(...)]container.- Each derive path is resolved via
resolve_derive_macro_path(lines 773-790), usingMacroKind::Derive. - Resolution blocks the container's expansion until ALL derives resolve — required because derive helper attributes must be in scope for the annotated item.
- After resolution, helper attributes are collected and stored in
helper_attrs, making them available asScope::DeriveHelpers.
The derive name is resolved identically to other macro paths — as a single-segment
name through the full scope chain with MacroKind::Derive sub-namespace filter, or
as a multi-segment path in MacroNS.
Derive Helper Attributes
When a derive macro declares helper attributes (e.g.,
#[proc_macro_derive(Serialize, attributes(serde))]), those helpers become available
as Scope::DeriveHelpers for the annotated item. They resolve as NonMacroAttr
items in the MacroNS.
8. How Attribute Macros Are Resolved
Source: rustc_resolve/src/macros.rs:268-271
#![allow(unused)] fn main() { InvocationKind::Attr { ref attr, derives: ref attr_derives, .. } => { derives = self.arenas.alloc_ast_paths(attr_derives); inner_attr = attr.style == ast::AttrStyle::Inner; (&attr.get_normal_item().path, MacroKind::Attr) } }
Attribute resolution uses MacroKind::Attr as the sub-namespace requirement. Since
attr and derive macros share a sub-namespace, they can shadow each other.
9. The Full Scope Search Order (for single-segment names)
Source: rustc_resolve/src/ident.rs:50-237
Type Namespace
- Ribs (type parameters,
implself types) - Module non-glob bindings (local definitions + named imports)
- Module glob bindings (glob imports)
- Walk up to hygienic lexical parent module, repeat steps 2-3
- Extern prelude items (from
extern cratedeclarations) - Extern prelude flags (from
--externcommand-line flags) - Tool prelude (from
#![register_tool]) - Standard library prelude (edition-specific prelude module)
- Builtin types (
u32,bool,str,f16,f128, etc.)
Value Namespace
- Ribs (local variables, function parameters)
- Module non-glob bindings
- Module glob bindings
- Walk up to hygienic lexical parent module, repeat steps 2-3
- Standard library prelude
Macro Namespace
- Derive helpers (
Scope::DeriveHelpers) — from derives on the current item - Derive helpers compat (
Scope::DeriveHelpersCompat) — legacy support macro_rules!scopes (Scope::MacroRules) — textual scope chain- Module non-glob bindings
- Module glob bindings
- Walk up to hygienic lexical parent, repeat steps 3-5
macro_useprelude (Scope::MacroUsePrelude) — from#[macro_use] extern crate- Standard library prelude (
Scope::StdLibPrelude) - Builtin attributes (
Scope::BuiltinAttrs) —#[inline],#[derive], etc.
Note: In Rust 2015, the MacroUsePrelude scope is always searched. In 2018+/2024,
it respects #[no_implicit_prelude] (ident.rs:154).
10. Cross-Crate Macro Resolution
#[macro_export] Macros
Source: rustc_resolve/src/build_reduced_graph.rs:1325-1360
#[macro_export] macro_rules! items appear at the crate root in MacroNS.
External crates access them via use crate_name::macro_name; or
crate_name::macro_name!().
When an external crate's module is first accessed, build_reduced_graph_external
(build_reduced_graph.rs:350) plants all children (including
Res::Def(DefKind::Macro(..), _)) into MacroNS.
Proc Macros from External Crates
Source: rustc_metadata/src/rmeta/decoder.rs:1059, rustc_metadata/src/creader.rs:643-728
- Proc-macro crates are loaded as dylibs.
- The
ProcMacroClientlist is extracted via dlsym. - Each proc macro is loaded lazily: when a
DefIdis first resolved toRes::Def(DefKind::Macro(..), def_id),get_macro_by_def_idloads and caches theSyntaxExtension. - From the consumer's perspective, proc macros are just names in
MacroNSof the external crate root — resolved viause.
11. Determinacy and the Fixed-Point Expansion Loop
The Determinacy Enum
Source: rustc_resolve/src/lib.rs:97-106
#![allow(unused)] fn main() { enum Determinacy { Determined, Undetermined, } }
Determined: Resolution is conclusive — name found or definitively absent.Undetermined: A pending macro expansion might introduce the name. Must retry.
The Expansion Loop
Source: rustc_expand/src/expand.rs:473-616
fully_expand_fragment drives the mutual fixed-point:
- Collect all macro invocations, replace with placeholders.
- Call
resolve_imports()— eagerly resolve available imports. - Main loop:
- Pop an invocation. Attempt resolution.
- If
Ok(ext): expand, collect new sub-invocations, mark progress. - If
Err(Indeterminate): push toundetermined_invocations. - When queue empty: call
resolve_imports()again. - If undetermined remain and no progress: enter force mode.
- Force mode treats all
UndeterminedasDeterminedfailures, allowing error recovery with dummy macro output.
"Time Travelling" — MacroRulesScope::Invocation
Source: rustc_resolve/src/macros.rs:67-83, ident.rs:592-598
When a macro invocation is placed into a module, the resolver creates a
MacroRulesScope::Invocation(invoc_id) barrier in the textual scope chain.
During name lookup, hitting an unexpanded Invocation barrier returns
Undetermined — because that invocation might produce a macro_rules! definition
that introduces the name being sought.
After expansion, the invocation's output macro_rules scope is stored in
output_macro_rules_scopes, and path compression replaces the barrier with the
actual output for subsequent lookups.
Import Resolution Fixed-Point
Source: rustc_resolve/src/imports.rs:729-751
resolve_imports is itself a fixed-point loop:
- Each iteration speculatively resolves indeterminate imports.
- Resolved imports are committed via
write_import_resolutions. - Loop terminates when
indeterminate_countstops decreasing.
The two systems form a mutual fixed-point: imports may depend on macro expansion
(a macro expanding to a use statement), and macro resolution may depend on imports
(a use bringing a macro into scope).
Finalization Consistency Check
Source: rustc_resolve/src/macros.rs:900-1033
After all expansion, finalize_macro_resolutions re-resolves every macro path and
checks consistency with the resolution used during expansion. If a resolution changed,
a delayed bug is emitted.
12. Glob Imports
Resolution Mechanism
Source: rustc_resolve/src/imports.rs:1749-1783
When use foo::* resolves:
- Iterate all
determined_decl()entries in the source module. - For each accessible binding, create an import declaration.
- Plant into the importing module via
try_plant_decl_into_local_module().
Priority: Non-Glob Always Wins
Source: rustc_resolve/src/imports.rs:284-321
The NameResolution structure maintains two slots:
non_glob_decl: Option<Decl>— item definitions and named importsglob_decl: Option<Decl>— glob imports
best_decl() returns self.non_glob_decl.or(self.glob_decl) — non-glob always wins.
Additionally, if there are pending single_imports that might define a name, the
glob result is blocked (determined_decl() returns None) until those resolve.
Glob Determinacy and Macro Expansion
A glob binding is NOT determined when its source module has unexpanded_invocations
(DeclData::determined() at lib.rs:1258). This means during expansion,
resolve_ident_in_module_globs_unadjusted() returns Err(Undetermined) rather than
committing to a glob result that might change.
Within-Module Scope Separation
Globs and non-globs are visited as separate scopes:
Scope::ModuleNonGlobs(module) => Scope::ModuleGlobs(module)
This structural separation enforces the priority rule at every step of the scope walk.
13. The Prelude System
Standard Library Prelude (Scope::StdLibPrelude)
- Discovered via
#[prelude_import]duringbuild_reduced_graph(build_reduced_graph.rs:728). - Stored as
self.prelude: Option<Module<'ra>>. - Gated by
use_prelude(false for modules with#[no_implicit_prelude]). - Lowest priority in the scope chain (after all module scopes and extern prelude).
Extern Prelude
Split into two scopes for priority:
ExternPreludeItems: fromextern cratedeclarations (higher priority).ExternPreludeFlags: from--externCLI flags (lower priority).- Always includes
core(unless#![no_core]) andstd(unless#![no_std]).
Tool Prelude (Scope::ToolPrelude)
- From
#![register_tool(foo)]. - Maps tool names to
Res::ToolMod. - Enables
#[rustfmt::skip],#[clippy::allow]etc.
Macro Use Prelude (Scope::MacroUsePrelude)
- From
#[macro_use] extern crate foo;. - Only for
MacroNS. - In 2015: always searched. In 2018+/2024: respects
#[no_implicit_prelude].
Builtin Types (Scope::BuiltinTypes)
- Primitive types: all
PrimTyvariants. - Only for
TypeNS, absolute lowest priority.
Builtin Attributes (Scope::BuiltinAttrs)
- All items from
BUILTIN_ATTRIBUTES(e.g.,derive,cfg,test). - Only for
MacroNS, lowest priority in that namespace.
14. Edition 2024 Specifics
New Prelude Items
Source: library/core/src/prelude/mod.rs:56-72, library/std/src/prelude/mod.rs:158-172
Rust 2024 prelude adds to the 2021 prelude:
core::future::Futurecore::future::IntoFuture
gen Keyword
Source: rustc_span/src/symbol.rs:112,2960
gen is reserved as an unused keyword in edition 2024+. Cannot be used as an
identifier without r#gen.
Resolution Order Unchanged
The resolution priority ordering algorithm is identical across all editions. What changes:
- The contents of the stdlib prelude module (more items in 2024).
- Keyword reservations (
gen). - How
::pathis interpreted (crate root in 2015, extern prelude in 2018+). MacroUsePreludesearch behavior (unconditional in 2015, conditional in 2018+).
15. Implications for Sage
Namespace Model
The current sage Namespace enum:
#![allow(unused)] fn main() { pub enum Namespace { Type, Value, Macro, } }
This is correct as the top-level structure. However, rustc's MacroNS has internal
sub-namespace filtering via MacroKinds. When resolving a name, the resolver must
know what kind of macro is being sought:
- Resolving
foo!()→ filter toMacroKinds::BANG - Resolving
#[foo]→ filter toMacroKinds::ATTR | MacroKinds::DERIVE - Resolving
#[derive(Foo)]→ filter toMacroKinds::ATTR | MacroKinds::DERIVE
A single Namespace::Macro variant (without sub-kind) works for module-level
storage. The sub-namespace filtering should happen at lookup time, not at storage
time.
Options for modeling this:
- Keep
Namespace::Macrosingular but pass an optionalMacroKindas a filter parameter to resolution functions. This matches rustc's approach. - Split into
Namespace::Macro(MacroKind)with three variants. This over-splits because attr and derive share a sub-namespace, and would require storing macros in up to two namespace slots (e.g., amacro_rules!that works as both bang and attr).
Option 1 is recommended.
Scope Chain Priorities
The scope walk for macro resolution must handle:
- Derive helper attributes (contextual, on the current item)
- Textual
macro_rules!scope (linked list, not module-based) - Module non-glob bindings
- Module glob bindings
- Walk up through parent modules
- Prelude(s)
Determinacy
When a glob import's source module has pending macro expansions, the glob result is indeterminate. This is essential for correctness of the fixed-point loop — a glob cannot be committed while the source module might still grow.
Key Source Files
| File | Role |
|---|---|
rustc_hir/src/def.rs | Namespace, MacroKind, MacroKinds, DefKind |
rustc_resolve/src/ident.rs | Scope visitor, resolution priority, sub-namespace filtering |
rustc_resolve/src/imports.rs | Import resolution fixed-point, NameResolution, globs |
rustc_resolve/src/macros.rs | MacroRulesScope, sub_namespace_match, macro resolution |
rustc_resolve/src/build_reduced_graph.rs | Module tree building, prelude discovery, #[macro_export] |
rustc_resolve/src/lib.rs | Determinacy, Scope enum, AmbiguityKind |
rustc_expand/src/expand.rs | fully_expand_fragment — the expansion/resolution loop |
rustc_expand/src/base.rs | SyntaxExtensionKind, Indeterminate |
rustc_metadata/src/rmeta/decoder.rs | Loading proc macros from external crate metadata |
library/std/src/prelude/mod.rs | Edition-specific prelude contents |