首页 文章

修复“没有规则预期令牌”宏错误

提问于
浏览
9

我正在尝试编写一个用于解构BSON数据的宏,如下所示:

let bson: Document = ...;
let (id, hash, name, path, modification_time, size, metadata, commit_data) = bson_destructure! {
    get id = from (bson), optional, name ("_id"), as ObjectId;
    get hash = from (bson), as String, through (|s| ContentHash::from_str(&s));
    get name = from (bson), as String;
    get path = from (bson), as Bson, through (PathBuf::from_bson);
    get modification_time = from (bson), as UtcDatetime, through (FileTime);
    get size = from (bson), as I64, through (|n| n as u64);
    get metadata = from (bson), as Document, through (Metadata::from_bson);
    get commit_data = from (bson), optional, as Document, through (CommitData::from_bson);
    ret (id, hash, name, path, modification_time, size, metadata, commit_data)
};

我为它编写了以下宏(非常大):

macro_rules! bson_destructure {
    // required field
    (
        @collect req,
        [$target:ident, $source:expr, $field:expr, Bson, $f:expr],
        [];
        $($rest:tt)*
    ) => {{
        let $target = try!(match $source.remove($field) {
            Some(v) => $f(v),
            None => Err(BsonDestructureError::MissingField {
                field_name: $field,
                expected: "Bson"
            }),
        });
        bson_destructure!($($rest)*)
    }};
    (
        @collect req,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [];
        $($rest:tt)*
    ) => {{
        let $target = try!(match $source.remove($field) {
            Some(v) => match v {
                ::ejdb::bson::Bson::$variant(v) => $f(v),
                v => Err(BsonDestructureError::InvalidType {
                    field_name: $field,
                    expected: stringify!($variant),
                    actual: v
                })
            },
            None => Err(BsonDestructureError::MissingField {
                field_name: $field,
                expected: stringify!($variant)
            }),
        });
        bson_destructure!($($rest)*)
    }};

    // optional field
    (
        @collect opt,
        [$target:ident, $source:expr, $field:expr, Bson, $f:expr],
        [];
        $($rest:tt)*
    ) => {{
        let $target = try!(match $source.remove($field) {
            Some(v) => $f(v).map(Some),
            None => Ok(None),
        });
        bson_destructure!($($rest)*)
    }};
    (
        @collect opt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [];
        $($rest:tt)*
    ) => {{
        let $target = try!(match $source.remove($field) {
            Some(v) => match v {
                ::ejdb::bson::Bson::$variant(v) => $f(v).map(Some),
                v => Err(BsonDestructureError::InvalidType {
                    field_name: $field,
                    expected: stringify!($variant),
                    actual: v
                })
            },
            None => Ok(None),
        });
        bson_destructure!($($rest)*)
    }};

    // change variant name
    (
        @collect $k:tt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [as $nv:ident, $($word:ident $arg:tt),*];
        $($rest:tt)*
    ) => {
        bson_destructure!(
            @collect $k,
            [$target, $source, $field, $nv, $f],
            [$($word $arg),*];
            $($rest)*
        )
    };

    // change final mapping function
    (
        @collect $k:tt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [through ($nf:expr), $($word:ident $arg:tt),*];
        $($rest:tt)*
    ) => {
        bson_destructure!(
            @collect $k,
            [$target, $source, $field, $variant, $nf],
            [$($word $arg),*];
            $($rest)*
        )
    };

    // change field name
    (
        @collect $k:tt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [name ($nn:expr), $($word:ident $arg:tt),*];
        $($rest:tt)*
    ) => {
        bson_destructure!(
            @collect $k,
            [$target, $source, $nn, $variant, $f],
            [$($word $arg),*];
            $($rest)*
        )
    };

    // main forms
    (get $target:ident = from ($source:expr), $($word:ident $arg:tt),*; $($rest:tt)*) => {
        bson_destructure!(
            @collect req,
            [$target, $source, stringify!($target), Bson, Ok],
            [$($word $arg),*];
            $($rest)*
        )
    };
    (get $target:ident = from ($source:expr), optional, $($word:ident $arg:tt),*; $($rest:tt)*) => {
        bson_destructure!(
            @collect opt,
            [$target, $source, stringify!($target), Bson, Ok],
            [$($word $arg),*];
            $($rest)*
        )
    };

    // final form
    (ret $e:expr) => { $e }
}

但是,上面的第一个示例导致以下编译错误:

src/db/data.rs:345:22: 345:25 error: no rules expected the token `opt`
src/db/data.rs:345             @collect opt,
                                        ^~~

