6.6. #[keyword(...)] 매직
#[keyword("name")] 속성은 표준 라이브러리가 새 키워드를 등록할 때 사용하는 메커니즘입니다.
6.6.1. 동기
키워드 의 예약 키워드 목록은 언어가 직접 정의하는 고정된 집합입니다. 그렇다면 이 외의 키워드를 추가하려고 할 때마다 매번 언어 사양을 수정해야 하는 걸까요?
표준 라이브러리는 종종 키워드처럼 보이는 토큰을 도입하고 싶을 때가 있습니다. 예를 들어 어떤 라이브러리 DSL 의 도메인 키워드 같은 것이 그렇습니다. #[keyword] 매직은 바로 이러한 상황에서 라이브러리가 언어 수준 키워드를 등록할 수 있도록 허가된 통로 역할을 합니다.
6.6.2. 형태
keyword_attr ::= "#" "[" "keyword" "(" string_lit ")" "]"
#[keyword("yield_priority")]
fn yield_priority_handler(...) { ... }
#[keyword("with")]
fn with_handler(...) { ... }
기본 형태는 #[keyword("name")] 이며, 인자로 키워드 이름 (문자열 리터럴, 식별자 모양) 을 받습니다. 핸들러는 자유 함수 입니다. 메서드나 트레잇 항목에는 부착할 수 없습니다.
struct 에 붙인 #[keyword("name")] 는 타입 키워드 를 등록합니다. 그 이름이 타입 자리에서 그 struct 를 가리킵니다 (int → rok::core::num::bigint::Int, float → rok::core::num::bigfloat::Float, stdlib 2.1.2). 타입 키워드는 표준 라이브러리만 등록할 수 있습니다. 함수와 struct 가 아닌 항목에 부착하면 에러입니다.
6.6.3. 등록의 효과
#[keyword("name")] 가 어딘가에 등록되면 다음과 같은 효과가 발생합니다.
- 그 컴파일 단위에서 식별자
name이 키워드로 토큰화됩니다. - 키워드의 사용은 핸들러 호출로 desugar 됩니다.
name(args)는 핸들러의 절대 경로 호출이고, 뒤에 블록이 오면 트레일링 클로저 인자가 됩니다.
#[keyword("with")]
fn with_handler(m: Mutex<i32>, body: fn()) { ... }
with(m) { critical(); }
// ⇒ pkg::with_handler(m) { critical(); } - 핸들러 + 트레일링 클로저
이전에는 name 을 일반 식별자로 쓸 수 있었지만, 등록 후에는 키워드 토큰이 되어 버립니다. 따라서 name 을 선언이나 멤버의 이름으로 쓰면 컴파일 에러입니다. let name, fn name, struct name, enum name, trait name, mod name, for name in, .name, ::name, name: (필드 / 인자 이름), name:: 이 그 자리입니다. f-string 의 {..} 안에서도 같은 규칙으로 바뀝니다.
6.6.4. 충돌 검사
#[keyword("if")] // ERROR - 'if' 는 예약 키워드
fn ... { ... }
키워드 의 예약 키워드나, 고정 위치에서만 키워드인 이름 (type, any, in, where, throws, rethrows, catch, finally, const, static, extern, mut, union) 과 경로 루트 (pkg, sup, rok) 는 등록할 수 없습니다. 등록 시점에 즉시 거부됩니다.
한 빌드 안에서 같은 키워드를 두 번 등록하면, 같은 패키지 안이든 서로 다른 라이브러리이든 에러입니다.
package_a 가 #[keyword("with")] 등록
package_b 도 #[keyword("with")] 등록 → 두 패키지를 한 빌드에 쓰면 ERROR
6.6.5. 적용 범위
#[keyword] 의 효력은 그 등록을 포함하는 컴파일 단위 (보통 패키지) 안에서만 유효합니다. 다른 패키지에서는 같은 이름이 일반 식별자로 작동합니다. 예외로, 표준 라이브러리의 타입 키워드 (int, float) 는 모든 패키지에 적용됩니다.
이러한 지역적 효과 덕분에 라이브러리 사이의 충돌이 줄어들고, 어떤 패키지가 어떤 키워드를 도입하는지 사용자가 의식적으로 결정할 수 있습니다.
6.6.6. 사용 패턴
#[keyword] 는 드물게 쓰이며, 주로 다음과 같은 경우에 도입됩니다.
- 도메인 특화 언어 (DSL). 표 데이터, 그리드, 회로 등 도메인 단어가 기존 식별자 자리에서 키워드처럼 동작하는 것이 자연스러울 때입니다.
- 연산자 매직으로 표현하기 어려운 블록 또는 본체 형태의 새 구문일 때입니다.
대부분의 경우에는 함수와 트레일링 클로저의 조합만으로도 키워드처럼 보이는 패턴을 만들 수 있습니다 (트레일링 클로저). 이러한 형태가 가능하다면 그쪽을 우선하는 편이 좋습니다.
6.6.7. scanner pre-pass 와의 결합
#[keyword] 의 등록을 언제 인식하느냐는 컴파일러의 scanner pre-pass 가 처리하는 부분입니다.
- 컴파일러는 parse 전에 source 파일들을 빠르게 스캔하여
#[keyword(...)]등록을 모읍니다. - 등록 표를 lexer 에 전달합니다.
- 정식 어휘 분석에서 등록된 이름들이 키워드 토큰으로 분리됩니다.
- 파서가 등록된 핸들러로 desugar 합니다.
이 pre-pass 덕분에 등록의 위치 (몇 번째 파일, 몇 번째 줄) 와 무관하게 컴파일 단위 안에서 일관성이 유지됩니다. 사용자께서는 등록 순서를 의식하실 필요가 없습니다.
6.6.8. 키워드 핸들러의 시그니처
컴파일러는 핸들러의 시그니처를 따로 검사하지 않습니다. 키워드 사용이 핸들러 호출로 바뀐 뒤 (6.6.3), 그 호출이 보통 함수 호출로 타입 검사됩니다. 따라서 핸들러의 매개변수가 키워드의 사용 형태 (인자, 트레일링 클로저) 를 정합니다.
6.6.9. 컨텍스트 키워드의 부재 (재확인)
키워드 에서 정한 컨텍스트 키워드 미도입 원칙은 등록된 키워드에도 그대로 적용됩니다.
#[keyword("with")] 가 등록되면 with 는 어디서든 키워드로 토큰화됩니다. "특정 위치에서만 키워드" 같은 형태는 도입하지 않습니다.
이러한 일관성은 lexer 와 parser 의 단순함을 유지하는 데 도움이 됩니다.
6.6.10. 등록의 가시성
#[keyword] 가 부착된 항목 자체의 가시성 (가시성) 은 항목 접근에만 적용되며, 키워드 등록 효과와는 별개의 사안입니다.
pub(pkg) fn keyword_handler(...) { ... }
#[keyword("with")]
fn another_handler(...) { ... } // 키워드 등록 - 패키지 전체에 적용
핸들러를 외부에서 호출할 수 있는지는 가시성으로 결정되지만, 키워드 등록 자체는 컴파일 단위 전체에 영향을 미칩니다.
6.6.11. #[keyword] 와 사용자 정의 매크로의 차이
다른 언어들의 매크로 시스템과 비교해 보면 다음과 같습니다.
- Rust proc macro: 임의 토큰 변환을 수행하며, 사용자가 자유롭게 코드를 생성할 수 있습니다. 위생은 별도 사안입니다.
- Lisp macro: 동적 매크로로, 런타임에도 등록할 수 있습니다.
- rok
#[keyword]: 컴파일 타임 키워드 등록 방식이며, 핸들러 함수가 desugar 를 결정합니다. 자유로운 토큰 변환은 불가능합니다.
rok 은 의도적으로 제한된 매크로 시스템을 갖추고 있습니다. 표현력과 도구 친화성의 트레이드오프에서 도구 친화성을 우선하는 셈입니다.
6.6.12. 등록된 키워드 vs 함수 호출
// 키워드 형태
with(mutex) { critical(); }
// 함수 호출 형태 (등록 없이)
with_lock(mutex) { critical(); } // 함수 + 트레일링 클로저
대부분의 DSL-like 패턴은 함수와 트레일링 클로저의 조합만으로도 충분합니다. #[keyword] 등록은 그러한 형태로 표현하기 부자연스러운 경우에만 한정해서 사용하는 편이 좋습니다.
6.6.13. 평가 시점
키워드 등록은 scanner pre-pass 시점에 처리되며, 이는 다른 메타 처리 (cfg, 메타 블록 확장 등) 보다 먼저 이루어집니다.
이러한 순서 덕분에 키워드 처리기 자체에 cfg 를 조건적으로 적용할 수도 있고, 메타 블록의 본체 안에서 키워드를 사용할 수도 있게 됩니다. 메타 블록이나 키워드 처리기의 #[cfg] 는 그것을 정의한 패키지의 cfg (그 패키지 자신의 feature, 9.10.4c) 로 평가합니다.
6.6.14. 사용 자제 권장
#[keyword] 는 드물게, 그리고 신중하게 사용하는 것이 좋습니다. 등록할 때마다 언어 표면이 늘어나며, 다른 사용자가 새 키워드를 보고 의미를 추측해야 하는 부담이 생기기 때문입니다.
가장 보편적으로 도입되는 언어 키워드라면 키워드 의 예약 목록에 직접 추가하는 편이 (즉, 언어 사양 변경의 형태가) 더 적절할 수도 있습니다.