用宣告式宏解析 Rust 語法之 enum parser

2023-06-17 15:00:17

上一篇用宣告式宏解析 Rust 語法
我們的 "macro parser" 解析了 functionstruct, 這篇來嘗試 parse 一下更復雜的 enum
為什麼說 enum 更復雜?因為它不像 struct 結構內都是 identifier: type 那樣規律。
enum 內部的 EnumItem 可能是一個簡單的 identifier, 也可能是 tuplestruct, 還可能是 inttype

Syntax
Enumeration :
enum IDENTIFIER GenericParams? WhereClause? { EnumItems? }

EnumItems :
EnumItem ( , EnumItem )* ,?

EnumItem :
OuterAttribute* Visibility?
IDENTIFIER ( EnumItemTuple | EnumItemStruct )? EnumItemDiscriminant?

EnumItemTuple :
( TupleFields? )

EnumItemStruct :
{ StructFields? }

EnumItemDiscriminant :
= Expression

還是直接看具體程式碼更直觀:

enum E1 {
    A,
    B(u8,),
    C{x: u8, },
}

enum E2 {
    A = 0,
    B = 1,
    C = -1,
}

注意 E1E2 預設不能混用,你需要加上 #[repr(inttype)], inttype 可以是:
i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize

#[repr(isize)]
enum E {
    A(String, ), // 預設 A(String)=0
    B(u8, String) = 1,
    C = 3,
}

