递归宏来解析Rust中的匹配臂

Mat*_*ahl 2 macros rust

我正在尝试编写一个宏来将一组规则扩展为执行令牌匹配的代码,但是在不导致宏扩展错误的情况下无法生成正确的代码.我知道我可以处理其他方式,但这里的关键问题不是如何解析令牌,而是如何编写一个可以递归扩展带有匹配臂的令牌树的宏.

我们的想法是,我们想要从字符串中读取一个令牌并将其打印出来.需要添加更多代码才能将其转换为更有用的代码,但此示例用于说明这种情况:

#[derive(Debug, PartialEq)]
enum Digit {
    One,
    Two,
    Three,
    Ten,
    Eleven,
}

#[test]
fn test1(buf: &str) {
    let buf = "111";
    let token = parse!(buf, {
        '1' => Digit::One,
        '2' => Digit::Two,
        '3' => Digit::Three,
    });
    assert_eq!(token, Some(Digit::One));
}
Run Code Online (Sandbox Code Playgroud)

我们想从这个例子中生成的代码是:

fn test1(buf: &str) {
    let token = {
        let mut chars = buf.chars().peekable();
        match chars.peek() {
            Some(&'1') => {
                chars.next().unwrap();
                Digit::One
            }
            Some(&'2') => {
                chars.next().unwrap();
                Digit::Two
            }
            Some(&'3') => {
                chars.next().unwrap();
                Digit::Three
            }
            Some(_) | None => None,
        }
    };
    assert_eq!(token, Some(Digit::One));
}
Run Code Online (Sandbox Code Playgroud)

忽略这样一个事实,即我们不从字符串中读取更多标记,因此chars.next().unwrap()它不是很有用.稍后会有用.

用于生成上述代码的宏很简单:

