Rust 선언형 매크로를 이용한 구문 분석

이전 글에서 Rust 선언형 매크로의 메타변수에 대해 다룬 내용을 바탕으로, 이번에는 Rust의 다양한 아이템 구문을 분석하는 방법을 살펴봅니다. Rust 크레이트(crate)는 여러 아이템(item)으로 구성되며, fn, struct, enum, impl, mod 등 각 정의가 하나의 아이템이 됩니다. 이 글에서는 Functionstruct 아이템의 구문 분석에 초점을 맞춥니다.

함수 아이템 분석

가장 기본적인 함수 정의부터 시작해보겠습니다.

fn display_message() {
    // 함수 본문
}

display_message 함수는 키워드 fn으로 시작하고, 그 뒤에 함수 이름($func_name: ident), 빈 매개변수 목록을 나타내는 (), 그리고 함수 본문인 $body_block: block이 따라옵니다. 이를 매치하는 매크로는 다음과 같이 작성할 수 있습니다.

macro_rules! parse_simple_fn {
    (fn $func_name: ident () $body_block: block) => {
        // 매칭된 함수 정의를 그대로 다시 생성
        fn $func_name() $body_block
    };
}

parse_simple_fn! {
    fn greet_user(){
        println!("안녕하세요!");
    }
}
// greet_user(); // 이 함수는 매크로에 의해 정의됩니다.

조금 더 복잡한 형태의 함수를 매치해봅시다. 흔히 볼 수 있는 속성, 가시성, 비동기 키워드, 매개변수, 반환 타입을 포함한 함수입니다.

#[allow(dead_code)]
pub async fn fetch_data(id: u32) -> String {
    format!("데이터-{}", id)
}

완벽한 함수 구문 정의는 Rust Reference에서 찾아볼 수 있습니다. 여기서는 분석 방법에 집중하고, 필요한 경우 복잡한 부분을 추가로 분해하여 처리할 수 있음을 보여줍니다.

macro_rules! parse_advanced_fn {
    (
        #[$attribute: meta]
        $visibility: vis async fn $func_name: ident ($param_name: ident : $param_type: ty) -> $return_type:ty $body_block: block
    ) => {
        // 매칭된 모든 요소를 다시 조합하여 새로운 함수 생성
        #[$attribute]
        $visibility async fn $func_name($param_name: $param_type) -> $return_type $body_block
    };
}

// parse_advanced_fn! { /* 위의 fetch_data 함수를 매칭하여 사용 */ }

속성의 개수나 매개변수의 개수가 고정적이지 않은 경우는 어떨까요?

#[derive(Debug)]
#[allow(unused_variables)]
pub async fn process_inputs(value1: i32, value2: f64) -> f64 {
    value1 as f64 * value2
}

이를 처리하기 위해 반복자($()*)와 선택적 패턴($(,)?)을 활용합니다.

macro_rules! parse_flexible_fn {
    (
        $(#[$attr: meta])* // 0개 이상의 속성
        $vis: vis async fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
    ) => {
        $(#[$attr])*
        $vis async fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
    };
}

함수에 async 키워드가 없을 수도 있습니다. 이때는 매크로에 추가적인 분기(rule)를 정의하여 처리할 수 있습니다.

macro_rules! parse_complete_fn {
    // async 함수 처리
    (
        $(#[$attr: meta])*
        $vis: vis async fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
    ) => {
        $(#[$attr])*
        $vis async fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
    };

    // 일반 함수 처리
    (
        $(#[$attr: meta])*
        $vis: vis fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
    ) => {
        $(#[$attr])*
        $vis fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
    };
}

이렇게 단순히 함수 정의를 복사하는 것이 무슨 이점이 있을까요? 매크로를 사용하면 매치된 구문 안에 원하는 코드를 삽입하여 컴파일 시점에 로직을 추가하거나 변경할 수 있습니다. 다음 예시는 함수 호출 전후에 로그를 출력하는 코드를 주입합니다.

macro_rules! enhance_function {
    (
        $(#[$attr: meta])*
        $vis: vis fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
    ) => {
        // 함수 정의 시점에 로그 출력
        println!("INFO: 함수 정의 감지됨 -> {}({})", stringify!($func_name), stringify!($($param_name: $param_type),*));
        $(#[$attr])*
        $vis fn $func_name($($param_name: $param_type),*) -> $return_type {
            // 함수 호출 시점에 로그 출력
            print!("TRACE: {} 호출됨 (", stringify!($func_name));
            $(print!("{}: {}, ", stringify!($param_name), $param_name);)*
            println!(")");

            // 원본 함수 본문 실행
            $func_body
        }
    };
}

enhance_function! {
    #[allow(unused_variables)]
    pub fn calculate_area(width: u32, height: u32) -> u32 {
        width * height
    }
}

// 매크로에 의해 생성된 함수 호출
calculate_area(10, 20);

위 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다.

INFO: 함수 정의 감지됨 -> calculate_area(width : u32, height : u32)
TRACE: calculate_area 호출됨 (width: 10, height: 20, )

구조체(struct) 아이템 분석

구조체에는 크게 두 가지 형태가 있습니다: 필드를 가진 구조체와 튜플 구조체, 그리고 유닛 구조체입니다.

#[derive(Debug)]
struct UserProfile {
    pub username: String,
    email: String,
}

#[derive(Clone)]
struct Point(f64, f64);

struct EmptyData;

이제 이들을 매치하는 매크로를 작성해봅시다.

macro_rules! parse_struct_definition {
    // 필드를 가진 구조체 (e.g., struct UserProfile { ... })
    (
        $(#[$struct_attr: meta])* // 구조체에 붙은 속성
        $vis: vis struct $struct_name: ident {
            $(
                $(#[$field_attr: meta])* // 필드에 붙은 속성
                $field_vis: vis $field_name: ident : $field_type: ty
            ),* // 0개 이상의 필드
            $(,)? // 선택적 후행 쉼표
        }
    ) => {
        $(#[$struct_attr])*
        $vis struct $struct_name {
            $(
                $(#[$field_attr])*
                $field_vis $field_name: $field_type
            ),*
        }
    };

    // 튜플 구조체 (e.g., struct Point(f64, f64);)
    (
        $(#[$struct_attr: meta])*
        $vis: vis struct $struct_name: ident (
            $($field_type: ty),* // 0개 이상의 필드 타입 (이름 없음)
            $(,)?
        );
    ) => {
        $(#[$struct_attr])*
        $vis struct $struct_name (
            $($field_type),*
        );
    };

    // 유닛 구조체 (e.g., struct EmptyData;)
    (
        $(#[$struct_attr: meta])*
        $vis: vis struct $struct_name: ident;
    ) => {
        $(#[$struct_attr])*
        $vis struct $struct_name;
    };
}

parse_struct_definition! {
    #[derive(Debug, Default)]
    pub struct Configuration {
        pub path: String,
        count: u32,
    }
}

parse_struct_definition! {
    #[derive(Copy, Clone)]
    pub struct Coordinates(f32, f32, f32);
}

parse_struct_definition! {
    struct Marker;
}

태그: Rust declarative macros Macro Rules Syntax Parsing Compile-time Metaprogramming

7월 17일 03:07에 게시됨