這篇文章的主要目的是: 以儘量簡單的程式碼記錄思考過程。所以先忽略掉 EnumIteminttype 的情況,
同時也忽略掉 EnumItemvisibility(pub) 和 meta(#[...])屬性, 以免程式碼太雜,難以肉眼 parse

第一次嘗試

首先匹配整個 enum, 先不管內部細節

macro_rules! enum_parser {
    (
        enum $name: ident {
            $($tt: tt)* // 把整個 enum body 當作一串 token tree
        }
    ) => {
        enum $name {
            $($tt)*
        }
    };
}

在上面這一步,我們就可以針對 enum 這個整體插入自己的程式碼了,但是對於內部 EnumItem 還沒摸到。
目前要解析的 EnumItem 有三種情況: enum E { A, B(u8), C{x: u8}, }, 那麼我需要定義一個輔助宏,專門來解析 $($tt)*, 從中萃取出一個個的 EnumItem 就行了

macro_rules! enum_parser_helper {
    // enum E{}
    () => {};

    // A,
    (
        $field: ident 
        $(, $($tail: tt)*)?
    ) => {};

    // B(u8,),
    (
        $field: ident ($($ty: ty),* $(,)?) 
        $(, $($tail: tt)*)?
    ) => {};

    // C{x:u8, },
    (
        $field: ident {$($inner_field: ident : $ty: ty),* $(,)?}
    ) => {};
}

macro_rules! enum_parser {
    (
        enum $name: ident {
            $($tt: tt)*
        }
    ) => {
        enum $name {
            enum_parser_helper!($($tt)*)
        }
    };
}

三種情況,加空 enum 的情況都匹配到了,雖然 => 右邊的 {} 裡面還沒填東西,但是大體的形狀是對的。好像也不比 struct 複雜多少嘛,
測試一下

enum_parser! {
    enum E {}
}

duang error!!!

error: expected one of `(`, `,`, `=`, `{`, or `}`, found `!`
   --> src/main.rs:459:35
    |
459 |                   enum_parser_helper!($($tt)*)
    |                                     ^ expected one of `(`, `,`, `=`, `{`, or `}`
...
464 | /     enum_parser! {
465 | |         enum E {}
466 | |     }
    | |_____- in this macro invocation
    |
    = help: enum variants can be `Variant`, `Variant = <integer>`, `Variant(Type, ..., TypeN)` or `Variant { fields: Types }`
    = note: this error originates in the macro `enum_parser` (in Nightly builds, run with -Z macro-backtrace for more info)

這啥情況,咋回事,咋不行呢?你這編譯器不講武德,直接給我像 C 語言那樣把我的 enum_parser_helper!($($tt)*) 展開不就完事了,幹嘛一言不合就報錯?

經過一頓抓耳撓腮之後,終於冷靜下來。

expected one of `(`, `,`, `=`, `{`, or `}` ? 這是把我的 enum_parser_helper 當成 enum 裡的 EnumItem了呀!
程式碼應該是被 enum_parser! 展開成這個樣子了:

enum E {
    enum_parser_helper!($($tt)*)
}

也就是說只有 enum_parser! 這一層做了程式碼展開,但是 enum_parser_helper! 沒幹活呀。
一個 macro 裡面是可以呼叫另一 macro 的啊,難道是不能這麼玩嗎?

於是我在搜尋引擎搜 rust macro does not expand in enum
找到了這個: Call macro inside macro repetition

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2cacd8ce561af93ceabd40b123b6549a

macro_rules! inner {
    (ok) => {}
}

macro_rules! outer {
    ( $($variants:ident),* ) => {
        enum Test {
            $($variants inner!(ok)),*
        }
    }
}

outer! {
    A,
    B
}

fn main() {

}

這跟我遇到的問題簡直就是完全一樣啊

Solved:

To do this kind of thing, you need what's known as a tt-muncher macro: It builds all of the text for the enum body first,
and then wraps it in the enum as the final step. A good reference for this kind of advanced macro programming is
The Little Book of Rust Macros.

大意是: 要做到這種事情,需要用到一種被稱為 tt-muncher 的 macro: 它先把 enum body 的部分組裝好,在最後一步再把它塞到 enum 裡去

我大概明白了他的意思,我可以先萃取出 enumname(ident) 和 body(tt), 然後把 name 當作 tt(token tree) 存起來(暫且存到一個 [...] 裡面),
遞迴處理 body 部分, 把 fieldtt 種提取出來, 再放到 [...] 中, 最終整個 enum 又重新變回了 tt, 然後統一展開 enum $name { $($tt)* },
不可謂不 nice!

繼續嘗試

enum_parser_helper {
    // 全部 field 處理完之後, enum 的全部內容就都在 [] 裡面了
    ([
        $(#[$meta: meta])* 
        enum $name: ident 
        $($tt: tt)*
    ]) => {
        // 最終的組裝展開
        $(#[$meta])*
        enum $name {
            $($tt)*
        }
    };

    // 萃取出 A,
    (
        [$($head: tt)*]
        $field: ident
        $(, $($tt: tt)*)?
    ) => {
        enum_parser_helper!([ $($head)* $field, ] $($($tt)*)? )
    };

    // 萃取出 B(u8,),
    (
        [$($head: tt)*]
        $field: ident ($($ty: ty),* $(,)?) 
        $(, $($tt: tt)*)?
    ) => {
        enum_parser_helper!( 
            [ 
                $($head)* 
                $field($($ty),*), 
            ]
            $($($tt)*)? 
        )
    };

    // 萃取出 B{x: u8,},
    (
        [$($head: tt)*]
        $field: ident {$($inner_field: ident : $ty: ty),* $(,)?} 
        $(, $($tt: tt)*)?
    ) => {
        enum_parser_helper!(
            [
                $($head)* 
                $field{$($inner_field: $ty),*},
            ] 
            $($($tt)*)?
        )
    };
}

macro_rules! enum_parser {
    () => {};

    (
        $(#[$meta: meta])*
        enum $name: ident {
            $($tt: tt)*
        }
    ) => {
        // [] 記憶體放所有的 tt
        enum_parser_helper!( [$(#[$meta])* enum $name] $($tt)* )
    };
}

搞定!

測試一下:

enum_parser! {}

enum_parser! {
    #[derive(Debug)]
    enum E {
        A,
        B(u8,),
        C{x:u8,},
    }
}

完事。

下一篇準備寫一下過程宏 proc_macro