선언적 매크로에서 입력을 토큰 트리 단위로 조금씩 소비하며 복잡한 문법을 파싱할 수 있는 ‘TT 먼처’ 패턴과, 그 성능 특성(특히 제곱 시간) 및 최적화 요령을 설명한다.
rustmacro_rules! mixed_rules { () => {}; (trace $name:ident; $($tail:tt)*) => { { println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; (trace $name:ident = $init:expr; $($tail:tt)*) => { { let $name = $init; println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; } fn main() { let a = 42; let b = "Ho-dee-oh-di-oh-di-oh!"; let c = (false, 2, 'c'); mixed_rules!( trace a; trace b; trace c; trace b = "They took her where they put the crazies."; trace b; ); }
이 패턴은 아마도 사용할 수 있는 매크로 파싱 기법 중 가장 강력한 축에 속하며, 상당히 복잡한 문법도 파싱할 수 있게 해줍니다. 하지만 과도하게 사용하면 컴파일 시간이 늘어날 수 있으므로 주의해서 사용해야 합니다.
_TT 먼처(TT muncher)_는 재귀적인 macro_rules! 매크로로, 입력을 한 번에 전부 처리하지 않고 한 단계씩 점진적으로 처리하는 방식으로 동작합니다. 각 단계에서 입력의 시작 부분에서 어떤 토큰 시퀀스를 매칭해 제거(먹어치움, munch)하고, 중간 출력을 생성한 다음, 남은 입력(tail)에 대해 다시 재귀 호출합니다.
이름에 “TT”가 들어가는 이유는, 처리되지 않은 입력 부분을 항상 $($tail:tt)* 형태로 캡처하기 때문입니다. 이는 tt 반복이 매크로 입력의 일부를 손실 없이(losslessly) 캡처할 수 있는 유일한 방법이기 때문입니다.
TT 먼처에 대한 ‘강한’ 제약은 macro_rules! 매크로 시스템 전체에 적용되는 제약과 사실상 동일합니다.
macro_rules!로 캡처 가능한 문법 구성 요소에 대해서만 매칭할 수 있습니다.하지만 매크로 재귀 제한(recursion limit)을 염두에 두는 것이 중요합니다. macro_rules!에는 꼬리 재귀 제거(tail recursion elimination)나 꼬리 재귀 최적화 같은 것이 전혀 없습니다. 따라서 TT 먼처를 작성할 때에는 재귀를 가능한 한 합리적인 선에서 제한하려고 노력하는 것이 권장됩니다. 이를 위해 입력 변형(variation)을 처리하는 추가 규칙을 더해(중간 레이어로 재귀하는 대신), 혹은 표준 반복을 더 쉽게 적용할 수 있도록 입력 문법에서 일정 부분 타협하는 방법을 사용할 수 있습니다.
TT 먼처는 본질적으로 제곱 시간(quadratic) 특성을 띱니다. 예를 들어 토큰 트리 하나를 소비하고 남은 입력에 대해 자기 자신을 재귀 호출하는 TT 먼처 규칙이 있다고 합시다. 여기에 100개의 토큰 트리를 전달하면:
이런 식으로 1까지 내려갑니다. 이는 전형적인 제곱 패턴이며, 입력이 길어지면 매크로 확장이 컴파일 시간을 크게 악화시킬 수 있습니다.
TT 먼처를 지나치게 많이, 특히 입력이 긴 경우에 사용하지 않도록 하세요. recursion_limit 속성의 기본값은 건전성 검사(sanity check)로 유용합니다. 만약 그 한도를 넘어야 한다면 문제가 될 가능성이 큽니다.
여러 항목을 한 번에 처리하기 위해 한 번만 호출되는 TT 먼처를 작성할지, 아니면 한 항목을 처리하기 위한 더 단순한 매크로를 여러 번 호출할지 선택할 수 있다면, 후자를 선호하세요. 예를 들어 다음처럼 호출되는 매크로를:
rust#![allow(unused)] fn main() { macro_rules! f { ($($tt:tt)*) => {} } f! { fn f_u8(x: u32) -> u8; fn f_u16(x: u32) -> u16; fn f_u32(x: u32) -> u32; fn f_u64(x: u64) -> u64; fn f_u128(x: u128) -> u128; } }
다음처럼 호출되도록 바꿀 수 있습니다:
rust#![allow(unused)] fn main() { macro_rules! f { ($($tt:tt)*) => {} } f! { fn f_u8(x: u32) -> u8; } f! { fn f_u16(x: u32) -> u16; } f! { fn f_u32(x: u32) -> u32; } f! { fn f_u64(x: u64) -> u64; } f! { fn f_u128(x: u128) -> u128; } }
입력이 길수록, 이런 변경이 컴파일 시간을 개선할 가능성이 커집니다.
또한 TT 먼처 매크로에 규칙이 많다면, 가장 자주 매칭되는 규칙을 가능한 한 앞쪽에 배치하세요. 이는 불필요한 매칭 실패를 피하는 데 도움이 됩니다. (사실 이는 TT 먼처뿐 아니라 어떤 종류의 선언적 매크로에도 적용되는 좋은 조언입니다.)
마지막으로 * 또는 +를 통한 일반적인 반복으로 매크로를 작성할 수 있다면, TT 먼처보다 그것을 우선해야 합니다. 특히 TT 먼처의 각 호출이 한 번에 토큰 하나만 처리하는 정도라면 그럴 가능성이 높습니다. 더 복잡한 경우에는 quote 크레이트 내부에서 사용되는 고급 기법이 있는데, 개념적 복잡성이 늘어나는 대가로 이 제곱 동작을 피할 수 있습니다. 자세한 내용은 이 코멘트를 참고하세요.