6.9. 템플릿 토큰과 확장
이 절에서는 메타 블록 의 본체 안에서 사용할 수 있는 템플릿 토큰의 전체 목록과 함께, 메타 블록이 언제 어떤 메커니즘으로 확장되는지를 정의합니다.
6.9.1. 템플릿 토큰의 형태
템플릿 토큰은 다음과 같은 문법 형태를 가집니다.
template_token ::= "@" identifier
| "@" identifier "." identifier
| "@" identifier "{" <template body> "}"
| "@" identifier "(" <args> ")" "{" <body> "}"
모든 템플릿 토큰은 @ 토큰 (구두점과 연산자 토큰) 으로 시작합니다. 대표적인 예는 다음과 같습니다.
@Self 대상 타입 이름
@field 현재 필드의 자리 표시
@field.name 현재 필드의 이름
@for_fields { ... } 필드 반복 블록
토큰 목록
| 토큰 | 값 | 절 |
|---|---|---|
@Self | 대상 타입 (Name<T, ..>) | 6.9.2 |
@Self.name | 타입 인자 없는 대상 타입 이름 | 6.9.2 |
@Self.is_unit / .is_tuple / .is_named | struct 모양 (부울) | 6.9.2 |
@if_struct / @if_enum / @if_union { .. } | 대상 종류가 맞을 때만 본문 | 6.9.2 |
@for_fields { .. } | 필드마다 본문 | 6.9.3 |
@field, @field.name, @field.type | 현재 필드 (접근 / 이름 / 타입) | 6.9.3 |
@field.is_pub, @field.has_attr(x) | 부울 | 6.9.3 |
@field.has_default, @field.default | 필드의 = expr 유무 (부울) / 그 식 | 6.9.3 |
@for_variants { .. } | enum 변형마다 본문 | 6.9.4 |
@variant.name, @variant.pattern | 현재 변형 (이름 / 매칭 패턴) | 6.9.4 |
@variant.is_unit / .is_tuple / .is_named | 부울 | 6.9.4 |
@for_variant_fields { .. } | 현재 변형의 필드마다 본문 | 6.9.4 |
@apply_if(cond) { .. } | cond 가 참일 때만 본문 | 6.9.5 |
@accept(kind, ..) | 대상 종류 제한 (attribute 전용) | 6.9.5 |
@where_all(Trait) | 타입 파라미터마다 T: Trait | 6.9.5 |
@has_derive_attr(a), @derive_attr(a) | 필드의 derive attribute (부울 / 인자) | 6.9.6 |
@item, @item.name / .params / .return_type / .body | 부착 대상 항목과 그 부분 (attribute 전용) | 6.9.7 |
@attribute | 사용처의 attribute 인자 (attribute 전용) | 6.9.7 |
<이름 토큰>.as_str() | 이름의 문자열 리터럴 | 6.9.3 |
목록에 없는 @이름 은 에러입니다. @where_for_fields 는 예약된 이름이며, 써도 에러입니다.
6.9.2. 타입 컨텍스트
@Self 대상 타입 이름
@Self 는 메타 블록이 적용되는 대상 타입의 이름을 나타냅니다.
#[impl_derive(MyTrait)] pub @derive {
impl MyTrait for @Self { // @Self → Point / 사용자 타입 이름
...
}
}
@if_struct { ... } / @if_enum { ... } / @if_union { ... }
이 토큰들은 대상 타입이 각각 struct, enum, union 일 때만 안의 코드를 생성하도록 해줍니다.
@if_struct {
fn struct_only_method() { ... }
}
@if_enum {
fn enum_only_method() { ... }
}
만약 대상 타입이 다른 종류라면 해당 블록은 무시되며, 코드가 생성되지 않습니다.
@Self.name / @Self.is_unit / .is_tuple / .is_named
@Self.name 은 타입 인자 없는 대상 타입 이름 (Pair<T> 의 Pair) 입니다. @Self.is_unit / @Self.is_tuple / @Self.is_named 는 대상 struct 가 unit struct, 튜플 struct, 이름 있는 필드 struct 인지를 나타내는 부울이며, @apply_if 에 씁니다.
6.9.3. 필드 반복
@for_fields { ... }
@for_fields 는 struct 나 union 의 필드 각각에 대해 블록을 반복합니다. @for_variants 안에서는 현재 변형의 필드를 반복하며 @for_variant_fields 와 같습니다. enum 에 @for_variants 없이 쓰면 에러입니다.
@for_fields {
println(self.@field);
}
위 코드는 각 필드마다 한 번씩 생성됩니다.
@field
@field 는 현재 필드의 자리 표시이며, 필드 접근 위치에서 사용합니다.
self.@field // self.x, self.y, ...
@Self { @field: 0i32 } // 필드 초기화 자리
@field.name 식별자 토큰
식별자가 필요한 자리에서는 필드 이름이 식별자 토큰으로 삽입됩니다.
let v = @field.name; // 식별자 자리에 그 이름이 들어감
fn @field.name() -> i32 { ... } // 함수 이름으로도 사용 가능
문자열이 필요하면 .as_str() 을 붙입니다. @field.name.as_str(), @variant.name.as_str(), @item.name.as_str(), @Self.as_str() 은 확장 시점에 그 이름의 문자열 리터럴 ("x") 로 바뀝니다. @Self.as_str() 은 타입 인자 없는 이름입니다. 문자열이나 f-string 안에 적은 @item.name 같은 이름 토큰은 글자 그대로 치환됩니다.
@field.type
@field.type 은 필드의 타입을 가리킵니다.
fn typed<T>() where T == @field.type { ... }
@field.is_pub
이 토큰은 필드가 pub 이면 참, 아니면 거짓의 값을 가집니다.
@apply_if(@field.is_pub) {
// pub 필드만 처리
}
@field.has_attr(X)
필드에 #[X] 가 부착되어 있으면 참이 됩니다.
@apply_if(@field.has_attr(skip)) {
// #[skip] 부착된 필드만 처리
}
@field.has_default / @field.default
@field.has_default 는 필드에 기본값 (x: i32 = 0, 2.3.3) 이 있으면 참이고, @field.default 는 그 = expr 의 식입니다. 기본값이 없는 필드에서 @field.default 를 쓰면 에러입니다.
@apply_if(@field.has_default) {
let @field.name = @field.default;
}
6.9.4. 변형 (variant) 반복
@for_variants { ... }
@for_variants 는 enum 의 각 variant 에 대해 블록을 반복합니다.
@for_variants {
@variant.name => /* ... */
}
@variant.name variant 이름
@variant.name 은 variant 의 이름입니다.
@variant.pattern 매칭 패턴
@variant.pattern 은 variant 의 매칭 패턴 형태로, 예를 들면 Shape::Circle(r), Shape::Pending, Shape::Click { x: x1, y: y1 } 와 같은 모양이 됩니다. 여기서 r, x1, y1 자리의 바인딩은 컴파일러가 만든 이름이며 (위생, 6.9.8), 그 값은 @for_variant_fields 안의 @field 로 꺼냅니다. 바인딩 이름을 템플릿이 직접 쓸 수는 없습니다.
match self {
@for_variants {
@variant.pattern => /* ... */
}
}
@variant.is_unit / @variant.is_tuple / @variant.is_named
이 토큰들은 variant 의 종류를 검사할 때 사용합니다.
@apply_if(@variant.is_tuple) {
// tuple variant 만 처리
}
@for_variant_fields { ... }
variant 안의 필드들에 대해 반복할 때는 @for_variant_fields 를 사용합니다. 그 안에서는 @field.name 등을 함께 사용할 수 있습니다.
@for_variants {
@variant.pattern => {
@for_variant_fields {
process(@field);
}
}
}
6.9.5. 조건부와 경계
@apply_if(cond) { ... }
@apply_if 는 cond 가 참일 때만 안의 코드를 생성합니다.
@apply_if(@field.is_pub) {
// 코드
}
@apply_if(!@field.has_attr(skip)) {
// skip 이 *없는* 필드만
}
@apply_if(@field.is_pub && @field.has_attr(serialize)) {
// 두 조건 모두 참
}
여기서 cond 는 템플릿 토큰의 부울 값 (예: @field.is_pub, @field.has_attr(...)) 과 일반 부울 연산자 (!, &&, ||) 를 결합한 식이며, 컴파일 타임에 평가됩니다.
else 분기는 별도의 키워드 없이 반대 조건의 @apply_if 를 사용해 표현합니다.
@accept(...)
@accept 는 derive 또는 attribute 의 대상 종류를 제한하는 데 사용합니다. 콤마나 | 로 구분된 항목 종류 목록을 받습니다.
#[impl_attribute(my_wrap)] pub @attribute {
@accept(struct, enum) // struct, enum 만 받음 - union 거부
@item
}
#[impl_attribute(timed)] pub @attribute {
@accept(fn) // fn 만 받음
fn @item.name(...) { ... }
}
가능한 종류는 다음과 같습니다.
| 종류 | 의미 |
|---|---|
struct | struct 항목 |
enum | enum 항목 |
union | union 항목 |
fn | 함수 또는 메서드 |
impl | impl 블록 |
trait | trait 항목 |
여러 종류를 콤마나 | 로 구분하면 그중 하나만 맞아도 허용됩니다. 만약 대상이 허용 종류가 아니라면 사용처에서 컴파일 에러가 발생합니다.
@where_all(Trait)
@where_all 은 모든 필드 타입이 Trait 를 충족해야 한다는 조건을 자동 where 절로 추가합니다.
#[impl_derive(Eq)] pub @derive {
@where_all(Eq) // 모든 필드의 타입이 Eq 면 OK
impl Eq for @Self {
fn eq(self, other: Self) -> bool {
@for_fields {
if !self.@field.eq(other.@field) { return false; }
}
true
}
}
}
이렇게 하면 생성되는 impl 에는 대상 타입의 타입 파라미터마다 T: Eq 가 추가됩니다. 구체 필드 타입에 대한 절은 넣지 않으며, 그 필드들이 Eq 를 구현하는지는 생성된 본문의 타입 검사가 봅니다. 템플릿의 impl 뒤에는 대상 타입의 제네릭 파라미터가 들어가며, 바운드는 @where_all 이 붙인 것뿐입니다.
6.9.6. derive_attribute 토큰
다음은 #[impl_derive_attribute(Trait, attr)] 안에서 추가로 사용할 수 있는 토큰들입니다.
@has_derive_attr(name)
이 토큰은 필드에 derive_attribute 의 대상 attr 가 부착되어 있는지를 나타냅니다.
@derive_attr(name) 그 attr 의 인자
@derive_attr(name) 은 부착된 attr 의 인자를 가리킵니다.
#[impl_derive_attribute(Serialize, rename)] pub @derive {
@for_fields {
@apply_if(@has_derive_attr(rename)) {
let key = @derive_attr(rename); // #[rename("...")] 의 인자
obj.set(key, ...);
}
}
}
6.9.7. attribute 전용 토큰
다음 토큰들은 #[impl_attribute(name)] 안에서 사용합니다.
@item 원본 항목 재사용
@item 은 부착 대상 항목 전체를 그대로 재사용할 때 사용합니다.
#[impl_attribute(deprecated_warning)] pub @attribute {
@item // 원본 그대로
}
// 위는 attribute 가 사실상 no-op - 원본 그대로 통과
@item.body
@item.body 는 함수의 본체이며, 함수 항목에 대해서만 의미가 있습니다.
@item.name, @item.params, @item.return_type
이 토큰들은 함수 시그니처의 각 부분을 가리킵니다.
#[impl_attribute(timed)] pub @attribute {
fn @item.name(@item.params) -> @item.return_type {
let start = now();
let r = { @item.body };
log(f"@item.name took {now() - start}");
r
}
}
@attribute #[...] 인자
@attribute 는 부착 시점에 적힌 인자를 의미합니다. 즉 #[impl_attribute(name)] 가 부착된 항목을 사용처가 #[name(args)] 형태로 사용했다면, @attribute 는 그 args 가 됩니다.
#[impl_attribute(prefix)] pub @attribute {
fn @item.name() {
let p = @attribute; // 사용처의 인자
log(f"{p}: {@item.name}");
@item.body
}
}
// 사용처
#[prefix("INFO")]
fn run() { ... }
// → 생성.
// fn run() {
// let p = "INFO";
// log(f"{p}: run");
// ...
// }
6.9.8. 호출부 (macro) 토큰
위의 토큰들은 정의가 붙은 타입이나 항목을 바인딩합니다. macro (6.8.12) 는 대신 호출자의 토큰을 매처로 바인딩하며, 아래 토큰을 씁니다. @for 와 @concat 은 위의 형태들과 공유합니다. shape 조건(@apply_if 와 그것이 검사하는 @Self/@field/@variant)은 공유하지 않습니다. macro 는 검사할 타입 shape 가 없습니다.
| 토큰 | 매처 / 템플릿에서 | 절 |
|---|---|---|
@name: <fragment> | 매처: fragment 하나 캡처 (expr, stmt, block, ty, pat, path, ident, literal, item, tt) | 6.9.8 |
@name | 템플릿: 캡처한 fragment, 또는 @for 안에서 현재 원소 | 6.9.8 |
@rep[sep = ",", trailing] { .. } | 매처: 하위 패턴을 반복 (구분자·끝 구분자 선택) | 6.9.8 |
@for @name { .. } | 템플릿: 캡처 시퀀스의 원소마다 본체 (여럿이면 lockstep) | 6.9.3 |
@concat(a, @name, ..) | 템플릿: 이름 토큰·문자열 리터럴을 인덱싱되는 식별자 하나로 이음 | 6.9.8 |
@name: <fragment> fragment 캡처
매처에서 @name: <fragment> 는 그 문법을 매칭해 바인딩합니다. tt 를 뺀 fragment 는 실제 구문 노드로 파싱되므로 캡처한 코드는 타입이 붙고 검사되며 도구가 그 안의 이름을 해석합니다. tt 는 rok 문법이 아닌 DSL 을 위해 원시 토큰 트리 하나 (토큰 또는 균형 그룹) 를 캡처합니다. 매처의 다른 리터럴 토큰은 그대로 나타나야 합니다. 템플릿에서 @name 은 캡처를 치환하며 호출 위치의 위생을 유지합니다 (6.9.9).
@rep / @for 반복
@rep[sep = ",", trailing] { <하위 패턴> } 은 하위 패턴을 0회 이상 매칭하고, 안에서 캡처한 메타변수는 시퀀스를 바인딩합니다. @for @name { <본체> } 는 @for_fields (6.9.3) 와 같은 반복자로, 여기서는 그 시퀀스에 적용됩니다. 한 @rep 의 메타변수들은 lockstep 으로 돌며 길이가 같아야 합니다. @rep 과 @for 는 중첩됩니다.
@concat(..) 이름 합성
@concat(..) 은 이름 토큰 (@name, @Self.name, 맨 식별자) 과 문자열 리터럴을 하나의 식별자로 잇되, 정의나 경로 위치에서만 쓸 수 있습니다. 만든 식별자는 기록되므로 심볼 검색과 정의로 이동이 생성된 항목에 도달합니다 (6.8.12). 이름이 아닌 조각이나 이름 위치 밖의 사용은 에러입니다.
6.9.9. 확장 메커니즘
모으기와 확장
메타 확장은 다음 흐름으로 진행됩니다.
- parse 전의 pre-pass 가 라이브러리와 사용자 패키지의 모든 파일에서 메타 블록을 모읍니다 (6.8.11).
- 파서가
#[derive(X)]나#[x]가 붙은 항목을 만나면, 그 메타 블록의 본체에서 템플릿 토큰을 항목의 값으로 치환하고 결과를 다시 파싱합니다. - 생성된 항목도 다시 메타 처리됩니다. 고정점에 도달할 때까지 반복합니다.
- 순환 (
A → B → A) 은 에러입니다 (6.8.8).
cfg 와의 관계
메타 블록 자체에 붙은 #[cfg(..)] 는 그 블록의 등록 을 정합니다. cfg 평가가 메타 블록 수집보다 먼저이므로, cfg 가 거짓인 블록의 derive / attribute 는 등록되지 않습니다. 그래서 서로 배타적인 cfg 아래에서 두 블록이 같은 이름을 정의할 수 있습니다. 블록을 둘러싼 모듈 (인라인 mod 와 파일 모듈) 의 #[cfg] 도 등록에 적용됩니다. 파일 안의 #![cfg] 는 아직 적용되지 않습니다.
항목에 붙은 #[cfg] 는 그 항목에서 생성된 모든 항목에 그대로 복사됩니다. 따라서 cfg 가 거짓인 항목은 생성물과 함께 사라지며, 결과는 다음 순서와 같습니다.
parse → cfg eval → meta expansion (fixed-point) → 이름 해소 → 타입 검사 → ...
위생 (hygiene)
템플릿이 묶는 지역 이름, 곧 let / for 의 바인딩과 클로저 매개변수 (f-string 의 {..} 안 포함) 는 확장할 때 겹치지 않는 이름으로 바뀝니다. 그래서 템플릿의 지역은 항목에서 온 이름 (@item.body, @item.params, let @field.name = ..) 을 가리거나 붙잡지 않습니다.
#[impl_derive(Foo)] pub @derive {
impl Foo for @Self {
fn foo(self) -> i32 {
let temp = compute(); // 확장 시 이름이 바뀜
temp * 2
}
}
}
// 사용처
#[derive(Foo)]
struct S { temp: str; } // 사용처의 'temp' 필드
let s = S { temp: "hi" };
let r = s.foo(); // 메타가 만든 'temp' 와 충돌 안 함
match arm 패턴이 묶는 이름과 S { x } 축약은 바꾸지 않습니다. 템플릿은 그 자리에 항목과 겹칠 수 있는 이름을 쓰지 않습니다. @variant.pattern 의 바인딩은 컴파일러가 만든 이름이므로 겹치지 않습니다.
자유 식별자의 해소
생성된 항목은 사용처의 모듈 에 놓입니다. 따라서 템플릿 안에서 묶이지 않은 식별자 (예: compute(), now(), log()) 는 메타 블록의 모듈이 아니라 사용처에서 해소됩니다.
템플릿 작성자는 절대 경로 (rok::core::.., 라이브러리의 pkg::..) 를 쓰거나, 사용처에서 보이는 이름 (묵시 use rok::core::**; 의 항목 등) 만 씁니다. 템플릿 안의 pkg:: 는 어디서 확장되든 메타 블록을 정의한 패키지를 가리킵니다.
#[impl_derive(MyHash)] pub @derive {
impl MyHash for @Self {
fn my_hash(self) -> u64 {
let h: u64 = 0u64;
@for_fields { h = mylib::hash::combine(h, self.@field.my_hash()); } // 절대 경로
h
}
}
}
진단 위치
확장 중의 에러와 확장 결과의 파싱 에러는 템플릿의 위치를 가리키고, 사용처 (#[derive(X)] / #[x]) 를 note 로 덧붙입니다. 템플릿 토큰을 거쳐 항목에서 온 토큰은 항목의 위치를 가리킵니다.
6.9.10. 디버깅
rokc build --emit=expanded (9.9.10) 는 사용자 패키지의 확장 결과를 소스 텍스트로 출력합니다. 확장마다 // expanded from this #[derive(X)] (file:line:col) 주석과 생성된 소스가 이어지며, 생성물이 다시 확장되면 바깥 확장 뒤에 이어집니다.
6.9.11. 정리
템플릿 토큰은 rok 메타 시스템의 코드 생성 어휘입니다. 단순한 형태이지만 실용적인 derive 와 attribute 의 핵심 사례를 모두 다룰 수 있습니다.
토큰 변환의 유연성을 제한하는 대신 디버깅 가능성과 도구 친화성, 위생 보장을 얻는 것이 rok 의 선택입니다.
이로써 6장 (메타 시스템) 을 마무리합니다.