如何在递归macro_rules中扩展子模式?

时间:2019-06-21 10:27:14

标签: rust rust-macros

我正在编写一个宏,以方便地将enum类型变量中的嵌套结构匹配到编译时模板。这个想法是利用Rust的模式匹配在结构的某些位置强制执行特定的值,或将变量绑定到其他有趣的位置。基本思想在我的实现中有效,但对于嵌套模式则无效。我认为问题在于,一旦宏输入的一部分被解析为$<name>:pat,以后就无法解析为$<name>:tt

为避免术语 pattern 的歧义,我将根据Rust文档使用以下表示法:

  • 模式是出现在match臂中的if let语句中,并由片段说明符$<name>:pat在宏中进行匹配。
  • matcher 是宏中语法规则的左侧。
  • 模板是宏输入的一部分,用于确定宏的扩展方式。

Playground MCVE

这是我正在使用的enum类型的简化版本:

#[derive(Debug, Clone)]
enum TaggedValue {
    Str(&'static str),
    Seq(Vec<TaggedValue>),
}

例如,以下表达式

use TaggedValue::*;
let expression = Seq(vec![
    Str("define"),
    Seq(vec![Str("mul"), Str("x"), Str("y")]),
    Seq(vec![Str("*"), Str("x"), Str("y")]),
]);

可以通过以下宏调用进行匹配:

match_template!(
    &expression,                               // dynamic input structure
    { println!("fn {}: {:?}", name, body) },   // action to take after successful match
    [Str("define"), [Str(name), _, _], body]   // template to match against
);

在这里,成功匹配后,标识符namebody绑定到expression中的相应子元素,并作为变量作为第二个参数传递给宏时可用。

这是我编写宏的努力:

macro_rules! match_template {
    // match sequence template with one element
    ($exp:expr, $action:block, [$single:pat]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single)
        } else {
            panic!("mismatch")
        }
    };

    // match sequence template with more than one element
    ($exp:expr, $action:block, [$first:pat, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            // match first pattern in sequence against first element of $expr
            match_template!(&seq[0], {
                // then match remaining patterns against remaining elements of $expr
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first)
        } else {
            panic!("mismatch")
        }
    };

    // match a non sequence template and perform $action on success
    ($exp:expr, $action:block, $atom:pat) => {
        if let $atom = $exp $action else {panic!("mismatch")}
    };
}

对于非嵌套模板,它可以正常工作,对于嵌套模板,我可以手动嵌套宏调用。但是,在单个宏调用中直接指定嵌套模板会失败,并出现编译错误。

match_template!(
    &expression,
    {
        match_template!(
            signature,
            { println!("fn {}: {:?}", name, body) },
            [Str(name), _, _]
        )
    },
    [Str("define"), signature, body]
);
// prints:
//   fn mul: Seq([Str("*"), Str("x"), Str("y")])

match_template!(
    &expression,
    { println!("fn {}: {:?}", name, body) },
    [Str("define"), [Str(name), _, _], body]
);
// error[E0529]: expected an array or slice, found `TaggedValue`
//   --> src/main.rs:66:25
//    |
// 66 |         [Str("define"), [Str(name), _, _], body]
//    |                         ^^^^^^^^^^^^^^^^^ pattern cannot match with input type `TaggedValue`

Playground MCVE

我怀疑错误是说[Str(name), _, _]作为单个切片模式被匹配,第三条宏规则接受了它,从而导致类型不匹配。但是,我希望它是令牌树,以便第二条规则可以将其分解为一系列模式。

我试图将第二个规则更改为($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) =>,但这只会导致错误在外部级别发生。

需要对宏进行哪些修改才能使其递归扩展此类模板?

(我不认为像Recursive macro to parse match arms in Rust那样的令牌咀嚼在这里也可以工作,因为我明确地希望将标识符绑定到模式中。)

这是我希望宏调用扩展到的内容(为简洁起见,忽略不匹配分​​支。此外,我通过在后缀seq变量来模拟宏卫生):

// macro invocation
match_template!(
    &expression,
    { println!("fn {}: {:?}", name, body) },
    [Str("define"), [Str(name), _, _], body]
);

// expansion
if let Seq(seq_1) = &expression {
    if let Str("define") = &seq_1[0] {
        if let Seq(seq_1a) = Seq(seq_1[1..].into()) {
            if let Seq(seq_2) = &seq_1a[0] {
                if let Str(name) = &seq_2[0] {
                    if let Seq(seq_2a) = Seq(seq_2[1..].into()) {
                        if let _ = &seq_2a[0] {
                            if let Seq(seq_2b) = Seq(seq_2a[1..].into()) {
                                if let _ = &seq_2b[0] {
                                    if let Seq(seq_1b) = Seq(seq_1a[1..].into()) {
                                        if let body = &seq_1b[0] {
                                            { println!("fn {}: {:?}", name, body) }
                                        }
                                    }
                                }
                            }
                        } 
                    } 
                } 
            } 
        } 
    } 
} 

完整的扩展有些冗长,但是这个略微缩短的版本体现了应该发生的事情的本质:

if let Seq(seq) = &expression {
    if let Str("define") = &seq[0] {
        if let Seq(signature) = &seq[1] {
            if let Str(name) = &signature[0] {
                if let body = &seq[2] {
                    println!("fn {}: {:?}", name, body)
                }
            }
        }
    }
}

最后,这是another playground link,它显示了递归扩展的各个步骤。非常密集。

1 个答案:

答案 0 :(得分:1)

确实,似乎问题在于该宏与逗号分隔的模式列表匹配。因此,在输入[Str("define"), [Str(name), _, _], body]中,宏将内部[...]解释为无法与类型TaggedValue的表达式匹配的切片模式。

解决方案是将输入扩展为令牌树。但是,这需要一个小技巧,因为单个令牌树无法表示每个模式。具体来说,格式为Variant(value)的模式由两个令牌树组成:Variant(value)。在调用宏的终端(非递归)规则之前,可以将这两个令牌重新组合成一个模式。

例如,在单元素模板中匹配此类模式的规则如下所示:

($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) =>

这些令牌与一起传递给宏的另一个调用

match_template!(&seq[0], $action, $single_variant $single_value)

根据终端规则将它们作为单个模式进行匹配

($exp:expr, $action:block, $atom:pat) =>

最终的宏定义包含两个附加规则以说明Variant(value)模式:

macro_rules! match_template {
    ($exp:expr, $action:block, [$single:tt]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], $action, $single_variant $single_value)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], {
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, [$first_variant:tt $first_value:tt, $($rest:tt)*]) => {
        if let Seq(seq) = $exp {
            match_template!(&seq[0], {
                match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
            }, $first_variant $first_value)
        } else {
            panic!("mismatch")
        }
    };

    ($exp:expr, $action:block, $atom:pat) => {
        if let $atom = $exp $action else {panic!("mismatch")}
    };
}

这里是完整示例的链接:playground