이름 해석(Name resolution)
이름 해석(Name resolution)
경로(path)와 그 밖의 식별자를 어떤 엔티티의 선언에 연결하는 과정을 이름 해석이라고 해요. 이름은 서로 다른 네임스페이스로 구분돼서, 서로 다른 네임스페이스의 엔티티가 충돌 없이 같은 이름을 공유할 수 있어요. 각 이름은 스코프(그 이름을 참조할 수 있는 소스 텍스트 영역) 안에서 유효하고, 그 이름에 대한 접근은 가시성(visibility)에 따라 제한될 수 있어요.
이름 해석은 컴파일 과정에서 세 단계로 나뉘어요.
- 확장 시점 해석(expansion-time resolution) — 첫 번째 단계로, 모든
use선언과 매크로 호출을 해석해요. - 일차 해석(primary resolution) — 두 번째 단계로, 아직 해석되지 않았고 타입 정보에 의존하지 않는 모든 이름을 해석해요.
- 타입 상대 해석(type-relative resolution) — 마지막 단계로, 타입 정보가 이용 가능해지면 남은 이름을 해석해요.
참고로 확장 시점 해석을 조기 해석(early resolution)이라고도 하고, 일차 해석을 후기 해석(late resolution)이라고도 불러요.
출처: Rust Reference
일반 규칙(General)
이 절의 규칙은 이름 해석의 모든 단계에 적용돼요.
스코프(Scopes)
이 부분은 다양한 스코프 안에서의 이름 해석에 대한 향후 확장을 위한 자리 표시자예요.
확장 시점 이름 해석(Expansion-time name resolution)
확장 시점 이름 해석은 매크로 확장을 완료하고 크레이트의 AST를 완전히 생성하는 데 필요한 이름 해석 단계예요. 이 단계는 매크로 호출과 use 선언의 해석을 필요로 해요. use 선언의 해석은 경로 기반 스코프로 해석되는 매크로 호출에 필요하고, 매크로 호출의 해석은 매크로를 확장하기 위해 필요해요.
확장 시점 이름 해석 이후에는 AST에 확장되지 않은 매크로 호출이 남아 있으면 안 돼요. 모든 매크로 호출은 최종 AST 또는 외부 크레이트에 존재하는 유효한 정의로 해석돼요.
#![allow(unused)]
fn main() {
m!(); // ERROR: Cannot find macro `m` in this scope.
}
이름의 해석은 안정적이어야 해요. 확장 후 완전히 확장된 AST의 이름은 매크로가 확장되고 임포트가 해석되는 순서와 무관하게 같은 정의로 해석되어야 해요.
매크로 확장 중에 선택된 모든 이름 해석 후보는 **추측적(speculative)**으로 간주돼요. 크레이트가 완전히 확장되면, 모든 추측적 임포트 해석은 매크로 확장이 새로운 모호성을 도입하지 않았는지 확인하기 위해 검증돼요. 매크로 확장의 반복적 특성 때문에, 매크로나 glob 임포트가 자기 자신의 기본 경로와 모호한 항목을 도입할 때 이른바 시간 여행 모호성(time traveling ambiguities)이 발생해요.
fn main() {}
macro_rules! f {
() => {
mod m {
pub(crate) use f;
}
}
}
f!();
const _: () = {
// Initially, we speculatively resolve `m` to the module in
// the crate root.
//
// Expansion of `f` introduces a second `m` module inside this
// body.
//
// Expansion-time resolution finalizes resolutions by re-
// resolving all imports and macro invocations, sees the
// introduced ambiguity and reports it as an error.
m::f!(); // ERROR: `m` is ambiguous.
};
임포트(Imports)
모든 use 선언은 이 해석 단계에서 완전히 해석돼요. 타입 상대 경로는 이 단계에서 해석될 수 없고 오류를 만들 거예요. 아래에서 보듯 use m::C 같은 것은 확장 시점에 해석되는 유효한 임포트이고, use m::A::V 같은 타입 상대 경로는 오류예요.
#![allow(unused)]
fn main() {
mod m {
pub const C: () = ();
pub enum E { V }
pub type A = E;
impl E {
pub const C: () = ();
}
}
// Valid imports resolved at expansion-time:
use m::C; // OK.
use m::E; // OK.
use m::A; // OK.
use m::E::V; // OK.
// Valid expressions resolved during type-relative resolution:
let _ = m::A::V; // OK.
let _ = m::E::C; // OK.
}
#![allow(unused)]
fn main() {
mod m {
pub const C: () = ();
pub enum E { V }
pub type A = E;
impl E {
pub const C: () = ();
}
}
// Invalid type-relative imports that can't resolve at expansion-time:
use m::A::V; // ERROR: Unresolved import `m::A::V`.
use m::E::C; // ERROR: Unresolved import `m::E::C`.
}
바깥 스코프의 use 선언으로 도입된 이름은, 이름 해석 모호성에 의해 제한되지 않는 한, 같은 네임스페이스에 같은 이름을 가진 안쪽 스코프의 후보에 의해 가려져요(shadowed).
#![allow(unused)]
fn main() {
pub mod m1 {
pub mod ambig {
pub const C: u8 = 1;
}
}
pub mod m2 {
pub mod ambig {
pub const C: u8 = 2;
}
}
// This introduces the name `ambig` in the outer scope.
use m1::ambig;
const _: () = {
// This shadows `ambig` in the inner scope.
use m2::ambig;
// The inner candidate is selected here
// as the resolution of `ambig`.
use ambig::C;
assert!(C == 2);
};
}
단일 스코프 안에서 use 선언으로 도입된 이름의 가리기(shadowing)는 다음 상황에서 허용돼요.
- glob 가리기(use glob shadowing)
- 매크로 텍스트 스코프 가리기(Macro textual scope shadowing)
모호성(Ambiguities)
확장 시점 해석에는 어떤 후보가 다른 후보를 가려야 하는지 컴파일러가 일관되게 결정할 수 없는 상황이 있어요. 어떤 매크로 정의, use 선언, 또는 모듈에 임포트나 매크로 호출의 이름이 가리킬 수 있는지가 여러 개라서죠. 이런 상황에서는 가리기가 허용되지 않고, 컴파일러는 대신 모호성 오류를 내보내요.
이름은 모호한 glob 임포트를 통해 해석될 수 없어요. glob 임포트는 이름이 사용되지 않는 한 같은 네임스페이스에 충돌하는 이름을 가져오는 것이 허용돼요. 모호한 glob 임포트의 충돌하는 후보를 가진 이름은 여전히 비-glob 임포트에 의해 가려질 수 있고, 오류 없이 사용될 수 있어요. 오류는 임포트 시점이 아니라 사용 시점에 발생해요.
#![allow(unused)]
fn main() {
mod m1 {
pub struct Ambig;
}
mod m2 {
pub struct Ambig;
}
// OK: This brings conficting names in the same namespace into scope
// but they have not been used yet.
use m1::*;
use m2::*;
const _: () = {
// The error happens when the name with the conflicting candidates
// is used.
let x = Ambig; // ERROR: `Ambig` is ambiguous.
};
}
#![allow(unused)]
fn main() {
mod m1 {
pub struct Ambig;
}
mod m2 {
pub struct Ambig;
}
use m1::*;
use m2::*; // OK: No name conflict.
const _: () = {
// This is permitted, since resolution is not through the
// ambiguous globs.
struct Ambig;
let x = Ambig; // OK.
};
}
여러 glob 임포트가 같은 이름을 가져오는 것이 허용되고, 그 임포트들이 (재수출을 따른) 같은 항목이라면 그 이름을 사용하는 것도 허용돼요. 그 이름의 가시성은 임포트들의 최대 가시성이에요.
mod m1 {
pub struct Ambig;
}
mod m2 {
// This reexports the same `Ambig` item from a second module.
pub use super::m1::Ambig;
}
mod m3 {
// These both import the same `Ambig`.
//
// The visibility of `Ambig` is `pub` because that is the
// maximum visibility between these two `use` declarations.
pub use super::m1::*;
use super::m2::*;
}
mod m4 {
// `Ambig` can be used through the `m3` globs and still has
// `pub` visibility.
pub use crate::m3::Ambig;
}
const _: () = {
// Therefore, we can use it here.
let _ = m4::Ambig; // OK.
};
fn main() {}
임포트와 매크로 호출의 이름은, 바깥 스코프에 다른 후보가 있을 때 glob 임포트를 통해 해석될 수 없어요.
#![allow(unused)]
fn main() {
mod glob {
pub mod ambig {
pub struct Name;
}
}
// Outer `ambig` candidate.
pub mod ambig {
pub struct Name;
}
const _: () = {
// Cannot resolve `ambig` through this glob
// because of the outer `ambig` candidate above.
use glob::*;
use ambig::Name; // ERROR: `ambig` is ambiguous.
};
}
#![allow(unused)]
fn main() {
// As above, but with macros.
pub mod m {
macro_rules! f {
() => {};
}
pub(crate) use f;
}
pub mod glob {
macro_rules! f {
() => {};
}
pub(crate) use f as ambig;
}
use m::f as ambig;
const _: () = {
use glob::*;
ambig!(); // ERROR: `ambig` is ambiguous.
};
}
참고로 core::panic!이나 std::panic! 중 하나가 표준 라이브러리 프렐류드 때문에 스코프 안으로 들어오고, 사용자가 쓴 glob 임포트가 다른 하나를 스코프 안으로 가져오면, rustc는 현재 모호함에도 불구하고 panic!의 사용을 허용해요. 사용자가 쓴 glob 임포트가 우선해서 이 모호성을 해결해요.
Rust 2021 이후에서 core::panic!과 std::panic!은 동일하게 동작해요. 하지만 이전 에디션에서는 다르죠. 오직 std::panic!만 포맷 인자로 String을 받아요.
extern crate core;
use ::core::prelude::v1::*;
fn main() {
panic!(std::string::String::new()); // ERROR.
}
이건 허용돼요.
#![no_std]
extern crate std;
use ::std::prelude::v1::*;
fn main() {
panic!(std::string::String::new()); // OK.
}
이 동작에 의존하지 마세요. 제거할 계획이에요.
이 모호성 오류는 확장 시점 해석에 한정돼요. 이후 해석 단계에서 주어진 이름에 대해 여러 후보를 갖는 것은 오류로 간주되지 않아요. 임포트 자체가 모호하지 않는 한, 항상 단일하고 모호하지 않은 가장 가까운 해석이 존재해요.
#![allow(unused)]
fn main() {
mod glob {
pub const AMBIG: u8 = 1;
}
mod outer {
pub const AMBIG: u8 = 2;
}
use outer::AMBIG;
const C: () = {
use glob::*;
assert!(AMBIG == 1);
// ^---- This `AMBIG` is resolved during primary resolution.
};
}
이름은 모호한 매크로 재수출을 통해 해석될 수 없어요. 매크로 재수출은 바깥 스코프의 같은 이름에 대한 텍스트 매크로 후보를 가릴 때 모호해요.
#![allow(unused)]
fn main() {
// Textual macro candidate.
macro_rules! ambig {
() => {}
}
// Path-based macro candidate.
macro_rules! path_based {
() => {}
}
pub fn f() {
// This reexport of the `path_based` macro definition
// as `ambig` may not shadow the `ambig` macro definition
// which is resolved via textual macro scope.
use path_based as ambig;
ambig!(); // ERROR: `ambig` is ambiguous.
}
}
이 제한은 컴파일러의 구현 세부 사항, 구체적으로는 현재의 스코프 방문 로직과 이 동작을 지원하는 복잡성 때문에 필요해요. 이 모호성 오류는 앞으로 제거될 수 있어요.
매크로(Macros)
매크로는 사용 가능한 스코프를 순회하며 사용 가능한 후보를 찾아서 해석돼요. 매크로는 함수형 매크로(function-like)용과 속성·파생 매크로용이라는 두 개의 하위 네임스페이스로 나뉘어요. 잘못된 하위 네임스페이스의 해석 후보는 무시돼요.
사용 가능한 스코프 종류는 다음 순서로 방문돼요. 각 종류는 하나 이상의 스코프를 나타내요.
- 파생 헬퍼(Derive helpers)
- 텍스트 스코프 매크로(Textual scope macros)
- 경로 기반 스코프 매크로(Path-based scope macros)
macro_use프렐류드- 표준 라이브러리 프렐류드
- 내장 속성(Builtin attributes)
참고로 컴파일러는 관련 매크로가 스코프를 도입하기 전에 사용된 파생 헬퍼를 해석하려 시도해요. 이 스코프는 올바르게 스코프에 있는 파생 헬퍼 후보를 해석하기 위한 스코프 다음에 방문돼요. 이 동작은 제거될 예정이에요. 자세한 내용은 파생 헬퍼 스코프를 참고하세요.
이 방문 순서는 앞으로 바뀔 수 있는데, 예를 들어 텍스트와 경로 기반 스코프 후보의 방문을 그 어휘 스코프에 따라 번갈아 하는 식이 될 수 있어요.
2018 에디션부터 #[...]가 있을 때 #![...] 프렐류드는 방문되지 않아요.
cfg와 cfg_attr이라는 이름은 매크로 속성 하위 네임스페이스에서 예약돼 있어요.
모호성(Ambiguities)
이름은 매크로 확장 안의 모호한 후보를 통해 해석될 수 없어요. 매크로 확장 안의 후보는, 첫 번째 후보의 매크로 확장 밖에서 온 같은 이름의 후보를 가릴 때, 그리고 해석되는 이름 호출도 첫 번째 후보의 매크로 확장 밖에서 온 것일 때, 모호해요.
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
// Introduce outer candidate definition for `ambig` macro invocation.
macro_rules! ambig {
() => {}
}
// Introduce a second candidate definition for `ambig` inside of a
// macro expansion.
define_ambig!();
// The definition of `ambig` from the second invocation
// of `define_ambig` is the innermost canadidate.
//
// The definition of `ambig` from the first invocation of
// `define_ambig` is the second candidate.
//
// The compiler checks that the first candidate is inside of a macro
// expansion, that the second candidate is not from within the same
// macro expansion, and that the name being resolved is not from
// within the same macro expansion.
ambig!(); // ERROR: `ambig` is ambiguous.
}
역방향은 모호한 것으로 간주되지 않아요.
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
// Swap order of definitions.
define_ambig!();
macro_rules! ambig {
() => {}
}
// The innermost candidate is now less expanded so it may shadow more
// the macro expanded definition above it.
ambig!();
}
해석되는 호출이 가장 안쪽 후보의 확장 안에 있다면 그것도 모호하지 않아요.
#![allow(unused)]
fn main() {
macro_rules! ambig {
() => {}
}
macro_rules! define_and_invoke_ambig {
() => {
// Define innermost candidate.
macro_rules! ambig {
() => {}
}
// Invocation of `ambig` is in the same expansion as the
// innermost candidate.
ambig!(); // OK
}
}
define_and_invoke_ambig!();
}
두 정의가 같은 매크로의 호출에서 온 것이라도 상관없어요. 가장 바깥쪽 후보는 여전히 "덜 확장된" 것으로 간주되는데, 그것이 가장 안쪽 후보의 정의를 담은 확장 안에 있지 않기 때문이에요.
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
macro_rules! ambig {
() => {}
}
}
}
define_ambig!();
define_ambig!();
ambig!(); // ERROR: `ambig` is ambiguous.
}
이것은 이름의 가장 안쪽 후보가 매크로 확장 안에서 온 것이라면 임포트에도 적용돼요.
#![allow(unused)]
fn main() {
macro_rules! define_ambig {
() => {
mod ambig {
pub struct Name;
}
}
}
mod ambig {
pub struct Name;
}
const _: () = {
// Introduce innermost candidate for
// `ambig` mod in this macro expansion.
define_ambig!();
use ambig::Name; // ERROR: `ambig` is ambiguous.
};
}
사용자 정의 속성이나 파생 매크로는 내장 비매크로 속성(예: inline)을 가릴 수 없어요.
// with-helper/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_derive(WithHelperAttr, attributes(non_exhaustive))]
// ^^^^^^^^^^^^^^
// User-defined attribute candidate.
// ...
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
// src/lib.rs
#[derive(with_helper::WithHelperAttr)]
// ERROR: `non_exhaustive` is ambiguous.
struct S;
이것은 내장 속성이 후보가 되는 이름이 무엇이든 적용돼요.
// with-helper/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_derive(WithHelperAttr, attributes(helper))]
// ^^^^^^
// User-defined attribute candidate.
// ...
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
// src/lib.rs
use inline as helper;
// ^----- Built-in attribute candidate via reexport.
#[derive(with_helper::WithHelperAttr)]
// ERROR: `helper` is ambiguous.
struct S;
일차 이름 해석(Primary name resolution)
이 부분은 일차 이름 해석에 대한 향후 확장을 위한 자리 표시자예요.
타입 상대 해석(Type-relative resolution)
이 부분은 타입 의존 해석에 대한 향후 확장을 위한 자리 표시자예요.