macro_rules! parse {
    ($e:expr, { $($p:pat => $t:expr),+ }) => {
        {
            let mut chars = $e.chars().peekable();
            match chars.peek() {
                $(Some(&$p) => {
                    chars.next().unwrap();
                    Some($t)
                },)+
                Some(_) | None => None
            }
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

现在让我们展开这个例子来处理一个更高级的匹配,并允许它用前瞻读取多个字符,所以只有字符匹配某些模式.如果不是,则不应读取无关字符.我们以与上一个示例类似的方式创建一个带匹配臂的令牌树,但是在这里我们要支持一个递归结构:

#[test]
fn test2() {
    let buf = "111";
    let token = parse!(buf, {
        '1' => {
            '0' => Digit::Ten,
            '1' => Digit::Eleven,
            _ => Digit::One,
        },
        '2' => Digit::Two,
        '3' => Digit::Three
    });
    assert_eq!(token, Some(Digit::Eleven));
}
Run Code Online (Sandbox Code Playgroud)

我们想从这个例子中生成的代码是:

fn test2() {
    let buf = "111";
    let token = {
        let mut chars = buf.chars().peekable();
        match chars.peek() {
            Some(&'1') => {
                chars.next().unwrap();
                match chars.peek() {
                    Some(&'0') => {
                        chars.next().unwrap();
                        Some(Digit::Ten)
                    },
                    Some(&'1') => {
                        chars.next().unwrap();
                        Some(Digit::Eleven)
                    },
                    Some(_) | None => Some(Digit::One)
                }
            },
            Some(&'2') => {
                chars.next().unwrap();
                Some(Digit::Two)
            },
            Some(&'3') => {
                chars.next().unwrap();
                Some(Digit::Three)
            },
            Some(_) | None => None,
        }
    };
    assert_eq!(token, Some(Digit::Eleven));
}
Run Code Online (Sandbox Code Playgroud)

尝试编写一个宏来处理这个可能大致如下:

macro_rules! expand {
    ($t:tt) => {{
        chars.next().unwrap();
        inner!($t)
    }};
    ($e:expr) => {{
        chars.next().unwrap();
        Some($e)
    }};
}

macro_rules! inner {
    ($i:ident, { $($p:pat => ???),+ }) => {
        match $i.peek() {
            $( Some(&$p) => expand!($i, ???), )+
            Some(_) | None => None
        }
    };
}

macro_rules! parse {
    ($e:expr, $t:tt) => {
        {
            let mut chars = $e.chars().peekable();
            inner!(chars, $t)
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

但是,我无法找到替换???inner! 用的东西,或者一个表达式或令牌树匹配宏.

  • 类似的东西此时$e:expr无法匹配令牌树.

  • 类似的东西$t:tt与枚举常量不匹配Digit::Two,这是一个完全有效的表达式.

  • 类似于$($rest:tt)*通用匹配器的东西会失败,因为Kleene-star闭包是贪婪的并且会尝试匹配以下逗号.

  • 一个递归宏观经济匹配的项目一个接一个,例如,沿着线的图形{ $p:pat => $t:expr, $($rest:tt)* }将是不可能的内扩大match在声明中inner!宏因为意想不到的东西,语法看起来像... => ...,所以这种扩张提供了一个错误,声称它期望=>后宏:

    match $e.peek() {
         Some(&$p) => ...$t...,
         inner!($rest)
                       ^ Expect => here
    }
    
    Run Code Online (Sandbox Code Playgroud)

这看起来像书中提到的语法要求之一.

更改匹配部分的语法不允许使用该pat 要求,因为需要后跟a =>(根据书中的宏章节).

DK.*_*DK. 5

当你需要根据这样的重复内部的不同匹配进行分支时,你需要进行增量解析.

所以.

macro_rules! parse {
Run Code Online (Sandbox Code Playgroud)

这是宏的入口点.它设置最外层,并将输入提供给一般的解析规则.我们向下传递,chars以便更深层可以找到它.

    ($buf:expr, {$($body:tt)*}) => {
        {
            let mut chars = $buf.chars().peekable();
            parse! { @parse chars, {}, $($body)* }
        }
    };
Run Code Online (Sandbox Code Playgroud)

终止规则:一旦我们用尽输入(以逗号为模),将累积的匹配臂代码片段转储到match表达式中,并附加最终的全部捕获臂.

    (@parse $chars:expr, {$($arms:tt)*}, $(,)*) => {
        match $chars.peek() {
            $($arms)*
            _ => None
        }
    };
Run Code Online (Sandbox Code Playgroud)

或者,如果指定了全能臂,则使用它.

    (@parse $chars:expr, {$($arms:tt)*}, _ => $e:expr $(,)*) => {
        match $chars.peek() {
            $($arms)*
            _ => Some($e)
        }
    };
Run Code Online (Sandbox Code Playgroud)

这会处理递归.如果我们看到一个块,我们$chars使用空代码累加器来提前并解析块的内容.所有这些的结果被附加到当前累加器( $($arms)).

    (@parse $chars:expr, {$($arms:tt)*}, $p:pat => { $($block:tt)* }, $($tail:tt)*) => {
        parse! {
            @parse
            $chars,
            {
                $($arms)*
                Some(&$p) => {
                    $chars.next().unwrap();
                    parse!(@parse $chars, {}, $($block)*)
                },
            },
            $($tail)*
        }
    };
Run Code Online (Sandbox Code Playgroud)

非递归情况.

    (@parse $chars:expr, {$($arms:tt)*}, $p:pat => $e:expr, $($tail:tt)*) => {
        parse! {
            @parse
            $chars,
            {
                $($arms)*
                Some(&$p) => Some($e),
            },
            $($tail)*
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

而且,为了完整性,其余的测试代码.请注意,我必须更改test1,因为它不是有效的测试.

#[derive(Debug, PartialEq)]
enum Digit { One, Two, Three, Ten, Eleven }

#[test]
fn test1() {
    let buf = "111";
    let token = parse!(buf, {
        '1' => Digit::One,
        '2' => Digit::Two,
        '3' => Digit::Three,
    });
    assert_eq!(token, Some(Digit::One));
}

#[test]
fn test2() {
    let buf = "111";
    let token = parse!(buf, {
        '1' => {
            '0' => Digit::Ten,
            '1' => Digit::Eleven,
            _ => Digit::One,
        },
        '2' => Digit::Two,
        '3' => Digit::Three,
    });
    assert_eq!(token, Some(Digit::Eleven));
}
Run Code Online (Sandbox Code Playgroud)