我有点惊讶它没有像往常一样显示错误位置(也就是说,没有指示扩展发生的地方),但是,当我评论使用宏的代码时,错误消失了 .

我不明白为什么它说没有规则期望这个标记,因为有这样的规则,但也许我不明白的东西 .

我很确定这是可能的,因为这大致是quick_error crate所做的,但似乎我的宏观写作技巧仍然缺乏 .

我应该如何修复宏,以便它可以像我期望的那样工作?

为完整起见,以下是 BsonDestructureError 的定义:

#[derive(Debug, Clone)]
pub enum BsonDestructureError {
    InvalidType {
        field_name: &'static str,
        expected: &'static str,
        actual: Bson
    },
    InvalidArrayItemType {
        index: usize,
        expected: &'static str,
        actual: Bson
    },
    MissingField {
        field_name: &'static str,
        expected: &'static str
    }
}

我也在使用从 ejdb crate重新出口的 bson 箱子 . Here是一个最小的例子,可以在稳定的Rust上使用cargo script运行 .

1 回答

  • 12

    cargo script ,一个递归的muncher,以及我最喜欢的内部规则语法;我怎么能不?

    首先,运行 cargo rustc -- -Z trace-macros 可以确定确切的问题 . 这将在扩展时输出每个规则,给我们一个"backtrace",经过一些手动重新格式化后,它看起来像这样:

    bson_destructure! {
        get id = from ( bson ) , optional , name ( "_id" ) , as ObjectId ;
        get hash = from ( bson ) , as String ;
        get name = from ( bson ) , as String ;
        get path = from ( bson ) , as Bson ;
        get modification_time = from ( bson ) , as UtcDatetime ;
        get size = from ( bson ) , as I64 , through ( | n | n as u64 ) ;
        get metadata = from ( bson ) , as Document ;
        get commit_data = from ( bson ) , optional , as Document ;
        ret ( id , hash , name , path , modification_time , size , metadata , commit_data )
    }
    
    bson_destructure! {
        @ collect opt ,
        [ id , bson , stringify ! ( id ) , Bson , Ok ] ,
        [ name ( "_id" ) , as ObjectId ] ;
    
        get hash = from ( bson ) , as String ;
        get name = from ( bson ) , as String ;
        get path = from ( bson ) , as Bson ;
        get modification_time = from ( bson ) , as UtcDatetime ;
        get size = from ( bson ) , as I64 , through ( | n | n as u64 ) ;
        get metadata = from ( bson ) , as Document ;
        get commit_data = from ( bson ) , optional , as Document ;
        ret ( id , hash , name , path , modification_time , size , metadata , commit_data )
    }
    
    bson_destructure! {
        @ collect opt ,
        [ id , bson , "_id" , Bson , Ok ] , [ as ObjectId ] ;
    
        get hash = from ( bson ) , as String ;
        get name = from ( bson ) , as String ;
        get path = from ( bson ) , as Bson ;
        get modification_time = from ( bson ) , as UtcDatetime ;
        get size = from ( bson ) , as I64 , through ( | n | n as u64 ) ;
        get metadata = from ( bson ) , as Document ;
        get commit_data = from ( bson ) , optional , as Document ;
        ret ( id , hash , name , path , modification_time , size , metadata , commit_data )
    }
    

    仔细阅读 bson_destructure! 中的规则会显示问题:没有与第三次扩展相匹配的规则 . 坦率地说,当涉及到递归规则时,报告理智的错误位置是垃圾;它指向 opt 令牌是无关紧要的 . 真正的问题是它无法找到匹配规则 .

    特别是,违规规则是这样的:

    // change variant name
    (
        @collect $k:tt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [as $nv:ident, $($word:ident $arg:tt),*];
        $($rest:tt)*
    ) => {
        ...
    };
    

    请注意 $nv:ident 之后立即出现逗号 . 另请注意,输入中没有此类逗号 . 这可以通过在重复内部移动逗号来解决,如下所示:

    // change field name
    (
        @collect $k:tt,
        [$target:ident, $source:expr, $field:expr, $variant:ident, $f:expr],
        [name ($nn:expr) $(, $word:ident $arg:tt)*];
        $($rest:tt)*
    ) => {
        ...
    };
    

    另一个替代方案(也是我通常使用的方法)是在第一次遇到输入时简单地改变输入,以确保始终存在尾随逗号 .

    由于本机依赖性,代码实际上不会在我的机器上编译,但我确实验证了进行此更改(此处以及具有类似问题的其他规则)允许它完成宏扩展 . 您可以使用 cargo rustc -- -Z unstable-options --pretty=expanded 检查输出看起来是否正确 .

相关问题